diff --git a/README.md b/README.md index 2b7e3edb..bc2c7f2c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ version, we recommend using legacy ## Getting started -- [Build a custom payment form using the Checkout Sessions API](https://docs.stripe.com/payments/accept-a-payment?payment-ui=elements&api-integration=checkout) +- [Build a custom checkout page using the Checkout Sessions API](https://docs.stripe.com/payments/accept-a-payment?payment-ui=elements&api-integration=checkout) - [Add React Stripe.js to your React app](https://stripe.com/docs/stripe-js/react#setup) - [Try it out using CodeSandbox](https://codesandbox.io/s/react-stripe-official-q1loc?fontsize=14&hidenavigation=1&theme=dark) @@ -25,7 +25,16 @@ version, we recommend using legacy - [Legacy `react-stripe-elements` docs](https://github.com/stripe/react-stripe-elements/#react-stripe-elements) - [Examples](examples) -### Minimal example +## Build a custom checkout page + +For a new custom checkout page, we recommend the +[Checkout Sessions API](https://docs.stripe.com/payments/accept-a-payment?payment-ui=elements&api-integration=checkout) +with `ui_mode: 'elements'`. This lets you combine Stripe Elements with your own +React layout while Checkout Sessions manages the checkout state. If you want to +own every part of your checkout, the lower-level +[Payment Intents API](https://docs.stripe.com/payments/accept-a-payment?payment-ui=elements&api-integration=payment-intents) +provides more fine-grained control, but requires significantly more code and +ongoing maintenance. First, install React Stripe.js and [Stripe.js](https://github.com/stripe/stripe-js). @@ -34,16 +43,8 @@ First, install React Stripe.js and npm install @stripe/react-stripe-js @stripe/stripe-js ``` -#### Using hooks - -> **Building a custom payment form?** Use the -> [Checkout Sessions API](https://docs.stripe.com/payments/accept-a-payment?payment-ui=elements&api-integration=checkout) -> integration shown below — the recommended approach for most integrations. -> Create a Checkout Session on your server with `ui_mode: 'elements'` and pass -> its `clientSecret` to `CheckoutElementsProvider`. - -Your server endpoint should create a Checkout Session and return its client -secret: +Create a Checkout Session on your server using trusted product and pricing data, +then return its client secret: ```js // POST /create-checkout-session @@ -63,6 +64,10 @@ const session = await stripe.checkout.sessions.create({ ], }); +if (!session.client_secret) { + throw new Error('Checkout Session is missing a client secret.'); +} + res.json({clientSecret: session.client_secret}); ``` @@ -81,151 +86,94 @@ import { const CheckoutForm = () => { const result = useCheckoutElements(); const [errorMessage, setErrorMessage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (event) => { event.preventDefault(); - if (result.type !== 'success') { + if (result.type !== 'success' || !result.checkout.canConfirm) { return; } + setIsSubmitting(true); + setErrorMessage(null); + try { - await result.checkout.confirm({ + const confirmResult = await result.checkout.confirm({ returnUrl: 'https://example.com/order/123/complete', }); + + if (confirmResult.type === 'error') { + setErrorMessage(confirmResult.error.message); + } } catch (error) { - setErrorMessage(error.message); + setErrorMessage( + error instanceof Error ? error.message : 'An unexpected error occurred.' + ); + } finally { + setIsSubmitting(false); } }; + if (result.type === 'loading') { + return
Loading checkout...
; + } + if (result.type === 'error') { return
{result.error.message}
; } + const {checkout} = result; + return ( -
- - - {errorMessage &&
{errorMessage}
} - + <> + +

Total: {checkout.total.total.amount}

+
+ + + {errorMessage &&
{errorMessage}
} + + ); }; // Use the publishable key for the same account that created the Checkout Session. const stripePromise = loadStripe('pk_test_...'); -const App = () => { - // Fetch clientSecret from your server when the page loads. - // e.g. POST /create-checkout-session → { clientSecret } - const clientSecret = '...'; +const clientSecretPromise = fetch('/create-checkout-session', { + method: 'POST', +}).then(async (response) => { + const body = await response.json(); - return ( - - - - ); -}; - -createRoot(document.getElementById('root')).render(); -``` - -#### Using PaymentElement directly - -For existing integrations or when you need fine-grained control over the -PaymentIntents flow, use `Elements` with `PaymentElement`: - -```jsx -import React from 'react'; -import {createRoot} from 'react-dom/client'; -import {loadStripe} from '@stripe/stripe-js'; -import { - PaymentElement, - Elements, - ElementsConsumer, -} from '@stripe/react-stripe-js'; - -class CheckoutForm extends React.Component { - handleSubmit = async (event) => { - event.preventDefault(); - const {stripe, elements} = this.props; - - if (elements == null) { - return; - } - - // Trigger form validation and wallet collection - const {error: submitError} = await elements.submit(); - if (submitError) { - // Show error to your customer - return; - } - - // Create the PaymentIntent and obtain clientSecret - const res = await fetch('/create-intent', { - method: 'POST', - }); - - const {client_secret: clientSecret} = await res.json(); - - const {error} = await stripe.confirmPayment({ - //`Elements` instance that was used to create the Payment Element - elements, - clientSecret, - confirmParams: { - return_url: 'https://example.com/order/123/complete', - }, - }); - - if (error) { - // This point will only be reached if there is an immediate error when - // confirming the payment. Show error to your customer (for example, payment - // details incomplete) - } else { - // Your customer will be redirected to your `return_url`. For some payment - // methods like iDEAL, your customer will be redirected to an intermediate - // site first to authorize the payment, then redirected to the `return_url`. - } - }; - - render() { - const {stripe} = this.props; - return ( -
- - - - ); + if (!response.ok) { + throw new Error(body.error ?? 'Unable to create a Checkout Session.'); } -} - -const InjectedCheckoutForm = () => ( - - {({stripe, elements}) => ( - - )} - -); -const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'); + return body.clientSecret; +}); const options = { - mode: 'payment', - amount: 1099, - currency: 'usd', - // Fully customizable with appearance API. - appearance: { - /*...*/ + clientSecret: clientSecretPromise, + elementsOptions: { + appearance: { + theme: 'stripe', + }, }, }; const App = () => ( - - - + + + ); createRoot(document.getElementById('root')).render();