diff --git a/backend/.env.example b/backend/.env.example index 11070817..44765b11 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -15,3 +15,8 @@ MONGODB_PASSWORD=dev_password # Frontend app URL FRONTEND_URL=http://localhost:3314 + +# Developer Sending Wallet Address +#(Public Address starts with a 'G', Secret Seed starts with an 'S' ) +DEV_WALLET_PUBLIC_ADDRESS= +DEV_WALLET_SECRECT_SEED= diff --git a/backend/package.json b/backend/package.json index 900b867a..2e1b6080 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,7 +22,8 @@ "express": "^4.17.1", "express-session": "^1.17.2", "mongodb": "^4.0.0", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "pi-backend": "^0.1.3" }, "devDependencies": { "@types/cors": "^2.8.11", diff --git a/backend/src/environments.ts b/backend/src/environments.ts index 10df4f4d..91f723e4 100644 --- a/backend/src/environments.ts +++ b/backend/src/environments.ts @@ -23,6 +23,8 @@ interface Environment { mongo_user: string, mongo_password: string, frontend_url: string, + wallet_public_address: string, + wallet_secret_seed: string, } const env: Environment = { @@ -34,6 +36,8 @@ const env: Environment = { mongo_user: process.env.MONGODB_USERNAME || '', mongo_password: process.env.MONGODB_PASSWORD || '', frontend_url: process.env.FRONTEND_URL || 'http://localhost:3314', + wallet_public_address: process.env.DEV_WALLET_PUBLIC_ADDRESS || '', + wallet_secret_seed: process.env.DEV_WALLET_SECRECT_SEED || '', }; export default env; diff --git a/backend/src/handlers/payments.ts b/backend/src/handlers/payments.ts index 79a4c9e5..1f38dc81 100644 --- a/backend/src/handlers/payments.ts +++ b/backend/src/handlers/payments.ts @@ -2,48 +2,68 @@ import axios from "axios"; import { Router } from "express"; import platformAPIClient from "../services/platformAPIClient"; import "../types/session"; +import PiNetwork from 'pi-backend'; +import env from "../environments"; -export default function mountPaymentsEndpoints(router: Router) { - // handle the incomplete payment - router.post('/incomplete', async (req, res) => { - const payment = req.body.payment; - const paymentId = payment.identifier; - const txid = payment.transaction && payment.transaction.txid; - const txURL = payment.transaction && payment.transaction._link; - - /* - implement your logic here - e.g. verifying the payment, delivering the item to the user, etc... - - below is a naive example - */ - - // find the incomplete order - const app = req.app; - const orderCollection = app.locals.orderCollection; - const order = await orderCollection.findOne({ pi_payment_id: paymentId }); - - // order doesn't exist - if (!order) { - return res.status(400).json({ message: "Order not found" }); - } +/* +* DEVELOPER NOTE: +* payment implementations are explained in our SDKs linked below, for more information +* User to App Payments - https://github.com/pi-apps/pi-platform-docs/blob/master/README.md +* App to User Payments - https://github.com/pi-apps/pi-platform-docs/blob/master/payments_advanced.md +*/ - // check the transaction on the Pi blockchain - const horizonResponse = await axios.create({ timeout: 20000 }).get(txURL); - const paymentIdOnBlock = horizonResponse.data.memo; +// DO NOT expose these values to public +const pi = new PiNetwork(env.pi_api_key, env.wallet_secret_seed); - // and check other data as well e.g. amount - if (paymentIdOnBlock !== order.pi_payment_id) { - return res.status(400).json({ message: "Payment id doesn't match." }); - } +export default function mountPaymentsEndpoints(router: Router) { + + // handle the incomplete payment + router.post('/incomplete', async (req, res) => { + const payment = req.body.payment; + const paymentId = payment.identifier; + const txid = payment.transaction && payment.transaction.txid; + const txURL = payment.transaction && payment.transaction._link; + + /* + DEVELOPER NOTE: + implement your logic here + e.g. verifying the payment, delivering the item to the user, etc... + + below is a naive example + */ + + // find the incomplete order + const app = req.app; + const orderCollection = app.locals.orderCollection; + const order = await orderCollection.findOne({ pi_payment_id: paymentId }); + + // order doesn't exist + if (!order) { + return res.status(400).json({ message: "Order not found" }); + } + + // check the transaction on the Pi blockchain + const horizonResponse = await axios.create({ timeout: 20000 }).get(txURL); + const paymentIdOnBlock = horizonResponse.data.memo; + + // and check other data as well e.g. amount + if (paymentIdOnBlock !== order.pi_payment_id) { + return res.status(400).json({ message: "Payment id doesn't match." }); + } + + // mark the order as paid + await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { txid, paid: true } }); + + // let Pi Servers know that the payment is completed + await platformAPIClient.post(`/v2/payments/${paymentId}/complete`, { txid }); + return res.status(200).json({ message: `Handled the incomplete payment ${paymentId}` }); + }); - // mark the order as paid - await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { txid, paid: true } }); - - // let Pi Servers know that the payment is completed - await platformAPIClient.post(`/v2/payments/${paymentId}/complete`, { txid }); - return res.status(200).json({ message: `Handled the incomplete payment ${paymentId}` }); - }); + /* + * + * USER TO APP PAYMENT + * + */ // approve the current payment router.post('/approve', async (req, res) => { @@ -55,9 +75,11 @@ export default function mountPaymentsEndpoints(router: Router) { const paymentId = req.body.paymentId; const currentPayment = await platformAPIClient.get(`/v2/payments/${paymentId}`); + console.log(currentPayment); const orderCollection = app.locals.orderCollection; /* + DEVELOPER NOTE: implement your logic here e.g. creating an order record, reserve an item if the quantity is limited, etc... */ @@ -66,10 +88,14 @@ export default function mountPaymentsEndpoints(router: Router) { pi_payment_id: paymentId, product_id: currentPayment.data.metadata.productId, user: req.session.currentUser.uid, + amount: currentPayment.data.amount, txid: null, paid: false, cancelled: false, - created_at: new Date() + completed: false, + created_at: new Date(), + is_refund: false, + refunded_at: null }); // let Pi Servers know that you're ready @@ -86,6 +112,7 @@ export default function mountPaymentsEndpoints(router: Router) { const orderCollection = app.locals.orderCollection; /* + DEVELOPER NOTE: implement your logic here e.g. verify the transaction, deliver the item to the user, etc... */ @@ -104,7 +131,8 @@ export default function mountPaymentsEndpoints(router: Router) { const paymentId = req.body.paymentId; const orderCollection = app.locals.orderCollection; - /* + /* + DEVELOPER NOTE: implement your logic here e.g. mark the order record to cancelled, etc... */ @@ -112,4 +140,120 @@ export default function mountPaymentsEndpoints(router: Router) { await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { cancelled: true } }); return res.status(200).json({ message: `Cancelled the payment ${paymentId}` }); }) -} + + + /* + * + * APP TO USER PAYMENT + * + */ + + // method that searches the database and returns any eligible refunds for the user + router.post('/refundable_payment', async (req, res) => { + if (!req.session.currentUser) { + + return res.status(401).json({ error: 'unauthorized', message: "User needs to sign in first" }); + } + + /* + DEVELOPER NOTE: + implement your logic here + e.g. mark the order record to cancelled, etc... + */ + + const app = req.app; + const user = req.session.currentUser.uid; + + const orderCollection = app.locals.orderCollection; + + const refundableOrders = await orderCollection.find({ user: user, paid: true, is_refund: false, refunded_at: null }).toArray(); + console.log(refundableOrders); + + return res.status(200).json({message: `Orders Eligible for Refund`, refundableOrders: refundableOrders }); + + }); + + // method that processes the refund + router.post('/refundable_payment/refund_payment', async (req, res) => { + if (!req.session.currentUser) { + return res.status(401).json({ error: 'unauthorized', message: "User needs to sign in first" }); + } + + /* + DEVELOPER NOTE: + implement your logic here + e.g. mark the order record to cancelled, etc... + */ + + // set the variables from the request to process the refund + const app = req.app; + const userUid = req.session.currentUser.uid; + + const refundedMemo = req.body.memo; + const refundedPaymentID = req.body.refundPaymentID; + + const orderCollection = app.locals.orderCollection; + + // find the order to refund + const order= await orderCollection.findOne({ pi_payment_id: refundedPaymentID }); + + // order doesn't exist + if (!order) { + return res.status(400).json({ message: "Order not found" }); + } + + // create the url with the TxID to query the blockchain for the transaction information + const horizonURL = `https://api.testnet.minepi.com/transactions/${order.txid}/operations` + + // check the transaction on the Pi testnet blockchain + const horizonResponse = await axios.get(horizonURL); + const paymentIdOnBlock = horizonResponse.data.memo; + + const horizonAmount = horizonResponse.data._embedded.records[0].amount; + const horizonTXID = horizonResponse.data._embedded.records[0].transaction_hash; + + // build Refund Transaction + const paymentData = { + amount: horizonAmount, // use the amount from the blockchain since this is the amount that was transacted + memo: refundedMemo, // this is just an example + metadata: {refunded_txid: horizonTXID}, + uid: userUid + } + + try { + // mark payment as refunded prior to refund to prevent double entry + await orderCollection.updateOne({ pi_payment_id: refundedPaymentID }, { $set: { refunded_at: new Date() } }); + + //Send Refund Transaction + const paymentId = await pi.createPayment(paymentData); + + // save the payment information in the DB + await orderCollection.insertOne({ + pi_payment_id: paymentId, + product_id: `Refunded Payment ${horizonTXID}`, + user: req.session.currentUser.uid, + amount: horizonAmount, + txid: null, + paid: false, + cancelled: false, + created_at: new Date(), + is_refund: true, + refunded_at: null + }); + + // it is strongly recommended that you store the txid along with the paymentId you stored earlier for your reference. + const refundTxId = await pi.submitPayment(paymentId); + + // update the Refund PaymentID with the txid + await orderCollection.updateOne({ pi_payment_id: paymentId }, { $set: { txid: refundTxId, paid: true } }); + + // complete the payment + await pi.completePayment(paymentId, refundTxId); + + // return success to the front end + return res.status(200).json({message: `Payment: ${refundedPaymentID} was refunded with transaction ${refundTxId}`, block_explorer_link: `https://blockexplorer.minepi.com/tx/${refundTxId}`}); + } catch (error) { + console.log(error) + } + }); +} \ No newline at end of file diff --git a/backend/yarn.lock b/backend/yarn.lock index 28787e36..bde80fe6 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -81,6 +81,11 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/eventsource@^1.1.2": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.11.tgz#a2c0bfd0436b7db42ed1b2b2117f7ec2e8478dc7" + integrity sha512-L7wLDZlWm5mROzv87W0ofIYeQP5K2UhoFnnUyEWLKM6UBb0ZNRgAqp98qE5DkgfBXdWfc2kYmw9KZm4NLjRbsw== + "@types/express-serve-static-core@^4.17.18": version "4.17.29" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c" @@ -124,6 +129,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== +"@types/node@>= 8": + version "20.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313" + integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q== + "@types/node@^18.7.23": version "18.7.23" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.23.tgz#75c580983846181ebe5f4abc40fe9dfb2d65665f" @@ -134,6 +144,13 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== +"@types/randombytes@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/randombytes/-/randombytes-2.0.0.tgz#0087ff5e60ae68023b9bc4398b406fea7ad18304" + integrity sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA== + dependencies: + "@types/node" "*" + "@types/range-parser@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" @@ -157,6 +174,11 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/urijs@^1.19.6": + version "1.19.19" + resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.19.tgz#2789369799907fc11e2bc6e3a00f6478c2281b95" + integrity sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg== + "@types/webidl-conversions@*": version "6.1.1" resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e" @@ -216,6 +238,18 @@ asn1.js@^5.4.1: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== + dependencies: + follow-redirects "^1.14.7" + axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -223,11 +257,25 @@ axios@^0.21.1: dependencies: follow-redirects "^1.14.0" +axios@^1.2.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base32.js@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" + integrity sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -240,6 +288,11 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" +bignumber.js@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-4.1.0.tgz#db6f14067c140bd46624815a7916c92d9b6c24b1" + integrity sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -295,7 +348,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.6.0: +buffer@^5.1.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -331,6 +384,13 @@ chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -392,6 +452,13 @@ cors@^2.8.5: object-assign "^4" vary "^1" +crc@^3.5.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== + dependencies: + buffer "^5.1.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -411,6 +478,11 @@ debug@^4.3.1: dependencies: ms "2.1.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + denque@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" @@ -426,6 +498,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -453,6 +530,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +es6-promise@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -463,6 +545,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +eventsource@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.2.tgz#bc75ae1c60209e7cb1541231980460343eaea7c2" + integrity sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA== + express-session@^1.17.2: version "1.17.3" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" @@ -539,6 +626,20 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.14.7, follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -681,6 +782,14 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +js-xdr@^1.1.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/js-xdr/-/js-xdr-1.3.0.tgz#e72e77c00bbdae62689062b95fe35ae2bd90df32" + integrity sha512-fjLTm2uBtFvWsE3l2J14VjTuuB8vJfeTtYuNS7LiLHDWIX2kt0l1pqq9334F8kODUkKPMuULjEcbGbkFFwhx5g== + dependencies: + lodash "^4.17.5" + long "^2.2.3" + kruptein@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/kruptein/-/kruptein-3.0.4.tgz#68dcd32ee9e6b611c86615a4616fee2ca3b9b769" @@ -688,6 +797,16 @@ kruptein@^3.0.0: dependencies: asn1.js "^5.4.1" +lodash@^4.17.21, lodash@^4.17.5: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +long@^2.2.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ== + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -718,7 +837,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -803,6 +922,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +node-gyp-build@^4.3.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -864,6 +988,14 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +pi-backend@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/pi-backend/-/pi-backend-0.1.3.tgz#833a066c2dbd041ba4aba02cb33cd6b64417011e" + integrity sha512-40ac7tBhJjzvZ8rwDZZNoJg3+0wtzmAo638oyG2LVDtO72dIAkQtFt+Zcz2OKxQpBZPDozAxGZ24fOrzydKPrw== + dependencies: + axios "^1.2.3" + stellar-sdk "^10.4.1" + picomatch@^2.0.4, picomatch@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -877,6 +1009,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -894,6 +1031,13 @@ random-bytes@~1.0.0: resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -937,7 +1081,7 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -988,6 +1132,14 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sha.js@^2.3.6: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -1010,6 +1162,13 @@ socks@^2.6.2: ip "^1.1.5" smart-buffer "^4.2.0" +sodium-native@^3.3.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.4.1.tgz#44616c07ccecea15195f553af88b3e574b659741" + integrity sha512-PaNN/roiFWzVVTL6OqjzYct38NSXewdl2wz8SRB51Br/MLIJPrbM3XexhVWkq7D3UWMysfrhKVf1v1phZq6MeQ== + dependencies: + node-gyp-build "^4.3.0" + source-map-support@^0.5.12: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -1035,6 +1194,43 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stellar-base@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-8.2.2.tgz#acae1eec0afd95e9e7a292086a310a32b957a65c" + integrity sha512-YVCIuJXU1bPn+vU0ded+g0D99DcpYXH9CEXfpYEDc4Gf04h65YjOVhGojQBm1hqVHq3rKT7m1tgfNACkU84FTA== + dependencies: + base32.js "^0.1.0" + bignumber.js "^4.0.0" + crc "^3.5.0" + js-xdr "^1.1.3" + lodash "^4.17.21" + sha.js "^2.3.6" + tweetnacl "^1.0.3" + optionalDependencies: + sodium-native "^3.3.0" + +stellar-sdk@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/stellar-sdk/-/stellar-sdk-10.4.1.tgz#823eb20e7f346b87c3bcaeeb11ec8128a1790d90" + integrity sha512-Wdm2UoLuN9SNrSEHO0R/I+iZuRwUkfny1xg4akhGCpO8LQZw8QzuMTJvbEoMT3sHT4/eWYiteVLp7ND21xZf5A== + dependencies: + "@types/eventsource" "^1.1.2" + "@types/node" ">= 8" + "@types/randombytes" "^2.0.0" + "@types/urijs" "^1.19.6" + axios "0.25.0" + bignumber.js "^4.0.0" + detect-node "^2.0.4" + es6-promise "^4.2.4" + eventsource "^1.1.1" + lodash "^4.17.21" + randombytes "^2.1.0" + stellar-base "^8.2.2" + toml "^2.3.0" + tslib "^1.10.0" + urijs "^1.19.1" + utility-types "^3.7.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -1062,6 +1258,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toml@^2.3.0: + version "2.3.6" + resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.6.tgz#25b0866483a9722474895559088b436fd11f861b" + integrity sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ== + tr46@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" @@ -1119,6 +1320,16 @@ tsconfig@^7.0.0: strip-bom "^3.0.0" strip-json-comments "^2.0.0" +tslib@^1.10.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -1144,6 +1355,16 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +urijs@^1.19.1: + version "1.19.11" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" + integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== + +utility-types@^3.7.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" diff --git a/frontend/README.md b/frontend/README.md index c421ed3c..bb88c05b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -11,9 +11,18 @@ Install dependencies by running `yarn install`. ### 2. Start the app: -Run the following command to start the app's development server: `yarn start` -This will open a browser window on http://localhost:3314 which you can close, since the demo app mostly needs -to run inside of the Pi Sandbox environment in order to work correctly in development. +Run the following command to start the app's development server: `yarn startNode17` +- This will open a browser window on http://localhost:3314 which you can close. + - Instead open the Pi Browser, head to the developer portal and get your sandbox development url, step 6 of the app checklist. + - Copy and paste that into the desktop browser of your choice. + - In the Pi App open the hamburger menu, select Pi Utilities from the dropdown and hit 'authorize sandbox'. + - Enter the code seen in your desktop browser into the 'authorize sandbox' page and click enter. + - The browser should now reload and your app front end will be displayed. + +- This is not a secure method for production since `--openssl-legacy-provider` will be enabled during the start up process. + - More information on it here, https://towardsdev.com/fixing-err-ossl-evp-unsupported-error-on-nodejs-17-25c21066601c + - You will need to research what is best for your app before deploying. + --- You've completed the frontend setup, return to [`doc/development.md`](../doc/development.md) to finish setting up the demo app diff --git a/frontend/package.json b/frontend/package.json index c129c0d4..e215f3fc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { + "@emotion/react": "latest", + "@emotion/styled": "latest", + "@mui/icons-material": "^5.14.3", + "@mui/joy": "^5.0.0-beta.1", + "@mui/material": "latest", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", @@ -14,6 +19,7 @@ "normalize.css": "^8.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-router-dom": "^6.14.2", "react-scripts": "4.0.3", "typescript": "^4.1.2", "web-vitals": "^1.0.1" diff --git a/frontend/src/Shop/components/Auth.tsx b/frontend/src/Shop/components/Auth.tsx new file mode 100644 index 00000000..55a3d0d4 --- /dev/null +++ b/frontend/src/Shop/components/Auth.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { User, AuthResult, UserContextType, WindowWithEnv, RefundType } from "./Types"; +import axios from "axios"; +import { onIncompletePaymentFound } from "./Payments"; + +/* DEVELOPER NOTE: + The useContext Hook is initiated here to pass the user information + between the various pages of the app. It is important to use the + react-dom links in your app so there is no re-render which causes + loss of the context. + + To read more on react context see, https://react.dev/reference/react/useContext +*/ + +export const UserContext = React.createContext(null); + +const _window: WindowWithEnv = window; +const backendURL = _window.__ENV && _window.__ENV.backendURL; + +const axiosClient = axios.create({ baseURL: `${backendURL}`, timeout: 20000, withCredentials: true}); + +const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = React.useState( { uid: '', username: '' } ) + const [showModal, setShowModal] = React.useState(false); + const [refunds, setRefunds] = React.useState( + [{ + _id: '', + pi_payment_id: '', + product_id: '', + user: '', + amount: 0, + txid: '', + paid: false, + cancelled: false, + completed: false, + created_at: '', + is_refund: false, + refunded_at: '' + }]); + + const signIn = async () => { + const scopes = ['username', 'payments', 'wallet_address']; + const authResult: AuthResult = await window.Pi.authenticate(scopes, onIncompletePaymentFound); + await signInUser(authResult); + setUser(authResult.user); + setShowModal(false); + saveRefunds(); + } + + const signInUser = async (authResult: AuthResult) => { + await axiosClient.post('/user/signin', {authResult}); + return setShowModal(false); + } + + const signOutUser = async() =>{ + const nullUser = { uid: '', username: '' }; + setUser(nullUser); + } + + const saveUser = () =>{ + user.uid === '' ? signIn() : signOutUser(); + } + + const saveShowModal = (value: boolean) => { + setShowModal(value); + } + + const saveRefunds = async() =>{ + const refundableOrders = await axiosClient.post('/payments/refundable_payment'); + const refundable: RefundType[] = refundableOrders.data.refundableOrders; + setRefunds(refundable) + } + + const onModalClose = () => { + saveShowModal(false); + } + + const userContext: UserContextType = { + user, + saveUser, + showModal, + saveShowModal, + refunds, + saveRefunds, + onModalClose, + } + + return ( + + {children} + + ) +} + +export default AuthProvider; diff --git a/frontend/src/Shop/components/Footer.tsx b/frontend/src/Shop/components/Footer.tsx new file mode 100644 index 00000000..69dee5d0 --- /dev/null +++ b/frontend/src/Shop/components/Footer.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { ColorPaletteProp } from '@mui/joy/styles'; +import Box from '@mui/joy/Box'; +import Divider from '@mui/joy/Divider'; +import List from '@mui/joy/List'; +import ListSubheader from '@mui/joy/ListSubheader'; +import ListItem from '@mui/joy/ListItem'; +import ListItemDecorator from '@mui/joy/ListItemDecorator'; +import ListItemButton from '@mui/joy/ListItemButton'; +import Typography from '@mui/joy/Typography'; +import Sheet from '@mui/joy/Sheet'; +import { Grid } from '@mui/material'; + +export default function ColorInversionFooter() { + const [ color ] = React.useState('neutral'); + + return ( + + + + + Developed by the Pi Core Team for Demonstation Purposes + + + + + + + Pi Websites + + + Website + + + Developers Site + + + Community Developers Guide + + + + + Developer Documentation + + + + + + + Pi Platform APIs + + + + + + + + Authentication + + + + + + + + User to App Payment SDK + + + + + + + + App to User Payments SDK + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/Shop/components/Header.tsx b/frontend/src/Shop/components/Header.tsx index 2fb12e4c..79d5c92b 100644 --- a/frontend/src/Shop/components/Header.tsx +++ b/frontend/src/Shop/components/Header.tsx @@ -1,36 +1,99 @@ -import React, { CSSProperties } from "react"; -import { User } from "../"; +import React from "react"; +import { UserContextType } from "./Types"; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import MenuIcon from '@mui/icons-material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import { Link } from "react-router-dom"; +import { Container } from "@mui/material"; +import { UserContext } from "./Auth"; -interface Props { - onSignIn: () => void; - onSignOut: () => void; - user: User | null -} - -const headerStyle: CSSProperties = { - padding: 8, - backgroundColor: "gray", - color: "white", - width: "100%", - display: "flex", - alignItems: "center", - justifyContent: "space-between", +const linkStyle = { + textDecoration: "none", + color: 'black' }; -export default function Header(props: Props) { - return ( -
-
Pi Bakery
+export default function Header() { + const { user, saveUser } = React.useContext(UserContext) as UserContextType; + const [anchorEl, setAnchorEl] = React.useState(null); -
- {props.user === null ? ( - - ) : ( -
- @{props.user.username} -
- )} -
-
+ const handleMenu = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + + + + + + + Home {/* DEVELOPER NOTE: USE LINK FROM REACT-DOM TO NOT LOSE CONTEXT*/} + + + User to App Payments + + + App to User Payments + + + + + + Pi Bakery + + + + { user.uid === '' ? ( + + Sign-In + + ) : ( + + + @{user.username} - Sign Out + + + )} + + + + ); } diff --git a/frontend/src/Shop/components/Payments.tsx b/frontend/src/Shop/components/Payments.tsx new file mode 100644 index 00000000..c98f1da4 --- /dev/null +++ b/frontend/src/Shop/components/Payments.tsx @@ -0,0 +1,46 @@ +import axios from 'axios'; +import { WindowWithEnv, PaymentDTO } from "./Types"; + +const _window: WindowWithEnv = window; +const backendURL = _window.__ENV && _window.__ENV.backendURL; + +const axiosClient = axios.create({ baseURL: `${backendURL}`, timeout: 20000, withCredentials: true}); +const config = {headers: {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}}; + +/* +* DEVELOPER NOTE: +* logic for all of the payment callback functions that can be called +* by the front end Pi SDK. See https://github.com/pi-apps/pi-platform-docs/blob/master/SDK_reference.md#callbacks-keys +* for more information. +* +* implement your own logic as needed +*/ + +export const onIncompletePaymentFound = (payment: PaymentDTO) => { + console.log("onIncompletePaymentFound", payment); + return axiosClient.post('/payments/incomplete', {payment}); + } + +export const onReadyForServerApproval = (paymentId: string) => { + console.log("onReadyForServerApproval", paymentId); + axiosClient.post('/payments/approve', {paymentId}, config); + } + +export const onReadyForServerCompletion = (paymentId: string, txid: string) => { + console.log("onReadyForServerCompletion", paymentId, txid); + axiosClient.post('/payments/complete', {paymentId, txid}, config); + } + +export const onCancel = (paymentId: string) => { + console.log("onCancel", paymentId); + return axiosClient.post('/payments/cancelled_payment', {paymentId}); + } + +export const onError = (error: Error, payment?: PaymentDTO) => { + console.error("onError", error); + if (payment) { + console.log(payment); + // handle the error accordingly + } + } + diff --git a/frontend/src/Shop/components/ProductCard.tsx b/frontend/src/Shop/components/ProductCard.tsx index db1c31ce..a1dea47e 100644 --- a/frontend/src/Shop/components/ProductCard.tsx +++ b/frontend/src/Shop/components/ProductCard.tsx @@ -1,4 +1,11 @@ -import React from 'react'; +import { Button, Grid } from '@mui/material'; +import Typography from '@mui/joy/Typography'; +import AspectRatio from '@mui/joy/AspectRatio'; + +/* DEVELOPER NOTE: +* the productCard is used to create the standard output of pies +* on the User to App payments page of the app. +*/ interface Props { name: string, @@ -11,24 +18,27 @@ interface Props { export default function ProductCard(props: Props) { return ( -
-
-
- {props.name} -
- -
+ + + + + {props.name} + + +

{props.name}

-

{props.description}

-
-
- -
+

{props.description}

+ + + {props.price} Test-π
- -
- - {props.pictureCaption} -
+ + + + + {props.pictureCaption} + + + ) } \ No newline at end of file diff --git a/frontend/src/Shop/components/Refund.tsx b/frontend/src/Shop/components/Refund.tsx new file mode 100644 index 00000000..38e77128 --- /dev/null +++ b/frontend/src/Shop/components/Refund.tsx @@ -0,0 +1,35 @@ +import { Dialog, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material'; + +/* DEVELOPER NOTE: +* this card displays the alert message that a refund has been processed +*/ + +interface Props { + refundedTransaction: { + message: string, + block_explorer_link: string + }, + showRefundAlert: boolean, + onRefundClose: () => void, +} + +export default function Refund(props: Props) { + return ( + + + + {props.refundedTransaction.message} + + + View on the Pi Block Explorer + + + + + + + ) +} \ No newline at end of file diff --git a/frontend/src/Shop/components/RefundCard.tsx b/frontend/src/Shop/components/RefundCard.tsx new file mode 100644 index 00000000..d1f11999 --- /dev/null +++ b/frontend/src/Shop/components/RefundCard.tsx @@ -0,0 +1,85 @@ +import AspectRatio from '@mui/joy/AspectRatio'; +import { Button, Grid } from '@mui/material'; + +/* DEVELOPER NOTE: +* the RefundCard is used to create the standard output of pies +* on the App to User refunds page of the app. +*/ + +interface Props { + name: string, + description: string, + pictureCaption: string, + pictureURL: string, + amount: number, + variant: boolean, + onClickRefund: () => void, +} + +export default function RefundCard(props: Props) { + + function buttonStyle(){ + if(props.variant){ + return 'outlined'; + } + else{ + return 'contained'; + } + } + + function buttonText(){ + if(props.variant){ + return 'Refund in Progress'; + } + else{ + return 'Refund'; + } + } + + return ( + + { (props.name === 'none')? + + + + + + + App to Pioneer Payment Demonstration +
    +
  1. Sign in above
  2. +
  3. Purchase a pie using test-Pi
  4. +
  5. A refund feature will become available
  6. +
+
+
+
+ : + + + + + {props.name} + + + + {props.name ==="lemon_pie_1" ?

Refund Order: Lemon Meringue Pie

:

Refund Order: Apple Pie

} +

{props.description}

+
+
+ + + + + Eligible Refund: {props.amount} Test-π
+ +
+ + {props.pictureCaption} + +
+
+ } +
+ ) +} \ No newline at end of file diff --git a/frontend/src/Shop/components/SignIn.tsx b/frontend/src/Shop/components/SignIn.tsx index 1286a9b2..daa0f979 100644 --- a/frontend/src/Shop/components/SignIn.tsx +++ b/frontend/src/Shop/components/SignIn.tsx @@ -1,32 +1,30 @@ -import React, { CSSProperties } from 'react'; +import { Dialog, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material'; + +/* DEVELOPER NOTE: +* this card displays the Sign-In alert when a user is not signed in +*/ interface Props { onSignIn: () => void, onModalClose: () => void, -} - -const modalStyle: CSSProperties = { - background: 'white', - position: 'absolute', - left: '15vw', - top: '40%', - width: '70vw', - height: '25vh', - border: '1px solid black', - textAlign: 'center', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center' + showModal: boolean, } export default function SignIn(props: Props) { return ( -
-

You need to sign in first.

-
- - -
-
+ + + + You need to sign in first. + + + + + + + ) } \ No newline at end of file diff --git a/frontend/src/Shop/components/Types.tsx b/frontend/src/Shop/components/Types.tsx new file mode 100644 index 00000000..b3b56f16 --- /dev/null +++ b/frontend/src/Shop/components/Types.tsx @@ -0,0 +1,74 @@ +/* DEVELOPER NOTE: +* this file contains all of the types and interfaces that +* are used by the various components of the front end. +* +* update or add, the types or interfaces your app needs +*/ + +export type AuthResult = { + accessToken: string, + user: { + uid: string, + username: string + } + }; + + export type User = AuthResult['user']; + + export type RefundType = { + _id: string, + pi_payment_id: string, + product_id: string, + user: string, + amount: number, + txid: string, + paid: boolean, + cancelled: boolean, + completed: boolean, + created_at: string, + is_refund: boolean, + refunded_at: string + }; + + export type UserContextType = { + user: { uid: string; username: string; }; + saveUser: () => void; + showModal: boolean; + saveShowModal: (value: boolean) => void; + refunds: RefundType[]; + saveRefunds: () => void; + onModalClose: () => void; + } + + export type MyPaymentMetadata = {}; + + export interface PaymentDTO { + amount: number, + user_uid: string, + created_at: string, + identifier: string, + metadata: Object, + memo: string, + status: { + developer_approved: boolean, + transaction_verified: boolean, + developer_completed: boolean, + cancelled: boolean, + user_cancelled: boolean, + }, + to_address: string, + transaction: null | { + txid: string, + verified: boolean, + _link: string, + }, + }; + + // Make TS accept the existence of our window.__ENV object - defined in index.html: + export interface WindowWithEnv extends Window { + __ENV?: { + backendURL: string, // REACT_APP_BACKEND_URL environment variable + sandbox: "true" | "false", // REACT_APP_SANDBOX_SDK environment variable - string, not boolean! + } + } + \ No newline at end of file diff --git a/frontend/src/Shop/index.tsx b/frontend/src/Shop/index.tsx index da9ece8f..15502219 100644 --- a/frontend/src/Shop/index.tsx +++ b/frontend/src/Shop/index.tsx @@ -1,152 +1,21 @@ -import React, {useState} from 'react'; -import axios from 'axios'; -import ProductCard from './components/ProductCard'; -import SignIn from './components/SignIn'; -import Header from './components/Header'; - -type MyPaymentMetadata = {}; - -type AuthResult = { - accessToken: string, - user: { - uid: string, - username: string - } -}; - -export type User = AuthResult['user']; - -interface PaymentDTO { - amount: number, - user_uid: string, - created_at: string, - identifier: string, - metadata: Object, - memo: string, - status: { - developer_approved: boolean, - transaction_verified: boolean, - developer_completed: boolean, - cancelled: boolean, - user_cancelled: boolean, - }, - to_address: string, - transaction: null | { - txid: string, - verified: boolean, - _link: string, - }, -}; - -// Make TS accept the existence of our window.__ENV object - defined in index.html: -interface WindowWithEnv extends Window { - __ENV?: { - backendURL: string, // REACT_APP_BACKEND_URL environment variable - sandbox: "true" | "false", // REACT_APP_SANDBOX_SDK environment variable - string, not boolean! - } -} - -const _window: WindowWithEnv = window; -const backendURL = _window.__ENV && _window.__ENV.backendURL; - -const axiosClient = axios.create({ baseURL: `${backendURL}`, timeout: 20000, withCredentials: true}); -const config = {headers: {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}}; - +import { Routes, Route, Navigate } from "react-router-dom"; +import HomePage from "./pages" +import UserToAppPayments from "./pages/UserToApp"; +import AppToUserPayments from "./pages/AppToUser"; +import AuthProvider from "./components/Auth"; export default function Shop() { - const [user, setUser] = useState(null); - const [showModal, setShowModal] = useState(false); - - const signIn = async () => { - const scopes = ['username', 'payments']; - const authResult: AuthResult = await window.Pi.authenticate(scopes, onIncompletePaymentFound); - signInUser(authResult); - setUser(authResult.user); - } - - const signOut = () => { - setUser(null); - signOutUser(); - } - - const signInUser = (authResult: AuthResult) => { - axiosClient.post('/user/signin', {authResult}); - return setShowModal(false); - } - - const signOutUser = () => { - return axiosClient.get('/user/signout'); - } - - const onModalClose = () => { - setShowModal(false); - } - - const orderProduct = async (memo: string, amount: number, paymentMetadata: MyPaymentMetadata) => { - if(user === null) { - return setShowModal(true); - } - const paymentData = { amount, memo, metadata: paymentMetadata }; - const callbacks = { - onReadyForServerApproval, - onReadyForServerCompletion, - onCancel, - onError - }; - const payment = await window.Pi.createPayment(paymentData, callbacks); - console.log(payment); - } - - const onIncompletePaymentFound = (payment: PaymentDTO) => { - console.log("onIncompletePaymentFound", payment); - return axiosClient.post('/payments/incomplete', {payment}); - } - - const onReadyForServerApproval = (paymentId: string) => { - console.log("onReadyForServerApproval", paymentId); - axiosClient.post('/payments/approve', {paymentId}, config); - } - - const onReadyForServerCompletion = (paymentId: string, txid: string) => { - console.log("onReadyForServerCompletion", paymentId, txid); - axiosClient.post('/payments/complete', {paymentId, txid}, config); - } - - const onCancel = (paymentId: string) => { - console.log("onCancel", paymentId); - return axiosClient.post('/payments/cancelled_payment', {paymentId}); - } - - const onError = (error: Error, payment?: PaymentDTO) => { - console.log("onError", error); - if (payment) { - console.log(payment); - // handle the error accordingly - } - } return ( <> -
- - orderProduct("Order Apple Pie", 3, { productId: 'apple_pie_1' })} - /> - orderProduct("Order Lemon Meringue Pie", 5, { productId: 'lemon_pie_1' })} - /> - - { showModal && } + + + }/> + } /> + } key="UserToApp" /> + } key="AppToUser" /> + + ); } diff --git a/frontend/src/Shop/pages/AppToUser.tsx b/frontend/src/Shop/pages/AppToUser.tsx new file mode 100644 index 00000000..1c1d769c --- /dev/null +++ b/frontend/src/Shop/pages/AppToUser.tsx @@ -0,0 +1,94 @@ +import RefundCard from "../components/RefundCard"; +import Footer from "../components/Footer"; +import SignIn from "../components/SignIn"; +import Header from "../components/Header"; +import { RefundType, UserContextType, WindowWithEnv } from "../components/Types"; +import { Typography } from "@mui/material"; +import { UserContext } from "../components/Auth"; +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import Refund from "../components/Refund"; + +const _window: WindowWithEnv = window; +const backendURL = _window.__ENV && _window.__ENV.backendURL; +const axiosClient = axios.create({ baseURL: `${backendURL}`, timeout: 35000, withCredentials: true}); + +/* DEVELOPER NOTE: +* this page retrieves and displays all the eligible refunds a user has. +* it will execute the refund when signaled by the user and return an alert when done. +*/ + +export default function AppToUserPayments() { + const { user, saveUser, saveShowModal, showModal, refunds, saveRefunds, onModalClose } = React.useContext(UserContext) as UserContextType; + const [showRefundAlert, setShowRefundAlert] = useState(false); + const [refundInfoState, setRefundInformation] = useState<{message: string, block_explorer_link: string}>({message: "", block_explorer_link: ""}); + const [refundProcessing, setRefundProcessing] = useState(false); + + const orderRefund = async (memo: string, originalPayment: RefundType) => { + if(refundProcessing){ + return console.log("refund already in progress"); + } + + setRefundProcessing(true); + if(user.uid === "") { + setRefundProcessing(false); + return saveShowModal(true); + } + + const refundPaymentID = originalPayment.pi_payment_id; + const refundPaymentAmount = originalPayment.amount; + const paymentData = { memo, refundPaymentID, refundPaymentAmount}; + const returnInfo = await axiosClient.post('/payments/refundable_payment/refund_payment', paymentData); + const refundInformation = returnInfo.data + console.log('refund information: ', refundInformation) + setRefundInformation(refundInformation); + + saveRefunds(); + setShowRefundAlert(true); + setRefundProcessing(false); + } + + useEffect(() => { + saveRefunds(); + // eslint-disable-next-line + },[]); + +return ( + <> +
+ + App to User Payments + + + {(user.uid === '' || refunds[0] === undefined) ? + saveRefunds()} + variant={refundProcessing} + /> + : + refunds.map((order: RefundType) =>{ + return orderRefund("User Requested Refund", order)} + variant={refundProcessing} + /> + }) + } + + { showModal && } + { showRefundAlert && setShowRefundAlert(false)} showRefundAlert={showRefundAlert} /> } + +