Skip to content
Merged
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
194 changes: 71 additions & 123 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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).
Expand All @@ -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
Expand All @@ -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});
```

Expand All @@ -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 <div>Loading checkout...</div>;
}

if (result.type === 'error') {
return <div>{result.error.message}</div>;
}

const {checkout} = result;

return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={result.type !== 'success'}>
Pay
</button>
{errorMessage && <div>{errorMessage}</div>}
</form>
<>
<ul>
{checkout.lineItems.map((lineItem) => (
<li key={lineItem.id}>
{lineItem.name}: {lineItem.total.amount}
</li>
))}
</ul>
<p>Total: {checkout.total.total.amount}</p>
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!checkout.canConfirm || isSubmitting}>
{isSubmitting ? 'Processing...' : 'Pay'}
</button>
{errorMessage && <div>{errorMessage}</div>}
</form>
</>
);
};

// 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 (
<CheckoutElementsProvider stripe={stripePromise} options={{clientSecret}}>
<CheckoutForm />
</CheckoutElementsProvider>
);
};

createRoot(document.getElementById('root')).render(<App />);
```

#### 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 (
<form onSubmit={this.handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe}>
Pay
</button>
</form>
);
if (!response.ok) {
throw new Error(body.error ?? 'Unable to create a Checkout Session.');
}
}

const InjectedCheckoutForm = () => (
<ElementsConsumer>
{({stripe, elements}) => (
<CheckoutForm stripe={stripe} elements={elements} />
)}
</ElementsConsumer>
);

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 = () => (
<Elements stripe={stripePromise} options={options}>
<InjectedCheckoutForm />
</Elements>
<CheckoutElementsProvider stripe={stripePromise} options={options}>
<CheckoutForm />
</CheckoutElementsProvider>
);

createRoot(document.getElementById('root')).render(<App />);
Expand Down
Loading