Skip to content

Commit

Permalink
example: Add Stripe embedded Checkout example (#60378)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsteele-stripe committed Jan 23, 2024
1 parent a7a403f commit f82445b
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 40 deletions.
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}`,
}),
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

0 comments on commit f82445b

Please sign in to comment.