Skip to content

Commit

Permalink
feat: Add stripe support
Browse files Browse the repository at this point in the history
  • Loading branch information
RiverTwilight committed Oct 18, 2024
1 parent db1cb95 commit f0aa7bb
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 5 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"@next-auth/prisma-adapter": "^1.0.3",
"@prisma/client": "^4.0.0",
"@react-email/components": "^0.0.22",
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^4.8.0",
"@supabase/ssr": "^0.5.1",
"@supabase/supabase-js": "^2.45.4",
"@svgr/webpack": "^5.5.0",
Expand Down Expand Up @@ -82,6 +84,7 @@
"resend": "^3.2.0",
"sharp": "0.32.6",
"socket.io": "^2.4.0",
"stripe": "^17.2.0",
"test-listen": "^1.1.0",
"ua-parser-js": "^0.7.21",
"uuid": "^8.3.0",
Expand Down
30 changes: 30 additions & 0 deletions src/pages/api/create-payment-intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "POST") {
try {
const { amount } = req.body;

// Create a PaymentIntent with the order amount and currency
const paymentIntent = await stripe.paymentIntents.create({
amount: parseFloat(amount) * 100, // Stripe expects the amount in cents
currency: "cny",
});

res.status(200).json({ clientSecret: paymentIntent.client_secret });
} catch (err: any) {
res.status(500).json({ statusCode: 500, message: err.message });
}
} else {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
}
111 changes: 107 additions & 4 deletions src/pages/donate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";
import {
PaymentElement,
useStripe,
useElements,
} from "@stripe/react-stripe-js";

const FREE_DONATION_WAYS = [
{
Expand Down Expand Up @@ -92,25 +99,28 @@ const FREE_DONATION_WAYS = [
const PAIED_DONATION_WAYS = [
{
title: "请我一杯咖啡",
amount: "¥6.00",
amount: 6,
tag: "¥6.00",
icon: <Coffee />,
href: "",
},
{
title: "请我一杯奶茶",
amount: "¥15.00",
tag: "¥15.00",
amount: 15,
icon: <LocalBar />,
href: "",
},
{
title: "请我一顿饭",
amount: "¥25.00",
tag: "¥25.00",
amount: 25,
icon: <Fastfood />,
href: "",
},
{
title: "自定义金额",
amount: "",
tag: "",
icon: <AttachMoney />,
href: "",
},
Expand Down Expand Up @@ -152,6 +162,10 @@ export const getStaticProps: GetStaticProps = ({ locale = defaultLocale }) => {
};
};

const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

const ProductItem = ({ href, ...props }) => (
<Grid item xs={6} sm={4}>
<OutlinedCard padding={1}>
Expand Down Expand Up @@ -186,8 +200,10 @@ const ProductItem = ({ href, ...props }) => (
</OutlinedCard>
</Grid>
);

const PaidOptionItem = ({ href, ...props }) => {
const [open, setOpen] = useState(false);
const [paymentIntent, setPaymentIntent] = useState(null);

const handleClick = () => {
setOpen(true);
Expand All @@ -197,6 +213,18 @@ const PaidOptionItem = ({ href, ...props }) => {
setOpen(false);
};

const handleStripePayment = async () => {
const response = await fetch("/api/create-payment-intent", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ amount: props.amount }),
});
const data = await response.json();
setPaymentIntent(data.clientSecret);
};

return (
<>
<Grid item xs={6} sm={4}>
Expand Down Expand Up @@ -273,6 +301,30 @@ const PaidOptionItem = ({ href, ...props }) => {
</Box>
</AccordionDetails>
</Accordion>
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Stripe</Typography>
</AccordionSummary>
<AccordionDetails>
{paymentIntent ? (
<Elements
stripe={stripePromise}
options={{ clientSecret: paymentIntent }}
>
<StripePaymentForm />
</Elements>
) : (
<Button
fullWidth
variant="contained"
color="primary"
onClick={handleStripePayment}
>
使用 Stripe 支付
</Button>
)}
</AccordionDetails>
</Accordion>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Expand All @@ -284,6 +336,57 @@ const PaidOptionItem = ({ href, ...props }) => {
);
};

const StripePaymentForm = () => {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);

const handleSubmit = async (event) => {
event.preventDefault();

if (!stripe || !elements) {
return;
}

setProcessing(true);

const result = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/donation-success`,
},
});

if (result.error) {
setError(result.error.message);
}

setProcessing(false);
};

return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={!stripe || processing}
sx={{ mt: 2 }}
>
{processing ? "处理中..." : "支付"}
</Button>
{error && (
<Typography color="error" sx={{ mt: 2 }}>
{error}
</Typography>
)}
</form>
);
};

export default function Donate() {
const { setAction } = useAction();

Expand Down
41 changes: 40 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2554,6 +2554,18 @@
dependencies:
"@sinonjs/commons" "^3.0.0"

"@stripe/react-stripe-js@^2.8.1":
version "2.8.1"
resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-2.8.1.tgz#63d9a666749f818b1bf4eb788eb1bc57eb496f3d"
integrity sha512-C410jVKOATinXLalWotab6E6jlWAlbqUDWL9q1km0p5UHrvnihjjYzA8imYXc4xc4Euf9GeKDQc4n35HKZvgwg==
dependencies:
prop-types "^15.7.2"

"@stripe/stripe-js@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-4.8.0.tgz#ba78f775347cb30e93e13aa408b345de15e3ff80"
integrity sha512-+4Cb0bVHlV4BJXxkJ3cCLSLuWxm3pXKtgcRacox146EuugjCzRRII5T5gUMgL4HpzrBLVwVxjKaZqntNWAXawQ==

"@supabase/[email protected]":
version "2.65.0"
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.65.0.tgz#e345c492f8cbc31cd6289968eae0e349ff0f39e9"
Expand Down Expand Up @@ -2984,6 +2996,13 @@
dependencies:
undici-types "~5.26.4"

"@types/node@>=8.1.0":
version "22.7.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b"
integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==
dependencies:
undici-types "~6.19.2"

"@types/node@^14.14.22":
version "14.18.63"
resolved "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
Expand Down Expand Up @@ -7524,7 +7543,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"

prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.8.1:
prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
Expand Down Expand Up @@ -7581,6 +7600,13 @@ qrcode@^1.4.4:
pngjs "^5.0.0"
yargs "^15.3.1"

qs@^6.11.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
dependencies:
side-channel "^1.0.6"

querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
Expand Down Expand Up @@ -8536,6 +8562,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==

stripe@^17.2.0:
version "17.2.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-17.2.0.tgz#1b70fb60b01e64a2bf64cccb42215a1abcca9d39"
integrity sha512-KuDplY9WrNKi07+uEFeguis/Mh+HC+hfEMy8P5snhQzCXUv01o+4KcIJ9auFxpv4Odp2R2krs9rZ9PhQl8+Sdw==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.11.0"

style-to-object@^0.4.0:
version "0.4.4"
resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec"
Expand Down Expand Up @@ -8962,6 +8996,11 @@ undici-types@~5.26.4:
resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==

unfetch@^4.2.0:
version "4.2.0"
resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
Expand Down

0 comments on commit f0aa7bb

Please sign in to comment.