Skip to content
Merged
Show file tree
Hide file tree
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
131 changes: 68 additions & 63 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

- [Learn how to accept a payment](https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements)
- [Build a custom payment form 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 @@ -36,104 +36,109 @@ 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:

```js
// POST /create-checkout-session
const session = await stripe.checkout.sessions.create({
ui_mode: 'elements',
mode: 'payment',
return_url: 'https://example.com/order/123/complete',
line_items: [
{
price_data: {
currency: 'usd',
product_data: {name: 'T-shirt'},
unit_amount: 1099,
},
quantity: 1,
},
],
});

res.json({clientSecret: session.client_secret});
```

Client:

```jsx
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
import {createRoot} from 'react-dom/client';
import {loadStripe} from '@stripe/stripe-js';
import {
PaymentElement,
Elements,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
CheckoutElementsProvider,
useCheckoutElements,
} from '@stripe/react-stripe-js/checkout';

const CheckoutForm = () => {
const stripe = useStripe();
const elements = useElements();

const result = useCheckoutElements();
const [errorMessage, setErrorMessage] = useState(null);

const handleSubmit = async (event) => {
event.preventDefault();

if (elements == null) {
if (result.type !== 'success') {
return;
}

// Trigger form validation and wallet collection
const {error: submitError} = await elements.submit();
if (submitError) {
// Show error to your customer
setErrorMessage(submitError.message);
return;
}

// Create the PaymentIntent and obtain clientSecret from your server endpoint
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)
try {
await result.checkout.confirm({
returnUrl: 'https://example.com/order/123/complete',
});
} catch (error) {
setErrorMessage(error.message);
} 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`.
}
};

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

return (
<form onSubmit={handleSubmit}>
<PaymentElement />
<button type="submit" disabled={!stripe || !elements}>
<button type="submit" disabled={result.type !== 'success'}>
Pay
</button>
{/* Show error message to your customers */}
{errorMessage && <div>{errorMessage}</div>}
</form>
);
};

const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
// Use the publishable key for the same account that created the Checkout Session.
const stripePromise = loadStripe('pk_test_...');

const options = {
mode: 'payment',
amount: 1099,
currency: 'usd',
// Fully customizable with appearance API.
appearance: {
/*...*/
},
};
const App = () => {
// Fetch clientSecret from your server when the page loads.
// e.g. POST /create-checkout-session → { clientSecret }
const clientSecret = '...';

const App = () => (
<Elements stripe={stripePromise} options={options}>
<CheckoutForm />
</Elements>
);
return (
<CheckoutElementsProvider stripe={stripePromise} options={{clientSecret}}>
<CheckoutForm />
</CheckoutElementsProvider>
);
};

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

#### Using class components
#### 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 ReactDOM from 'react-dom';
import {createRoot} from 'react-dom/client';
import {loadStripe} from '@stripe/stripe-js';
import {
PaymentElement,
Expand Down Expand Up @@ -223,7 +228,7 @@ const App = () => (
</Elements>
);

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

### TypeScript support
Expand Down
34 changes: 13 additions & 21 deletions examples/hooks/14-Issuing-Elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,16 @@ const IssuingElementsDemo: React.FC<IssuingElementsDemoProps> = ({
const handleGetElement = useCallback(() => {
if (!elements) return;

const numberEl: StripeIssuingCardNumberDisplayElement | null = elements.getElement(
IssuingCardNumberDisplayElement
);
const cvcEl: StripeIssuingCardCvcDisplayElement | null = elements.getElement(
IssuingCardCvcDisplayElement
);
const expiryEl: StripeIssuingCardExpiryDisplayElement | null = elements.getElement(
IssuingCardExpiryDisplayElement
);
const pinEl: StripeIssuingCardPinDisplayElement | null = elements.getElement(
IssuingCardPinDisplayElement
);
const copyEl: StripeIssuingCardCopyButtonElement | null = elements.getElement(
IssuingCardCopyButtonElement
);
const numberEl: StripeIssuingCardNumberDisplayElement | null =
elements.getElement(IssuingCardNumberDisplayElement);
const cvcEl: StripeIssuingCardCvcDisplayElement | null =
elements.getElement(IssuingCardCvcDisplayElement);
const expiryEl: StripeIssuingCardExpiryDisplayElement | null =
elements.getElement(IssuingCardExpiryDisplayElement);
const pinEl: StripeIssuingCardPinDisplayElement | null =
elements.getElement(IssuingCardPinDisplayElement);
const copyEl: StripeIssuingCardCopyButtonElement | null =
elements.getElement(IssuingCardCopyButtonElement);

console.log('[getElement results]', {
number: numberEl,
Expand Down Expand Up @@ -192,12 +187,9 @@ const IssuingElementsDemo: React.FC<IssuingElementsDemoProps> = ({
};

// Type-safe options for each copy button variant
const toCopyValues: Array<StripeIssuingCardCopyButtonElementOptions['toCopy']> = [
'number',
'cvc',
'expiry',
'pin',
];
const toCopyValues: Array<
StripeIssuingCardCopyButtonElementOptions['toCopy']
> = ['number', 'cvc', 'expiry', 'pin'];

return (
<div>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"eslint-plugin-react-hooks": "^1.7.0",
"fork-ts-checker-webpack-plugin": "^4.0.3",
"jest": "^25.1.0",
"prettier": "^1.19.1",
"prettier": "^2.8.8",
"react": "18.1.0",
"react-docgen-typescript-loader": "^3.6.0",
"react-dom": "18.1.0",
Expand Down
4 changes: 2 additions & 2 deletions src/checkout/components/CheckoutContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ const mapStateToCheckoutResult = <
const {getSession: _getSession, ...otherCheckoutActions} = checkoutActions;
return {
type: 'success',
checkout: ({
checkout: {
...session,
...sdkMethods,
...otherCheckoutActions,
} as unknown) as T,
} as unknown as T,
};
} else if (checkoutState.type === 'loading') {
return {type: 'loading'};
Expand Down
36 changes: 12 additions & 24 deletions src/checkout/components/CheckoutElementsProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,8 @@ describe('CheckoutElementsProvider', () => {
);

const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk;
const {
getSession: _getSession,
...otherCheckoutActions
} = testMockCheckoutActions;
const {getSession: _getSession, ...otherCheckoutActions} =
testMockCheckoutActions;

const expectedCheckout = {
...elementsMethods,
Expand Down Expand Up @@ -217,10 +215,8 @@ describe('CheckoutElementsProvider', () => {
// Every action key from the SDK (except getSession, whose fields are
// spread onto checkout as session data) must be a function. Derived
// from the mock so this stays honest if the shared mock is updated.
const {
getSession: _getSession,
...expectedActionKeys
} = testMockCheckoutActions;
const {getSession: _getSession, ...expectedActionKeys} =
testMockCheckoutActions;
Object.keys(expectedActionKeys).forEach((key) => {
expect(typeof (checkout as any)[key]).toBe('function');
});
Expand Down Expand Up @@ -253,10 +249,8 @@ describe('CheckoutElementsProvider', () => {
}
const {checkout} = result.current;

const {
getSession: _getSession,
...expectedActionKeys
} = testMockCheckoutActions;
const {getSession: _getSession, ...expectedActionKeys} =
testMockCheckoutActions;
Object.keys(expectedActionKeys).forEach((key) => {
expect(typeof (checkout as any)[key]).toBe('function');
});
Expand Down Expand Up @@ -347,10 +341,8 @@ describe('CheckoutElementsProvider', () => {
);

const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk;
const {
getSession: _getSession,
...otherCheckoutActions
} = testMockCheckoutActions;
const {getSession: _getSession, ...otherCheckoutActions} =
testMockCheckoutActions;

const expectedCheckout = {
...elementsMethods,
Expand Down Expand Up @@ -397,10 +389,8 @@ describe('CheckoutElementsProvider', () => {
);

const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk;
const {
getSession: _getSession,
...otherCheckoutActions
} = testMockCheckoutActions;
const {getSession: _getSession, ...otherCheckoutActions} =
testMockCheckoutActions;

const expectedCheckout = {
...elementsMethods,
Expand Down Expand Up @@ -452,10 +442,8 @@ describe('CheckoutElementsProvider', () => {
);

const {on: _on, loadActions: _loadActions, ...elementsMethods} = mockSdk;
const {
getSession: _getSession,
...otherCheckoutActions
} = testMockCheckoutActions;
const {getSession: _getSession, ...otherCheckoutActions} =
testMockCheckoutActions;

const expectedCheckout = {
...elementsMethods,
Expand Down
6 changes: 3 additions & 3 deletions src/checkout/components/CheckoutElementsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ const maybeSdk = (
}
};

export const CheckoutElementsProvider: FunctionComponent<PropsWithChildren<
CheckoutElementsProviderProps
>> = (({
export const CheckoutElementsProvider: FunctionComponent<
PropsWithChildren<CheckoutElementsProviderProps>
> = (({
stripe: rawStripeProp,
options,
children,
Expand Down
6 changes: 2 additions & 4 deletions src/checkout/components/CheckoutFormProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,8 @@ describe('CheckoutFormProvider', () => {
);

const {on: _on, loadActions: _loadActions, ...sdkMethods} = mockSdk;
const {
getSession: _getSession,
...otherCheckoutActions
} = testMockCheckoutActions;
const {getSession: _getSession, ...otherCheckoutActions} =
testMockCheckoutActions;

expect(result.current).toEqual({
type: 'success',
Expand Down
6 changes: 3 additions & 3 deletions src/checkout/components/CheckoutFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ interface PrivateCheckoutFormProviderProps {
const INVALID_STRIPE_ERROR =
'Invalid prop `stripe` supplied to `CheckoutFormProvider`. We recommend using the `loadStripe` utility from `@stripe/stripe-js`. See https://stripe.com/docs/stripe-js/react#elements-props-stripe for details.';

export const CheckoutFormProvider: FunctionComponent<PropsWithChildren<
CheckoutFormProviderProps
>> = (({
export const CheckoutFormProvider: FunctionComponent<
PropsWithChildren<CheckoutFormProviderProps>
> = (({
stripe: rawStripeProp,
options,
children,
Expand Down
Loading
Loading