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 7 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
34 changes: 25 additions & 9 deletions examples/with-stripe-typescript/app/actions/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

import type { Stripe } from "stripe";

import { redirect } from "next/navigation";
import { headers } from "next/headers";
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 checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create({
mode: "payment",
Expand All @@ -29,13 +34,24 @@ 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: `${headers().get(
'origin'
)}/donate-with-checkout/result?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${headers().get('origin')}/donate-with-checkout`,
}),
...(ui_mode === 'embedded' && {
return_url: `${headers().get(
'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
99 changes: 71 additions & 28 deletions examples/with-stripe-typescript/app/components/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
"use client";

import React, { useState } from "react";
import type Stripe from 'stripe'

import React, { useState } from 'react'

import CustomDonationInput from "@/components/CustomDonationInput";
import StripeTestCards from "@/components/StripeTestCards";

import { formatAmountForDisplay } from "@/utils/stripe-helpers";
import * as config from "@/config";
import { createCheckoutSession } from "@/actions/stripe";
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 {
const [loading] = useState<boolean>(false);
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,57 @@ export default function CheckoutForm(): JSX.Element {
[e.currentTarget.name]: e.currentTarget.value,
});

const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
manovotny marked this conversation as resolved.
Show resolved Hide resolved
e
): Promise<void> => {
e.preventDefault()

if (props.uiMode === 'hosted') {
const { url } = await createCheckoutSession(
new FormData(e.target as HTMLFormElement)
)

window.location.assign(url as string)
manovotny marked this conversation as resolved.
Show resolved Hide resolved
}

const { client_secret } = await createCheckoutSession(
new FormData(e.target as HTMLFormElement)
)

setClientSecret(client_secret)
}

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>
);
<React.Fragment>
manovotny marked this conversation as resolved.
Show resolved Hide resolved
<form onSubmit={handleSubmit}>
<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}
</React.Fragment>
manovotny marked this conversation as resolved.
Show resolved Hide resolved
)
}
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
@@ -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,5 @@
'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,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} />
</>
)
}
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
Loading
Loading