+6
-6
app/actions/stripe.ts
+6
-6
app/actions/stripe.ts
···
9
9
import { stripe } from "@/lib/stripe";
10
10
11
11
export async function createCheckoutSession(
12
-
data: FormData,
12
+
data: FormData
13
13
): Promise<{ client_secret: string | null; url: string | null }> {
14
14
const ui_mode = data.get(
15
-
"uiMode",
15
+
"uiMode"
16
16
) as Stripe.Checkout.SessionCreateParams.UiMode;
17
17
18
18
const headersList = await headers();
19
19
const originHeader = headersList.get("origin");
20
20
const hostHeader = headersList.get("host");
21
-
21
+
22
22
let origin: string;
23
23
if (originHeader) {
24
24
origin = originHeader;
···
42
42
},
43
43
unit_amount: formatAmountForStripe(
44
44
Number(data.get("customDonation") as string),
45
-
CURRENCY,
45
+
CURRENCY
46
46
),
47
47
},
48
48
},
···
64
64
}
65
65
66
66
export async function createPaymentIntent(
67
-
data: FormData,
67
+
data: FormData
68
68
): Promise<{ client_secret: string }> {
69
69
const paymentIntent: Stripe.PaymentIntent =
70
70
await stripe.paymentIntents.create({
71
71
amount: formatAmountForStripe(
72
72
Number(data.get("customDonation") as string),
73
-
CURRENCY,
73
+
CURRENCY
74
74
),
75
75
automatic_payment_methods: { enabled: true },
76
76
currency: CURRENCY,
+4
-3
app/components/CheckoutForm.tsx
+4
-3
app/components/CheckoutForm.tsx
···
28
28
const [clientSecret, setClientSecret] = useState<string | null>(null);
29
29
30
30
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
31
-
e,
31
+
e
32
32
): void =>
33
33
setInput({
34
34
...input,
···
37
37
38
38
const formAction = async (data: FormData): Promise<void> => {
39
39
const uiMode = data.get(
40
-
"uiMode",
40
+
"uiMode"
41
41
) as Stripe.Checkout.SessionCreateParams.UiMode;
42
42
const { client_secret, url } = await createCheckoutSession(data);
43
43
···
66
66
type="submit"
67
67
disabled={loading}
68
68
>
69
-
Purchase {formatAmountForDisplay(input.customDonation, config.CURRENCY)}
69
+
Purchase{" "}
70
+
{formatAmountForDisplay(input.customDonation, config.CURRENCY)}
70
71
</button>
71
72
</form>
72
73
{clientSecret ? (
-191
app/components/ElementsForm.tsx
-191
app/components/ElementsForm.tsx
···
1
-
"use client";
2
-
3
-
import type { StripeError } from "@stripe/stripe-js";
4
-
5
-
import * as React from "react";
6
-
import {
7
-
useStripe,
8
-
useElements,
9
-
PaymentElement,
10
-
Elements,
11
-
} from "@stripe/react-stripe-js";
12
-
13
-
import CustomDonationInput from "./CustomDonationInput";
14
-
import TestCards from "./TestCards";
15
-
16
-
import { formatAmountForDisplay } from "@/utils/stripe-helpers";
17
-
import * as config from "@/config";
18
-
import getStripe from "@/utils/get-stripejs";
19
-
import { createPaymentIntent } from "@/actions/stripe";
20
-
21
-
function CheckoutForm(): JSX.Element {
22
-
const [input, setInput] = React.useState<{
23
-
customDonation: number;
24
-
cardholderName: string;
25
-
}>({
26
-
customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
27
-
cardholderName: "",
28
-
});
29
-
const [paymentType, setPaymentType] = React.useState<string>("");
30
-
const [payment, setPayment] = React.useState<{
31
-
status: "initial" | "processing" | "error";
32
-
}>({ status: "initial" });
33
-
const [errorMessage, setErrorMessage] = React.useState<string>("");
34
-
35
-
const stripe = useStripe();
36
-
const elements = useElements();
37
-
38
-
const PaymentStatus = ({ status }: { status: string }) => {
39
-
switch (status) {
40
-
case "processing":
41
-
case "requires_payment_method":
42
-
case "requires_confirmation":
43
-
return <h2>Processing...</h2>;
44
-
45
-
case "requires_action":
46
-
return <h2>Authenticating...</h2>;
47
-
48
-
case "succeeded":
49
-
return <h2>Payment Succeeded 🥳</h2>;
50
-
51
-
case "error":
52
-
return (
53
-
<>
54
-
<h2>Error 😭</h2>
55
-
<p className="error-message">{errorMessage}</p>
56
-
</>
57
-
);
58
-
59
-
default:
60
-
return null;
61
-
}
62
-
};
63
-
64
-
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
65
-
setInput({
66
-
...input,
67
-
[e.currentTarget.name]: e.currentTarget.value,
68
-
});
69
-
70
-
elements?.update({ amount: input.customDonation * 100 });
71
-
};
72
-
73
-
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (e) => {
74
-
try {
75
-
e.preventDefault();
76
-
// Abort if form isn't valid
77
-
if (!e.currentTarget.reportValidity()) return;
78
-
if (!elements || !stripe) return;
79
-
80
-
setPayment({ status: "processing" });
81
-
82
-
const { error: submitError } = await elements.submit();
83
-
84
-
if (submitError) {
85
-
setPayment({ status: "error" });
86
-
setErrorMessage(submitError.message ?? "An unknown error occurred");
87
-
88
-
return;
89
-
}
90
-
91
-
// Create a PaymentIntent with the specified amount.
92
-
const { client_secret: clientSecret } = await createPaymentIntent(
93
-
new FormData(e.target as HTMLFormElement),
94
-
);
95
-
96
-
// Confirm payment with the payment processor
97
-
const { error: confirmError } = await stripe!.confirmPayment({
98
-
elements,
99
-
clientSecret,
100
-
confirmParams: {
101
-
return_url: `${window.location.origin}/donate-with-elements/result`,
102
-
payment_method_data: {
103
-
billing_details: {
104
-
name: input.cardholderName,
105
-
},
106
-
},
107
-
},
108
-
});
109
-
110
-
if (confirmError) {
111
-
setPayment({ status: "error" });
112
-
setErrorMessage(confirmError.message ?? "An unknown error occurred");
113
-
}
114
-
} catch (err) {
115
-
const { message } = err as StripeError;
116
-
117
-
setPayment({ status: "error" });
118
-
setErrorMessage(message ?? "An unknown error occurred");
119
-
}
120
-
};
121
-
122
-
return (
123
-
<>
124
-
<form onSubmit={handleSubmit}>
125
-
<CustomDonationInput
126
-
className="elements-style"
127
-
name="customDonation"
128
-
value={input.customDonation}
129
-
min={config.MIN_AMOUNT}
130
-
max={config.MAX_AMOUNT}
131
-
step={config.AMOUNT_STEP}
132
-
currency={config.CURRENCY}
133
-
onChange={handleInputChange}
134
-
/>
135
-
<TestCards />
136
-
<fieldset className="elements-style">
137
-
<legend>Your payment details:</legend>
138
-
{paymentType === "card" ? (
139
-
<input
140
-
placeholder="Cardholder name"
141
-
className="elements-style"
142
-
type="Text"
143
-
name="cardholderName"
144
-
onChange={handleInputChange}
145
-
required
146
-
/>
147
-
) : null}
148
-
<div className="FormRow elements-style">
149
-
<PaymentElement
150
-
onChange={(e) => {
151
-
setPaymentType(e.value.type);
152
-
}}
153
-
/>
154
-
</div>
155
-
</fieldset>
156
-
<button
157
-
className="elements-style-background"
158
-
type="submit"
159
-
disabled={
160
-
!["initial", "succeeded", "error"].includes(payment.status) ||
161
-
!stripe
162
-
}
163
-
>
164
-
Purchase {formatAmountForDisplay(input.customDonation, config.CURRENCY)}
165
-
</button>
166
-
</form>
167
-
<PaymentStatus status={payment.status} />
168
-
</>
169
-
);
170
-
}
171
-
172
-
export default function ElementsForm(): JSX.Element {
173
-
return (
174
-
<Elements
175
-
stripe={getStripe()}
176
-
options={{
177
-
appearance: {
178
-
variables: {
179
-
colorIcon: "#6772e5",
180
-
fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
181
-
},
182
-
},
183
-
currency: config.CURRENCY,
184
-
mode: "payment",
185
-
amount: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
186
-
}}
187
-
>
188
-
<CheckoutForm />
189
-
</Elements>
190
-
);
191
-
}
-11
app/components/StripeTestCards.tsx
-11
app/components/StripeTestCards.tsx
-5
app/donate-with-elements/page.tsx
-5
app/donate-with-elements/page.tsx
-5
app/donate-with-elements/result/error.tsx
-5
app/donate-with-elements/result/error.tsx
-18
app/donate-with-elements/result/layout.tsx
-18
app/donate-with-elements/result/layout.tsx
···
1
-
import type { Metadata } from "next";
2
-
3
-
export const metadata: Metadata = {
4
-
title: "Payment Intent Result",
5
-
};
6
-
7
-
export default function ResultLayout({
8
-
children,
9
-
}: {
10
-
children: React.ReactNode;
11
-
}): JSX.Element {
12
-
return (
13
-
<div className="page-container">
14
-
<h1>Payment Intent Result</h1>
15
-
{children}
16
-
</div>
17
-
);
18
-
}
-24
app/donate-with-elements/result/page.tsx
-24
app/donate-with-elements/result/page.tsx
···
1
-
import type { Stripe } from "stripe";
2
-
3
-
import PrintObject from "@/components/PrintObject";
4
-
import { stripe } from "@/lib/stripe";
5
-
6
-
export default async function ResultPage({
7
-
searchParams,
8
-
}: {
9
-
searchParams: { payment_intent: string };
10
-
}): Promise<JSX.Element> {
11
-
if (!searchParams.payment_intent)
12
-
throw new Error("Please provide a valid payment_intent (`pi_...`)");
13
-
14
-
const paymentIntent: Stripe.PaymentIntent =
15
-
await stripe.paymentIntents.retrieve(searchParams.payment_intent);
16
-
17
-
return (
18
-
<>
19
-
<h2>Status: {paymentIntent.status}</h2>
20
-
<h3>Payment Intent response:</h3>
21
-
<PrintObject content={paymentIntent} />
22
-
</>
23
-
);
24
-
}
-5
app/donate-with-embedded-checkout/page.tsx
-5
app/donate-with-embedded-checkout/page.tsx
-5
app/donate-with-embedded-checkout/result/error.tsx
-5
app/donate-with-embedded-checkout/result/error.tsx
-18
app/donate-with-embedded-checkout/result/layout.tsx
-18
app/donate-with-embedded-checkout/result/layout.tsx
···
1
-
import type { Metadata } from "next";
2
-
3
-
export const metadata: Metadata = {
4
-
title: "Checkout Session Result",
5
-
};
6
-
7
-
export default function ResultLayout({
8
-
children,
9
-
}: {
10
-
children: React.ReactNode;
11
-
}): JSX.Element {
12
-
return (
13
-
<div className="page-container">
14
-
<h1>Checkout Session Result</h1>
15
-
{children}
16
-
</div>
17
-
);
18
-
}
-28
app/donate-with-embedded-checkout/result/page.tsx
-28
app/donate-with-embedded-checkout/result/page.tsx
···
1
-
import type { Stripe } from "stripe";
2
-
3
-
import PrintObject from "@/components/PrintObject";
4
-
import { stripe } from "@/lib/stripe";
5
-
6
-
export default async function ResultPage({
7
-
searchParams,
8
-
}: {
9
-
searchParams: { session_id: string };
10
-
}): Promise<JSX.Element> {
11
-
if (!searchParams.session_id)
12
-
throw new Error("Please provide a valid session_id (`cs_test_...`)");
13
-
14
-
const checkoutSession: Stripe.Checkout.Session =
15
-
await stripe.checkout.sessions.retrieve(searchParams.session_id, {
16
-
expand: ["line_items", "payment_intent"],
17
-
});
18
-
19
-
const paymentIntent = checkoutSession.payment_intent as Stripe.PaymentIntent;
20
-
21
-
return (
22
-
<>
23
-
<h2>Status: {paymentIntent.status}</h2>
24
-
<h3>Checkout Session response:</h3>
25
-
<PrintObject content={checkoutSession} />
26
-
</>
27
-
);
28
-
}
+3
-3
app/layout.tsx
+3
-3
app/layout.tsx
···
1
1
import type { Metadata } from "next";
2
+
import { SpeedInsights } from "@vercel/speed-insights/next";
2
3
3
4
import "../styles.css";
4
5
···
24
25
<div className="container">
25
26
<header>
26
27
<div className="header-content">
27
-
<h1>
28
-
Easy PDS
29
-
</h1>
28
+
<h1>Easy PDS</h1>
30
29
</div>
31
30
</header>
32
31
{children}
33
32
</div>
33
+
<SpeedInsights />
34
34
</body>
35
35
</html>
36
36
);
+1
-4
app/page.tsx
+1
-4
app/page.tsx
···
10
10
return (
11
11
<ul className="card-list">
12
12
<li>
13
-
<Link
14
-
href="/checkout"
15
-
className="card checkout-style-background"
16
-
>
13
+
<Link href="/checkout" className="card checkout-style-background">
17
14
<h2 className="bottom">Purchase Hosting</h2>
18
15
<img src="/checkout-one-time-payments.svg" />
19
16
</Link>
+3
-3
config/index.ts
+3
-3
config/index.ts
···
1
-
export const CURRENCY = "usd";
1
+
export const CURRENCY = "eur";
2
2
// Set your amount limits: Use float for decimal currencies and
3
3
// Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal.
4
4
export const MIN_AMOUNT = 10.0;
5
-
export const MAX_AMOUNT = 5000.0;
6
-
export const AMOUNT_STEP = 5.0;
5
+
export const MAX_AMOUNT = 100.0;
6
+
export const AMOUNT_STEP = 1.0;
+36
-1
package-lock.json
+36
-1
package-lock.json
···
1
1
{
2
-
"name": "with-stripe-typescript-app",
2
+
"name": "eny-space",
3
3
"lockfileVersion": 3,
4
4
"requires": true,
5
5
"packages": {
···
7
7
"dependencies": {
8
8
"@stripe/react-stripe-js": "2.4.0",
9
9
"@stripe/stripe-js": "2.2.2",
10
+
"@vercel/speed-insights": "^1.3.1",
10
11
"next": "latest",
11
12
"react": "18.2.0",
12
13
"react-dom": "18.2.0",
···
689
690
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
690
691
"dev": true,
691
692
"license": "MIT"
693
+
},
694
+
"node_modules/@vercel/speed-insights": {
695
+
"version": "1.3.1",
696
+
"resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz",
697
+
"integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==",
698
+
"license": "Apache-2.0",
699
+
"peerDependencies": {
700
+
"@sveltejs/kit": "^1 || ^2",
701
+
"next": ">= 13",
702
+
"react": "^18 || ^19 || ^19.0.0-rc",
703
+
"svelte": ">= 4",
704
+
"vue": "^3",
705
+
"vue-router": "^4"
706
+
},
707
+
"peerDependenciesMeta": {
708
+
"@sveltejs/kit": {
709
+
"optional": true
710
+
},
711
+
"next": {
712
+
"optional": true
713
+
},
714
+
"react": {
715
+
"optional": true
716
+
},
717
+
"svelte": {
718
+
"optional": true
719
+
},
720
+
"vue": {
721
+
"optional": true
722
+
},
723
+
"vue-router": {
724
+
"optional": true
725
+
}
726
+
}
692
727
},
693
728
"node_modules/call-bind-apply-helpers": {
694
729
"version": "1.0.2",