Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

example: Add Stripe embedded Checkout example #60378

Merged
merged 19 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/with-stripe-typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Copy the `.env.local.example` file into a file named `.env.local` in the root di
cp .env.local.example .env.local
```

You will need a Stripe account ([register](https://dashboard.stripe.com/register)) to run this sample. Go to the Stripe [developer dashboard](https://stripe.com/docs/development#api-keys) to find your API keys and replace them in the `.env.local` file.
You will need a Stripe account ([register](https://dashboard.stripe.com/register)) to run this sample. Go to the Stripe [developer dashboard](https://dashboard.stripe.com/apikeys) to find your API keys and replace them in the `.env.local` file.

```bash
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<replace-with-your-publishable-key>
Expand Down
28 changes: 21 additions & 7 deletions examples/with-stripe-typescript/app/actions/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

import type { Stripe } from "stripe";

import { redirect } from "next/navigation";
import { headers } from "next/headers";

import { CURRENCY } from "@/config";
import { formatAmountForStripe } from "@/utils/stripe-helpers";
import { stripe } from "@/lib/stripe";

export async function createCheckoutSession(data: FormData): Promise<void> {
export async function createCheckoutSession(
data: FormData,
): Promise<{ client_secret: string | null; url: string | null }> {
const ui_mode = data.get(
"uiMode",
) as Stripe.Checkout.SessionCreateParams.UiMode;

const origin: string = headers().get("origin") as string;

const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create({
mode: "payment",
Expand All @@ -29,13 +36,20 @@ export async function createCheckoutSession(data: FormData): Promise<void> {
},
},
],
success_url: `${headers().get(
"origin",
)}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${headers().get("origin")}/donate-with-checkout`,
...(ui_mode === "hosted" && {
success_url: `${origin}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/donate-with-checkout`,
}),
...(ui_mode === "embedded" && {
return_url: `${origin}/donate-with-embedded-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
}),
jsteele-stripe marked this conversation as resolved.
Show resolved Hide resolved
ui_mode,
});

redirect(checkoutSession.url as string);
return {
client_secret: checkoutSession.client_secret,
url: checkoutSession.url,
};
}

export async function createPaymentIntent(
Expand Down
76 changes: 55 additions & 21 deletions examples/with-stripe-typescript/app/components/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import type Stripe from "stripe";

import React, { useState } from "react";

import CustomDonationInput from "@/components/CustomDonationInput";
Expand All @@ -8,12 +10,22 @@ import StripeTestCards from "@/components/StripeTestCards";
import { formatAmountForDisplay } from "@/utils/stripe-helpers";
import * as config from "@/config";
import { createCheckoutSession } from "@/actions/stripe";
import getStripe from "@/utils/get-stripejs";
import {
EmbeddedCheckout,
EmbeddedCheckoutProvider,
} from "@stripe/react-stripe-js";

interface CheckoutFormProps {
uiMode: Stripe.Checkout.SessionCreateParams.UiMode;
}

export default function CheckoutForm(): JSX.Element {
export default function CheckoutForm(props: CheckoutFormProps): JSX.Element {
const [loading] = useState<boolean>(false);
const [input, setInput] = useState<{ customDonation: number }>({
customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP),
});
const [clientSecret, setClientSecret] = useState<string | null>(null);

const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
e,
Expand All @@ -23,26 +35,48 @@ export default function CheckoutForm(): JSX.Element {
[e.currentTarget.name]: e.currentTarget.value,
});

const formAction = async (data: FormData): Promise<void> => {
const uiMode = data.get(
"uiMode",
) as Stripe.Checkout.SessionCreateParams.UiMode;
const { client_secret, url } = await createCheckoutSession(data);

if (uiMode === "embedded") return setClientSecret(client_secret);

window.location.assign(url as string);
};

return (
<form action={createCheckoutSession}>
<CustomDonationInput
className="checkout-style"
name="customDonation"
min={config.MIN_AMOUNT}
max={config.MAX_AMOUNT}
step={config.AMOUNT_STEP}
currency={config.CURRENCY}
onChange={handleInputChange}
value={input.customDonation}
/>
<StripeTestCards />
<button
className="checkout-style-background"
type="submit"
disabled={loading}
>
Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)}
</button>
</form>
<>
<form action={formAction}>
<input type="hidden" name="uiMode" value={props.uiMode} />
<CustomDonationInput
className="checkout-style"
name="customDonation"
min={config.MIN_AMOUNT}
max={config.MAX_AMOUNT}
step={config.AMOUNT_STEP}
currency={config.CURRENCY}
onChange={handleInputChange}
value={input.customDonation}
/>
<StripeTestCards />
<button
className="checkout-style-background"
type="submit"
disabled={loading}
>
Donate {formatAmountForDisplay(input.customDonation, config.CURRENCY)}
</button>
</form>
{clientSecret ? (
<EmbeddedCheckoutProvider
stripe={getStripe()}
options={{ clientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
) : null}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import type { Metadata } from "next";
import CheckoutForm from "@/components/CheckoutForm";

export const metadata: Metadata = {
title: "Donate with Checkout | Next.js + TypeScript Example",
title: "Donate with hosted Checkout | Next.js + TypeScript Example",
};

export default function DonatePage(): JSX.Element {
return (
<div className="page-container">
<h1>Donate with Checkout</h1>
<h1>Donate with hosted Checkout</h1>
<p>Donate to our project 💖</p>
<CheckoutForm />
<CheckoutForm uiMode="hosted" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

export default function Error({ error }: { error: Error }) {
return <h2>{error.message}</h2>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

export default function Error({ error }: { error: Error }) {
return <h2>{error.message}</h2>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Metadata } from "next";

import CheckoutForm from "@/components/CheckoutForm";

export const metadata: Metadata = {
title: "Donate with embedded Checkout | Next.js + TypeScript Example",
};

export default function DonatePage(): JSX.Element {
return (
<div className="page-container">
<h1>Donate with embedded Checkout</h1>
<p>Donate to our project 💖</p>
<CheckoutForm uiMode="embedded" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Error({ error }: { error: Error }) {
return <h2>{error.message}</h2>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Checkout Session Result",
};

export default function ResultLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return (
<div className="page-container">
<h1>Checkout Session Result</h1>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Stripe } from "stripe";

import PrintObject from "@/components/PrintObject";
import { stripe } from "@/lib/stripe";

export default async function ResultPage({
searchParams,
}: {
searchParams: { session_id: string };
}): Promise<JSX.Element> {
if (!searchParams.session_id)
throw new Error("Please provide a valid session_id (`cs_test_...`)");

const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.retrieve(searchParams.session_id, {
expand: ["line_items", "payment_intent"],
});

const paymentIntent = checkoutSession.payment_intent as Stripe.PaymentIntent;

return (
<>
<h2>Status: {paymentIntent.status}</h2>
<h3>Checkout Session response:</h3>
<PrintObject content={checkoutSession} />
</>
);
}
11 changes: 10 additions & 1 deletion examples/with-stripe-typescript/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ export const metadata: Metadata = {
export default function IndexPage(): JSX.Element {
return (
<ul className="card-list">
<li>
<Link
href="/donate-with-embedded-checkout"
className="card checkout-style-background"
>
<h2 className="bottom">Donate with embedded Checkout</h2>
<img src="/checkout-one-time-payments.svg" />
</Link>
</li>
<li>
<Link
href="/donate-with-checkout"
className="card checkout-style-background"
>
<h2 className="bottom">Donate with Checkout</h2>
<h2 className="bottom">Donate with hosted Checkout</h2>
<img src="/checkout-one-time-payments.svg" />
</Link>
</li>
Expand Down
6 changes: 3 additions & 3 deletions examples/with-stripe-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
"start": "next start"
},
"dependencies": {
"@stripe/react-stripe-js": "2.1.1",
"@stripe/stripe-js": "1.54.1",
"@stripe/react-stripe-js": "2.4.0",
"@stripe/stripe-js": "2.2.2",
"next": "latest",
"react": "18.2.0",
"react-dom": "18.2.0",
"server-only": "0.0.1",
"stripe": "12.14.0"
"stripe": "14.8.0"
},
"devDependencies": {
"@types/node": "20.4.6",
Expand Down
26 changes: 13 additions & 13 deletions pnpm-lock.yaml

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