+7
.env.local.example
+7
.env.local.example
+101
-1
README.md
+101
-1
README.md
···
1
-
# eny.space
1
+
# Easy PDS
2
+
3
+
A full-stack TypeScript application using Next.js for processing donations.
4
+
5
+
## Features
6
+
7
+
- **Checkout** - Custom amount donations with hosted and embedded checkout
8
+
- **Payment Elements** - Custom payment form with Payment Element
9
+
- **Webhook handling** - Server-side webhook processing for payment events
10
+
11
+
## Tech Stack
12
+
13
+
- **Frontend**: Next.js, React, TypeScript
14
+
- **Backend**: Next.js Server Actions and Route Handlers
15
+
16
+
## Getting Started
17
+
18
+
### Prerequisites
19
+
20
+
- Node.js 18+ installed
21
+
- A payment processor account
22
+
23
+
### Installation
24
+
25
+
1. Clone the repository and install dependencies:
26
+
27
+
```bash
28
+
npm install
29
+
# or
30
+
yarn install
31
+
# or
32
+
pnpm install
33
+
```
34
+
35
+
2. Set up environment variables:
36
+
37
+
Create a `.env.local` file in the root directory:
38
+
39
+
```bash
40
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key
41
+
STRIPE_SECRET_KEY=your_secret_key
42
+
STRIPE_WEBHOOK_SECRET=your_webhook_secret
43
+
```
44
+
45
+
Get your API keys from your payment processor dashboard.
46
+
47
+
3. Start the development server:
48
+
49
+
```bash
50
+
npm run dev
51
+
# or
52
+
yarn dev
53
+
# or
54
+
pnpm dev
55
+
```
56
+
57
+
The application will be available at `http://localhost:3000`.
58
+
59
+
### Webhook Setup
60
+
61
+
#### Local Development
62
+
63
+
1. Install the payment processor CLI and link your account.
64
+
65
+
2. Start webhook forwarding to your local server:
66
+
67
+
```bash
68
+
# Example command - adjust based on your payment processor
69
+
webhook listen --forward-to localhost:3000/api/webhooks
70
+
```
71
+
72
+
3. Copy the webhook secret from the CLI output and add it to your `.env.local` file.
73
+
74
+
#### Production
75
+
76
+
1. Deploy your application and copy the webhook URL (e.g., `https://your-domain.com/api/webhooks`).
77
+
78
+
2. Create a webhook endpoint in your payment processor dashboard.
79
+
80
+
3. Add the webhook signing secret to your production environment variables.
81
+
82
+
## Testing
83
+
84
+
Use test cards for testing payments. Common test cards:
85
+
- `4242 4242 4242 4242` - Successful payment
86
+
- `4000 0027 6000 3184` - 3D Secure authentication required
87
+
88
+
## Deployment
89
+
90
+
This application can be deployed to any platform that supports Next.js, such as [Vercel](https://vercel.com), Netlify, or your own infrastructure.
91
+
92
+
Make sure to set all required environment variables in your deployment platform.
93
+
94
+
## Project Structure
95
+
96
+
- `app/` - Next.js app directory with pages and components
97
+
- `app/actions/` - Server actions for payment operations
98
+
- `app/api/webhooks/` - Webhook handler route
99
+
- `lib/` - Payment processor client configuration
100
+
- `components/` - React components for payment forms
101
+
- `utils/` - Utility functions
+80
app/actions/stripe.ts
+80
app/actions/stripe.ts
···
1
+
"use server";
2
+
3
+
import type { Stripe } from "stripe";
4
+
5
+
import { headers } from "next/headers";
6
+
7
+
import { CURRENCY } from "@/config";
8
+
import { formatAmountForStripe } from "@/utils/stripe-helpers";
9
+
import { stripe } from "@/lib/stripe";
10
+
11
+
export async function createCheckoutSession(
12
+
data: FormData,
13
+
): Promise<{ client_secret: string | null; url: string | null }> {
14
+
const ui_mode = data.get(
15
+
"uiMode",
16
+
) as Stripe.Checkout.SessionCreateParams.UiMode;
17
+
18
+
const headersList = await headers();
19
+
const originHeader = headersList.get("origin");
20
+
const hostHeader = headersList.get("host");
21
+
22
+
let origin: string;
23
+
if (originHeader) {
24
+
origin = originHeader;
25
+
} else if (hostHeader) {
26
+
origin = `https://${hostHeader}`;
27
+
} else {
28
+
origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
29
+
}
30
+
31
+
const checkoutSession: Stripe.Checkout.Session =
32
+
await stripe.checkout.sessions.create({
33
+
mode: "payment",
34
+
submit_type: "donate",
35
+
line_items: [
36
+
{
37
+
quantity: 1,
38
+
price_data: {
39
+
currency: CURRENCY,
40
+
product_data: {
41
+
name: "Custom amount donation",
42
+
},
43
+
unit_amount: formatAmountForStripe(
44
+
Number(data.get("customDonation") as string),
45
+
CURRENCY,
46
+
),
47
+
},
48
+
},
49
+
],
50
+
...(ui_mode === "hosted" && {
51
+
success_url: `${origin}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
52
+
cancel_url: `${origin}/donate-with-checkout`,
53
+
}),
54
+
...(ui_mode === "embedded" && {
55
+
return_url: `${origin}/donate-with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
56
+
}),
57
+
ui_mode,
58
+
});
59
+
60
+
return {
61
+
client_secret: checkoutSession.client_secret,
62
+
url: checkoutSession.url,
63
+
};
64
+
}
65
+
66
+
export async function createPaymentIntent(
67
+
data: FormData,
68
+
): Promise<{ client_secret: string }> {
69
+
const paymentIntent: Stripe.PaymentIntent =
70
+
await stripe.paymentIntents.create({
71
+
amount: formatAmountForStripe(
72
+
Number(data.get("customDonation") as string),
73
+
CURRENCY,
74
+
),
75
+
automatic_payment_methods: { enabled: true },
76
+
currency: CURRENCY,
77
+
});
78
+
79
+
return { client_secret: paymentIntent.client_secret as string };
80
+
}
+66
app/api/webhooks/route.ts
+66
app/api/webhooks/route.ts
···
1
+
import type { Stripe } from "stripe";
2
+
3
+
import { NextResponse } from "next/server";
4
+
5
+
import { stripe } from "@/lib/stripe";
6
+
7
+
export async function POST(req: Request) {
8
+
let event: Stripe.Event;
9
+
10
+
try {
11
+
event = stripe.webhooks.constructEvent(
12
+
await (await req.blob()).text(),
13
+
req.headers.get("stripe-signature") as string,
14
+
process.env.STRIPE_WEBHOOK_SECRET as string,
15
+
);
16
+
} catch (err) {
17
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
18
+
// On error, log and return the error message.
19
+
if (!(err instanceof Error)) console.log(err);
20
+
console.log(`❌ Error message: ${errorMessage}`);
21
+
return NextResponse.json(
22
+
{ message: `Webhook Error: ${errorMessage}` },
23
+
{ status: 400 },
24
+
);
25
+
}
26
+
27
+
// Successfully constructed event.
28
+
console.log("✅ Success:", event.id);
29
+
30
+
const permittedEvents: string[] = [
31
+
"checkout.session.completed",
32
+
"payment_intent.succeeded",
33
+
"payment_intent.payment_failed",
34
+
];
35
+
36
+
if (permittedEvents.includes(event.type)) {
37
+
let data;
38
+
39
+
try {
40
+
switch (event.type) {
41
+
case "checkout.session.completed":
42
+
data = event.data.object as Stripe.Checkout.Session;
43
+
console.log(`💰 CheckoutSession status: ${data.payment_status}`);
44
+
break;
45
+
case "payment_intent.payment_failed":
46
+
data = event.data.object as Stripe.PaymentIntent;
47
+
console.log(`❌ Payment failed: ${data.last_payment_error?.message}`);
48
+
break;
49
+
case "payment_intent.succeeded":
50
+
data = event.data.object as Stripe.PaymentIntent;
51
+
console.log(`💰 PaymentIntent status: ${data.status}`);
52
+
break;
53
+
default:
54
+
throw new Error(`Unhandled event: ${event.type}`);
55
+
}
56
+
} catch (error) {
57
+
console.log(error);
58
+
return NextResponse.json(
59
+
{ message: "Webhook handler failed" },
60
+
{ status: 500 },
61
+
);
62
+
}
63
+
}
64
+
// Return a response to acknowledge receipt of the event.
65
+
return NextResponse.json({ message: "Received" }, { status: 200 });
66
+
}
+82
app/components/CheckoutForm.tsx
+82
app/components/CheckoutForm.tsx
···
1
+
"use client";
2
+
3
+
import type Stripe from "stripe";
4
+
5
+
import React, { useState } from "react";
6
+
7
+
import CustomDonationInput from "@/components/CustomDonationInput";
8
+
import TestCards from "@/components/TestCards";
9
+
10
+
import { formatAmountForDisplay } from "@/utils/stripe-helpers";
11
+
import * as config from "@/config";
12
+
import { createCheckoutSession } from "@/actions/stripe";
13
+
import getStripe from "@/utils/get-stripejs";
14
+
import {
15
+
EmbeddedCheckout,
16
+
EmbeddedCheckoutProvider,
17
+
} from "@stripe/react-stripe-js";
18
+
19
+
interface CheckoutFormProps {
20
+
uiMode: Stripe.Checkout.SessionCreateParams.UiMode;
21
+
}
22
+
23
+
export default function CheckoutForm(props: CheckoutFormProps): JSX.Element {
24
+
const [loading] = useState<boolean>(false);
25
+
const [input, setInput] = useState<{ customDonation: number }>({
26
+
customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
27
+
});
28
+
const [clientSecret, setClientSecret] = useState<string | null>(null);
29
+
30
+
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
31
+
e,
32
+
): void =>
33
+
setInput({
34
+
...input,
35
+
[e.currentTarget.name]: e.currentTarget.value,
36
+
});
37
+
38
+
const formAction = async (data: FormData): Promise<void> => {
39
+
const uiMode = data.get(
40
+
"uiMode",
41
+
) as Stripe.Checkout.SessionCreateParams.UiMode;
42
+
const { client_secret, url } = await createCheckoutSession(data);
43
+
44
+
if (uiMode === "embedded") return setClientSecret(client_secret);
45
+
46
+
window.location.assign(url as string);
47
+
};
48
+
49
+
return (
50
+
<>
51
+
<form action={formAction}>
52
+
<input type="hidden" name="uiMode" value={props.uiMode} />
53
+
<CustomDonationInput
54
+
className="checkout-style"
55
+
name="customDonation"
56
+
min={config.MIN_AMOUNT}
57
+
max={config.MAX_AMOUNT}
58
+
step={config.AMOUNT_STEP}
59
+
currency={config.CURRENCY}
60
+
onChange={handleInputChange}
61
+
value={input.customDonation}
62
+
/>
63
+
<TestCards />
64
+
<button
65
+
className="checkout-style-background"
66
+
type="submit"
67
+
disabled={loading}
68
+
>
69
+
Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)}
70
+
</button>
71
+
</form>
72
+
{clientSecret ? (
73
+
<EmbeddedCheckoutProvider
74
+
stripe={getStripe()}
75
+
options={{ clientSecret }}
76
+
>
77
+
<EmbeddedCheckout />
78
+
</EmbeddedCheckoutProvider>
79
+
) : null}
80
+
</>
81
+
);
82
+
}
+38
app/components/CustomDonationInput.tsx
+38
app/components/CustomDonationInput.tsx
···
1
+
import { formatAmountForDisplay } from "@/utils/stripe-helpers";
2
+
3
+
export default function CustomDonationInput({
4
+
name,
5
+
min,
6
+
max,
7
+
currency,
8
+
step,
9
+
onChange,
10
+
value,
11
+
className,
12
+
}: {
13
+
name: string;
14
+
min: number;
15
+
max: number;
16
+
currency: string;
17
+
step: number;
18
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
19
+
value: number;
20
+
className?: string;
21
+
}): JSX.Element {
22
+
return (
23
+
<label>
24
+
Custom donation amount ({formatAmountForDisplay(min, currency)}-
25
+
{formatAmountForDisplay(max, currency)}):
26
+
<input
27
+
type="range"
28
+
name={name}
29
+
min={min}
30
+
max={max}
31
+
step={step}
32
+
onChange={onChange}
33
+
value={value}
34
+
className={className}
35
+
></input>
36
+
</label>
37
+
);
38
+
}
+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
+
Donate {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
+
}
+10
app/components/PrintObject.tsx
+10
app/components/PrintObject.tsx
···
1
+
import type { Stripe } from "stripe";
2
+
3
+
export default function PrintObject({
4
+
content,
5
+
}: {
6
+
content: Stripe.PaymentIntent | Stripe.Checkout.Session;
7
+
}): JSX.Element {
8
+
const formattedContent: string = JSON.stringify(content, null, 2);
9
+
return <pre>{formattedContent}</pre>;
10
+
}
+11
app/components/StripeTestCards.tsx
+11
app/components/StripeTestCards.tsx
+11
app/components/TestCards.tsx
+11
app/components/TestCards.tsx
+17
app/donate-with-checkout/page.tsx
+17
app/donate-with-checkout/page.tsx
···
1
+
import type { Metadata } from "next";
2
+
3
+
import CheckoutForm from "@/components/CheckoutForm";
4
+
5
+
export const metadata: Metadata = {
6
+
title: "Donate",
7
+
};
8
+
9
+
export default function DonatePage(): JSX.Element {
10
+
return (
11
+
<div className="page-container">
12
+
<h1>Donate</h1>
13
+
<p>Donate to our project 💖</p>
14
+
<CheckoutForm uiMode="hosted" />
15
+
</div>
16
+
);
17
+
}
+5
app/donate-with-checkout/result/error.tsx
+5
app/donate-with-checkout/result/error.tsx
+18
app/donate-with-checkout/result/layout.tsx
+18
app/donate-with-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-checkout/result/page.tsx
+28
app/donate-with-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
+
}
+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
+
}
+37
app/layout.tsx
+37
app/layout.tsx
···
1
+
import type { Metadata } from "next";
2
+
3
+
import "../styles.css";
4
+
5
+
interface LayoutProps {
6
+
children: React.ReactNode;
7
+
}
8
+
9
+
export const metadata: Metadata = {
10
+
title: {
11
+
default: "Easy PDS",
12
+
template: "%s | Easy PDS",
13
+
},
14
+
twitter: {
15
+
card: "summary_large_image",
16
+
description: "Easy PDS - Secure donation platform.",
17
+
},
18
+
};
19
+
20
+
export default function RootLayout({ children }: LayoutProps) {
21
+
return (
22
+
<html lang="en">
23
+
<body>
24
+
<div className="container">
25
+
<header>
26
+
<div className="header-content">
27
+
<h1>
28
+
Easy PDS
29
+
</h1>
30
+
</div>
31
+
</header>
32
+
{children}
33
+
</div>
34
+
</body>
35
+
</html>
36
+
);
37
+
}
+23
app/page.tsx
+23
app/page.tsx
···
1
+
import type { Metadata } from "next";
2
+
3
+
import Link from "next/link";
4
+
5
+
export const metadata: Metadata = {
6
+
title: "Home",
7
+
};
8
+
9
+
export default function IndexPage(): JSX.Element {
10
+
return (
11
+
<ul className="card-list">
12
+
<li>
13
+
<Link
14
+
href="/donate-with-checkout"
15
+
className="card checkout-style-background"
16
+
>
17
+
<h2 className="bottom">Donate</h2>
18
+
<img src="/checkout-one-time-payments.svg" />
19
+
</Link>
20
+
</li>
21
+
</ul>
22
+
);
23
+
}
-32
components/Layout.tsx
-32
components/Layout.tsx
···
1
-
import React, { ReactNode } from "react";
2
-
import Link from "next/link";
3
-
import Head from "next/head";
4
-
5
-
type Props = {
6
-
children?: ReactNode;
7
-
title?: string;
8
-
};
9
-
10
-
const Layout = ({ children, title = "This is the default title" }: Props) => (
11
-
<div>
12
-
<Head>
13
-
<title>{title}</title>
14
-
<meta charSet="utf-8" />
15
-
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
16
-
</Head>
17
-
<header>
18
-
<nav>
19
-
<Link href="/">Home</Link> | <Link href="/about">About</Link> |{" "}
20
-
<Link href="/users">Users List</Link> |{" "}
21
-
<a href="/api/users">Users API</a>
22
-
</nav>
23
-
</header>
24
-
{children}
25
-
<footer>
26
-
<hr />
27
-
<span>I'm here to stay (Footer)</span>
28
-
</footer>
29
-
</div>
30
-
);
31
-
32
-
export default Layout;
-19
components/List.tsx
-19
components/List.tsx
···
1
-
import * as React from "react";
2
-
import ListItem from "./ListItem";
3
-
import { User } from "../interfaces";
4
-
5
-
type Props = {
6
-
items: User[];
7
-
};
8
-
9
-
const List = ({ items }: Props) => (
10
-
<ul>
11
-
{items.map((item) => (
12
-
<li key={item.id}>
13
-
<ListItem data={item} />
14
-
</li>
15
-
))}
16
-
</ul>
17
-
);
18
-
19
-
export default List;
-16
components/ListDetail.tsx
-16
components/ListDetail.tsx
···
1
-
import * as React from "react";
2
-
3
-
import { User } from "../interfaces";
4
-
5
-
type ListDetailProps = {
6
-
item: User;
7
-
};
8
-
9
-
const ListDetail = ({ item: user }: ListDetailProps) => (
10
-
<div>
11
-
<h1>Detail for {user.name}</h1>
12
-
<p>ID: {user.id}</p>
13
-
</div>
14
-
);
15
-
16
-
export default ListDetail;
-16
components/ListItem.tsx
-16
components/ListItem.tsx
···
1
-
import React from "react";
2
-
import Link from "next/link";
3
-
4
-
import { User } from "../interfaces";
5
-
6
-
type Props = {
7
-
data: User;
8
-
};
9
-
10
-
const ListItem = ({ data }: Props) => (
11
-
<Link href="/users/[id]" as={`/users/${data.id}`}>
12
-
{data.id}:{data.name}
13
-
</Link>
14
-
);
15
-
16
-
export default ListItem;
+6
config/index.ts
+6
config/index.ts
···
1
+
export const CURRENCY = "usd";
2
+
// Set your amount limits: Use float for decimal currencies and
3
+
// Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal.
4
+
export const MIN_AMOUNT = 10.0;
5
+
export const MAX_AMOUNT = 5000.0;
6
+
export const AMOUNT_STEP = 5.0;
-10
interfaces/index.ts
-10
interfaces/index.ts
···
1
-
// You can include shared interfaces/types in a separate file
2
-
// and then use them in any component by importing them. For
3
-
// example, to import the interface below do:
4
-
//
5
-
// import { User } from 'path/to/interfaces';
6
-
7
-
export type User = {
8
-
id: number;
9
-
name: string;
10
-
};
+8
lib/stripe.ts
+8
lib/stripe.ts
+1
next.config.js
+1
next.config.js
···
1
+
module.exports = {};
+357
-47
package-lock.json
+357
-47
package-lock.json
···
1
1
{
2
-
"name": "eny-space",
2
+
"name": "with-stripe-typescript-app",
3
3
"lockfileVersion": 3,
4
4
"requires": true,
5
5
"packages": {
6
6
"": {
7
7
"dependencies": {
8
-
"@stripe/stripe-js": "^8.6.0",
8
+
"@stripe/react-stripe-js": "2.4.0",
9
+
"@stripe/stripe-js": "2.2.2",
9
10
"next": "latest",
10
-
"react": "^18.2.0",
11
-
"react-dom": "^18.2.0"
11
+
"react": "18.2.0",
12
+
"react-dom": "18.2.0",
13
+
"server-only": "0.0.1",
14
+
"stripe": "14.8.0"
12
15
},
13
16
"devDependencies": {
14
-
"@types/node": "^12.12.21",
15
-
"@types/react": "^17.0.2",
16
-
"@types/react-dom": "^17.0.1",
17
-
"typescript": "^4.8.3"
17
+
"@types/node": "20.4.6",
18
+
"@types/react": "18.2.8",
19
+
"typescript": "5.1.6"
18
20
}
19
21
},
20
22
"node_modules/@emnapi/runtime": {
···
627
629
"node": ">= 10"
628
630
}
629
631
},
630
-
"node_modules/@stripe/stripe-js": {
631
-
"version": "8.6.0",
632
-
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz",
633
-
"integrity": "sha512-EB0/GGgs4hfezzkiMkinlRgWtjz8fSdwVQhwYS7Sg/RQrSvuNOz+ssPjD+lAzqaYTCB0zlbrt0fcqVziLJrufQ==",
632
+
"node_modules/@stripe/react-stripe-js": {
633
+
"version": "2.4.0",
634
+
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.4.0.tgz",
635
+
"integrity": "sha512-1jVQEL3OuhuzNlf4OdfqovHt+MkWh8Uh8xpLxx/xUFUDdF+7/kDOrGKy+xJO3WLCfZUL7NAy+/ypwXbbYZi0tg==",
634
636
"license": "MIT",
635
-
"engines": {
636
-
"node": ">=12.16"
637
+
"dependencies": {
638
+
"prop-types": "^15.7.2"
639
+
},
640
+
"peerDependencies": {
641
+
"@stripe/stripe-js": "^1.44.1 || ^2.0.0",
642
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
643
+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
637
644
}
645
+
},
646
+
"node_modules/@stripe/stripe-js": {
647
+
"version": "2.2.2",
648
+
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.2.2.tgz",
649
+
"integrity": "sha512-LvFZRZEBoMe6vXC6RoOAIbXWo/0JDdndq43ekL9M6affcM7PtF5KALmwt91BazW7q49sbSl0l7TunWhhSwEW4w==",
650
+
"license": "MIT"
638
651
},
639
652
"node_modules/@swc/helpers": {
640
653
"version": "0.5.15",
···
646
659
}
647
660
},
648
661
"node_modules/@types/node": {
649
-
"version": "12.20.55",
650
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
651
-
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
652
-
"dev": true,
662
+
"version": "20.4.6",
663
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz",
664
+
"integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==",
653
665
"license": "MIT"
654
666
},
655
667
"node_modules/@types/prop-types": {
···
660
672
"license": "MIT"
661
673
},
662
674
"node_modules/@types/react": {
663
-
"version": "17.0.90",
664
-
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz",
665
-
"integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==",
675
+
"version": "18.2.8",
676
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz",
677
+
"integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==",
666
678
"dev": true,
667
679
"license": "MIT",
668
680
"dependencies": {
669
681
"@types/prop-types": "*",
670
-
"@types/scheduler": "^0.16",
671
-
"csstype": "^3.2.2"
682
+
"@types/scheduler": "*",
683
+
"csstype": "^3.0.2"
672
684
}
673
685
},
674
-
"node_modules/@types/react-dom": {
675
-
"version": "17.0.26",
676
-
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.26.tgz",
677
-
"integrity": "sha512-Z+2VcYXJwOqQ79HreLU/1fyQ88eXSSFh6I3JdrEHQIfYSI0kCQpTGvOrbE6jFGGYXKsHuwY9tBa/w5Uo6KzrEg==",
686
+
"node_modules/@types/scheduler": {
687
+
"version": "0.26.0",
688
+
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
689
+
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
678
690
"dev": true,
691
+
"license": "MIT"
692
+
},
693
+
"node_modules/call-bind-apply-helpers": {
694
+
"version": "1.0.2",
695
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
696
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
679
697
"license": "MIT",
680
-
"peerDependencies": {
681
-
"@types/react": "^17.0.0"
698
+
"dependencies": {
699
+
"es-errors": "^1.3.0",
700
+
"function-bind": "^1.1.2"
701
+
},
702
+
"engines": {
703
+
"node": ">= 0.4"
682
704
}
683
705
},
684
-
"node_modules/@types/scheduler": {
685
-
"version": "0.16.8",
686
-
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
687
-
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
688
-
"dev": true,
689
-
"license": "MIT"
706
+
"node_modules/call-bound": {
707
+
"version": "1.0.4",
708
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
709
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
710
+
"license": "MIT",
711
+
"dependencies": {
712
+
"call-bind-apply-helpers": "^1.0.2",
713
+
"get-intrinsic": "^1.3.0"
714
+
},
715
+
"engines": {
716
+
"node": ">= 0.4"
717
+
},
718
+
"funding": {
719
+
"url": "https://github.com/sponsors/ljharb"
720
+
}
690
721
},
691
722
"node_modules/caniuse-lite": {
692
723
"version": "1.0.30001760",
···
731
762
"node": ">=8"
732
763
}
733
764
},
765
+
"node_modules/dunder-proto": {
766
+
"version": "1.0.1",
767
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
768
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
769
+
"license": "MIT",
770
+
"dependencies": {
771
+
"call-bind-apply-helpers": "^1.0.1",
772
+
"es-errors": "^1.3.0",
773
+
"gopd": "^1.2.0"
774
+
},
775
+
"engines": {
776
+
"node": ">= 0.4"
777
+
}
778
+
},
779
+
"node_modules/es-define-property": {
780
+
"version": "1.0.1",
781
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
782
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
783
+
"license": "MIT",
784
+
"engines": {
785
+
"node": ">= 0.4"
786
+
}
787
+
},
788
+
"node_modules/es-errors": {
789
+
"version": "1.3.0",
790
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
791
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
792
+
"license": "MIT",
793
+
"engines": {
794
+
"node": ">= 0.4"
795
+
}
796
+
},
797
+
"node_modules/es-object-atoms": {
798
+
"version": "1.1.1",
799
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
800
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
801
+
"license": "MIT",
802
+
"dependencies": {
803
+
"es-errors": "^1.3.0"
804
+
},
805
+
"engines": {
806
+
"node": ">= 0.4"
807
+
}
808
+
},
809
+
"node_modules/function-bind": {
810
+
"version": "1.1.2",
811
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
812
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
813
+
"license": "MIT",
814
+
"funding": {
815
+
"url": "https://github.com/sponsors/ljharb"
816
+
}
817
+
},
818
+
"node_modules/get-intrinsic": {
819
+
"version": "1.3.0",
820
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
821
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
822
+
"license": "MIT",
823
+
"dependencies": {
824
+
"call-bind-apply-helpers": "^1.0.2",
825
+
"es-define-property": "^1.0.1",
826
+
"es-errors": "^1.3.0",
827
+
"es-object-atoms": "^1.1.1",
828
+
"function-bind": "^1.1.2",
829
+
"get-proto": "^1.0.1",
830
+
"gopd": "^1.2.0",
831
+
"has-symbols": "^1.1.0",
832
+
"hasown": "^2.0.2",
833
+
"math-intrinsics": "^1.1.0"
834
+
},
835
+
"engines": {
836
+
"node": ">= 0.4"
837
+
},
838
+
"funding": {
839
+
"url": "https://github.com/sponsors/ljharb"
840
+
}
841
+
},
842
+
"node_modules/get-proto": {
843
+
"version": "1.0.1",
844
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
845
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
846
+
"license": "MIT",
847
+
"dependencies": {
848
+
"dunder-proto": "^1.0.1",
849
+
"es-object-atoms": "^1.0.0"
850
+
},
851
+
"engines": {
852
+
"node": ">= 0.4"
853
+
}
854
+
},
855
+
"node_modules/gopd": {
856
+
"version": "1.2.0",
857
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
858
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
859
+
"license": "MIT",
860
+
"engines": {
861
+
"node": ">= 0.4"
862
+
},
863
+
"funding": {
864
+
"url": "https://github.com/sponsors/ljharb"
865
+
}
866
+
},
867
+
"node_modules/has-symbols": {
868
+
"version": "1.1.0",
869
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
870
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
871
+
"license": "MIT",
872
+
"engines": {
873
+
"node": ">= 0.4"
874
+
},
875
+
"funding": {
876
+
"url": "https://github.com/sponsors/ljharb"
877
+
}
878
+
},
879
+
"node_modules/hasown": {
880
+
"version": "2.0.2",
881
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
882
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
883
+
"license": "MIT",
884
+
"dependencies": {
885
+
"function-bind": "^1.1.2"
886
+
},
887
+
"engines": {
888
+
"node": ">= 0.4"
889
+
}
890
+
},
734
891
"node_modules/js-tokens": {
735
892
"version": "4.0.0",
736
893
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
···
747
904
},
748
905
"bin": {
749
906
"loose-envify": "cli.js"
907
+
}
908
+
},
909
+
"node_modules/math-intrinsics": {
910
+
"version": "1.1.0",
911
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
912
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
913
+
"license": "MIT",
914
+
"engines": {
915
+
"node": ">= 0.4"
750
916
}
751
917
},
752
918
"node_modules/nanoid": {
···
819
985
}
820
986
}
821
987
},
988
+
"node_modules/object-assign": {
989
+
"version": "4.1.1",
990
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
991
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
992
+
"license": "MIT",
993
+
"engines": {
994
+
"node": ">=0.10.0"
995
+
}
996
+
},
997
+
"node_modules/object-inspect": {
998
+
"version": "1.13.4",
999
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1000
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1001
+
"license": "MIT",
1002
+
"engines": {
1003
+
"node": ">= 0.4"
1004
+
},
1005
+
"funding": {
1006
+
"url": "https://github.com/sponsors/ljharb"
1007
+
}
1008
+
},
822
1009
"node_modules/picocolors": {
823
1010
"version": "1.1.1",
824
1011
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
···
853
1040
"node": "^10 || ^12 || >=14"
854
1041
}
855
1042
},
1043
+
"node_modules/prop-types": {
1044
+
"version": "15.8.1",
1045
+
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
1046
+
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
1047
+
"license": "MIT",
1048
+
"dependencies": {
1049
+
"loose-envify": "^1.4.0",
1050
+
"object-assign": "^4.1.1",
1051
+
"react-is": "^16.13.1"
1052
+
}
1053
+
},
1054
+
"node_modules/qs": {
1055
+
"version": "6.14.0",
1056
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
1057
+
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
1058
+
"license": "BSD-3-Clause",
1059
+
"dependencies": {
1060
+
"side-channel": "^1.1.0"
1061
+
},
1062
+
"engines": {
1063
+
"node": ">=0.6"
1064
+
},
1065
+
"funding": {
1066
+
"url": "https://github.com/sponsors/ljharb"
1067
+
}
1068
+
},
856
1069
"node_modules/react": {
857
-
"version": "18.3.1",
858
-
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
859
-
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1070
+
"version": "18.2.0",
1071
+
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
1072
+
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
860
1073
"license": "MIT",
861
1074
"dependencies": {
862
1075
"loose-envify": "^1.1.0"
···
866
1079
}
867
1080
},
868
1081
"node_modules/react-dom": {
869
-
"version": "18.3.1",
870
-
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
871
-
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1082
+
"version": "18.2.0",
1083
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
1084
+
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
872
1085
"license": "MIT",
873
1086
"dependencies": {
874
1087
"loose-envify": "^1.1.0",
875
-
"scheduler": "^0.23.2"
1088
+
"scheduler": "^0.23.0"
876
1089
},
877
1090
"peerDependencies": {
878
-
"react": "^18.3.1"
1091
+
"react": "^18.2.0"
879
1092
}
880
1093
},
1094
+
"node_modules/react-is": {
1095
+
"version": "16.13.1",
1096
+
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
1097
+
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
1098
+
"license": "MIT"
1099
+
},
881
1100
"node_modules/scheduler": {
882
1101
"version": "0.23.2",
883
1102
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
···
899
1118
"engines": {
900
1119
"node": ">=10"
901
1120
}
1121
+
},
1122
+
"node_modules/server-only": {
1123
+
"version": "0.0.1",
1124
+
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
1125
+
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
1126
+
"license": "MIT"
902
1127
},
903
1128
"node_modules/sharp": {
904
1129
"version": "0.34.5",
···
945
1170
"@img/sharp-win32-x64": "0.34.5"
946
1171
}
947
1172
},
1173
+
"node_modules/side-channel": {
1174
+
"version": "1.1.0",
1175
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1176
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1177
+
"license": "MIT",
1178
+
"dependencies": {
1179
+
"es-errors": "^1.3.0",
1180
+
"object-inspect": "^1.13.3",
1181
+
"side-channel-list": "^1.0.0",
1182
+
"side-channel-map": "^1.0.1",
1183
+
"side-channel-weakmap": "^1.0.2"
1184
+
},
1185
+
"engines": {
1186
+
"node": ">= 0.4"
1187
+
},
1188
+
"funding": {
1189
+
"url": "https://github.com/sponsors/ljharb"
1190
+
}
1191
+
},
1192
+
"node_modules/side-channel-list": {
1193
+
"version": "1.0.0",
1194
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1195
+
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1196
+
"license": "MIT",
1197
+
"dependencies": {
1198
+
"es-errors": "^1.3.0",
1199
+
"object-inspect": "^1.13.3"
1200
+
},
1201
+
"engines": {
1202
+
"node": ">= 0.4"
1203
+
},
1204
+
"funding": {
1205
+
"url": "https://github.com/sponsors/ljharb"
1206
+
}
1207
+
},
1208
+
"node_modules/side-channel-map": {
1209
+
"version": "1.0.1",
1210
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1211
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1212
+
"license": "MIT",
1213
+
"dependencies": {
1214
+
"call-bound": "^1.0.2",
1215
+
"es-errors": "^1.3.0",
1216
+
"get-intrinsic": "^1.2.5",
1217
+
"object-inspect": "^1.13.3"
1218
+
},
1219
+
"engines": {
1220
+
"node": ">= 0.4"
1221
+
},
1222
+
"funding": {
1223
+
"url": "https://github.com/sponsors/ljharb"
1224
+
}
1225
+
},
1226
+
"node_modules/side-channel-weakmap": {
1227
+
"version": "1.0.2",
1228
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1229
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1230
+
"license": "MIT",
1231
+
"dependencies": {
1232
+
"call-bound": "^1.0.2",
1233
+
"es-errors": "^1.3.0",
1234
+
"get-intrinsic": "^1.2.5",
1235
+
"object-inspect": "^1.13.3",
1236
+
"side-channel-map": "^1.0.1"
1237
+
},
1238
+
"engines": {
1239
+
"node": ">= 0.4"
1240
+
},
1241
+
"funding": {
1242
+
"url": "https://github.com/sponsors/ljharb"
1243
+
}
1244
+
},
948
1245
"node_modules/source-map-js": {
949
1246
"version": "1.2.1",
950
1247
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
···
954
1251
"node": ">=0.10.0"
955
1252
}
956
1253
},
1254
+
"node_modules/stripe": {
1255
+
"version": "14.8.0",
1256
+
"resolved": "https://registry.npmjs.org/stripe/-/stripe-14.8.0.tgz",
1257
+
"integrity": "sha512-Qdecqk7lx095BE829NWxrG1+69NjPuHrpZqeR61i2KO00fpKkjMX9TYjwFU+WjQD6ZcNmCGOwNfnz5VnI5bjIg==",
1258
+
"license": "MIT",
1259
+
"dependencies": {
1260
+
"@types/node": ">=8.1.0",
1261
+
"qs": "^6.11.0"
1262
+
},
1263
+
"engines": {
1264
+
"node": ">=12.*"
1265
+
}
1266
+
},
957
1267
"node_modules/styled-jsx": {
958
1268
"version": "5.1.6",
959
1269
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
···
984
1294
"license": "0BSD"
985
1295
},
986
1296
"node_modules/typescript": {
987
-
"version": "4.9.5",
988
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
989
-
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
1297
+
"version": "5.1.6",
1298
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
1299
+
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
990
1300
"dev": true,
991
1301
"license": "Apache-2.0",
992
1302
"bin": {
···
994
1304
"tsserver": "bin/tsserver"
995
1305
},
996
1306
"engines": {
997
-
"node": ">=4.2.0"
1307
+
"node": ">=14.17"
998
1308
}
999
1309
}
1000
1310
}
+10
-14
package.json
+10
-14
package.json
···
1
1
{
2
-
"name": "eny.space",
3
-
"version": "1.0.0",
4
-
"description": "eny.space",
5
-
"author": "Krekeny GmbH",
6
-
"license": "MIT",
7
2
"private": true,
8
3
"scripts": {
9
4
"dev": "next",
10
5
"build": "next build",
11
-
"start": "next start",
12
-
"type-check": "tsc"
6
+
"start": "next start"
13
7
},
14
8
"dependencies": {
15
-
"@stripe/stripe-js": "^8.6.0",
9
+
"@stripe/react-stripe-js": "2.4.0",
10
+
"@stripe/stripe-js": "2.2.2",
16
11
"next": "latest",
17
-
"react": "^18.2.0",
18
-
"react-dom": "^18.2.0"
12
+
"react": "18.2.0",
13
+
"react-dom": "18.2.0",
14
+
"server-only": "0.0.1",
15
+
"stripe": "14.8.0"
19
16
},
20
17
"devDependencies": {
21
-
"@types/node": "^12.12.21",
22
-
"@types/react": "^17.0.2",
23
-
"@types/react-dom": "^17.0.1",
24
-
"typescript": "^4.8.3"
18
+
"@types/node": "20.4.6",
19
+
"@types/react": "18.2.8",
20
+
"typescript": "5.1.6"
25
21
}
26
22
}
-14
pages/about.tsx
-14
pages/about.tsx
···
1
-
import Link from "next/link";
2
-
import Layout from "../components/Layout";
3
-
4
-
const AboutPage = () => (
5
-
<Layout title="About | Next.js + TypeScript Example">
6
-
<h1>About</h1>
7
-
<p>This is the about page</p>
8
-
<p>
9
-
<Link href="/">Go home</Link>
10
-
</p>
11
-
</Layout>
12
-
);
13
-
14
-
export default AboutPage;
-16
pages/api/users/index.ts
-16
pages/api/users/index.ts
···
1
-
import { NextApiRequest, NextApiResponse } from "next";
2
-
import { sampleUserData } from "@/utils/sample-data";
3
-
4
-
const handler = (_req: NextApiRequest, res: NextApiResponse) => {
5
-
try {
6
-
if (!Array.isArray(sampleUserData)) {
7
-
throw new Error("Cannot find user data");
8
-
}
9
-
10
-
res.status(200).json(sampleUserData);
11
-
} catch (err: any) {
12
-
res.status(500).json({ statusCode: 500, message: err.message });
13
-
}
14
-
};
15
-
16
-
export default handler;
-13
pages/index.tsx
-13
pages/index.tsx
···
1
-
import Link from "next/link";
2
-
import Layout from "../components/Layout";
3
-
4
-
const IndexPage = () => (
5
-
<Layout title="Home | Next.js + TypeScript Example">
6
-
<h1>Hello Next.js 👋</h1>
7
-
<p>
8
-
<Link href="/about">About</Link>
9
-
</p>
10
-
</Layout>
11
-
);
12
-
13
-
export default IndexPage;
-61
pages/users/[id].tsx
-61
pages/users/[id].tsx
···
1
-
import { GetStaticProps, GetStaticPaths } from "next";
2
-
3
-
import { User } from "../../interfaces";
4
-
import { sampleUserData } from "../../utils/sample-data";
5
-
import Layout from "../../components/Layout";
6
-
import ListDetail from "../../components/ListDetail";
7
-
8
-
type Props = {
9
-
item?: User;
10
-
errors?: string;
11
-
};
12
-
13
-
const StaticPropsDetail = ({ item, errors }: Props) => {
14
-
if (errors) {
15
-
return (
16
-
<Layout title="Error | Next.js + TypeScript Example">
17
-
<p>
18
-
<span style={{ color: "red" }}>Error:</span> {errors}
19
-
</p>
20
-
</Layout>
21
-
);
22
-
}
23
-
24
-
return (
25
-
<Layout
26
-
title={`${
27
-
item ? item.name : "User Detail"
28
-
} | Next.js + TypeScript Example`}
29
-
>
30
-
{item && <ListDetail item={item} />}
31
-
</Layout>
32
-
);
33
-
};
34
-
35
-
export default StaticPropsDetail;
36
-
37
-
export const getStaticPaths: GetStaticPaths = async () => {
38
-
// Get the paths we want to pre-render based on users
39
-
const paths = sampleUserData.map((user) => ({
40
-
params: { id: user.id.toString() },
41
-
}));
42
-
43
-
// We'll pre-render only these paths at build time.
44
-
// { fallback: false } means other routes should 404.
45
-
return { paths, fallback: false };
46
-
};
47
-
48
-
// This function gets called at build time on server-side.
49
-
// It won't be called on client-side, so you can even do
50
-
// direct database queries.
51
-
export const getStaticProps: GetStaticProps = async ({ params }) => {
52
-
try {
53
-
const id = params?.id;
54
-
const item = sampleUserData.find((data) => data.id === Number(id));
55
-
// By returning { props: item }, the StaticPropsDetail component
56
-
// will receive `item` as a prop at build time
57
-
return { props: { item } };
58
-
} catch (err: any) {
59
-
return { props: { errors: err.message } };
60
-
}
61
-
};
-35
pages/users/index.tsx
-35
pages/users/index.tsx
···
1
-
import { GetStaticProps } from "next";
2
-
import Link from "next/link";
3
-
4
-
import { User } from "../../interfaces";
5
-
import { sampleUserData } from "../../utils/sample-data";
6
-
import Layout from "../../components/Layout";
7
-
import List from "../../components/List";
8
-
9
-
type Props = {
10
-
items: User[];
11
-
};
12
-
13
-
const WithStaticProps = ({ items }: Props) => (
14
-
<Layout title="Users List | Next.js + TypeScript Example">
15
-
<h1>Users List</h1>
16
-
<p>
17
-
Example fetching data from inside <code>getStaticProps()</code>.
18
-
</p>
19
-
<p>You are currently on: /users</p>
20
-
<List items={items} />
21
-
<p>
22
-
<Link href="/">Go home</Link>
23
-
</p>
24
-
</Layout>
25
-
);
26
-
27
-
export const getStaticProps: GetStaticProps = async () => {
28
-
// Example for including static props in a Next.js function component page.
29
-
// Don't forget to include the respective types for any props passed into
30
-
// the component.
31
-
const items: User[] = sampleUserData;
32
-
return { props: { items } };
33
-
};
34
-
35
-
export default WithStaticProps;
+1
public/checkout-one-time-payments.svg
+1
public/checkout-one-time-payments.svg
···
1
+
<svg height="156" viewBox="0 0 240 156" width="240" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd" transform="translate(0 1)"><rect fill="#fff" height="154" opacity=".8" rx="4" width="240"/><path d="m99 0v154" opacity=".068034" stroke="#000" stroke-linecap="square"/><text style="opacity:.836844;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:18;font-weight:500;letter-spacing:-.45;fill:#8f6ed5"><tspan x="14" y="48">$29.90</tspan></text><path d="m18 17h23c1.1045695 0 2 .8954305 2 2s-.8954305 2-2 2h-23c-1.1045695 0-2-.8954305-2-2s.8954305-2 2-2z" fill="#8f6ed5"/><g transform="translate(112 109)"><path d="m3.58943928 0h109.02112172c1.248126 0 1.700727.12995586 2.157023.37398579s.8144.60213393 1.05843 1.05843023c.24403.45629629.373986.90889702.373986 2.15702326v12.42112142c0 1.2481263-.129956 1.700727-.373986 2.1570233s-.602134.8144003-1.05843 1.0584302-.908897.3739858-2.157023.3739858h-109.02112172c-1.24812624 0-1.70072697-.1299559-2.15702326-.3739858-.4562963-.2440299-.8144003-.6021339-1.05843023-1.0584302s-.37398579-.908897-.37398579-2.1570233v-12.42112142c0-1.24812624.12995586-1.70072697.37398579-2.15702326.24402993-.4562963.60213393-.8144003 1.05843023-1.05843023.45629629-.24402993.90889702-.37398579 2.15702326-.37398579z" fill="#8f6ed5"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:10;font-weight:500;letter-spacing:.12;fill:#f7fafc"><tspan x="29" y="14">Pay $29.90</tspan></text></g><g transform="translate(112 83)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><g fill-rule="nonzero" opacity=".5" transform="translate(95.2 4.2)"><rect fill="#ebf1f8" height="10.7" rx="1.4" stroke="#a450b5" stroke-width=".5" width="16.3" x=".25" y=".25"/><path d="m2.30141283 4.14010612c-.36619921-.20085195-.7841356-.36238968-1.25141283-.47449094l.01960035-.08729771h1.91588511c.25968832.00910242.47040833.08723662.53897699.36318047l.41637443 1.98511543.12755149.59789495 1.1661881-2.94619085h1.2591758l-1.87173549 4.31595242h-1.25924092zm5.11856981 3.75874553h-1.1908025l.7448132-4.32053418h1.19073738zm4.31676486-4.21490945-.1619471.93296757-.1077042-.04587865c-.2154736-.0873588-.4999065-.17465651-.8870297-.16543191-.4701478 0-.68099808.18834069-.68588189.3722218 0 .20226923.25506499.33556776.67155609.53325523.6860773.30795505 1.0043062.6848197.9995526 1.17665586-.0096374.89631352-.8230844 1.47550848-2.07255766 1.47550848-.53422342-.00464284-1.04878137-.11057303-1.32807004-.22994303l.16663551-.96986596.15680278.06903178c.38712314.16103343.64186253.22982085 1.11728491.22982085.34284328 0 .7104312-.13342072.7151196-.42286548 0-.18846286-.1566074-.32646533-.61731322-.53789807-.45074287-.20691207-1.05340471-.55158225-1.04363709-1.17213519.00507916-.8410881.83317755-1.42938548 2.00913331-1.42938548.4606407 0 .8331775.09652231 1.0680561.1839422zm1.5826792 2.68429772h.989785c-.0489683-.21601449-.27447-1.2502083-.27447-1.2502083l-.0832201-.37228289c-.058801.16085015-.1616214.42286547-.1566725.41364087 0 0-.3773555.96064136-.4754224 1.20885032zm1.4698958-2.78992245.9606775 4.32047309h-1.1025684s-.1078996-.49641792-.1420862-.64810457h-1.5288922c-.0442148.11484934-.2499207.64810457-.2499207.64810457h-1.2494733l1.7687848-3.96199655c.122551-.28040343.3383502-.35847654.6223924-.35847654z" fill="#8f6ed5"/></g><g fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500"><text opacity=".67"><tspan x="5.6" y="12.9">4242</tspan></text><text opacity=".67"><tspan x="33.6" y="12.9">11/26</tspan></text><text opacity=".67"><tspan x="61.6" y="12.9">934</tspan></text></g></g><g transform="translate(112 57)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">Jenny Rosen</tspan></text></g><g transform="translate(112 31)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">jenny@rosen.com</tspan></text></g></g></svg>
public/checkout_demo.gif
public/checkout_demo.gif
This is a binary file and will not be displayed.
+1
public/elements-card-payment.svg
+1
public/elements-card-payment.svg
···
1
+
<svg height="64" viewBox="0 0 202 64" width="202" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#fff" height="28" opacity=".8" rx="4" width="202"/><g transform="translate(0 36)"><rect fill="#4e5663" height="28" rx="4" width="202"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:14;font-weight:500;letter-spacing:-.154;fill:#fff"><tspan x="63" y="19">Pay $29.90</tspan></text></g><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="500" opacity=".797503"><tspan x="9" y="19">4242 4242 4242 4242</tspan></text><g fill-rule="nonzero" opacity=".5" transform="translate(171 6)"><rect height="15.5" rx="2" stroke="#4f566b" stroke-width=".5" width="23.5" x=".25" y=".25"/><path d="m3.28773262 5.91443732c-.52314173-.28693137-1.12019372-.51769955-1.78773262-.6778442l.0280005-.12471102h2.73697872c.37098332.01300346.67201191.12462375.76996714.51882925l.59482061 2.83587917.18221641.85413565 1.66598301-4.20884407h1.79882257l-2.67390785 6.1656463h-1.79891559zm7.31224258 5.36963648h-1.70114643l1.06401886-6.1721917h1.70105337zm6.1668069-6.02129922-.2313529 1.3328108-.1538632-.06554092c-.3078194-.12479829-.7141522-.2495093-1.2671852-.2363313-.6716398 0-.9728545.26905813-.9798314.53174543 0 .28895604.3643786.47938251.9593659.76179318.9801104.43993579 1.4347231.97831385 1.4279323 1.68093694-.0137677 1.28044789-1.1758348 2.10786929-2.9607967 2.10786929-.7631763-.0066327-1.4982591-.1579615-1.8972429-.3284901l.2380507-1.38552277.224004.09861682c.5530331.23004776.9169465.32831555 1.5961213.32831555.4897761 0 1.0149017-.19060107 1.0215995-.60409358 0-.26923267-.2237249-.46637906-.8818761-.76842582-.6439184-.29558867-1.5048639-.78797464-1.4909101-1.67447884.0072559-1.20155443 1.1902536-2.04197926 2.8701904-2.04197926.6580581 0 1.1902536.13788902 1.5257944.26277458zm2.2609703 3.83471102h1.4139786c-.0699548-.30859213-.3921-1.78601186-.3921-1.78601186l-.1188858-.5318327c-.0840015.22978594-.2308878.60409353-.2238179.59091553 0 0-.5390794 1.3723448-.6791749 1.72692903zm2.0998512-3.9856035 1.3723964 6.1721044h-1.5750977s-.1541422-.7091684-.2029803-.9258637h-2.1841317c-.0631639.1640705-.3570296.9258637-.3570296.9258637h-1.7849619l2.5268355-5.65999506c.1750729-.40057632.4833574-.51210934.889132-.51210934z" fill="#3c4257"/></g></g></svg>
public/elements_demo.gif
public/elements_demo.gif
This is a binary file and will not be displayed.
public/logo.png
public/logo.png
This is a binary file and will not be displayed.
+315
styles.css
+315
styles.css
···
1
+
/* Variables */
2
+
:root {
3
+
--body-color: #fcfdfe;
4
+
--checkout-color: #8f6ed5;
5
+
--elements-color: #6772e5;
6
+
--body-font-family:
7
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
8
+
--h1-color: #1a1f36;
9
+
--h2-color: #7b818a;
10
+
--h3-color: #a3acb9;
11
+
--radius: 6px;
12
+
--container-width-max: 1280px;
13
+
--page-width-max: 600px;
14
+
--transition-duration: 2s;
15
+
}
16
+
17
+
body {
18
+
margin: 0;
19
+
padding: 0;
20
+
background: var(--body-color);
21
+
overflow-y: scroll;
22
+
}
23
+
24
+
* {
25
+
box-sizing: border-box;
26
+
font-family: var(--body-font-family);
27
+
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
28
+
}
29
+
30
+
#__next {
31
+
display: flex;
32
+
justify-content: center;
33
+
}
34
+
35
+
.container {
36
+
max-width: var(--container-width-max);
37
+
padding: 45px 25px;
38
+
display: flex;
39
+
flex-direction: row;
40
+
}
41
+
42
+
.page-container {
43
+
padding-bottom: 60px;
44
+
max-width: var(--page-width-max);
45
+
}
46
+
47
+
h1 {
48
+
font-weight: 600;
49
+
color: var(--h1-color);
50
+
margin: 6px 0 12px;
51
+
font-size: 27px;
52
+
line-height: 32px;
53
+
}
54
+
55
+
h1 span.light {
56
+
color: var(--h3-color);
57
+
}
58
+
59
+
h2 {
60
+
color: var(--h2-color);
61
+
margin: 8px 0;
62
+
}
63
+
64
+
h3 {
65
+
font-size: 17px;
66
+
color: var(--h3-color);
67
+
margin: 8px 0;
68
+
}
69
+
70
+
a {
71
+
color: var(--checkout-color);
72
+
text-decoration: none;
73
+
}
74
+
75
+
header {
76
+
position: relative;
77
+
flex: 0 0 250px;
78
+
padding-right: 48px;
79
+
}
80
+
81
+
.header-content {
82
+
position: sticky;
83
+
top: 45px;
84
+
}
85
+
86
+
.logo img {
87
+
height: 20px;
88
+
margin-bottom: 52px;
89
+
}
90
+
91
+
ul,
92
+
li {
93
+
list-style: none;
94
+
padding: 0;
95
+
margin: 0;
96
+
}
97
+
98
+
.card-list {
99
+
display: flex;
100
+
flex-wrap: wrap;
101
+
align-content: flex-start;
102
+
padding-top: 64px;
103
+
}
104
+
105
+
.card {
106
+
display: block;
107
+
border-radius: 10px;
108
+
position: relative;
109
+
padding: 12px;
110
+
height: 320px;
111
+
flex: 0 0 33%;
112
+
min-width: 304px;
113
+
width: 33%;
114
+
margin: 0 20px 20px 0;
115
+
text-decoration: none;
116
+
box-shadow:
117
+
-20px 20px 60px #abacad,
118
+
20px -20px 60px #ffffff;
119
+
}
120
+
.card h2 {
121
+
color: #fff;
122
+
}
123
+
.card h2.bottom {
124
+
position: absolute;
125
+
bottom: 10px;
126
+
}
127
+
128
+
.card img {
129
+
width: 80%;
130
+
position: absolute;
131
+
top: 50%;
132
+
left: 50%;
133
+
transform: translate(-50%, -50%);
134
+
}
135
+
136
+
.error-message {
137
+
color: #ef2961;
138
+
}
139
+
140
+
.FormRow,
141
+
fieldset,
142
+
input[type="number"],
143
+
input[type="text"] {
144
+
border-radius: var(--radius);
145
+
padding: 5px 12px;
146
+
width: 100%;
147
+
background: #fff;
148
+
appearance: none;
149
+
font-size: 16px;
150
+
margin-top: 10px;
151
+
}
152
+
153
+
input[type="range"] {
154
+
margin: 5px 0;
155
+
width: 100%;
156
+
}
157
+
158
+
button {
159
+
border-radius: var(--radius);
160
+
color: white;
161
+
font-size: larger;
162
+
border: 0;
163
+
padding: 12px 16px;
164
+
margin-top: 10px;
165
+
font-weight: 600;
166
+
cursor: pointer;
167
+
transition: all 0.2s ease;
168
+
display: block;
169
+
width: 100%;
170
+
}
171
+
button:disabled {
172
+
opacity: 0.5;
173
+
cursor: not-allowed;
174
+
}
175
+
176
+
.elements-style {
177
+
color: var(--elements-color);
178
+
border: 1px solid var(--elements-color);
179
+
}
180
+
.elements-style-background {
181
+
background: var(--elements-color);
182
+
transition: box-shadow var(--transition-duration);
183
+
}
184
+
.card.elements-style-background:hover {
185
+
box-shadow:
186
+
20px 20px 60px #464e9c,
187
+
-20px -20px 60px #8896ff;
188
+
}
189
+
.checkout-style {
190
+
color: var(--checkout-color);
191
+
border: 1px solid var(--checkout-color);
192
+
}
193
+
.checkout-style-background {
194
+
background: var(--checkout-color);
195
+
transition: box-shadow var(--transition-duration);
196
+
}
197
+
.card.checkout-style-background:hover {
198
+
box-shadow:
199
+
20px 20px 60px #614b91,
200
+
-20px -20px 60px #bd91ff;
201
+
}
202
+
203
+
/* Test card number */
204
+
.test-card-notice {
205
+
display: block;
206
+
margin-block-start: 1em;
207
+
margin-block-end: 1em;
208
+
margin-inline-start: 0px;
209
+
margin-inline-end: 0px;
210
+
}
211
+
.card-number {
212
+
display: inline;
213
+
white-space: nowrap;
214
+
font-family: Menlo, Consolas, monospace;
215
+
color: #3c4257;
216
+
font-weight: 500;
217
+
}
218
+
.card-number span {
219
+
display: inline-block;
220
+
width: 4px;
221
+
}
222
+
223
+
/* Code block */
224
+
code,
225
+
pre {
226
+
font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace;
227
+
font-size: 12px;
228
+
background: rgba(0, 0, 0, 0.03);
229
+
padding: 12px;
230
+
border-radius: var(--radius);
231
+
max-height: 500px;
232
+
width: var(--page-width-max);
233
+
overflow: auto;
234
+
}
235
+
236
+
.banner {
237
+
max-width: 825px;
238
+
margin: 0 auto;
239
+
font-size: 14px;
240
+
background: var(--body-color);
241
+
color: #6a7c94;
242
+
border-radius: 50px;
243
+
box-shadow:
244
+
-20px 20px 60px #abacad,
245
+
20px -20px 60px #ffffff;
246
+
display: flex;
247
+
align-items: center;
248
+
box-sizing: border-box;
249
+
padding: 15px;
250
+
line-height: 1.15;
251
+
position: fixed;
252
+
bottom: 2vh;
253
+
left: 0;
254
+
right: 0;
255
+
text-align: center;
256
+
justify-content: center;
257
+
}
258
+
259
+
@media only screen and (max-width: 980px) {
260
+
.container {
261
+
flex-direction: column;
262
+
}
263
+
264
+
.header-content {
265
+
max-width: 280px;
266
+
position: relative;
267
+
top: 0;
268
+
}
269
+
270
+
.card {
271
+
margin: 0 20px 20px 0;
272
+
box-shadow: none;
273
+
}
274
+
275
+
.card-list {
276
+
padding-top: 0;
277
+
}
278
+
279
+
.banner {
280
+
box-shadow: none;
281
+
bottom: 0;
282
+
}
283
+
}
284
+
285
+
@media only screen and (max-width: 600px) {
286
+
.container {
287
+
flex-direction: column;
288
+
}
289
+
290
+
.card {
291
+
display: block;
292
+
border-radius: 8px;
293
+
flex: 1 0 100%;
294
+
max-width: 100%;
295
+
padding-left: 0;
296
+
padding-right: 0;
297
+
margin: 0 0 20px 0;
298
+
box-shadow: none;
299
+
}
300
+
301
+
.card-list {
302
+
padding-top: 0;
303
+
}
304
+
305
+
code,
306
+
pre,
307
+
h3 {
308
+
display: none;
309
+
}
310
+
311
+
.banner {
312
+
box-shadow: none;
313
+
bottom: 0;
314
+
}
315
+
}
+30
-9
tsconfig.json
+30
-9
tsconfig.json
···
1
1
{
2
2
"compilerOptions": {
3
-
"target": "es5",
3
+
"target": "ES2019",
4
4
"lib": [
5
5
"dom",
6
6
"dom.iterable",
···
8
8
],
9
9
"allowJs": true,
10
10
"skipLibCheck": true,
11
-
"strict": false,
11
+
"strict": true,
12
12
"forceConsistentCasingInFileNames": true,
13
13
"noEmit": true,
14
14
"esModuleInterop": true,
15
15
"module": "esnext",
16
16
"moduleResolution": "node",
17
17
"resolveJsonModule": true,
18
+
"allowSyntheticDefaultImports": true,
18
19
"isolatedModules": true,
20
+
"incremental": true,
19
21
"jsx": "react-jsx",
22
+
"baseUrl": ".",
20
23
"paths": {
21
-
"@/*": [
22
-
"./*"
24
+
"@/actions/*": [
25
+
"app/actions/*"
26
+
],
27
+
"@/components/*": [
28
+
"app/components/*"
29
+
],
30
+
"@/config": [
31
+
"config/"
32
+
],
33
+
"@/lib/*": [
34
+
"lib/*"
35
+
],
36
+
"@/utils/*": [
37
+
"utils/*"
23
38
]
24
39
},
25
-
"incremental": true
40
+
"plugins": [
41
+
{
42
+
"name": "next"
43
+
}
44
+
]
26
45
},
46
+
"exclude": [
47
+
"node_modules"
48
+
],
27
49
"include": [
28
50
"next-env.d.ts",
29
51
"**/*.ts",
30
-
"**/*.tsx"
31
-
],
32
-
"exclude": [
33
-
"node_modules"
52
+
"**/*.tsx",
53
+
".next/types/**/*.ts",
54
+
".next/dev/types/**/*.ts"
34
55
]
35
56
}
+31
-8
types/routes.d.ts
+31
-8
types/routes.d.ts
···
1
1
// This file is generated automatically by Next.js
2
2
// Do not edit this file manually
3
3
4
-
type AppRoutes = never
5
-
type PageRoutes = "/" | "/about" | "/users" | "/users/[id]"
6
-
type LayoutRoutes = never
4
+
type AppRoutes = "/" | "/donate-with-checkout" | "/donate-with-checkout/result" | "/donate-with-elements" | "/donate-with-elements/result" | "/donate-with-embedded-checkout" | "/donate-with-embedded-checkout/result"
5
+
type AppRouteHandlerRoutes = "/api/webhooks"
6
+
type PageRoutes = never
7
+
type LayoutRoutes = "/" | "/donate-with-checkout/result" | "/donate-with-elements/result" | "/donate-with-embedded-checkout/result"
7
8
type RedirectRoutes = never
8
9
type RewriteRoutes = never
9
-
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
10
+
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes | AppRouteHandlerRoutes
10
11
11
12
12
13
interface ParamMap {
13
14
"/": {}
14
-
"/about": {}
15
-
"/users": {}
16
-
"/users/[id]": { "id": string; }
15
+
"/api/webhooks": {}
16
+
"/donate-with-checkout": {}
17
+
"/donate-with-checkout/result": {}
18
+
"/donate-with-elements": {}
19
+
"/donate-with-elements/result": {}
20
+
"/donate-with-embedded-checkout": {}
21
+
"/donate-with-embedded-checkout/result": {}
17
22
}
18
23
19
24
20
25
export type ParamsOf<Route extends Routes> = ParamMap[Route]
21
26
22
27
interface LayoutSlotMap {
28
+
"/": never
29
+
"/donate-with-checkout/result": never
30
+
"/donate-with-elements/result": never
31
+
"/donate-with-embedded-checkout/result": never
23
32
}
24
33
25
34
26
-
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
35
+
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap, AppRouteHandlerRoutes }
27
36
28
37
declare global {
29
38
/**
···
55
64
children: React.ReactNode
56
65
} & {
57
66
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
67
+
}
68
+
69
+
/**
70
+
* Context for Next.js App Router route handlers
71
+
* @example
72
+
* ```tsx
73
+
* export async function GET(request: NextRequest, context: RouteContext<'/api/users/[id]'>) {
74
+
* const { id } = await context.params
75
+
* return Response.json({ id })
76
+
* }
77
+
* ```
78
+
*/
79
+
interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes> {
80
+
params: Promise<ParamMap[AppRouteHandlerRoute]>
58
81
}
59
82
}
+114
-42
types/validator.ts
+114
-42
types/validator.ts
···
2
2
// Do not edit this file manually
3
3
// This file validates that all pages and layouts export the correct types
4
4
5
+
import type { AppRoutes, LayoutRoutes, ParamMap, AppRouteHandlerRoutes } from "./routes.js"
6
+
import type { ResolvingMetadata, ResolvingViewport } from "next/types.js"
7
+
import type { NextRequest } from 'next/server.js'
5
8
6
-
import type { NextApiHandler } from "next/types.js"
9
+
type AppPageConfig<Route extends AppRoutes = AppRoutes> = {
10
+
default: React.ComponentType<{ params: Promise<ParamMap[Route]> } & any> | ((props: { params: Promise<ParamMap[Route]> } & any) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
11
+
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
12
+
generateMetadata?: (
13
+
props: { params: Promise<ParamMap[Route]> } & any,
14
+
parent: ResolvingMetadata
15
+
) => Promise<any> | any
16
+
generateViewport?: (
17
+
props: { params: Promise<ParamMap[Route]> } & any,
18
+
parent: ResolvingViewport
19
+
) => Promise<any> | any
20
+
metadata?: any
21
+
viewport?: any
22
+
}
7
23
8
-
type PagesPageConfig = {
9
-
default: React.ComponentType<any> | ((props: any) => React.ReactNode | Promise<React.ReactNode> | never | void)
10
-
getStaticProps?: (context: any) => Promise<any> | any
11
-
getStaticPaths?: (context: any) => Promise<any> | any
12
-
getServerSideProps?: (context: any) => Promise<any> | any
13
-
getInitialProps?: (context: any) => Promise<any> | any
14
-
/**
15
-
* Segment configuration for legacy Pages Router pages.
16
-
* Validated at build-time by parsePagesSegmentConfig.
17
-
*/
18
-
config?: {
19
-
maxDuration?: number
20
-
runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const
21
-
regions?: string[]
22
-
}
24
+
type LayoutConfig<Route extends LayoutRoutes = LayoutRoutes> = {
25
+
default: React.ComponentType<LayoutProps<Route>> | ((props: LayoutProps<Route>) => React.ReactNode | Promise<React.ReactNode> | never | void | Promise<void>)
26
+
generateStaticParams?: (props: { params: ParamMap[Route] }) => Promise<any[]> | any[]
27
+
generateMetadata?: (
28
+
props: { params: Promise<ParamMap[Route]> } & any,
29
+
parent: ResolvingMetadata
30
+
) => Promise<any> | any
31
+
generateViewport?: (
32
+
props: { params: Promise<ParamMap[Route]> } & any,
33
+
parent: ResolvingViewport
34
+
) => Promise<any> | any
35
+
metadata?: any
36
+
viewport?: any
23
37
}
24
38
25
-
type ApiRouteConfig = {
26
-
default: (req: any, res: any) => ReturnType<NextApiHandler>
27
-
config?: {
28
-
api?: {
29
-
bodyParser?: boolean | { sizeLimit?: string }
30
-
responseLimit?: string | number | boolean
31
-
externalResolver?: boolean
32
-
}
33
-
runtime?: 'edge' | 'experimental-edge' | 'nodejs' | string // necessary unless config is exported as const
34
-
maxDuration?: number
35
-
}
39
+
type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRoutes> = {
40
+
GET?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
41
+
POST?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
42
+
PUT?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
43
+
PATCH?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
44
+
DELETE?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
45
+
HEAD?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
46
+
OPTIONS?: (request: NextRequest, context: { params: Promise<ParamMap[Route]> }) => Promise<Response | void> | Response | void
36
47
}
37
48
38
49
50
+
// Validate ../../app/donate-with-checkout/page.tsx
51
+
{
52
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-checkout">> = Specific
53
+
const handler = {} as typeof import("../../app/donate-with-checkout/page.js")
54
+
type __Check = __IsExpected<typeof handler>
55
+
// @ts-ignore
56
+
type __Unused = __Check
57
+
}
39
58
59
+
// Validate ../../app/donate-with-checkout/result/page.tsx
60
+
{
61
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-checkout/result">> = Specific
62
+
const handler = {} as typeof import("../../app/donate-with-checkout/result/page.js")
63
+
type __Check = __IsExpected<typeof handler>
64
+
// @ts-ignore
65
+
type __Unused = __Check
66
+
}
40
67
68
+
// Validate ../../app/donate-with-elements/page.tsx
69
+
{
70
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-elements">> = Specific
71
+
const handler = {} as typeof import("../../app/donate-with-elements/page.js")
72
+
type __Check = __IsExpected<typeof handler>
73
+
// @ts-ignore
74
+
type __Unused = __Check
75
+
}
41
76
77
+
// Validate ../../app/donate-with-elements/result/page.tsx
78
+
{
79
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-elements/result">> = Specific
80
+
const handler = {} as typeof import("../../app/donate-with-elements/result/page.js")
81
+
type __Check = __IsExpected<typeof handler>
82
+
// @ts-ignore
83
+
type __Unused = __Check
84
+
}
42
85
43
-
// Validate ../../pages/about.tsx
86
+
// Validate ../../app/donate-with-embedded-checkout/page.tsx
44
87
{
45
-
type __IsExpected<Specific extends PagesPageConfig> = Specific
46
-
const handler = {} as typeof import("../../pages/about.js")
88
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-embedded-checkout">> = Specific
89
+
const handler = {} as typeof import("../../app/donate-with-embedded-checkout/page.js")
47
90
type __Check = __IsExpected<typeof handler>
48
91
// @ts-ignore
49
92
type __Unused = __Check
50
93
}
51
94
52
-
// Validate ../../pages/index.tsx
95
+
// Validate ../../app/donate-with-embedded-checkout/result/page.tsx
53
96
{
54
-
type __IsExpected<Specific extends PagesPageConfig> = Specific
55
-
const handler = {} as typeof import("../../pages/index.js")
97
+
type __IsExpected<Specific extends AppPageConfig<"/donate-with-embedded-checkout/result">> = Specific
98
+
const handler = {} as typeof import("../../app/donate-with-embedded-checkout/result/page.js")
56
99
type __Check = __IsExpected<typeof handler>
57
100
// @ts-ignore
58
101
type __Unused = __Check
59
102
}
60
103
61
-
// Validate ../../pages/users/[id].tsx
104
+
// Validate ../../app/page.tsx
62
105
{
63
-
type __IsExpected<Specific extends PagesPageConfig> = Specific
64
-
const handler = {} as typeof import("../../pages/users/[id].js")
106
+
type __IsExpected<Specific extends AppPageConfig<"/">> = Specific
107
+
const handler = {} as typeof import("../../app/page.js")
65
108
type __Check = __IsExpected<typeof handler>
66
109
// @ts-ignore
67
110
type __Unused = __Check
68
111
}
69
112
70
-
// Validate ../../pages/users/index.tsx
113
+
// Validate ../../app/api/webhooks/route.ts
71
114
{
72
-
type __IsExpected<Specific extends PagesPageConfig> = Specific
73
-
const handler = {} as typeof import("../../pages/users/index.js")
115
+
type __IsExpected<Specific extends RouteHandlerConfig<"/api/webhooks">> = Specific
116
+
const handler = {} as typeof import("../../app/api/webhooks/route.js")
74
117
type __Check = __IsExpected<typeof handler>
75
118
// @ts-ignore
76
119
type __Unused = __Check
77
120
}
78
121
79
-
// Validate ../../pages/api/users/index.ts
122
+
123
+
124
+
125
+
126
+
// Validate ../../app/donate-with-checkout/result/layout.tsx
80
127
{
81
-
type __IsExpected<Specific extends ApiRouteConfig> = Specific
82
-
const handler = {} as typeof import("../../pages/api/users/index.js")
128
+
type __IsExpected<Specific extends LayoutConfig<"/donate-with-checkout/result">> = Specific
129
+
const handler = {} as typeof import("../../app/donate-with-checkout/result/layout.js")
130
+
type __Check = __IsExpected<typeof handler>
131
+
// @ts-ignore
132
+
type __Unused = __Check
133
+
}
134
+
135
+
// Validate ../../app/donate-with-elements/result/layout.tsx
136
+
{
137
+
type __IsExpected<Specific extends LayoutConfig<"/donate-with-elements/result">> = Specific
138
+
const handler = {} as typeof import("../../app/donate-with-elements/result/layout.js")
83
139
type __Check = __IsExpected<typeof handler>
84
140
// @ts-ignore
85
141
type __Unused = __Check
86
142
}
87
143
144
+
// Validate ../../app/donate-with-embedded-checkout/result/layout.tsx
145
+
{
146
+
type __IsExpected<Specific extends LayoutConfig<"/donate-with-embedded-checkout/result">> = Specific
147
+
const handler = {} as typeof import("../../app/donate-with-embedded-checkout/result/layout.js")
148
+
type __Check = __IsExpected<typeof handler>
149
+
// @ts-ignore
150
+
type __Unused = __Check
151
+
}
88
152
153
+
// Validate ../../app/layout.tsx
154
+
{
155
+
type __IsExpected<Specific extends LayoutConfig<"/">> = Specific
156
+
const handler = {} as typeof import("../../app/layout.js")
157
+
type __Check = __IsExpected<typeof handler>
158
+
// @ts-ignore
159
+
type __Unused = __Check
160
+
}
+11
-7
utils/get-stripejs.ts
+11
-7
utils/get-stripejs.ts
···
1
+
/**
2
+
* This is a singleton to ensure we only instantiate Stripe once.
3
+
*/
1
4
import { Stripe, loadStripe } from "@stripe/stripe-js";
2
5
3
6
let stripePromise: Promise<Stripe | null>;
4
-
const getStripe = () => {
5
-
if (!stripePromise) {
6
-
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
7
-
}
8
-
return stripePromise;
9
-
};
10
7
11
-
export default getStripe;
8
+
export default function getStripe(): Promise<Stripe | null> {
9
+
if (!stripePromise)
10
+
stripePromise = loadStripe(
11
+
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string,
12
+
);
13
+
14
+
return stripePromise;
15
+
}
-9
utils/sample-data.ts
-9
utils/sample-data.ts
+30
utils/stripe-helpers.ts
+30
utils/stripe-helpers.ts
···
1
+
export function formatAmountForDisplay(
2
+
amount: number,
3
+
currency: string,
4
+
): string {
5
+
let numberFormat = new Intl.NumberFormat(["en-US"], {
6
+
style: "currency",
7
+
currency: currency,
8
+
currencyDisplay: "symbol",
9
+
});
10
+
return numberFormat.format(amount);
11
+
}
12
+
13
+
export function formatAmountForStripe(
14
+
amount: number,
15
+
currency: string,
16
+
): number {
17
+
let numberFormat = new Intl.NumberFormat(["en-US"], {
18
+
style: "currency",
19
+
currency: currency,
20
+
currencyDisplay: "symbol",
21
+
});
22
+
const parts = numberFormat.formatToParts(amount);
23
+
let zeroDecimalCurrency: boolean = true;
24
+
for (let part of parts) {
25
+
if (part.type === "decimal") {
26
+
zeroDecimalCurrency = false;
27
+
}
28
+
}
29
+
return zeroDecimalCurrency ? amount : Math.round(amount * 100);
30
+
}