Bruno Golden Edition
+ ++ For the feature set, please refer to pricing page. +
diff --git a/next.config.js b/next.config.js index 2892feb..280aa2b 100644 --- a/next.config.js +++ b/next.config.js @@ -4,5 +4,8 @@ module.exports = { styledComponents: { pure: true, } + }, + publicRuntimeConfig: { + PAYPAL_ORDERS_API: process.env.NODE_ENV === 'development' ? 'http://localhost:4000/api/paypal/orders' : 'https://payments.usebruno.com/api/paypal/orders' } } diff --git a/package-lock.json b/package-lock.json index e82e99d..caec045 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "bruno-website", "dependencies": { + "@paypal/react-paypal-js": "^8.1.3", "@tabler/icons": "^1.100.0", "@vercel/analytics": "^1.1.0", "feed": "^4.2.2", @@ -22,6 +23,9 @@ "devDependencies": { "eslint": "8.4.1", "eslint-config-next": "12.0.7" + }, + "engines": { + "node": "18.x" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -852,6 +856,35 @@ "node": ">= 8" } }, + "node_modules/@paypal/paypal-js": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-7.1.1.tgz", + "integrity": "sha512-fVpExBrHINGHsyODYKBXSMinxrgPKzmVW7ZCaCqUaMl7kTSR1ZsVi2IhgLvsPpXF4LWSAMkPTP/J6aetQ6ZlNA==", + "dependencies": { + "promise-polyfill": "^8.3.0" + } + }, + "node_modules/@paypal/react-paypal-js": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@paypal/react-paypal-js/-/react-paypal-js-8.1.3.tgz", + "integrity": "sha512-hEm27iYP/UHS3XPBhDdiK2U4PH1FxrOD5O3f9tstAVLJd82l/laCjq751HiESSm63PVOoFeKE41Fe1mYGab+oA==", + "dependencies": { + "@paypal/paypal-js": "^7.0.0", + "@paypal/sdk-constants": "^1.0.122" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@paypal/sdk-constants": { + "version": "1.0.136", + "resolved": "https://registry.npmjs.org/@paypal/sdk-constants/-/sdk-constants-1.0.136.tgz", + "integrity": "sha512-nDYZ4RUqHeT12LH3xK1be0156abo/0yO51u+7wModqck6VNG6MYxhgPZyzQSYXOwA7vrSue2Juosht4WPtb/Nw==", + "dependencies": { + "hi-base32": "^0.5.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", @@ -3017,6 +3050,11 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -4227,6 +4265,11 @@ "node": ">=0.4.0" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6066,6 +6109,31 @@ "fastq": "^1.6.0" } }, + "@paypal/paypal-js": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@paypal/paypal-js/-/paypal-js-7.1.1.tgz", + "integrity": "sha512-fVpExBrHINGHsyODYKBXSMinxrgPKzmVW7ZCaCqUaMl7kTSR1ZsVi2IhgLvsPpXF4LWSAMkPTP/J6aetQ6ZlNA==", + "requires": { + "promise-polyfill": "^8.3.0" + } + }, + "@paypal/react-paypal-js": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@paypal/react-paypal-js/-/react-paypal-js-8.1.3.tgz", + "integrity": "sha512-hEm27iYP/UHS3XPBhDdiK2U4PH1FxrOD5O3f9tstAVLJd82l/laCjq751HiESSm63PVOoFeKE41Fe1mYGab+oA==", + "requires": { + "@paypal/paypal-js": "^7.0.0", + "@paypal/sdk-constants": "^1.0.122" + } + }, + "@paypal/sdk-constants": { + "version": "1.0.136", + "resolved": "https://registry.npmjs.org/@paypal/sdk-constants/-/sdk-constants-1.0.136.tgz", + "integrity": "sha512-nDYZ4RUqHeT12LH3xK1be0156abo/0yO51u+7wModqck6VNG6MYxhgPZyzQSYXOwA7vrSue2Juosht4WPtb/Nw==", + "requires": { + "hi-base32": "^0.5.0" + } + }, "@rushstack/eslint-patch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", @@ -7648,6 +7716,11 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==" + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -8497,6 +8570,11 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 9255ed3..ad4a68d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "next lint" }, "dependencies": { + "@paypal/react-paypal-js": "^8.1.3", "@tabler/icons": "^1.100.0", "@vercel/analytics": "^1.1.0", "feed": "^4.2.2", diff --git a/src/components/PaypalCheckout/index.js b/src/components/PaypalCheckout/index.js new file mode 100644 index 0000000..01f7046 --- /dev/null +++ b/src/components/PaypalCheckout/index.js @@ -0,0 +1,114 @@ +import { useState } from "react"; +import { PayPalButtons } from "@paypal/react-paypal-js"; +import getConfig from 'next/config'; +import { IconRefresh } from '@tabler/icons'; + +const { publicRuntimeConfig } = getConfig(); + +const PaypalCheckout = ({ email, onSuccess, onError }) => { + const [loading, setLoading] = useState(false); + + const handleCreateOrder = async () => { + try { + setLoading(true); + const response = await fetch(publicRuntimeConfig.PAYPAL_ORDERS_API, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email + }), + }); + + const orderData = await response.json(); + setLoading(false); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + onError(error); + setLoading(false); + throw error; + } + }; + + const handleApproveOrder = async (data, actions) => { + try { + setLoading(true); + const response = await fetch(`${publicRuntimeConfig.PAYPAL_ORDERS_API}/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + setLoading(false); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0]; + + if (errorDetail?.issue === "INSTRUMENT_DECLINED") { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error( + `${errorDetail.description} (${orderData.debug_id})`, + ); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + onSuccess(orderData); + } + } catch (error) { + setLoading(false); + onError(error); + } + }; + + return ( + <> +