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 ( + <> +
+
Order Summary
+ {loading && } +
+
+
+ Product: Golden Edition Individual License +
+
+ Email: anoop@usebruno.com +
+
+ Amount: $9 +
+
+
+ +
+ + ); +}; + +export default PaypalCheckout; \ No newline at end of file diff --git a/src/globalStyles.js b/src/globalStyles.js index b685f0e..711fe00 100644 --- a/src/globalStyles.js +++ b/src/globalStyles.js @@ -8,6 +8,24 @@ import { createGlobalStyle } from 'styled-components'; */ const GlobalStyle = createGlobalStyle` + @keyframes rotateClockwise { + 0% { + transform: scaleY(-1) rotate(0deg); + } + 100% { + transform: scaleY(-1) rotate(360deg); + } + } + + @keyframes rotateCounterClockwise { + 0% { + transform: scaleY(-1) rotate(360deg); + } + 100% { + transform: scaleY(-1) rotate(0deg); + } + } + html, body { margin: 0; padding: 0; @@ -122,6 +140,13 @@ const GlobalStyle = createGlobalStyle` } } } + + div.buy-golden-edition-page { + .loading-icon { + transform: scaleY(-1); + animation: rotateCounterClockwise 1s linear infinite; + } + } `; export default GlobalStyle; \ No newline at end of file diff --git a/src/pages/_app.js b/src/pages/_app.js index b354342..9f8f92e 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -2,12 +2,25 @@ import { ThemeProvider } from 'styled-components'; import '../styles/globals.css'; import '../styles/markdown.css'; +import { PayPalScriptProvider } from "@paypal/react-paypal-js"; + +const PAYPAL_CLIENT_ID = 'ATDXVIF6j9IH_LFiDp1aPI-5sxmUtO3DL_GbmhBgHC9i2jFe8Af95z3huPvyAUZwuhLADABGBiqPgq2r'; + import theme from '../themes/default'; function MyApp({ Component, pageProps }) { + const paypalOptions = { + "client-id": PAYPAL_CLIENT_ID, + "enable-funding": "card", + "disable-funding": "paylater,venmo", + "data-sdk-integration-source": "integrationbuilder_sc", + }; + return ( - + + + ) } diff --git a/src/pages/_document.js b/src/pages/_document.js index ba2933f..682d249 100644 --- a/src/pages/_document.js +++ b/src/pages/_document.js @@ -1,6 +1,5 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheet } from 'styled-components'; - export default class MyDocument extends Document { static async getInitialProps(ctx) { const sheet = new ServerStyleSheet(); diff --git a/src/pages/buy-golden-edition.js b/src/pages/buy-golden-edition.js new file mode 100644 index 0000000..af4eb59 --- /dev/null +++ b/src/pages/buy-golden-edition.js @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import Head from 'next/head'; +import Bruno from 'components/Bruno'; +import Navbar from 'components/Navbar'; +import PaypalCheckout from 'components/PaypalCheckout'; +import GlobalStyle from '../globalStyles'; + +const HeartIcon = () => { + return + + ; +}; + +const validateEmail = (input) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(input); +} + +export default function BuyGoldenEdition() { + const [email, setEmail] = useState(''); + const [emailReady, setEmailReady] = useState(false); + const [emailError, setEmailError] = useState(''); + const [error, setError] = useState(null); + const [purchaseSuccess, setPurchaseSuccess] = useState(false); + + const handleContinue = () => { + const isEmailValid = validateEmail(email); + if (!isEmailValid) { + setEmailError(true); + } else { + setEmailError(false); + setEmailReady(true); + } + }; + + const handleError = (error) => { + setError('Something went wrong. Please try again later.'); + }; + + const handleSuccess = (data) => { + setPurchaseSuccess(true); + }; + + return ( +
+ + Buy Golden Edition + + + + +
+ + +
+
+ +
+

Bruno Golden Edition

+ +
+ You can Pre-Order Golden Edition today at $19 $9 ! +
+ +
+ We expect Golden Edition to be available by 1st March 2024.
+ For the feature set, please refer to pricing page. +
+ +
+ Thank you for your support! +
+ + {!purchaseSuccess && !emailReady && ( +
+ + setEmail(e.target.value)} + /> + {emailError && + Please enter a valid email address. + } + +
+ )} + + {!purchaseSuccess && emailReady && ( +
+ +
+ )} + + {purchaseSuccess && ( +
+
Thank you for your purchase!
+
We will send you the license details via email when Golden Edition is ready.
+
+ )} + + {error &&
{error}
} +
+
+
+ ) +}; \ No newline at end of file diff --git a/src/pages/pricing.js b/src/pages/pricing.js index 55031fb..dfa02cb 100644 --- a/src/pages/pricing.js +++ b/src/pages/pricing.js @@ -45,8 +45,6 @@ export default function Pricing() { 'Custom Themes' ]; - - const goldenEditonOrganizations = [ 'Centralized License Management', 'Intergration with Secret Managers',