Skip to content

Commit

Permalink
feat: paypal payment integration
Browse files Browse the repository at this point in the history
  • Loading branch information
helloanoop committed Jan 21, 2024
1 parent c7b7b69 commit 60c9a03
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 4 deletions.
3 changes: 3 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
78 changes: 78 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
114 changes: 114 additions & 0 deletions src/components/PaypalCheckout/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="flex items-center">
<div className="font-semibold text-lg mr-1">Order Summary</div>
{loading && <IconRefresh size={18} className="loading-icon" />}
</div>
<div className="mt-2 mb-4">
<div className='flex mb-1'>
<span className='mr-1' style={{width: '65px'}}>Product</span>: Golden Edition Individual License
</div>
<div className='flex mb-1'>
<span className='mr-1' style={{width: '65px'}}>Email</span>: [email protected]
</div>
<div className='flex'>
<span className='mr-1' style={{width: '65px'}}>Amount</span>: $9
</div>
</div>
<div style={{maxWidth: '300px'}}>
<PayPalButtons
style={{
shape: "rect",
layout: "vertical",
}}
createOrder={handleCreateOrder}
onApprove={handleApproveOrder}
/>
</div>
</>
);
};

export default PaypalCheckout;
25 changes: 25 additions & 0 deletions src/globalStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
15 changes: 14 additions & 1 deletion src/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<PayPalScriptProvider options={paypalOptions}>
<Component {...pageProps} />
</PayPalScriptProvider>
</ThemeProvider>
)
}
Expand Down
1 change: 0 additions & 1 deletion src/pages/_document.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Loading

0 comments on commit 60c9a03

Please sign in to comment.