From f82445b01c885c2dce65c99043666f4a3efdbd9d Mon Sep 17 00:00:00 2001 From: Jonathan Steele <83410553+jsteele-stripe@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:09:04 +0000 Subject: [PATCH] example: Add Stripe embedded Checkout example (#60378) --- examples/with-stripe-typescript/README.md | 2 +- .../app/actions/stripe.ts | 28 +++++-- .../app/components/CheckoutForm.tsx | 76 ++++++++++++++----- .../app/donate-with-checkout/page.tsx | 6 +- .../app/donate-with-checkout/result/error.tsx | 2 - .../app/donate-with-elements/result/error.tsx | 2 - .../donate-with-embedded-checkout/page.tsx | 17 +++++ .../result/error.tsx | 3 + .../result/layout.tsx | 18 +++++ .../result/page.tsx | 28 +++++++ examples/with-stripe-typescript/app/page.tsx | 11 ++- examples/with-stripe-typescript/package.json | 6 +- 12 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 examples/with-stripe-typescript/app/donate-with-embedded-checkout/page.tsx create mode 100644 examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/error.tsx create mode 100644 examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/layout.tsx create mode 100644 examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/page.tsx diff --git a/examples/with-stripe-typescript/README.md b/examples/with-stripe-typescript/README.md index 77da54231b2aa..03daf75c95090 100644 --- a/examples/with-stripe-typescript/README.md +++ b/examples/with-stripe-typescript/README.md @@ -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= diff --git a/examples/with-stripe-typescript/app/actions/stripe.ts b/examples/with-stripe-typescript/app/actions/stripe.ts index 8f0955d9fa0b9..43acbc47318eb 100644 --- a/examples/with-stripe-typescript/app/actions/stripe.ts +++ b/examples/with-stripe-typescript/app/actions/stripe.ts @@ -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 { +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", @@ -29,13 +36,20 @@ export async function createCheckoutSession(data: FormData): Promise { }, }, ], - 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( diff --git a/examples/with-stripe-typescript/app/components/CheckoutForm.tsx b/examples/with-stripe-typescript/app/components/CheckoutForm.tsx index e459461416ef5..bf2fc54b64cf7 100644 --- a/examples/with-stripe-typescript/app/components/CheckoutForm.tsx +++ b/examples/with-stripe-typescript/app/components/CheckoutForm.tsx @@ -1,5 +1,7 @@ "use client"; +import type Stripe from "stripe"; + import React, { useState } from "react"; import CustomDonationInput from "@/components/CustomDonationInput"; @@ -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(false); const [input, setInput] = useState<{ customDonation: number }>({ customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), }); + const [clientSecret, setClientSecret] = useState(null); const handleInputChange: React.ChangeEventHandler = ( e, @@ -23,26 +35,48 @@ export default function CheckoutForm(): JSX.Element { [e.currentTarget.name]: e.currentTarget.value, }); + const formAction = async (data: FormData): Promise => { + 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 ( -
- - - - + <> +
+ + + + + + {clientSecret ? ( + + + + ) : null} + ); } diff --git a/examples/with-stripe-typescript/app/donate-with-checkout/page.tsx b/examples/with-stripe-typescript/app/donate-with-checkout/page.tsx index 312dbbbf20a8c..fb5ad615e7c5c 100644 --- a/examples/with-stripe-typescript/app/donate-with-checkout/page.tsx +++ b/examples/with-stripe-typescript/app/donate-with-checkout/page.tsx @@ -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 (
-

Donate with Checkout

+

Donate with hosted Checkout

Donate to our project 💖

- +
); } diff --git a/examples/with-stripe-typescript/app/donate-with-checkout/result/error.tsx b/examples/with-stripe-typescript/app/donate-with-checkout/result/error.tsx index 0d7fd41bd3178..3b5a579090fb4 100644 --- a/examples/with-stripe-typescript/app/donate-with-checkout/result/error.tsx +++ b/examples/with-stripe-typescript/app/donate-with-checkout/result/error.tsx @@ -1,5 +1,3 @@ -"use client"; - export default function Error({ error }: { error: Error }) { return

{error.message}

; } diff --git a/examples/with-stripe-typescript/app/donate-with-elements/result/error.tsx b/examples/with-stripe-typescript/app/donate-with-elements/result/error.tsx index 0d7fd41bd3178..3b5a579090fb4 100644 --- a/examples/with-stripe-typescript/app/donate-with-elements/result/error.tsx +++ b/examples/with-stripe-typescript/app/donate-with-elements/result/error.tsx @@ -1,5 +1,3 @@ -"use client"; - export default function Error({ error }: { error: Error }) { return

{error.message}

; } diff --git a/examples/with-stripe-typescript/app/donate-with-embedded-checkout/page.tsx b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/page.tsx new file mode 100644 index 0000000000000..b33bf21b7f11b --- /dev/null +++ b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/page.tsx @@ -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 ( +
+

Donate with embedded Checkout

+

Donate to our project 💖

+ +
+ ); +} diff --git a/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/error.tsx b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/error.tsx new file mode 100644 index 0000000000000..3b5a579090fb4 --- /dev/null +++ b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/error.tsx @@ -0,0 +1,3 @@ +export default function Error({ error }: { error: Error }) { + return

{error.message}

; +} diff --git a/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/layout.tsx b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/layout.tsx new file mode 100644 index 0000000000000..1c6c096c03b17 --- /dev/null +++ b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/layout.tsx @@ -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 ( +
+

Checkout Session Result

+ {children} +
+ ); +} diff --git a/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/page.tsx b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/page.tsx new file mode 100644 index 0000000000000..fc360bc77890a --- /dev/null +++ b/examples/with-stripe-typescript/app/donate-with-embedded-checkout/result/page.tsx @@ -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 { + 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 ( + <> +

Status: {paymentIntent.status}

+

Checkout Session response:

+ + + ); +} diff --git a/examples/with-stripe-typescript/app/page.tsx b/examples/with-stripe-typescript/app/page.tsx index f9335b3c83d06..61c2c2acaf2d5 100644 --- a/examples/with-stripe-typescript/app/page.tsx +++ b/examples/with-stripe-typescript/app/page.tsx @@ -9,12 +9,21 @@ export const metadata: Metadata = { export default function IndexPage(): JSX.Element { return (
    +
  • + +

    Donate with embedded Checkout

    + + +
  • -

    Donate with Checkout

    +

    Donate with hosted Checkout

  • diff --git a/examples/with-stripe-typescript/package.json b/examples/with-stripe-typescript/package.json index deca451495d89..7847d1faf19de 100644 --- a/examples/with-stripe-typescript/package.json +++ b/examples/with-stripe-typescript/package.json @@ -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",