` if you do not want to use hooks.
183 |
184 | import {ElementsConsumer} from '@stripe/react-stripe-js';
185 |
186 | const CheckoutForm = (props) => {
187 | const {stripe, elements} = props;
188 |
189 | // the rest of CheckoutForm...
190 | };
191 |
192 | const InjectedCheckoutForm = () => (
193 |
194 | {({stripe, elements}) => (
195 |
196 | )}
197 |
198 | );
199 | ```
200 |
201 |
202 |
203 | ## 5. Pass in the Element instance to other Stripe.js methods.
204 |
205 | React Stripe.js does not have the automatic Element detection.
206 |
207 | #### Before
208 |
209 | ```jsx
210 | import {injectStripe, CardElement} from 'react-stripe-elements';
211 |
212 | const CheckoutForm = (props) => {
213 | const {stripe, elements} = props;
214 |
215 | const handleSubmit = (event) => {
216 | event.preventDefault();
217 |
218 | // Element will be inferred and is not passed to Stripe.js methods.
219 | // e.g. stripe.createToken
220 | stripe.createToken();
221 | };
222 |
223 | return (
224 |
228 | );
229 | };
230 |
231 | const InjectedCheckoutForm = injectStripe(CheckoutForm);
232 | ```
233 |
234 | #### After
235 |
236 | ```jsx
237 | import {useStripe, useElements, CardElement} from '@stripe/react-stripe-js';
238 |
239 | const CheckoutForm = (props) => {
240 | const stripe = useStripe();
241 | const elements = useElements();
242 |
243 | const handleSubmit = (event) => {
244 | event.preventDefault();
245 |
246 | // Use elements.getElement to get a reference to the mounted Element.
247 | const cardElement = elements.getElement(CardElement);
248 |
249 | // Pass the Element directly to other Stripe.js methods:
250 | // e.g. createToken - https://stripe.com/docs/js/tokens_sources/create_token?type=cardElement
251 | stripe.createToken(cardElement);
252 |
253 | // or createPaymentMethod - https://stripe.com/docs/js/payment_methods/create_payment_method
254 | stripe.createPaymentMethod({
255 | type: 'card',
256 | card: cardElement,
257 | });
258 |
259 | // or confirmCardPayment - https://stripe.com/docs/js/payment_intents/confirm_card_payment
260 | stripe.confirmCardPayment(paymentIntentClientSecret, {
261 | payment_method: {
262 | card: cardElement,
263 | },
264 | });
265 | };
266 |
267 | return (
268 |
272 | );
273 | };
274 | ```
275 |
276 |
277 |
278 | ---
279 |
280 | ### More Information
281 |
282 | - [React Stripe.js Docs](https://stripe.com/docs/stripe-js/react)
283 | - [Examples](https://github.com/stripe/react-stripe-js/tree/master/examples)
284 |
--------------------------------------------------------------------------------
/examples/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: '../.eslintrc.yml'
3 | rules:
4 | import/no-extraneous-dependencies: 0
5 |
--------------------------------------------------------------------------------
/examples/class-components/0-Card-Minimal.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {CardElement, Elements, ElementsConsumer} from '../../src';
8 | import '../styles/common.css';
9 |
10 | class CheckoutForm extends React.Component {
11 | handleSubmit = async (event) => {
12 | // Block native form submission.
13 | event.preventDefault();
14 |
15 | const {stripe, elements} = this.props;
16 |
17 | if (!stripe || !elements) {
18 | // Stripe.js has not loaded yet. Make sure to disable
19 | // form submission until Stripe.js has loaded.
20 | return;
21 | }
22 |
23 | // Get a reference to a mounted CardElement. Elements knows how
24 | // to find your CardElement because there can only ever be one of
25 | // each type of element.
26 | const card = elements.getElement(CardElement);
27 |
28 | if (card == null) {
29 | return;
30 | }
31 |
32 | const {error, paymentMethod} = await stripe.createPaymentMethod({
33 | type: 'card',
34 | card,
35 | });
36 |
37 | if (error) {
38 | console.log('[error]', error);
39 | } else {
40 | console.log('[PaymentMethod]', paymentMethod);
41 | }
42 | };
43 |
44 | render() {
45 | const {stripe} = this.props;
46 | return (
47 |
68 | );
69 | }
70 | }
71 |
72 | const InjectedCheckoutForm = () => {
73 | return (
74 |
75 | {({elements, stripe}) => (
76 |
77 | )}
78 |
79 | );
80 | };
81 |
82 | // Make sure to call `loadStripe` outside of a component’s render to avoid
83 | // recreating the `Stripe` object on every render.
84 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
85 |
86 | const App = () => {
87 | return (
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default App;
95 |
--------------------------------------------------------------------------------
/examples/class-components/1-Card-Detailed.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {CardElement, Elements, ElementsConsumer} from '../../src';
8 |
9 | import '../styles/common.css';
10 | import '../styles/2-Card-Detailed.css';
11 |
12 | const CARD_OPTIONS = {
13 | iconStyle: 'solid',
14 | style: {
15 | base: {
16 | iconColor: '#c4f0ff',
17 | color: '#fff',
18 | fontWeight: 500,
19 | fontFamily: 'Roboto, Open Sans, Segoe UI, sans-serif',
20 | fontSize: '16px',
21 | fontSmoothing: 'antialiased',
22 | ':-webkit-autofill': {
23 | color: '#fce883',
24 | },
25 | '::placeholder': {
26 | color: '#87BBFD',
27 | },
28 | },
29 | invalid: {
30 | iconColor: '#FFC7EE',
31 | color: '#FFC7EE',
32 | },
33 | },
34 | };
35 |
36 | const CardField = ({onChange}) => (
37 |
38 |
39 |
40 | );
41 |
42 | const Field = ({
43 | label,
44 | id,
45 | type,
46 | placeholder,
47 | required,
48 | autoComplete,
49 | value,
50 | onChange,
51 | }) => (
52 |
53 |
56 |
66 |
67 | );
68 |
69 | const SubmitButton = ({processing, error, children, disabled}) => (
70 |
77 | );
78 |
79 | const ErrorMessage = ({children}) => (
80 |
81 |
91 | {children}
92 |
93 | );
94 |
95 | const ResetButton = ({onClick}) => (
96 |
104 | );
105 |
106 | const DEFAULT_STATE = {
107 | error: null,
108 | cardComplete: false,
109 | processing: false,
110 | paymentMethod: null,
111 | email: '',
112 | phone: '',
113 | name: '',
114 | };
115 |
116 | class CheckoutForm extends React.Component {
117 | constructor(props) {
118 | super(props);
119 | this.state = DEFAULT_STATE;
120 | }
121 |
122 | handleSubmit = async (event) => {
123 | event.preventDefault();
124 |
125 | const {stripe, elements} = this.props;
126 | const {email, phone, name, error, cardComplete} = this.state;
127 |
128 | if (!stripe || !elements) {
129 | // Stripe.js has not loaded yet. Make sure to disable
130 | // form submission until Stripe.js has loaded.
131 | return;
132 | }
133 |
134 | const card = elements.getElement(CardElement);
135 |
136 | if (card == null) {
137 | return;
138 | }
139 |
140 | if (error) {
141 | card.focus();
142 | return;
143 | }
144 |
145 | if (cardComplete) {
146 | this.setState({processing: true});
147 | }
148 |
149 | const payload = await stripe.createPaymentMethod({
150 | type: 'card',
151 | card,
152 | billing_details: {
153 | email,
154 | phone,
155 | name,
156 | },
157 | });
158 |
159 | this.setState({processing: false});
160 |
161 | if (payload.error) {
162 | this.setState({error: payload.error});
163 | } else {
164 | this.setState({paymentMethod: payload.paymentMethod});
165 | }
166 | };
167 |
168 | reset = () => {
169 | this.setState(DEFAULT_STATE);
170 | };
171 |
172 | render() {
173 | const {error, processing, paymentMethod, name, email, phone} = this.state;
174 | const {stripe} = this.props;
175 | return paymentMethod ? (
176 |
177 |
178 | Payment successful
179 |
180 |
181 | Thanks for trying Stripe Elements. No money was charged, but we
182 | generated a PaymentMethod: {paymentMethod.id}
183 |
184 |
185 |
186 | ) : (
187 |
241 | );
242 | }
243 | }
244 |
245 | const InjectedCheckoutForm = () => (
246 |
247 | {({stripe, elements}) => (
248 |
249 | )}
250 |
251 | );
252 |
253 | const ELEMENTS_OPTIONS = {
254 | fonts: [
255 | {
256 | cssSrc: 'https://fonts.googleapis.com/css?family=Roboto',
257 | },
258 | ],
259 | };
260 |
261 | // Make sure to call `loadStripe` outside of a component’s render to avoid
262 | // recreating the `Stripe` object on every render.
263 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
264 |
265 | const App = () => {
266 | return (
267 |
268 |
269 |
270 |
271 |
272 | );
273 | };
274 |
275 | export default App;
276 |
--------------------------------------------------------------------------------
/examples/class-components/2-Split-Card.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {
8 | CardNumberElement,
9 | CardCvcElement,
10 | CardExpiryElement,
11 | Elements,
12 | ElementsConsumer,
13 | } from '../../src';
14 |
15 | import {logEvent, Result, ErrorResult} from '../util';
16 | import '../styles/common.css';
17 |
18 | const ELEMENT_OPTIONS = {
19 | style: {
20 | base: {
21 | fontSize: '18px',
22 | color: '#424770',
23 | letterSpacing: '0.025em',
24 | '::placeholder': {
25 | color: '#aab7c4',
26 | },
27 | },
28 | invalid: {
29 | color: '#9e2146',
30 | },
31 | },
32 | };
33 |
34 | class CheckoutForm extends React.Component {
35 | constructor(props) {
36 | super(props);
37 | this.state = {
38 | name: '',
39 | postal: '',
40 | errorMessage: null,
41 | paymentMethod: null,
42 | };
43 | }
44 |
45 | handleSubmit = async (event) => {
46 | event.preventDefault();
47 | const {stripe, elements} = this.props;
48 | const {name, postal} = this.state;
49 |
50 | if (!stripe || !elements) {
51 | // Stripe.js has not loaded yet. Make sure to disable
52 | // form submission until Stripe.js has loaded.
53 | return;
54 | }
55 |
56 | const card = elements.getElement(CardNumberElement);
57 |
58 | if (card == null) {
59 | return;
60 | }
61 |
62 | const payload = await stripe.createPaymentMethod({
63 | type: 'card',
64 | card,
65 | billing_details: {
66 | name,
67 | address: {
68 | postal_code: postal,
69 | },
70 | },
71 | });
72 |
73 | if (payload.error) {
74 | console.log('[error]', payload.error);
75 | this.setState({
76 | errorMessage: payload.error.message,
77 | paymentMethod: null,
78 | });
79 | } else {
80 | console.log('[PaymentMethod]', payload.paymentMethod);
81 | this.setState({
82 | paymentMethod: payload.paymentMethod,
83 | errorMessage: null,
84 | });
85 | }
86 | };
87 |
88 | render() {
89 | const {stripe} = this.props;
90 | const {postal, name, paymentMethod, errorMessage} = this.state;
91 |
92 | return (
93 |
149 | );
150 | }
151 | }
152 |
153 | const InjectedCheckoutForm = () => (
154 |
155 | {({stripe, elements}) => (
156 |
157 | )}
158 |
159 | );
160 |
161 | // Make sure to call `loadStripe` outside of a component’s render to avoid
162 | // recreating the `Stripe` object on every render.
163 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
164 |
165 | const App = () => {
166 | return (
167 |
168 |
169 |
170 | );
171 | };
172 |
173 | export default App;
174 |
--------------------------------------------------------------------------------
/examples/class-components/3-Payment-Request-Button.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment with the PaymentRequestButton using the official Stripe docs.
3 | // https://stripe.com/docs/stripe-js/elements/payment-request-button#react
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {
8 | PaymentRequestButtonElement,
9 | Elements,
10 | ElementsConsumer,
11 | } from '../../src';
12 |
13 | import {Result, ErrorResult} from '../util';
14 | import '../styles/common.css';
15 |
16 | const NotAvailableResult = () => (
17 |
18 |
19 | PaymentRequest is not available in your browser.
20 |
21 | {window.location.protocol !== 'https:' && (
22 |
23 | Try using{' '}
24 |
25 | ngrok
26 | {' '}
27 | to view this demo over https.
28 |
29 | )}
30 |
31 | );
32 |
33 | const ELEMENT_OPTIONS = {
34 | style: {
35 | paymentRequestButton: {
36 | type: 'buy',
37 | theme: 'dark',
38 | },
39 | },
40 | };
41 |
42 | class CheckoutForm extends React.Component {
43 | constructor(props) {
44 | super(props);
45 | this.state = {
46 | canMakePayment: false,
47 | hasCheckedAvailability: false,
48 | errorMessage: null,
49 | };
50 | }
51 |
52 | async componentDidUpdate() {
53 | const {stripe} = this.props;
54 |
55 | if (stripe && !this.paymentRequest) {
56 | // Create PaymentRequest after Stripe.js loads.
57 | this.createPaymentRequest(stripe);
58 | }
59 | }
60 |
61 | async createPaymentRequest(stripe) {
62 | this.paymentRequest = stripe.paymentRequest({
63 | country: 'US',
64 | currency: 'usd',
65 | total: {
66 | label: 'Demo total',
67 | amount: 100,
68 | },
69 | });
70 |
71 | this.paymentRequest.on('paymentmethod', async (event) => {
72 | this.setState({paymentMethod: event.paymentMethod});
73 | event.complete('success');
74 | });
75 |
76 | const canMakePaymentRes = await this.paymentRequest.canMakePayment();
77 | if (canMakePaymentRes) {
78 | this.setState({canMakePayment: true, hasCheckedAvailability: true});
79 | } else {
80 | this.setState({canMakePayment: false, hasCheckedAvailability: true});
81 | }
82 | }
83 |
84 | render() {
85 | const {
86 | canMakePayment,
87 | hasCheckedAvailability,
88 | errorMessage,
89 | paymentMethod,
90 | } = this.state;
91 | return (
92 |
116 | );
117 | }
118 | }
119 |
120 | const InjectedCheckoutForm = () => (
121 |
122 | {({stripe}) => }
123 |
124 | );
125 |
126 | // Make sure to call `loadStripe` outside of a component’s render to avoid
127 | // recreating the `Stripe` object on every render.
128 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
129 |
130 | const App = () => {
131 | return (
132 |
133 |
134 |
135 | );
136 | };
137 |
138 | export default App;
139 |
--------------------------------------------------------------------------------
/examples/class-components/4-IBAN.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a SEPA Debit payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/sepa-debit/accept-a-payment
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {IbanElement, Elements, ElementsConsumer} from '../../src';
8 |
9 | import {logEvent, Result, ErrorResult} from '../util';
10 | import '../styles/common.css';
11 |
12 | const ELEMENT_OPTIONS = {
13 | supportedCountries: ['SEPA'],
14 | style: {
15 | base: {
16 | fontSize: '18px',
17 | color: '#424770',
18 | letterSpacing: '0.025em',
19 | '::placeholder': {
20 | color: '#aab7c4',
21 | },
22 | },
23 | invalid: {
24 | color: '#9e2146',
25 | },
26 | },
27 | };
28 |
29 | class CheckoutForm extends React.Component {
30 | constructor(props) {
31 | super(props);
32 | this.state = {name: '', email: '', errorMessage: null, paymentMethod: null};
33 | }
34 |
35 | handleSubmit = async (event) => {
36 | event.preventDefault();
37 |
38 | const {stripe, elements} = this.props;
39 | const {name, email} = this.state;
40 |
41 | if (!stripe || !elements) {
42 | // Stripe.js has not loaded yet. Make sure to disable
43 | // form submission until Stripe.js has loaded.
44 | return;
45 | }
46 |
47 | const ibanElement = elements.getElement(IbanElement);
48 |
49 | if (ibanElement == null) {
50 | return;
51 | }
52 |
53 | const payload = await stripe.createPaymentMethod({
54 | type: 'sepa_debit',
55 | sepa_debit: ibanElement,
56 | billing_details: {
57 | name,
58 | email,
59 | },
60 | });
61 |
62 | if (payload.error) {
63 | console.log('[error]', payload.error);
64 | this.setState({
65 | errorMessage: payload.error.message,
66 | paymentMethod: null,
67 | });
68 | } else {
69 | console.log('[PaymentMethod]', payload.paymentMethod);
70 | this.setState({
71 | paymentMethod: payload.paymentMethod,
72 | errorMessage: null,
73 | });
74 | }
75 | };
76 |
77 | render() {
78 | const {errorMessage, paymentMethod, name, email} = this.state;
79 | const {stripe} = this.props;
80 | return (
81 |
120 | );
121 | }
122 | }
123 |
124 | const InjectedCheckoutForm = () => (
125 |
126 | {({stripe, elements}) => (
127 |
128 | )}
129 |
130 | );
131 |
132 | // Make sure to call `loadStripe` outside of a component’s render to avoid
133 | // recreating the `Stripe` object on every render.
134 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
135 |
136 | const App = () => {
137 | return (
138 |
139 |
140 |
141 | );
142 | };
143 |
144 | export default App;
145 |
--------------------------------------------------------------------------------
/examples/hooks/0-Card-Minimal.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {CardElement, Elements, useElements, useStripe} from '../../src';
8 |
9 | import '../styles/common.css';
10 |
11 | const CheckoutForm = () => {
12 | const stripe = useStripe();
13 | const elements = useElements();
14 |
15 | const handleSubmit = async (event) => {
16 | // Block native form submission.
17 | event.preventDefault();
18 |
19 | if (!stripe || !elements) {
20 | // Stripe.js has not loaded yet. Make sure to disable
21 | // form submission until Stripe.js has loaded.
22 | return;
23 | }
24 |
25 | // Get a reference to a mounted CardElement. Elements knows how
26 | // to find your CardElement because there can only ever be one of
27 | // each type of element.
28 | const card = elements.getElement(CardElement);
29 |
30 | if (card == null) {
31 | return;
32 | }
33 |
34 | // Use your card Element with other Stripe.js APIs
35 | const {error, paymentMethod} = await stripe.createPaymentMethod({
36 | type: 'card',
37 | card,
38 | });
39 |
40 | if (error) {
41 | console.log('[error]', error);
42 | } else {
43 | console.log('[PaymentMethod]', paymentMethod);
44 | }
45 | };
46 |
47 | return (
48 |
69 | );
70 | };
71 |
72 | // Make sure to call `loadStripe` outside of a component’s render to avoid
73 | // recreating the `Stripe` object on every render.
74 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
75 |
76 | const App = () => {
77 | return (
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default App;
85 |
--------------------------------------------------------------------------------
/examples/hooks/1-Card-Detailed.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React, {useState} from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {CardElement, Elements, useElements, useStripe} from '../../src';
8 |
9 | import '../styles/common.css';
10 | import '../styles/2-Card-Detailed.css';
11 |
12 | const CARD_OPTIONS = {
13 | iconStyle: 'solid',
14 | style: {
15 | base: {
16 | iconColor: '#c4f0ff',
17 | color: '#fff',
18 | fontWeight: 500,
19 | fontFamily: 'Roboto, Open Sans, Segoe UI, sans-serif',
20 | fontSize: '16px',
21 | fontSmoothing: 'antialiased',
22 | ':-webkit-autofill': {
23 | color: '#fce883',
24 | },
25 | '::placeholder': {
26 | color: '#87bbfd',
27 | },
28 | },
29 | invalid: {
30 | iconColor: '#ffc7ee',
31 | color: '#ffc7ee',
32 | },
33 | },
34 | };
35 |
36 | const CardField = ({onChange}) => (
37 |
38 |
39 |
40 | );
41 |
42 | const Field = ({
43 | label,
44 | id,
45 | type,
46 | placeholder,
47 | required,
48 | autoComplete,
49 | value,
50 | onChange,
51 | }) => (
52 |
53 |
56 |
66 |
67 | );
68 |
69 | const SubmitButton = ({processing, error, children, disabled}) => (
70 |
77 | );
78 |
79 | const ErrorMessage = ({children}) => (
80 |
81 |
91 | {children}
92 |
93 | );
94 |
95 | const ResetButton = ({onClick}) => (
96 |
104 | );
105 |
106 | const CheckoutForm = () => {
107 | const stripe = useStripe();
108 | const elements = useElements();
109 | const [error, setError] = useState(null);
110 | const [cardComplete, setCardComplete] = useState(false);
111 | const [processing, setProcessing] = useState(false);
112 | const [paymentMethod, setPaymentMethod] = useState(null);
113 | const [billingDetails, setBillingDetails] = useState({
114 | email: '',
115 | phone: '',
116 | name: '',
117 | });
118 |
119 | const handleSubmit = async (event) => {
120 | event.preventDefault();
121 |
122 | if (!stripe || !elements) {
123 | // Stripe.js has not loaded yet. Make sure to disable
124 | // form submission until Stripe.js has loaded.
125 | return;
126 | }
127 |
128 | const card = elements.getElement(CardElement);
129 |
130 | if (card == null) {
131 | return;
132 | }
133 |
134 | if (error) {
135 | card.focus();
136 | return;
137 | }
138 |
139 | if (cardComplete) {
140 | setProcessing(true);
141 | }
142 |
143 | const payload = await stripe.createPaymentMethod({
144 | type: 'card',
145 | card,
146 | billing_details: billingDetails,
147 | });
148 |
149 | setProcessing(false);
150 |
151 | if (payload.error) {
152 | setError(payload.error);
153 | } else {
154 | setPaymentMethod(payload.paymentMethod);
155 | }
156 | };
157 |
158 | const reset = () => {
159 | setError(null);
160 | setProcessing(false);
161 | setPaymentMethod(null);
162 | setBillingDetails({
163 | email: '',
164 | phone: '',
165 | name: '',
166 | });
167 | };
168 |
169 | return paymentMethod ? (
170 |
171 |
172 | Payment successful
173 |
174 |
175 | Thanks for trying Stripe Elements. No money was charged, but we
176 | generated a PaymentMethod: {paymentMethod.id}
177 |
178 |
179 |
180 | ) : (
181 |
233 | );
234 | };
235 |
236 | const ELEMENTS_OPTIONS = {
237 | fonts: [
238 | {
239 | cssSrc: 'https://fonts.googleapis.com/css?family=Roboto',
240 | },
241 | ],
242 | };
243 |
244 | // Make sure to call `loadStripe` outside of a component’s render to avoid
245 | // recreating the `Stripe` object on every render.
246 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
247 |
248 | const App = () => {
249 | return (
250 |
251 |
252 |
253 |
254 |
255 | );
256 | };
257 |
258 | export default App;
259 |
--------------------------------------------------------------------------------
/examples/hooks/11-Custom-Checkout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {loadStripe} from '@stripe/stripe-js';
3 | import {
4 | PaymentElement,
5 | CheckoutProvider,
6 | useCheckout,
7 | BillingAddressElement,
8 | } from '../../src/checkout';
9 |
10 | import '../styles/common.css';
11 |
12 | const CustomerDetails = ({phoneNumber, setPhoneNumber, email, setEmail}) => {
13 | const handlePhoneNumberChange = (event) => {
14 | setPhoneNumber(event.target.value);
15 | };
16 |
17 | const handleEmailChange = (event) => {
18 | setEmail(event.target.value);
19 | };
20 |
21 | return (
22 |
23 |
Customer Details
24 |
25 |
33 |
34 |
42 |
43 | );
44 | };
45 |
46 | const CheckoutForm = () => {
47 | const checkoutState = useCheckout();
48 | const [status, setStatus] = React.useState();
49 | const [loading, setLoading] = React.useState(false);
50 | const [phoneNumber, setPhoneNumber] = React.useState('');
51 | const [email, setEmail] = React.useState('');
52 |
53 | const handleSubmit = async (event) => {
54 | event.preventDefault();
55 | setStatus(undefined);
56 |
57 | if (checkoutState.type === 'loading') {
58 | setStatus('Loading...');
59 | return;
60 | } else if (checkoutState.type === 'error') {
61 | setStatus(`Error: ${checkoutState.error.message}`);
62 | return;
63 | }
64 |
65 | try {
66 | setLoading(true);
67 | await checkoutState.checkout.confirm({
68 | email,
69 | phoneNumber,
70 | returnUrl: window.location.href,
71 | });
72 | setLoading(false);
73 | } catch (err) {
74 | console.error(err);
75 | setStatus(err.message);
76 | }
77 | };
78 |
79 | const buttonDisabled = checkoutState.type !== 'success' || loading;
80 |
81 | return (
82 |
98 | );
99 | };
100 |
101 | const THEMES = ['stripe', 'flat', 'night'];
102 |
103 | const App = () => {
104 | const [pk, setPK] = React.useState(
105 | window.sessionStorage.getItem('react-stripe-js-pk') || ''
106 | );
107 | const [clientSecret, setClientSecret] = React.useState('');
108 |
109 | React.useEffect(() => {
110 | window.sessionStorage.setItem('react-stripe-js-pk', pk || '');
111 | }, [pk]);
112 |
113 | const [stripePromise, setStripePromise] = React.useState();
114 | const [theme, setTheme] = React.useState('stripe');
115 |
116 | const handleSubmit = (e) => {
117 | e.preventDefault();
118 | setStripePromise(loadStripe(pk));
119 | };
120 |
121 | const handleThemeChange = (e) => {
122 | setTheme(e.target.value);
123 | };
124 |
125 | const handleUnload = () => {
126 | setStripePromise(null);
127 | setClientSecret(null);
128 | };
129 |
130 | return (
131 | <>
132 |
161 | {stripePromise && clientSecret && (
162 |
169 |
170 |
171 | )}
172 | >
173 | );
174 | };
175 |
176 | export default App;
177 |
--------------------------------------------------------------------------------
/examples/hooks/12-Embedded-Checkout.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use
2 | // Embedded Checkout.
3 | // Learn how to accept a payment using the official Stripe docs.
4 | // https://stripe.com/docs/payments/accept-a-payment#web
5 |
6 | import React from 'react';
7 | import {loadStripe} from '@stripe/stripe-js';
8 | import {EmbeddedCheckoutProvider, EmbeddedCheckout} from '../../src';
9 |
10 | import '../styles/common.css';
11 |
12 | const App = () => {
13 | const [pk, setPK] = React.useState(
14 | window.sessionStorage.getItem('react-stripe-js-pk') || ''
15 | );
16 | const [clientSecret, setClientSecret] = React.useState(
17 | window.sessionStorage.getItem('react-stripe-js-embedded-client-secret') ||
18 | ''
19 | );
20 |
21 | React.useEffect(() => {
22 | window.sessionStorage.setItem('react-stripe-js-pk', pk || '');
23 | }, [pk]);
24 | React.useEffect(() => {
25 | window.sessionStorage.setItem(
26 | 'react-stripe-js-embedded-client-secret',
27 | clientSecret || ''
28 | );
29 | }, [clientSecret]);
30 |
31 | const [stripePromise, setStripePromise] = React.useState();
32 |
33 | const handleSubmit = (e) => {
34 | e.preventDefault();
35 | setStripePromise(loadStripe(pk));
36 | };
37 |
38 | const handleUnload = () => {
39 | setStripePromise(null);
40 | };
41 |
42 | return (
43 | <>
44 |
63 | {stripePromise && clientSecret && (
64 |
68 |
69 |
70 | )}
71 | >
72 | );
73 | };
74 |
75 | export default App;
76 |
--------------------------------------------------------------------------------
/examples/hooks/13-Payment-Form-Element.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {loadStripe} from '@stripe/stripe-js';
3 | import {
4 | PaymentFormElement,
5 | CheckoutProvider,
6 | useCheckout,
7 | } from '../../src/checkout';
8 |
9 | import '../styles/common.css';
10 |
11 | const CheckoutPaymentForm = () => {
12 | const checkoutState = useCheckout();
13 |
14 | const handleSubmit = async (event) => {
15 | event.preventDefault();
16 |
17 | try {
18 | await checkoutState.checkout.confirm({
19 | returnUrl: window.location.href,
20 | });
21 | } catch (err) {
22 | console.error(err);
23 | }
24 | };
25 |
26 | return (
27 |
30 | );
31 | };
32 |
33 | const THEMES = ['stripe', 'flat', 'night'];
34 |
35 | const App = () => {
36 | const [pk, setPK] = React.useState(
37 | window.sessionStorage.getItem('react-stripe-js-pk') || ''
38 | );
39 | const [clientSecret, setClientSecret] = React.useState('');
40 |
41 | React.useEffect(() => {
42 | window.sessionStorage.setItem('react-stripe-js-pk', pk || '');
43 | }, [pk]);
44 |
45 | const [stripePromise, setStripePromise] = React.useState();
46 | const [theme, setTheme] = React.useState('stripe');
47 |
48 | const handleSubmit = (e) => {
49 | e.preventDefault();
50 | setStripePromise(
51 | loadStripe(pk, {
52 | betas: ['custom_checkout_habanero_1'],
53 | })
54 | );
55 | };
56 |
57 | const handleThemeChange = (e) => {
58 | setTheme(e.target.value);
59 | };
60 |
61 | const handleUnload = () => {
62 | setStripePromise(null);
63 | setClientSecret(null);
64 | };
65 |
66 | console.log(stripePromise, clientSecret);
67 |
68 | return (
69 | <>
70 |
99 | {stripePromise && clientSecret && (
100 |
107 |
108 |
109 | )}
110 | >
111 | );
112 | };
113 |
114 | export default App;
115 |
--------------------------------------------------------------------------------
/examples/hooks/2-Split-Card.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React, {useState} from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {
8 | CardNumberElement,
9 | CardCvcElement,
10 | CardExpiryElement,
11 | Elements,
12 | useElements,
13 | useStripe,
14 | } from '../../src';
15 |
16 | import {logEvent, Result, ErrorResult} from '../util';
17 | import '../styles/common.css';
18 |
19 | const ELEMENT_OPTIONS = {
20 | style: {
21 | base: {
22 | fontSize: '18px',
23 | color: '#424770',
24 | letterSpacing: '0.025em',
25 | '::placeholder': {
26 | color: '#aab7c4',
27 | },
28 | },
29 | invalid: {
30 | color: '#9e2146',
31 | },
32 | },
33 | };
34 |
35 | const CheckoutForm = () => {
36 | const elements = useElements();
37 | const stripe = useStripe();
38 | const [name, setName] = useState('');
39 | const [postal, setPostal] = useState('');
40 | const [errorMessage, setErrorMessage] = useState(null);
41 | const [paymentMethod, setPaymentMethod] = useState(null);
42 |
43 | const handleSubmit = async (event) => {
44 | event.preventDefault();
45 |
46 | if (!stripe || !elements) {
47 | // Stripe.js has not loaded yet. Make sure to disable
48 | // form submission until Stripe.js has loaded.
49 | return;
50 | }
51 |
52 | const card = elements.getElement(CardNumberElement);
53 |
54 | if (card == null) {
55 | return;
56 | }
57 |
58 | const payload = await stripe.createPaymentMethod({
59 | type: 'card',
60 | card,
61 | billing_details: {
62 | name,
63 | address: {
64 | postal_code: postal,
65 | },
66 | },
67 | });
68 |
69 | if (payload.error) {
70 | console.log('[error]', payload.error);
71 | setErrorMessage(payload.error.message);
72 | setPaymentMethod(null);
73 | } else {
74 | console.log('[PaymentMethod]', payload.paymentMethod);
75 | setPaymentMethod(payload.paymentMethod);
76 | setErrorMessage(null);
77 | }
78 | };
79 |
80 | return (
81 |
135 | );
136 | };
137 |
138 | // Make sure to call `loadStripe` outside of a component’s render to avoid
139 | // recreating the `Stripe` object on every render.
140 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
141 |
142 | const App = () => {
143 | return (
144 |
145 |
146 |
147 | );
148 | };
149 |
150 | export default App;
151 |
--------------------------------------------------------------------------------
/examples/hooks/3-Payment-Request-Button.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment with the PaymentRequestButton using the official Stripe docs.
3 | // https://stripe.com/docs/stripe-js/elements/payment-request-button#react
4 |
5 | import React, {useState, useEffect} from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {PaymentRequestButtonElement, Elements, useStripe} from '../../src';
8 |
9 | import {Result, ErrorResult} from '../util';
10 | import '../styles/common.css';
11 |
12 | const NotAvailableResult = () => (
13 |
14 |
15 | PaymentRequest is not available in your browser.
16 |
17 | {window.location.protocol !== 'https:' && (
18 |
19 | Try using{' '}
20 |
21 | ngrok
22 | {' '}
23 | to view this demo over https.
24 |
25 | )}
26 |
27 | );
28 |
29 | const ELEMENT_OPTIONS = {
30 | style: {
31 | paymentRequestButton: {
32 | type: 'buy',
33 | theme: 'dark',
34 | },
35 | },
36 | };
37 |
38 | const CheckoutForm = () => {
39 | const stripe = useStripe();
40 | const [paymentRequest, setPaymentRequest] = useState(null);
41 | const [errorMessage, setErrorMessage] = useState(null);
42 | const [notAvailable, setNotAvailable] = useState(false);
43 | const [paymentMethod, setPaymentMethod] = useState(null);
44 |
45 | useEffect(() => {
46 | if (!stripe) {
47 | // We can't create a PaymentRequest until Stripe.js loads.
48 | return;
49 | }
50 |
51 | const pr = stripe.paymentRequest({
52 | country: 'US',
53 | currency: 'usd',
54 | total: {
55 | label: 'Demo total',
56 | amount: 100,
57 | },
58 | });
59 |
60 | pr.on('paymentmethod', async (event) => {
61 | setPaymentMethod(event.paymentMethod);
62 | event.complete('success');
63 | });
64 |
65 | pr.canMakePayment().then((canMakePaymentRes) => {
66 | if (canMakePaymentRes) {
67 | setPaymentRequest(pr);
68 | } else {
69 | setNotAvailable(true);
70 | }
71 | });
72 | }, [stripe]);
73 |
74 | return (
75 |
96 | );
97 | };
98 |
99 | // Make sure to call `loadStripe` outside of a component’s render to avoid
100 | // recreating the `Stripe` object on every render.
101 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
102 |
103 | const App = () => {
104 | return (
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default App;
112 |
--------------------------------------------------------------------------------
/examples/hooks/4-IBAN.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a SEPA Debit payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/sepa-debit/accept-a-payment
4 |
5 | import React, {useState} from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {IbanElement, Elements, useElements, useStripe} from '../../src';
8 |
9 | import {logEvent, Result, ErrorResult} from '../util';
10 | import '../styles/common.css';
11 |
12 | const ELEMENT_OPTIONS = {
13 | supportedCountries: ['SEPA'],
14 | style: {
15 | base: {
16 | fontSize: '18px',
17 | color: '#424770',
18 | letterSpacing: '0.025em',
19 | '::placeholder': {
20 | color: '#aab7c4',
21 | },
22 | },
23 | invalid: {
24 | color: '#9e2146',
25 | },
26 | },
27 | };
28 |
29 | const CheckoutForm = () => {
30 | const stripe = useStripe();
31 | const elements = useElements();
32 | const [name, setName] = useState('');
33 | const [email, setEmail] = useState('');
34 | const [errorMessage, setErrorMessage] = useState(null);
35 | const [paymentMethod, setPaymentMethod] = useState(null);
36 |
37 | const handleSubmit = async (event) => {
38 | event.preventDefault();
39 |
40 | if (!stripe || !elements) {
41 | // Stripe.js has not loaded yet. Make sure to disable
42 | // form submission until Stripe.js has loaded.
43 | return;
44 | }
45 |
46 | const ibanElement = elements.getElement(IbanElement);
47 |
48 | if (ibanElement == null) {
49 | return;
50 | }
51 |
52 | const payload = await stripe.createPaymentMethod({
53 | type: 'sepa_debit',
54 | sepa_debit: ibanElement,
55 | billing_details: {
56 | name,
57 | email,
58 | },
59 | });
60 |
61 | if (payload.error) {
62 | console.log('[error]', payload.error);
63 | setErrorMessage(payload.error.message);
64 | setPaymentMethod(null);
65 | } else {
66 | console.log('[PaymentMethod]', payload.paymentMethod);
67 | setPaymentMethod(payload.paymentMethod);
68 | setErrorMessage(null);
69 | }
70 | };
71 |
72 | return (
73 |
110 | );
111 | };
112 |
113 | // Make sure to call `loadStripe` outside of a component’s render to avoid
114 | // recreating the `Stripe` object on every render.
115 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
116 |
117 | const App = () => {
118 | return (
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default App;
126 |
--------------------------------------------------------------------------------
/examples/hooks/9-Payment-Element.js:
--------------------------------------------------------------------------------
1 | // This example shows you how to set up React Stripe.js and use Elements.
2 | // Learn how to accept a payment using the official Stripe docs.
3 | // https://stripe.com/docs/payments/accept-a-payment#web
4 |
5 | import React from 'react';
6 | import {loadStripe} from '@stripe/stripe-js';
7 | import {PaymentElement, Elements, useElements, useStripe} from '../../src';
8 |
9 | import '../styles/common.css';
10 |
11 | const CheckoutForm = () => {
12 | const [status, setStatus] = React.useState();
13 | const [loading, setLoading] = React.useState(false);
14 | const stripe = useStripe();
15 | const elements = useElements();
16 |
17 | const handleSubmit = async (event) => {
18 | // Block native form submission.
19 | event.preventDefault();
20 |
21 | if (!stripe || !elements) {
22 | // Stripe.js has not loaded yet. Make sure to disable
23 | // form submission until Stripe.js has loaded.
24 | return;
25 | }
26 |
27 | setLoading(true);
28 |
29 | stripe
30 | .confirmPayment({
31 | elements,
32 | redirect: 'if_required',
33 | confirmParams: {return_url: window.location.href},
34 | })
35 | .then((res) => {
36 | setLoading(false);
37 | if (res.error) {
38 | console.error(res.error);
39 | setStatus(res.error.message);
40 | } else {
41 | setStatus(res.paymentIntent.status);
42 | }
43 | });
44 | };
45 |
46 | return (
47 |
54 | );
55 | };
56 |
57 | const THEMES = ['stripe', 'flat', 'none'];
58 |
59 | const App = () => {
60 | const [pk, setPK] = React.useState(
61 | window.sessionStorage.getItem('react-stripe-js-pk') || ''
62 | );
63 | const [clientSecret, setClientSecret] = React.useState('');
64 |
65 | React.useEffect(() => {
66 | window.sessionStorage.setItem('react-stripe-js-pk', pk || '');
67 | }, [pk]);
68 |
69 | const [stripePromise, setStripePromise] = React.useState();
70 | const [theme, setTheme] = React.useState('stripe');
71 |
72 | const handleSubmit = (e) => {
73 | e.preventDefault();
74 | setStripePromise(loadStripe(pk));
75 | };
76 |
77 | const handleThemeChange = (e) => {
78 | setTheme(e.target.value);
79 | };
80 |
81 | const handleUnload = () => {
82 | setStripePromise(null);
83 | setClientSecret(null);
84 | };
85 |
86 | return (
87 | <>
88 |
117 | {stripePromise && clientSecret && (
118 |
122 |
123 |
124 | )}
125 | >
126 | );
127 | };
128 |
129 | export default App;
130 |
--------------------------------------------------------------------------------
/examples/styles/2-Card-Detailed.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | .AppWrapper input,
6 | .AppWrapper button {
7 | all: unset;
8 | -webkit-appearance: none;
9 | -moz-appearance: none;
10 | appearance: none;
11 | outline: none;
12 | border-style: none;
13 | }
14 |
15 | .AppWrapper {
16 | width: 500px;
17 | height: 400px;
18 | position: relative;
19 | }
20 |
21 | @keyframes fade {
22 | from {
23 | opacity: 0;
24 | transform: scale3D(0.95, 0.95, 0.95);
25 | }
26 | to {
27 | opacity: 1;
28 | transform: scale3D(1, 1, 1);
29 | }
30 | }
31 |
32 | .AppWrapper .Form {
33 | animation: fade 200ms ease-out;
34 | }
35 |
36 | .AppWrapper .FormGroup {
37 | margin: 0 15px 20px;
38 | padding: 0;
39 | border-style: none;
40 | background-color: #7795f8;
41 | will-change: opacity, transform;
42 | box-shadow: 0 6px 9px rgba(50, 50, 93, 0.06), 0 2px 5px rgba(0, 0, 0, 0.08),
43 | inset 0 1px 0 #829fff;
44 | border-radius: 4px;
45 | }
46 |
47 | .AppWrapper .FormRow {
48 | display: -ms-flexbox;
49 | display: flex;
50 | -ms-flex-align: center;
51 | align-items: center;
52 | margin-left: 15px;
53 | border-top: 1px solid #819efc;
54 | }
55 |
56 | .AppWrapper .FormRow:first-child {
57 | border-top: none;
58 | }
59 |
60 | .AppWrapper .FormRowLabel {
61 | all: unset;
62 | width: 15%;
63 | min-width: 70px;
64 | padding: 11px 0;
65 | color: #c4f0ff;
66 | overflow: hidden;
67 | text-overflow: ellipsis;
68 | white-space: nowrap;
69 | }
70 |
71 | @keyframes void-animation-out {
72 | 0%,
73 | to {
74 | opacity: 1;
75 | }
76 | }
77 | .AppWrapper .FormRowInput:-webkit-autofill {
78 | -webkit-text-fill-color: #fce883;
79 | /* Hack to hide the default webkit autofill */
80 | transition: background-color 100000000s;
81 | animation: 1ms void-animation-out;
82 | }
83 |
84 | .AppWrapper .FormRowInput {
85 | font-size: 16px;
86 | width: 100%;
87 | padding: 11px 15px 11px 0;
88 | color: #fff;
89 | background-color: transparent;
90 | animation: 1ms void-animation-out;
91 | }
92 |
93 | .AppWrapper .FormRowInput::placeholder {
94 | color: #87bbfd;
95 | }
96 |
97 | .AppWrapper .StripeElement--webkit-autofill {
98 | background: transparent !important;
99 | }
100 |
101 | .AppWrapper .StripeElement {
102 | width: 100%;
103 | padding: 11px 15px 11px 0;
104 | margin: 0;
105 | background: none;
106 | }
107 |
108 | .AppWrapper .SubmitButton {
109 | text-align: center;
110 | display: block;
111 | font-size: 16px;
112 | width: calc(100% - 30px);
113 | height: 40px;
114 | margin: 40px 15px 0;
115 | background-color: #f6a4eb;
116 | box-shadow: 0 6px 9px rgba(50, 50, 93, 0.06), 0 2px 5px rgba(0, 0, 0, 0.08),
117 | inset 0 1px 0 #ffb9f6;
118 | border-radius: 4px;
119 | color: #fff;
120 | font-weight: 600;
121 | cursor: pointer;
122 | transition: all 100ms ease-in-out;
123 | will-change: transform, background-color, box-shadow;
124 | }
125 |
126 | .AppWrapper .SubmitButton:active {
127 | background-color: #d782d9;
128 | box-shadow: 0 6px 9px rgba(50, 50, 93, 0.06), 0 2px 5px rgba(0, 0, 0, 0.08),
129 | inset 0 1px 0 #e298d8;
130 | transform: scale(0.99);
131 | }
132 |
133 | .AppWrapper .SubmitButton.SubmitButton--error {
134 | transform: translateY(15px);
135 | }
136 | .AppWrapper .SubmitButton.SubmitButton--error:active {
137 | transform: scale(0.99) translateY(15px);
138 | }
139 |
140 | .AppWrapper .SubmitButton:disabled {
141 | opacity: 0.5;
142 | cursor: default;
143 | background-color: #7795f8;
144 | box-shadow: none;
145 | }
146 |
147 | .AppWrapper .ErrorMessage {
148 | color: #fff;
149 | position: absolute;
150 | display: flex;
151 | justify-content: center;
152 | padding: 0 15px;
153 | font-size: 13px;
154 | margin-top: 0px;
155 | width: 100%;
156 | transform: translateY(-15px);
157 | opacity: 0;
158 | animation: fade 150ms ease-out;
159 | animation-delay: 50ms;
160 | animation-fill-mode: forwards;
161 | will-change: opacity, transform;
162 | }
163 |
164 | .AppWrapper .ErrorMessage svg {
165 | margin-right: 10px;
166 | }
167 |
168 | .AppWrapper .Result {
169 | margin-top: 50px;
170 | text-align: center;
171 | animation: fade 200ms ease-out;
172 | }
173 |
174 | .AppWrapper .ResultTitle {
175 | color: #fff;
176 | font-weight: 500;
177 | margin-bottom: 8px;
178 | font-size: 17px;
179 | text-align: center;
180 | }
181 |
182 | .AppWrapper .ResultMessage {
183 | color: #9cdbff;
184 | font-size: 14px;
185 | font-weight: 400;
186 | margin-bottom: 25px;
187 | line-height: 1.6em;
188 | text-align: center;
189 | }
190 |
191 | .AppWrapper .ResetButton {
192 | border: 0;
193 | cursor: pointer;
194 | background: transparent;
195 | }
196 |
--------------------------------------------------------------------------------
/examples/styles/common.css:
--------------------------------------------------------------------------------
1 | /* These styles are used if a demo specific stylesheet is not present */
2 |
3 | *,
4 | *:before,
5 | *:after {
6 | box-sizing: border-box;
7 | }
8 |
9 | body,
10 | html {
11 | background-color: #f6f9fc;
12 | font-size: 18px;
13 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
14 | }
15 |
16 | form {
17 | max-width: 800px;
18 | margin: 80px auto;
19 | }
20 |
21 | label {
22 | color: #6b7c93;
23 | font-weight: 300;
24 | letter-spacing: 0.025em;
25 | margin-top: 16px;
26 | display: block;
27 | }
28 |
29 | button {
30 | white-space: nowrap;
31 | border: 0;
32 | outline: 0;
33 | display: inline-block;
34 | height: 40px;
35 | line-height: 40px;
36 | padding: 0 14px;
37 | box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
38 | color: #fff;
39 | border-radius: 4px;
40 | font-size: 15px;
41 | font-weight: 600;
42 | text-transform: uppercase;
43 | letter-spacing: 0.025em;
44 | background-color: #6772e5;
45 | text-decoration: none;
46 | -webkit-transition: all 150ms ease;
47 | transition: all 150ms ease;
48 | margin-top: 10px;
49 | }
50 |
51 | button:hover {
52 | color: #fff;
53 | cursor: pointer;
54 | background-color: #7795f8;
55 | transform: translateY(-1px);
56 | box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
57 | }
58 |
59 | button[disabled] {
60 | opacity: 0.6;
61 | }
62 |
63 | input,
64 | select {
65 | display: block;
66 | border: none;
67 | font-size: 18px;
68 | margin: 10px 0 20px 0;
69 | max-width: 100%;
70 | padding: 10px 14px;
71 | box-shadow: rgba(50, 50, 93, 0.14902) 0px 1px 3px,
72 | rgba(0, 0, 0, 0.0196078) 0px 1px 0px;
73 | border-radius: 4px;
74 | background: white;
75 | color: #424770;
76 | letter-spacing: 0.025em;
77 | width: 500px;
78 | }
79 |
80 | input::placeholder {
81 | color: #aab7c4;
82 | }
83 |
84 | .result,
85 | .error {
86 | font-size: 16px;
87 | font-weight: bold;
88 | margin-top: 10px;
89 | margin-bottom: 20px;
90 | }
91 |
92 | .error {
93 | color: #e4584c;
94 | }
95 |
96 | .result {
97 | color: #666ee8;
98 | }
99 |
100 | /*
101 | The StripeElement class is applied to the Element container by default.
102 | More info: https://stripe.com/docs/stripe-js/reference#element-options
103 | */
104 |
105 | .StripeElement {
106 | display: block;
107 | margin: 10px 0 20px 0;
108 | max-width: 500px;
109 | padding: 10px 14px;
110 | box-shadow: rgba(50, 50, 93, 0.14902) 0px 1px 3px,
111 | rgba(0, 0, 0, 0.0196078) 0px 1px 0px;
112 | border-radius: 4px;
113 | background: white;
114 | }
115 |
116 | .StripeElement--focus {
117 | box-shadow: rgba(50, 50, 93, 0.109804) 0px 4px 6px,
118 | rgba(0, 0, 0, 0.0784314) 0px 1px 3px;
119 | -webkit-transition: all 150ms ease;
120 | transition: all 150ms ease;
121 | }
122 |
123 | .StripeElement.loading {
124 | height: 41.6px;
125 | opacity: 0.6;
126 | }
127 |
--------------------------------------------------------------------------------
/examples/util.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import React, {useState, useEffect} from 'react';
3 |
4 | export const logEvent = (name) => (event) => {
5 | console.log(`[${name}]`, event);
6 | };
7 |
8 | export const Result = ({children}) => {children}
;
9 |
10 | export const ErrorResult = ({children}) => (
11 | {children}
12 | );
13 |
14 | // Demo hook to dynamically change font size based on window size.
15 | export const useDynamicFontSize = () => {
16 | const [fontSize, setFontSize] = useState(
17 | window.innerWidth < 450 ? '14px' : '18px'
18 | );
19 |
20 | useEffect(() => {
21 | const onResize = () => {
22 | setFontSize(window.innerWidth < 450 ? '14px' : '18px');
23 | };
24 |
25 | window.addEventListener('resize', onResize);
26 |
27 | return () => {
28 | window.removeEventListener('resize', onResize);
29 | };
30 | }, []);
31 |
32 | return fontSize;
33 | };
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@stripe/react-stripe-js",
3 | "version": "5.4.1",
4 | "description": "React components for Stripe.js and Stripe Elements",
5 | "main": "dist/react-stripe.js",
6 | "module": "dist/react-stripe.esm.mjs",
7 | "jsnext:main": "dist/react-stripe.esm.mjs",
8 | "browser:min": "dist/react-stripe.umd.min.js",
9 | "browser": "dist/react-stripe.umd.js",
10 | "types": "dist/react-stripe.d.ts",
11 | "releaseCandidate": false,
12 | "exports": {
13 | ".": {
14 | "require": "./dist/react-stripe.js",
15 | "import": "./dist/react-stripe.esm.mjs",
16 | "default": "./dist/react-stripe.esm.mjs",
17 | "types": "./dist/react-stripe.d.ts"
18 | },
19 | "./checkout": {
20 | "require": "./dist/checkout.js",
21 | "import": "./dist/checkout.esm.mjs",
22 | "default": "./dist/checkout.esm.mjs",
23 | "types": "./dist/checkout.d.ts"
24 | }
25 | },
26 | "typesVersions": {
27 | "*": {
28 | "checkout": [
29 | "dist/checkout.d.ts"
30 | ]
31 | }
32 | },
33 | "scripts": {
34 | "test": "yarn run lint && yarn run lint:prettier && yarn run test:unit && yarn test:package-types && yarn run typecheck",
35 | "test:package-types": "attw --pack .",
36 | "test:unit": "jest",
37 | "lint": "eslint --max-warnings=0 '{src,examples}/**/*.{ts,tsx,js}'",
38 | "lint:prettier": "prettier './**/*.js' './**/*.ts' './**/*.tsx' './**/*.css' './**/*.md' --list-different",
39 | "typecheck": "tsc",
40 | "build": "yarn run clean && yarn run rollup -c --bundleConfigAsCjs && yarn checkimport",
41 | "checkimport": "scripts/check-imports",
42 | "clean": "rimraf dist",
43 | "prettier:fix": "prettier './**/*.js' './**/*.ts' './**/*.tsx' './**/*.css' './**/*.md' --write",
44 | "prepublishOnly": "echo \"\nPlease use ./scripts/publish instead\n\" && exit 1",
45 | "doctoc": "doctoc README.md",
46 | "storybook": "start-storybook -p 6006 "
47 | },
48 | "keywords": [
49 | "React",
50 | "Stripe",
51 | "Elements"
52 | ],
53 | "author": "Stripe (https://www.stripe.com)",
54 | "license": "MIT",
55 | "repository": {
56 | "type": "git",
57 | "url": "https://github.com/stripe/react-stripe-js.git"
58 | },
59 | "files": [
60 | "dist",
61 | "src",
62 | "checkout.js",
63 | "checkout.d.ts"
64 | ],
65 | "jest": {
66 | "preset": "ts-jest/presets/js-with-ts",
67 | "setupFilesAfterEnv": [
68 | "/test/setupJest.js"
69 | ],
70 | "globals": {
71 | "ts-jest": {
72 | "diagnostics": {
73 | "ignoreCodes": [
74 | 151001
75 | ]
76 | }
77 | },
78 | "_VERSION": true
79 | }
80 | },
81 | "dependencies": {
82 | "prop-types": "^15.7.2"
83 | },
84 | "devDependencies": {
85 | "@arethetypeswrong/cli": "^0.15.3",
86 | "@babel/cli": "^7.7.0",
87 | "@babel/core": "^7.7.2",
88 | "@babel/preset-env": "^7.7.1",
89 | "@babel/preset-react": "^7.7.0",
90 | "@rollup/plugin-babel": "^6.0.4",
91 | "@rollup/plugin-commonjs": "^25.0.7",
92 | "@rollup/plugin-node-resolve": "^15.2.3",
93 | "@rollup/plugin-replace": "^5.0.5",
94 | "@rollup/plugin-terser": "^0.4.4",
95 | "@storybook/react": "^6.5.0-beta.8",
96 | "@stripe/stripe-js": "8.5.2",
97 | "@testing-library/jest-dom": "^5.16.4",
98 | "@testing-library/react": "^13.1.1",
99 | "@testing-library/react-hooks": "^8.0.0",
100 | "@types/jest": "^25.1.1",
101 | "@types/react": "^18.0.0",
102 | "@types/react-dom": "^18.0.0",
103 | "@typescript-eslint/eslint-plugin": "^2.18.0",
104 | "@typescript-eslint/parser": "^2.18.0",
105 | "babel-eslint": "^10.0.3",
106 | "babel-jest": "^24.9.0",
107 | "babel-loader": "^8.0.6",
108 | "eslint": "6.6.0",
109 | "eslint-config-airbnb": "18.0.1",
110 | "eslint-config-prettier": "^6.10.0",
111 | "eslint-plugin-import": "^2.18.2",
112 | "eslint-plugin-jest": "^22.6.3",
113 | "eslint-plugin-jsx-a11y": "^6.2.3",
114 | "eslint-plugin-prettier": "^3.1.2",
115 | "eslint-plugin-react": "^7.14.3",
116 | "eslint-plugin-react-hooks": "^1.7.0",
117 | "fork-ts-checker-webpack-plugin": "^4.0.3",
118 | "jest": "^25.1.0",
119 | "prettier": "^1.19.1",
120 | "react": "18.1.0",
121 | "react-docgen-typescript-loader": "^3.6.0",
122 | "react-dom": "18.1.0",
123 | "react-test-renderer": "^18.0.0",
124 | "rimraf": "^2.6.2",
125 | "rollup": "^4.12.0",
126 | "rollup-plugin-ts": "^3.4.5",
127 | "ts-jest": "^25.1.0",
128 | "ts-loader": "^6.2.1",
129 | "typescript": "^4.1.2"
130 | },
131 | "resolutions": {
132 | "@types/react": "18.0.5"
133 | },
134 | "peerDependencies": {
135 | "@stripe/stripe-js": ">=8.0.0 <9.0.0",
136 | "react": ">=16.8.0 <20.0.0",
137 | "react-dom": ">=16.8.0 <20.0.0"
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import {babel} from '@rollup/plugin-babel';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import {nodeResolve} from '@rollup/plugin-node-resolve';
4 | import replace from '@rollup/plugin-replace';
5 | import terser from '@rollup/plugin-terser';
6 | import ts from 'rollup-plugin-ts';
7 | import pkg from './package.json';
8 |
9 | const PLUGINS = [
10 | commonjs(),
11 | ts(),
12 | nodeResolve(),
13 | babel({
14 | extensions: ['.ts', '.js', '.tsx', '.jsx'],
15 | }),
16 | replace({
17 | 'process.env.NODE_ENV': JSON.stringify('production'),
18 | _VERSION: JSON.stringify(pkg.version),
19 | preventAssignment: true,
20 | }),
21 | ];
22 |
23 | export default [
24 | {
25 | input: 'src/index.ts',
26 | external: ['react', 'prop-types'],
27 | output: [
28 | {file: pkg.main, format: 'cjs'},
29 | {file: pkg.module, format: 'es'},
30 | ],
31 | plugins: PLUGINS,
32 | },
33 | // Checkout subpath build
34 | {
35 | input: 'src/checkout/index.ts',
36 | external: ['react', 'prop-types'],
37 | output: [
38 | {file: 'dist/checkout.js', format: 'cjs'},
39 | {file: 'dist/checkout.esm.mjs', format: 'es'},
40 | ],
41 | plugins: PLUGINS,
42 | },
43 | // UMD build with inline PropTypes
44 | {
45 | input: 'src/index.ts',
46 | external: ['react'],
47 | output: [
48 | {
49 | name: 'ReactStripe',
50 | file: pkg.browser,
51 | format: 'umd',
52 | globals: {
53 | react: 'React',
54 | },
55 | },
56 | ],
57 | plugins: PLUGINS,
58 | },
59 | // Minified UMD Build without PropTypes
60 | {
61 | input: 'src/index.ts',
62 | external: ['react'],
63 | output: [
64 | {
65 | name: 'ReactStripe',
66 | file: pkg['browser:min'],
67 | format: 'umd',
68 | globals: {
69 | react: 'React',
70 | },
71 | },
72 | ],
73 | plugins: [...PLUGINS, terser()],
74 | },
75 | ];
76 |
--------------------------------------------------------------------------------
/scripts/check-imports:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | BASE_DIR="$(dirname "$0")/..";
4 |
5 | checkImport() {
6 | file=$1
7 | regexp=$2
8 | message=$3
9 | grep "${regexp}" "${BASE_DIR}${file}"
10 |
11 | case $? in
12 | 1) true
13 | ;;
14 | 0)
15 | echo "Found disallowed import in ${file}"
16 | echo "${message}"
17 | false
18 | ;;
19 | *)
20 | false
21 | ;;
22 | esac
23 | }
24 |
25 | checkImport "/dist/react-stripe.d.ts" 'import [^*{]' 'Please only use * or named imports for types' && \
26 | checkImport "/dist/react-stripe.esm.mjs" 'import.*{' 'Please do not use named imports for dependencies'
--------------------------------------------------------------------------------
/scripts/is_release_candidate.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const {releaseCandidate} = require('../package.json');
3 |
4 | // coerce boolean to 0 or 1 and default undefined to 0
5 | console.log(+!!releaseCandidate);
6 |
--------------------------------------------------------------------------------
/scripts/publish:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 | IFS=$'\n\t'
5 |
6 | RELEASE_TYPE=${1:-}
7 | IS_RELEASE_CANDIDATE=$(node scripts/is_release_candidate.js)
8 |
9 | echo_help() {
10 | cat << EOF
11 | USAGE:
12 | ./scripts/publish
13 |
14 | ARGS:
15 |
16 | A Semantic Versioning release type used to bump the version number. Either "patch", "minor", or "major".
17 | EOF
18 | }
19 |
20 | verify_prerequisites() {
21 | echo "Verifying prerequisites..."
22 |
23 | # Check npm login status
24 | if ! npm whoami &> /dev/null; then
25 | echo "Error! You are not logged in to npm."
26 | echo "Please run 'npm login' and try again."
27 | exit 1
28 | fi
29 |
30 | # Check yarn login status
31 | if ! yarn login --silent &> /dev/null; then
32 | echo "Error! You are not logged in to yarn."
33 | echo "Please run 'yarn login' and try again."
34 | exit 1
35 | fi
36 |
37 | # Check for hub command
38 | if ! which hub &> /dev/null; then
39 | echo "Error! 'hub' command not found."
40 | echo "Please install hub with 'brew install hub'."
41 | exit 1
42 | fi
43 |
44 | # Check GitHub token
45 | if [[ -z "${GITHUB_TOKEN:-}" ]]; then
46 | echo "Error! GITHUB_TOKEN environment variable is not set."
47 | exit 1
48 | fi
49 |
50 | # Check git signing configuration
51 | if [ "$(git config --get gpg.format)" != "ssh" ]; then
52 | echo "Error! Git is not configured to use SSH for commit signing."
53 | echo "Please run: git config --global gpg.format ssh"
54 | exit 1
55 | fi
56 |
57 | # Check signing key configuration
58 | if [ -z "$(git config --get user.signingkey)" ]; then
59 | echo "Error! Git signing key is not configured."
60 | echo "Please run: git config --global user.signingkey ~/.ssh/id_ed25519.pub"
61 | exit 1
62 | fi
63 |
64 | # Check if signing key exists
65 | local signing_key=$(git config --get user.signingkey)
66 | if [ ! -f "$signing_key" ]; then
67 | echo "Error! Git signing key does not exist at: $signing_key"
68 | echo "Please set up SSH key for signing as described in the documentation."
69 | exit 1
70 | fi
71 |
72 | # Check if commit signing is enabled
73 | if [ "$(git config --get commit.gpgsign)" != "true" ]; then
74 | echo "Error! Git commit signing is not enabled."
75 | echo "Please run: git config --global commit.gpgsign true"
76 | exit 1
77 | fi
78 |
79 | echo "All prerequisites verified successfully!"
80 | }
81 |
82 | create_github_release() {
83 | if which hub | grep -q "not found"; then
84 | create_github_release_fallback
85 | return
86 | fi
87 |
88 | # Get the last two non-release-candidate releases. For example, `("v1.3.1" "v1.3.2")`
89 | local versions=($(git tag --sort version:refname | grep '^v' | grep -v "rc" | tail -n 2))
90 |
91 | # If we didn't find exactly two previous version versions, give up
92 | if [ ${#versions[@]} -ne 2 ]; then
93 | create_github_release_fallback
94 | return
95 | fi
96 |
97 | local previous_version="${versions[0]}"
98 | local current_version="${versions[1]}"
99 | local commit_titles=$(git log --pretty=format:"- %s" "$previous_version".."$current_version"^)
100 | local release_notes="$(cat << EOF
101 | $current_version
102 |
103 |
104 |
105 |
106 | $commit_titles
107 |
108 | ### New features
109 |
110 | ### Fixes
111 |
112 | ### Changed
113 |
114 | EOF
115 | )"
116 |
117 | echo "Creating GitHub release"
118 | echo ""
119 | echo -n " "
120 | hub release create -em "$release_notes" "$current_version"
121 | }
122 |
123 | create_github_release_fallback() {
124 | cat << EOF
125 | Remember to create a release on GitHub with a changelog notes:
126 |
127 | https://github.com/stripe/stripe-js/releases/new
128 |
129 | EOF
130 | }
131 |
132 | verify_commit_is_signed() {
133 | local commit_hash=$(git log -1 --format="%H")
134 |
135 | if git show --no-patch --pretty=format:"%G?" "$commit_hash" | grep "N" &> /dev/null; then
136 | echo "Error! Commit $commit_hash is not signed"
137 | echo "Please follow https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account and sign your commit"
138 | exit 1
139 | fi
140 | }
141 |
142 | # Require release type when not a beta release
143 | # Not necessary for beta releases because the prerelease versions
144 | # can be incremented automatically
145 | if [ "$IS_RELEASE_CANDIDATE" -ne 1 ]; then
146 |
147 | # Show help if no arguments passed
148 | if [ $# -eq 0 ]; then
149 | echo "Error! Missing release type argument"
150 | echo ""
151 | echo_help
152 | exit 1
153 | fi
154 |
155 | # Validate passed release type
156 | case $RELEASE_TYPE in
157 | patch | minor | major)
158 | ;;
159 |
160 | *)
161 | echo "Error! Invalid release type supplied"
162 | echo ""
163 | echo_help
164 | exit 1
165 | ;;
166 | esac
167 | fi
168 |
169 | # Show help message if -h, --help, or help passed
170 | case "${1:-}" in
171 | -h | --help | help)
172 | echo_help
173 | exit 0
174 | ;;
175 | esac
176 |
177 | # Make sure our working dir is the repo root directory
178 | cd "$(git rev-parse --show-toplevel)"
179 |
180 | verify_prerequisites
181 |
182 | echo "Fetching git remotes"
183 | git fetch
184 |
185 | GIT_STATUS=$(git status)
186 |
187 | if ! grep -q 'On branch master' <<< "$GIT_STATUS"; then
188 | echo "Error! Must be on master branch to publish"
189 | exit 1
190 | fi
191 |
192 | if ! grep -q "Your branch is up to date with 'origin/master'." <<< "$GIT_STATUS"; then
193 | echo "Error! Must be up to date with origin/master to publish"
194 | exit 1
195 | fi
196 |
197 | if ! grep -q 'working tree clean' <<< "$GIT_STATUS"; then
198 | echo "Error! Cannot publish with dirty working tree"
199 | exit 1
200 | fi
201 |
202 | echo "Installing dependencies according to lockfile"
203 | yarn install --frozen-lockfile
204 |
205 | echo "Bumping package.json $RELEASE_TYPE version and tagging commit"
206 | if [ "$IS_RELEASE_CANDIDATE" -eq 1 ]; then
207 | # The Github changelog is based on tag history, so do not create tags for beta versions
208 | # rc = release candidate
209 | if [ -z "$RELEASE_TYPE" ]; then
210 | # increment only the prerelease version if necessary, e.g.
211 | # 1.2.3-rc.0 -> 1.2.3-rc.1
212 | # 1.2.3 -> 1.2.3-rc.0
213 | yarn version --prerelease --preid=rc
214 | else
215 | # always increment the main version, e.g.
216 | # patch: 1.2.3-rc.0 -> 1.2.4-rc.0
217 | # patch: 1.2.3 -> 1.2.4-rc.0
218 | # major: 1.2.3 -> 2.0.0-rc.0
219 | yarn version "--pre$RELEASE_TYPE" --preid=rc
220 | fi
221 | else
222 | # increment the main version with no prerelease version, e.g.
223 | # patch: 1.2.3-rc.0 -> 1.2.4
224 | # major: 1.2.3 -> 2.0.0
225 | yarn version "--$RELEASE_TYPE"
226 | fi
227 |
228 | echo "Building"
229 | yarn run build
230 |
231 | echo "Running tests"
232 | yarn run test
233 |
234 | verify_commit_is_signed
235 |
236 | echo "Pushing git commit and tag"
237 | git push --follow-tags
238 |
239 | if [ "$IS_RELEASE_CANDIDATE" -ne 1 ]; then
240 | # Create release after commit and tag are pushed to ensure package.json
241 | # is bumped in the GitHub release.
242 | create_github_release
243 | fi
244 |
245 | echo "Publishing release"
246 | if [ "$IS_RELEASE_CANDIDATE" -eq 1 ]; then
247 | yarn --ignore-scripts publish --tag=rc --non-interactive --access=public
248 | else
249 | yarn --ignore-scripts publish --non-interactive --access=public
250 | fi
251 |
252 | echo "Publish successful!"
253 | echo ""
254 |
--------------------------------------------------------------------------------
/src/checkout/components/CheckoutProvider.tsx:
--------------------------------------------------------------------------------
1 | import {FunctionComponent, PropsWithChildren, ReactNode} from 'react';
2 | import * as stripeJs from '@stripe/stripe-js';
3 |
4 | import React from 'react';
5 | import PropTypes from 'prop-types';
6 |
7 | import {parseStripeProp} from '../../utils/parseStripeProp';
8 | import {usePrevious} from '../../utils/usePrevious';
9 | import {isEqual} from '../../utils/isEqual';
10 | import {
11 | ElementsContext,
12 | ElementsContextValue,
13 | parseElementsContext,
14 | } from '../../components/Elements';
15 | import {registerWithStripeJs} from '../../utils/registerWithStripeJs';
16 |
17 | type State =
18 | | {
19 | type: 'loading';
20 | sdk: stripeJs.StripeCheckout | null;
21 | }
22 | | {
23 | type: 'success';
24 | sdk: stripeJs.StripeCheckout;
25 | checkoutActions: stripeJs.LoadActionsSuccess;
26 | session: stripeJs.StripeCheckoutSession;
27 | }
28 | | {type: 'error'; error: {message: string}};
29 |
30 | type CheckoutContextValue = {
31 | stripe: stripeJs.Stripe | null;
32 | checkoutState: State;
33 | };
34 |
35 | const CheckoutContext = React.createContext(null);
36 | CheckoutContext.displayName = 'CheckoutContext';
37 |
38 | const validateCheckoutContext = (
39 | ctx: CheckoutContextValue | null,
40 | useCase: string
41 | ): CheckoutContextValue => {
42 | if (!ctx) {
43 | throw new Error(
44 | `Could not find CheckoutProvider context; You need to wrap the part of your app that ${useCase} in a provider.`
45 | );
46 | }
47 | return ctx;
48 | };
49 |
50 | interface CheckoutProviderProps {
51 | /**
52 | * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object.
53 | * The easiest way to initialize a `Stripe` object is with the the [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
54 | * Once this prop has been set, it can not be changed.
55 | *
56 | * You can also pass in `null` or a `Promise` resolving to `null` if you are performing an initial server-side render or when generating a static site.
57 | */
58 | stripe: PromiseLike | stripeJs.Stripe | null;
59 | options: stripeJs.StripeCheckoutOptions;
60 | }
61 |
62 | interface PrivateCheckoutProviderProps {
63 | stripe: unknown;
64 | options: stripeJs.StripeCheckoutOptions;
65 | children?: ReactNode;
66 | }
67 | const INVALID_STRIPE_ERROR =
68 | 'Invalid prop `stripe` supplied to `CheckoutProvider`. We recommend using the `loadStripe` utility from `@stripe/stripe-js`. See https://stripe.com/docs/stripe-js/react#elements-props-stripe for details.';
69 |
70 | const maybeSdk = (state: State): stripeJs.StripeCheckout | null => {
71 | if (state.type === 'success' || state.type === 'loading') {
72 | return state.sdk;
73 | } else {
74 | return null;
75 | }
76 | };
77 |
78 | export const CheckoutProvider: FunctionComponent> = (({
81 | stripe: rawStripeProp,
82 | options,
83 | children,
84 | }: PrivateCheckoutProviderProps) => {
85 | const parsed = React.useMemo(
86 | () => parseStripeProp(rawStripeProp, INVALID_STRIPE_ERROR),
87 | [rawStripeProp]
88 | );
89 |
90 | const [state, setState] = React.useState({type: 'loading', sdk: null});
91 | const [stripe, setStripe] = React.useState(null);
92 |
93 | // Ref used to avoid calling initCheckout multiple times when options changes
94 | const initCheckoutCalledRef = React.useRef(false);
95 |
96 | React.useEffect(() => {
97 | let isMounted = true;
98 |
99 | const init = ({stripe}: {stripe: stripeJs.Stripe}) => {
100 | if (stripe && isMounted && !initCheckoutCalledRef.current) {
101 | // Only update context if the component is still mounted
102 | // and stripe is not null. We allow stripe to be null to make
103 | // handling SSR easier.
104 | initCheckoutCalledRef.current = true;
105 | const sdk = stripe.initCheckout(options);
106 | setState({type: 'loading', sdk});
107 |
108 | sdk
109 | .loadActions()
110 | .then((result) => {
111 | if (result.type === 'success') {
112 | const {actions} = result;
113 | setState({
114 | type: 'success',
115 | sdk,
116 | checkoutActions: actions,
117 | session: actions.getSession(),
118 | });
119 |
120 | sdk.on('change', (session) => {
121 | setState((prevState) => {
122 | if (prevState.type === 'success') {
123 | return {
124 | type: 'success',
125 | sdk: prevState.sdk,
126 | checkoutActions: prevState.checkoutActions,
127 | session,
128 | };
129 | } else {
130 | return prevState;
131 | }
132 | });
133 | });
134 | } else {
135 | setState({type: 'error', error: result.error});
136 | }
137 | })
138 | .catch((error) => {
139 | setState({type: 'error', error});
140 | });
141 | }
142 | };
143 |
144 | if (parsed.tag === 'async') {
145 | parsed.stripePromise.then((stripe) => {
146 | setStripe(stripe);
147 | if (stripe) {
148 | init({stripe});
149 | } else {
150 | // Only update context if the component is still mounted
151 | // and stripe is not null. We allow stripe to be null to make
152 | // handling SSR easier.
153 | }
154 | });
155 | } else if (parsed.tag === 'sync') {
156 | setStripe(parsed.stripe);
157 | init({stripe: parsed.stripe});
158 | }
159 |
160 | return () => {
161 | isMounted = false;
162 | };
163 | }, [parsed, options, setState]);
164 |
165 | // Warn on changes to stripe prop
166 | const prevStripe = usePrevious(rawStripeProp);
167 | React.useEffect(() => {
168 | if (prevStripe !== null && prevStripe !== rawStripeProp) {
169 | console.warn(
170 | 'Unsupported prop change on CheckoutProvider: You cannot change the `stripe` prop after setting it.'
171 | );
172 | }
173 | }, [prevStripe, rawStripeProp]);
174 |
175 | // Apply updates to elements when options prop has relevant changes
176 | const sdk = maybeSdk(state);
177 | const prevOptions = usePrevious(options);
178 | React.useEffect(() => {
179 | // Ignore changes while checkout sdk is not initialized.
180 | if (!sdk) {
181 | return;
182 | }
183 |
184 | // Handle appearance changes
185 | const previousAppearance = prevOptions?.elementsOptions?.appearance;
186 | const currentAppearance = options?.elementsOptions?.appearance;
187 | const hasAppearanceChanged = !isEqual(
188 | currentAppearance,
189 | previousAppearance
190 | );
191 | if (currentAppearance && hasAppearanceChanged) {
192 | sdk.changeAppearance(currentAppearance);
193 | }
194 |
195 | // Handle fonts changes
196 | const previousFonts = prevOptions?.elementsOptions?.fonts;
197 | const currentFonts = options?.elementsOptions?.fonts;
198 | const hasFontsChanged = !isEqual(previousFonts, currentFonts);
199 |
200 | if (currentFonts && hasFontsChanged) {
201 | sdk.loadFonts(currentFonts);
202 | }
203 | }, [options, prevOptions, sdk]);
204 |
205 | // Attach react-stripe-js version to stripe.js instance
206 | React.useEffect(() => {
207 | registerWithStripeJs(stripe);
208 | }, [stripe]);
209 |
210 | // Use useMemo to prevent unnecessary re-renders of child components
211 | // when the context value object reference changes but the actual values haven't
212 | const contextValue = React.useMemo(
213 | () => ({
214 | stripe,
215 | checkoutState: state,
216 | }),
217 | [stripe, state]
218 | );
219 |
220 | return (
221 |
222 | {children}
223 |
224 | );
225 | }) as FunctionComponent>;
226 |
227 | CheckoutProvider.propTypes = {
228 | stripe: PropTypes.any,
229 | options: PropTypes.shape({
230 | clientSecret: PropTypes.oneOfType([
231 | PropTypes.string,
232 | PropTypes.instanceOf(Promise),
233 | ]).isRequired,
234 | elementsOptions: PropTypes.object,
235 | }).isRequired,
236 | } as PropTypes.ValidationMap;
237 |
238 | export const useElementsOrCheckoutContextWithUseCase = (
239 | useCaseString: string
240 | ): CheckoutContextValue | ElementsContextValue => {
241 | const checkout = React.useContext(CheckoutContext);
242 | const elements = React.useContext(ElementsContext);
243 |
244 | if (checkout) {
245 | if (elements) {
246 | throw new Error(
247 | `You cannot wrap the part of your app that ${useCaseString} in both and providers.`
248 | );
249 | } else {
250 | return checkout;
251 | }
252 | } else {
253 | return parseElementsContext(elements, useCaseString);
254 | }
255 | };
256 |
257 | type StripeCheckoutActions = Omit<
258 | stripeJs.StripeCheckout,
259 | 'on' | 'loadActions'
260 | > &
261 | Omit;
262 |
263 | export type StripeCheckoutValue = StripeCheckoutActions &
264 | stripeJs.StripeCheckoutSession;
265 |
266 | export type StripeUseCheckoutResult =
267 | | {type: 'loading'}
268 | | {
269 | type: 'success';
270 | checkout: StripeCheckoutValue;
271 | }
272 | | {type: 'error'; error: {message: string}};
273 |
274 | const mapStateToUseCheckoutResult = (
275 | checkoutState: State
276 | ): StripeUseCheckoutResult => {
277 | if (checkoutState.type === 'success') {
278 | const {sdk, session, checkoutActions} = checkoutState;
279 | const {on: _on, loadActions: _loadActions, ...elementsMethods} = sdk;
280 | const {getSession: _getSession, ...otherCheckoutActions} = checkoutActions;
281 | const actions = {
282 | ...elementsMethods,
283 | ...otherCheckoutActions,
284 | };
285 | return {
286 | type: 'success',
287 | checkout: {
288 | ...session,
289 | ...actions,
290 | },
291 | };
292 | } else if (checkoutState.type === 'loading') {
293 | return {
294 | type: 'loading',
295 | };
296 | } else {
297 | return {
298 | type: 'error',
299 | error: checkoutState.error,
300 | };
301 | }
302 | };
303 |
304 | export const useCheckout = (): StripeUseCheckoutResult => {
305 | const ctx = React.useContext(CheckoutContext);
306 | const {checkoutState} = validateCheckoutContext(ctx, 'calls useCheckout()');
307 | return mapStateToUseCheckoutResult(checkoutState);
308 | };
309 |
--------------------------------------------------------------------------------
/src/checkout/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | useCheckout,
3 | CheckoutProvider,
4 | StripeUseCheckoutResult,
5 | StripeCheckoutValue,
6 | } from './components/CheckoutProvider';
7 | export * from './types';
8 | import React from 'react';
9 | import createElementComponent from '../components/createElementComponent';
10 | import {isServer} from '../utils/isServer';
11 | import {
12 | CurrencySelectorElementComponent,
13 | BillingAddressElementComponent,
14 | ShippingAddressElementComponent,
15 | PaymentElementComponent,
16 | PaymentFormElementComponent,
17 | ExpressCheckoutElementComponent,
18 | TaxIdElementComponent,
19 | } from './types';
20 |
21 | export const CurrencySelectorElement: CurrencySelectorElementComponent = createElementComponent(
22 | 'currencySelector',
23 | isServer
24 | );
25 |
26 | export const PaymentElement: PaymentElementComponent = createElementComponent(
27 | 'payment',
28 | isServer
29 | );
30 |
31 | export const PaymentFormElement: PaymentFormElementComponent = createElementComponent(
32 | 'paymentForm',
33 | isServer
34 | );
35 |
36 | export const ExpressCheckoutElement: ExpressCheckoutElementComponent = createElementComponent(
37 | 'expressCheckout',
38 | isServer
39 | );
40 |
41 | export const TaxIdElement: TaxIdElementComponent = createElementComponent(
42 | 'taxId',
43 | isServer
44 | );
45 |
46 | const AddressElementBase = createElementComponent('address', isServer) as any;
47 |
48 | export const BillingAddressElement: BillingAddressElementComponent = ((
49 | props
50 | ) => {
51 | const {options, ...rest} = props as any;
52 | const merged = {...options, mode: 'billing'};
53 | return React.createElement(AddressElementBase, {...rest, options: merged});
54 | }) as BillingAddressElementComponent;
55 |
56 | export const ShippingAddressElement: ShippingAddressElementComponent = ((
57 | props
58 | ) => {
59 | const {options, ...rest} = props as any;
60 | const merged = {...options, mode: 'shipping'};
61 | return React.createElement(AddressElementBase, {...rest, options: merged});
62 | }) as ShippingAddressElementComponent;
63 |
--------------------------------------------------------------------------------
/src/checkout/types/index.ts:
--------------------------------------------------------------------------------
1 | import {FunctionComponent} from 'react';
2 | import * as stripeJs from '@stripe/stripe-js';
3 | import {StripeError} from '@stripe/stripe-js';
4 | import {
5 | ElementProps,
6 | PaymentElementProps as RootPaymentElementProps,
7 | ExpressCheckoutElementProps as RootExpressCheckoutElementProps,
8 | AddressElementProps as RootAddressElementProps,
9 | } from '../../types';
10 |
11 | export interface CurrencySelectorElementProps extends ElementProps {
12 | /**
13 | * Triggered when the Element is fully rendered and can accept imperative `element.focus()` calls.
14 | * Called with a reference to the underlying [Element instance](https://stripe.com/docs/js/element).
15 | */
16 | onReady?: (element: stripeJs.StripeCurrencySelectorElement) => any;
17 |
18 | /**
19 | * Triggered when the escape key is pressed within the Element.
20 | */
21 | onEscape?: () => any;
22 |
23 | /**
24 | * Triggered when the Element fails to load.
25 | */
26 | onLoadError?: (event: {
27 | elementType: 'currencySelector';
28 | error: StripeError;
29 | }) => any;
30 |
31 | /**
32 | * Triggered when the [loader](https://stripe.com/docs/js/elements_object/create#stripe_elements-options-loader) UI is mounted to the DOM and ready to be displayed.
33 | */
34 | onLoaderStart?: (event: {elementType: 'currencySelector'}) => any;
35 | }
36 |
37 | export type CurrencySelectorElementComponent = FunctionComponent<
38 | CurrencySelectorElementProps
39 | >;
40 |
41 | export type BillingAddressElementProps = Omit<
42 | RootAddressElementProps,
43 | 'options'
44 | > & {
45 | options?: stripeJs.StripeCheckoutAddressElementOptions;
46 | };
47 |
48 | export type BillingAddressElementComponent = FunctionComponent<
49 | BillingAddressElementProps
50 | >;
51 |
52 | export type ShippingAddressElementProps = Omit<
53 | RootAddressElementProps,
54 | 'options'
55 | > & {
56 | options?: stripeJs.StripeCheckoutAddressElementOptions;
57 | };
58 |
59 | export type ShippingAddressElementComponent = FunctionComponent<
60 | ShippingAddressElementProps
61 | >;
62 |
63 | export type PaymentElementProps = Omit & {
64 | options?: stripeJs.StripeCheckoutPaymentElementOptions;
65 | };
66 |
67 | export type PaymentElementComponent = FunctionComponent;
68 |
69 | export type PaymentFormElementComponent = FunctionComponent<{}>;
70 |
71 | export type ExpressCheckoutElementProps = Omit<
72 | RootExpressCheckoutElementProps,
73 | | 'options'
74 | | 'onClick'
75 | | 'onCancel'
76 | | 'onShippingAddressChange'
77 | | 'onShippingRateChange'
78 | > & {options?: stripeJs.StripeCheckoutExpressCheckoutElementOptions};
79 |
80 | export type ExpressCheckoutElementComponent = FunctionComponent<
81 | ExpressCheckoutElementProps
82 | >;
83 |
84 | export interface TaxIdElementProps extends ElementProps {
85 | options: stripeJs.StripeTaxIdElementOptions;
86 | onChange?: (event: stripeJs.StripeTaxIdElementChangeEvent) => any;
87 | onReady?: (element: stripeJs.StripeTaxIdElement) => any;
88 | onEscape?: () => any;
89 | onLoadError?: (event: {elementType: 'taxId'; error: StripeError}) => any;
90 | onLoaderStart?: (event: {elementType: 'taxId'}) => any;
91 | }
92 |
93 | export type TaxIdElementComponent = FunctionComponent;
94 |
--------------------------------------------------------------------------------
/src/components/Elements.tsx:
--------------------------------------------------------------------------------
1 | // Must use `import *` or named imports for React's types
2 | import {
3 | FunctionComponent,
4 | PropsWithChildren,
5 | ReactElement,
6 | ReactNode,
7 | } from 'react';
8 | import * as stripeJs from '@stripe/stripe-js';
9 |
10 | import React from 'react';
11 | import PropTypes from 'prop-types';
12 |
13 | import {usePrevious} from '../utils/usePrevious';
14 | import {
15 | extractAllowedOptionsUpdates,
16 | UnknownOptions,
17 | } from '../utils/extractAllowedOptionsUpdates';
18 | import {parseStripeProp} from '../utils/parseStripeProp';
19 | import {registerWithStripeJs} from '../utils/registerWithStripeJs';
20 |
21 | export interface ElementsContextValue {
22 | elements: stripeJs.StripeElements | null;
23 | stripe: stripeJs.Stripe | null;
24 | }
25 |
26 | export const ElementsContext = React.createContext(
27 | null
28 | );
29 | ElementsContext.displayName = 'ElementsContext';
30 |
31 | export const parseElementsContext = (
32 | ctx: ElementsContextValue | null,
33 | useCase: string
34 | ): ElementsContextValue => {
35 | if (!ctx) {
36 | throw new Error(
37 | `Could not find Elements context; You need to wrap the part of your app that ${useCase} in an provider.`
38 | );
39 | }
40 |
41 | return ctx;
42 | };
43 |
44 | interface ElementsProps {
45 | /**
46 | * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object.
47 | * The easiest way to initialize a `Stripe` object is with the the [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
48 | * Once this prop has been set, it can not be changed.
49 | *
50 | * You can also pass in `null` or a `Promise` resolving to `null` if you are performing an initial server-side render or when generating a static site.
51 | */
52 | stripe: PromiseLike | stripeJs.Stripe | null;
53 |
54 | /**
55 | * Optional [Elements configuration options](https://stripe.com/docs/js/elements_object/create).
56 | * Once the stripe prop has been set, these options cannot be changed.
57 | */
58 | options?: stripeJs.StripeElementsOptions;
59 | }
60 |
61 | interface PrivateElementsProps {
62 | stripe: unknown;
63 | options?: UnknownOptions;
64 | children?: ReactNode;
65 | }
66 |
67 | /**
68 | * The `Elements` provider allows you to use [Element components](https://stripe.com/docs/stripe-js/react#element-components) and access the [Stripe object](https://stripe.com/docs/js/initializing) in any nested component.
69 | * Render an `Elements` provider at the root of your React app so that it is available everywhere you need it.
70 | *
71 | * To use the `Elements` provider, call `loadStripe` from `@stripe/stripe-js` with your publishable key.
72 | * The `loadStripe` function will asynchronously load the Stripe.js script and initialize a `Stripe` object.
73 | * Pass the returned `Promise` to `Elements`.
74 | *
75 | * @docs https://docs.stripe.com/sdks/stripejs-react?ui=elements#elements-provider
76 | */
77 | export const Elements: FunctionComponent> = (({
78 | stripe: rawStripeProp,
79 | options,
80 | children,
81 | }: PrivateElementsProps) => {
82 | const parsed = React.useMemo(() => parseStripeProp(rawStripeProp), [
83 | rawStripeProp,
84 | ]);
85 |
86 | // For a sync stripe instance, initialize into context
87 | const [ctx, setContext] = React.useState(() => ({
88 | stripe: parsed.tag === 'sync' ? parsed.stripe : null,
89 | elements: parsed.tag === 'sync' ? parsed.stripe.elements(options) : null,
90 | }));
91 |
92 | React.useEffect(() => {
93 | let isMounted = true;
94 |
95 | const safeSetContext = (stripe: stripeJs.Stripe) => {
96 | setContext((ctx) => {
97 | // no-op if we already have a stripe instance (https://github.com/stripe/react-stripe-js/issues/296)
98 | if (ctx.stripe) return ctx;
99 | return {
100 | stripe,
101 | elements: stripe.elements(options),
102 | };
103 | });
104 | };
105 |
106 | // For an async stripePromise, store it in context once resolved
107 | if (parsed.tag === 'async' && !ctx.stripe) {
108 | parsed.stripePromise.then((stripe) => {
109 | if (stripe && isMounted) {
110 | // Only update Elements context if the component is still mounted
111 | // and stripe is not null. We allow stripe to be null to make
112 | // handling SSR easier.
113 | safeSetContext(stripe);
114 | }
115 | });
116 | } else if (parsed.tag === 'sync' && !ctx.stripe) {
117 | // Or, handle a sync stripe instance going from null -> populated
118 | safeSetContext(parsed.stripe);
119 | }
120 |
121 | return () => {
122 | isMounted = false;
123 | };
124 | }, [parsed, ctx, options]);
125 |
126 | // Warn on changes to stripe prop
127 | const prevStripe = usePrevious(rawStripeProp);
128 | React.useEffect(() => {
129 | if (prevStripe !== null && prevStripe !== rawStripeProp) {
130 | console.warn(
131 | 'Unsupported prop change on Elements: You cannot change the `stripe` prop after setting it.'
132 | );
133 | }
134 | }, [prevStripe, rawStripeProp]);
135 |
136 | // Apply updates to elements when options prop has relevant changes
137 | const prevOptions = usePrevious(options);
138 | React.useEffect(() => {
139 | if (!ctx.elements) {
140 | return;
141 | }
142 |
143 | const updates = extractAllowedOptionsUpdates(options, prevOptions, [
144 | 'clientSecret',
145 | 'fonts',
146 | ]);
147 |
148 | if (updates) {
149 | ctx.elements.update(updates);
150 | }
151 | }, [options, prevOptions, ctx.elements]);
152 |
153 | // Attach react-stripe-js version to stripe.js instance
154 | React.useEffect(() => {
155 | registerWithStripeJs(ctx.stripe);
156 | }, [ctx.stripe]);
157 |
158 | return (
159 | {children}
160 | );
161 | }) as FunctionComponent>;
162 |
163 | Elements.propTypes = {
164 | stripe: PropTypes.any,
165 | options: PropTypes.object as any,
166 | };
167 |
168 | export const useElementsContextWithUseCase = (
169 | useCaseMessage: string
170 | ): ElementsContextValue => {
171 | const ctx = React.useContext(ElementsContext);
172 | return parseElementsContext(ctx, useCaseMessage);
173 | };
174 |
175 | /**
176 | * @docs https://stripe.com/docs/stripe-js/react#useelements-hook
177 | */
178 | export const useElements = (): stripeJs.StripeElements | null => {
179 | const {elements} = useElementsContextWithUseCase('calls useElements()');
180 | return elements;
181 | };
182 |
183 | interface ElementsConsumerProps {
184 | children: (props: ElementsContextValue) => ReactNode;
185 | }
186 |
187 | /**
188 | * @docs https://stripe.com/docs/stripe-js/react#elements-consumer
189 | */
190 | export const ElementsConsumer: FunctionComponent = ({
191 | children,
192 | }) => {
193 | const ctx = useElementsContextWithUseCase('mounts ');
194 |
195 | // Assert to satisfy the busted React.FC return type (it should be ReactNode)
196 | return children(ctx) as ReactElement | null;
197 | };
198 |
199 | ElementsConsumer.propTypes = {
200 | children: PropTypes.func.isRequired,
201 | };
202 |
--------------------------------------------------------------------------------
/src/components/EmbeddedCheckout.client.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render, act} from '@testing-library/react';
3 |
4 | import * as EmbeddedCheckoutProviderModule from './EmbeddedCheckoutProvider';
5 | import {EmbeddedCheckout} from './EmbeddedCheckout';
6 | import * as mocks from '../../test/mocks';
7 |
8 | const {EmbeddedCheckoutProvider} = EmbeddedCheckoutProviderModule;
9 |
10 | describe('EmbeddedCheckout on the client', () => {
11 | let mockStripe: any;
12 | let mockStripePromise: any;
13 | let mockEmbeddedCheckout: any;
14 | let mockEmbeddedCheckoutPromise: any;
15 | const fakeClientSecret = 'cs_123_secret_abc';
16 | const fetchClientSecret = () => Promise.resolve(fakeClientSecret);
17 | const fakeOptions = {fetchClientSecret};
18 |
19 | beforeEach(() => {
20 | mockStripe = mocks.mockStripe();
21 | mockStripePromise = Promise.resolve(mockStripe);
22 | mockEmbeddedCheckout = mocks.mockEmbeddedCheckout();
23 | mockEmbeddedCheckoutPromise = Promise.resolve(mockEmbeddedCheckout);
24 | mockStripe.initEmbeddedCheckout.mockReturnValue(
25 | mockEmbeddedCheckoutPromise
26 | );
27 |
28 | jest.spyOn(React, 'useLayoutEffect');
29 | });
30 |
31 | afterEach(() => {
32 | jest.restoreAllMocks();
33 | });
34 |
35 | it('passes id to the wrapping DOM element', async () => {
36 | const {container} = render(
37 |
41 |
42 |
43 | );
44 | await act(async () => await mockStripePromise);
45 |
46 | const embeddedCheckoutDiv = container.firstChild as Element;
47 | expect(embeddedCheckoutDiv.id).toBe('foo');
48 | });
49 |
50 | it('passes className to the wrapping DOM element', async () => {
51 | const {container} = render(
52 |
56 |
57 |
58 | );
59 | await act(async () => await mockStripePromise);
60 |
61 | const embeddedCheckoutDiv = container.firstChild as Element;
62 | expect(embeddedCheckoutDiv).toHaveClass('bar');
63 | });
64 |
65 | it('mounts Embedded Checkout', async () => {
66 | const {container} = render(
67 |
68 |
69 |
70 | );
71 |
72 | await act(() => mockEmbeddedCheckoutPromise);
73 |
74 | expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
75 | });
76 |
77 | it('does not mount until Embedded Checkout has been initialized', async () => {
78 | // Render with no stripe instance and client secret
79 | const {container, rerender} = render(
80 |
84 |
85 |
86 | );
87 | expect(mockEmbeddedCheckout.mount).not.toBeCalled();
88 |
89 | // Set stripe prop
90 | rerender(
91 |
95 |
96 |
97 | );
98 | expect(mockEmbeddedCheckout.mount).not.toBeCalled();
99 |
100 | // Set fetchClientSecret
101 | rerender(
102 |
106 |
107 |
108 | );
109 | expect(mockEmbeddedCheckout.mount).not.toBeCalled();
110 |
111 | // Resolve initialization promise
112 | await act(() => mockEmbeddedCheckoutPromise);
113 |
114 | expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
115 | });
116 |
117 | it('unmounts Embedded Checkout when the component unmounts', async () => {
118 | const {container, rerender} = render(
119 |
120 |
121 |
122 | );
123 |
124 | await act(() => mockEmbeddedCheckoutPromise);
125 |
126 | expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
127 |
128 | rerender(
129 |
133 | );
134 | expect(mockEmbeddedCheckout.unmount).toBeCalled();
135 | });
136 |
137 | it('does not throw when the Embedded Checkout instance is already destroyed when unmounting', async () => {
138 | const {container, rerender} = render(
139 |
140 |
141 |
142 | );
143 |
144 | await act(() => mockEmbeddedCheckoutPromise);
145 |
146 | expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
147 |
148 | mockEmbeddedCheckout.unmount.mockImplementation(() => {
149 | throw new Error('instance has been destroyed');
150 | });
151 |
152 | expect(() => {
153 | rerender(
154 |
158 | );
159 | }).not.toThrow();
160 | });
161 |
162 | it('still works with clientSecret param (deprecated)', async () => {
163 | const {container} = render(
164 |
168 |
169 |
170 | );
171 |
172 | await act(() => mockEmbeddedCheckoutPromise);
173 |
174 | expect(mockEmbeddedCheckout.mount).toBeCalledWith(container.firstChild);
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/src/components/EmbeddedCheckout.server.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | import React from 'react';
6 | import {renderToString} from 'react-dom/server';
7 |
8 | import * as EmbeddedCheckoutProviderModule from './EmbeddedCheckoutProvider';
9 | import {EmbeddedCheckout} from './EmbeddedCheckout';
10 |
11 | const {EmbeddedCheckoutProvider} = EmbeddedCheckoutProviderModule;
12 |
13 | describe('EmbeddedCheckout on the server (without stripe and clientSecret props)', () => {
14 | beforeEach(() => {
15 | jest.spyOn(React, 'useLayoutEffect');
16 | });
17 |
18 | afterEach(() => {
19 | jest.restoreAllMocks();
20 | });
21 |
22 | it('passes id to the wrapping DOM element', () => {
23 | const result = renderToString(
24 |
25 |
26 |
27 | );
28 |
29 | expect(result).toBe('');
30 | });
31 |
32 | it('passes className to the wrapping DOM element', () => {
33 | const result = renderToString(
34 |
35 |
36 |
37 | );
38 | expect(result).toEqual('');
39 | });
40 |
41 | it('throws when Embedded Checkout is mounted outside of EmbeddedCheckoutProvider context', () => {
42 | // Prevent the console.errors to keep the test output clean
43 | jest.spyOn(console, 'error');
44 | (console.error as any).mockImplementation(() => {});
45 |
46 | expect(() => renderToString()).toThrow(
47 | ' must be used within '
48 | );
49 | });
50 |
51 | it('does not call useLayoutEffect', () => {
52 | renderToString(
53 |
54 |
55 |
56 | );
57 |
58 | expect(React.useLayoutEffect).not.toHaveBeenCalled();
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/EmbeddedCheckout.tsx:
--------------------------------------------------------------------------------
1 | import React, {FunctionComponent} from 'react';
2 | import {useEmbeddedCheckoutContext} from './EmbeddedCheckoutProvider';
3 | import {isServer} from '../utils/isServer';
4 |
5 | interface EmbeddedCheckoutProps {
6 | /**
7 | * Passes through to the Embedded Checkout container.
8 | */
9 | id?: string;
10 |
11 | /**
12 | * Passes through to the Embedded Checkout container.
13 | */
14 | className?: string;
15 | }
16 |
17 | const EmbeddedCheckoutClientElement = ({
18 | id,
19 | className,
20 | }: EmbeddedCheckoutProps) => {
21 | const {embeddedCheckout} = useEmbeddedCheckoutContext();
22 |
23 | const isMounted = React.useRef(false);
24 | const domNode = React.useRef(null);
25 |
26 | React.useLayoutEffect(() => {
27 | if (!isMounted.current && embeddedCheckout && domNode.current !== null) {
28 | embeddedCheckout.mount(domNode.current);
29 | isMounted.current = true;
30 | }
31 |
32 | // Clean up on unmount
33 | return () => {
34 | if (isMounted.current && embeddedCheckout) {
35 | try {
36 | embeddedCheckout.unmount();
37 | isMounted.current = false;
38 | } catch (e) {
39 | // Do nothing.
40 | // Parent effects are destroyed before child effects, so
41 | // in cases where both the EmbeddedCheckoutProvider and
42 | // the EmbeddedCheckout component are removed at the same
43 | // time, the embeddedCheckout instance will be destroyed,
44 | // which causes an error when calling unmount.
45 | }
46 | }
47 | };
48 | }, [embeddedCheckout]);
49 |
50 | return ;
51 | };
52 |
53 | // Only render the wrapper in a server environment.
54 | const EmbeddedCheckoutServerElement = ({
55 | id,
56 | className,
57 | }: EmbeddedCheckoutProps) => {
58 | // Validate that we are in the right context by calling useEmbeddedCheckoutContext.
59 | useEmbeddedCheckoutContext();
60 | return ;
61 | };
62 |
63 | type EmbeddedCheckoutComponent = FunctionComponent;
64 |
65 | export const EmbeddedCheckout: EmbeddedCheckoutComponent = isServer
66 | ? EmbeddedCheckoutServerElement
67 | : EmbeddedCheckoutClientElement;
68 |
--------------------------------------------------------------------------------
/src/components/EmbeddedCheckoutProvider.tsx:
--------------------------------------------------------------------------------
1 | import {FunctionComponent, PropsWithChildren, ReactNode} from 'react';
2 | import React from 'react';
3 |
4 | import {usePrevious} from '../utils/usePrevious';
5 | import {UnknownOptions} from '../utils/extractAllowedOptionsUpdates';
6 | import {parseStripeProp} from '../utils/parseStripeProp';
7 | import {registerWithStripeJs} from '../utils/registerWithStripeJs';
8 | import * as stripeJs from '@stripe/stripe-js';
9 |
10 | type EmbeddedCheckoutPublicInterface = {
11 | mount(location: string | HTMLElement): void;
12 | unmount(): void;
13 | destroy(): void;
14 | };
15 |
16 | export type EmbeddedCheckoutContextValue = {
17 | embeddedCheckout: EmbeddedCheckoutPublicInterface | null;
18 | };
19 |
20 | const EmbeddedCheckoutContext = React.createContext(
21 | null
22 | );
23 | EmbeddedCheckoutContext.displayName = 'EmbeddedCheckoutProviderContext';
24 |
25 | export const useEmbeddedCheckoutContext = (): EmbeddedCheckoutContextValue => {
26 | const ctx = React.useContext(EmbeddedCheckoutContext);
27 | if (!ctx) {
28 | throw new Error(
29 | ' must be used within '
30 | );
31 | }
32 | return ctx;
33 | };
34 |
35 | interface EmbeddedCheckoutProviderProps {
36 | /**
37 | * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise`
38 | * resolving to a `Stripe` object.
39 | * The easiest way to initialize a `Stripe` object is with the the
40 | * [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
41 | * Once this prop has been set, it can not be changed.
42 | *
43 | * You can also pass in `null` or a `Promise` resolving to `null` if you are
44 | * performing an initial server-side render or when generating a static site.
45 | */
46 | stripe: PromiseLike | stripeJs.Stripe | null;
47 | /**
48 | * Embedded Checkout configuration options.
49 | * You can initially pass in `null` to `options.clientSecret` or
50 | * `options.fetchClientSecret` if you are performing an initial server-side
51 | * render or when generating a static site.
52 | */
53 | options: {
54 | clientSecret?: string | null;
55 | fetchClientSecret?: (() => Promise) | null;
56 | onComplete?: () => void;
57 | onShippingDetailsChange?: (
58 | event: stripeJs.StripeEmbeddedCheckoutShippingDetailsChangeEvent
59 | ) => Promise;
60 | onLineItemsChange?: (
61 | event: stripeJs.StripeEmbeddedCheckoutLineItemsChangeEvent
62 | ) => Promise;
63 | };
64 | }
65 |
66 | interface PrivateEmbeddedCheckoutProviderProps {
67 | stripe: unknown;
68 | options: UnknownOptions;
69 | children?: ReactNode;
70 | }
71 | const INVALID_STRIPE_ERROR =
72 | 'Invalid prop `stripe` supplied to `EmbeddedCheckoutProvider`. We recommend using the `loadStripe` utility from `@stripe/stripe-js`. See https://stripe.com/docs/stripe-js/react#elements-props-stripe for details.';
73 |
74 | export const EmbeddedCheckoutProvider: FunctionComponent> = ({
77 | stripe: rawStripeProp,
78 | options,
79 | children,
80 | }: PrivateEmbeddedCheckoutProviderProps) => {
81 | const parsed = React.useMemo(() => {
82 | return parseStripeProp(rawStripeProp, INVALID_STRIPE_ERROR);
83 | }, [rawStripeProp]);
84 |
85 | const embeddedCheckoutPromise = React.useRef | null>(null);
86 | const loadedStripe = React.useRef(null);
87 |
88 | const [ctx, setContext] = React.useState({
89 | embeddedCheckout: null,
90 | });
91 |
92 | React.useEffect(() => {
93 | // Don't support any ctx updates once embeddedCheckout or stripe is set.
94 | if (loadedStripe.current || embeddedCheckoutPromise.current) {
95 | return;
96 | }
97 |
98 | const setStripeAndInitEmbeddedCheckout = (stripe: stripeJs.Stripe) => {
99 | if (loadedStripe.current || embeddedCheckoutPromise.current) return;
100 |
101 | loadedStripe.current = stripe;
102 | embeddedCheckoutPromise.current = loadedStripe.current
103 | .initEmbeddedCheckout(options as any)
104 | .then((embeddedCheckout) => {
105 | setContext({embeddedCheckout});
106 | });
107 | };
108 |
109 | // For an async stripePromise, store it once resolved
110 | if (
111 | parsed.tag === 'async' &&
112 | !loadedStripe.current &&
113 | (options.clientSecret || options.fetchClientSecret)
114 | ) {
115 | parsed.stripePromise.then((stripe) => {
116 | if (stripe) {
117 | setStripeAndInitEmbeddedCheckout(stripe);
118 | }
119 | });
120 | } else if (
121 | parsed.tag === 'sync' &&
122 | !loadedStripe.current &&
123 | (options.clientSecret || options.fetchClientSecret)
124 | ) {
125 | // Or, handle a sync stripe instance going from null -> populated
126 | setStripeAndInitEmbeddedCheckout(parsed.stripe);
127 | }
128 | }, [parsed, options, ctx, loadedStripe]);
129 |
130 | React.useEffect(() => {
131 | // cleanup on unmount
132 | return () => {
133 | // If embedded checkout is fully initialized, destroy it.
134 | if (ctx.embeddedCheckout) {
135 | embeddedCheckoutPromise.current = null;
136 | ctx.embeddedCheckout.destroy();
137 | } else if (embeddedCheckoutPromise.current) {
138 | // If embedded checkout is still initializing, destroy it once
139 | // it's done. This could be caused by unmounting very quickly
140 | // after mounting.
141 | embeddedCheckoutPromise.current.then(() => {
142 | embeddedCheckoutPromise.current = null;
143 | if (ctx.embeddedCheckout) {
144 | ctx.embeddedCheckout.destroy();
145 | }
146 | });
147 | }
148 | };
149 | }, [ctx.embeddedCheckout]);
150 |
151 | // Attach react-stripe-js version to stripe.js instance
152 | React.useEffect(() => {
153 | registerWithStripeJs(loadedStripe);
154 | }, [loadedStripe]);
155 |
156 | // Warn on changes to stripe prop.
157 | // The stripe prop value can only go from null to non-null once and
158 | // can't be changed after that.
159 | const prevStripe = usePrevious(rawStripeProp);
160 | React.useEffect(() => {
161 | if (prevStripe !== null && prevStripe !== rawStripeProp) {
162 | console.warn(
163 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the `stripe` prop after setting it.'
164 | );
165 | }
166 | }, [prevStripe, rawStripeProp]);
167 |
168 | // Warn on changes to options.
169 | const prevOptions = usePrevious(options);
170 | React.useEffect(() => {
171 | if (prevOptions == null) {
172 | return;
173 | }
174 |
175 | if (options == null) {
176 | console.warn(
177 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot unset options after setting them.'
178 | );
179 | return;
180 | }
181 |
182 | if (
183 | options.clientSecret === undefined &&
184 | options.fetchClientSecret === undefined
185 | ) {
186 | console.warn(
187 | 'Invalid props passed to EmbeddedCheckoutProvider: You must provide one of either `options.fetchClientSecret` or `options.clientSecret`.'
188 | );
189 | }
190 |
191 | if (
192 | prevOptions.clientSecret != null &&
193 | options.clientSecret !== prevOptions.clientSecret
194 | ) {
195 | console.warn(
196 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the client secret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.'
197 | );
198 | }
199 |
200 | if (
201 | prevOptions.fetchClientSecret != null &&
202 | options.fetchClientSecret !== prevOptions.fetchClientSecret
203 | ) {
204 | console.warn(
205 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change fetchClientSecret after setting it. Unmount and create a new instance of EmbeddedCheckoutProvider instead.'
206 | );
207 | }
208 |
209 | if (
210 | prevOptions.onComplete != null &&
211 | options.onComplete !== prevOptions.onComplete
212 | ) {
213 | console.warn(
214 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onComplete option after setting it.'
215 | );
216 | }
217 |
218 | if (
219 | prevOptions.onShippingDetailsChange != null &&
220 | options.onShippingDetailsChange !== prevOptions.onShippingDetailsChange
221 | ) {
222 | console.warn(
223 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onShippingDetailsChange option after setting it.'
224 | );
225 | }
226 |
227 | if (
228 | prevOptions.onLineItemsChange != null &&
229 | options.onLineItemsChange !== prevOptions.onLineItemsChange
230 | ) {
231 | console.warn(
232 | 'Unsupported prop change on EmbeddedCheckoutProvider: You cannot change the onLineItemsChange option after setting it.'
233 | );
234 | }
235 | }, [prevOptions, options]);
236 |
237 | return (
238 |
239 | {children}
240 |
241 | );
242 | };
243 |
--------------------------------------------------------------------------------
/src/components/FinancialAccountDisclosure.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from '@testing-library/react';
3 | import {FinancialAccountDisclosure} from './FinancialAccountDisclosure';
4 | import {StripeErrorType} from '@stripe/stripe-js';
5 | import {mockStripe as baseMockStripe} from '../../test/mocks';
6 |
7 | const apiError: StripeErrorType = 'api_error';
8 |
9 | const mockSuccessfulStripeJsCall = () => {
10 | return {
11 | ...baseMockStripe(),
12 | createFinancialAccountDisclosure: jest.fn(() =>
13 | Promise.resolve({
14 | htmlElement: document.createElement('div'),
15 | })
16 | ),
17 | };
18 | };
19 |
20 | const mockStripeJsWithError = () => {
21 | return {
22 | ...baseMockStripe(),
23 | createFinancialAccountDisclosure: jest.fn(() =>
24 | Promise.resolve({
25 | error: {
26 | type: apiError,
27 | message: 'This is a test error',
28 | },
29 | })
30 | ),
31 | };
32 | };
33 |
34 | describe('FinancialAccountDisclosure', () => {
35 | let mockStripe: any;
36 |
37 | beforeEach(() => {
38 | mockStripe = mockSuccessfulStripeJsCall();
39 | });
40 |
41 | afterEach(() => {
42 | jest.restoreAllMocks();
43 | });
44 |
45 | it('should render', () => {
46 | render();
47 | });
48 |
49 | it('should render with options', () => {
50 | const options = {
51 | businessName: 'Test Business',
52 | learnMoreLink: 'https://test.com',
53 | };
54 | render(
55 |
56 | );
57 | });
58 |
59 | it('should render when there is an error', () => {
60 | mockStripe = mockStripeJsWithError();
61 | render();
62 | });
63 |
64 | it('should render with an onLoad callback', async () => {
65 | const onLoad = jest.fn();
66 | render();
67 | await new Promise((resolve) => setTimeout(resolve, 0));
68 | expect(onLoad).toHaveBeenCalled();
69 | });
70 |
71 | it('should not call onLoad if there is an error', async () => {
72 | const onLoad = jest.fn();
73 | mockStripe = mockStripeJsWithError();
74 | render();
75 | await new Promise((resolve) => setTimeout(resolve, 0));
76 | expect(onLoad).not.toHaveBeenCalled();
77 | });
78 |
79 | it('should render with an onError callback', async () => {
80 | const onError = jest.fn();
81 | mockStripe = mockStripeJsWithError();
82 | render(
83 |
84 | );
85 | await new Promise((resolve) => setTimeout(resolve, 0));
86 | expect(onError).toHaveBeenCalled();
87 | });
88 |
89 | it('should not call onError if there is no error', async () => {
90 | const onError = jest.fn();
91 | render(
92 |
93 | );
94 | await new Promise((resolve) => setTimeout(resolve, 0));
95 | expect(onError).not.toHaveBeenCalled();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/components/FinancialAccountDisclosure.tsx:
--------------------------------------------------------------------------------
1 | import * as stripeJs from '@stripe/stripe-js';
2 | import React, {FunctionComponent} from 'react';
3 | import {parseStripeProp} from '../utils/parseStripeProp';
4 | import {registerWithStripeJs} from '../utils/registerWithStripeJs';
5 | import {StripeError} from '@stripe/stripe-js';
6 | import {usePrevious} from '../utils/usePrevious';
7 |
8 | interface FinancialAccountDisclosureProps {
9 | /**
10 | * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object.
11 | * The easiest way to initialize a `Stripe` object is with the the [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
12 | * Once this prop has been set, it can not be changed.
13 | *
14 | * You can also pass in `null` or a `Promise` resolving to `null` if you are performing an initial server-side render or when generating a static site.
15 | */
16 | stripe: PromiseLike | stripeJs.Stripe | null;
17 |
18 | /**
19 | * Callback function called after the disclosure content loads.
20 | */
21 | onLoad?: () => void;
22 |
23 | /**
24 | * Callback function called when an error occurs during disclosure creation.
25 | */
26 | onError?: (error: StripeError) => void;
27 |
28 | /**
29 | * Optional Financial Account Disclosure configuration options.
30 | *
31 | * businessName: The name of your business as you would like it to appear in the disclosure. If not provided, the business name will be inferred from the Stripe account.
32 | * learnMoreLink: A supplemental link to for your users to learn more about Financial Accounts for platforms or any other relevant information included in the disclosure.
33 | */
34 | options?: {
35 | businessName?: string;
36 | learnMoreLink?: string;
37 | };
38 | }
39 |
40 | export const FinancialAccountDisclosure: FunctionComponent = ({
41 | stripe: rawStripeProp,
42 | onLoad,
43 | onError,
44 | options,
45 | }) => {
46 | const businessName = options?.businessName;
47 | const learnMoreLink = options?.learnMoreLink;
48 |
49 | const containerRef = React.useRef(null);
50 | const parsed = React.useMemo(() => parseStripeProp(rawStripeProp), [
51 | rawStripeProp,
52 | ]);
53 | const [stripeState, setStripeState] = React.useState(
54 | parsed.tag === 'sync' ? parsed.stripe : null
55 | );
56 |
57 | React.useEffect(() => {
58 | let isMounted = true;
59 |
60 | if (parsed.tag === 'async') {
61 | parsed.stripePromise.then((stripePromise: stripeJs.Stripe | null) => {
62 | if (stripePromise && isMounted) {
63 | setStripeState(stripePromise);
64 | }
65 | });
66 | } else if (parsed.tag === 'sync') {
67 | setStripeState(parsed.stripe);
68 | }
69 |
70 | return () => {
71 | isMounted = false;
72 | };
73 | }, [parsed]);
74 |
75 | // Warn on changes to stripe prop
76 | const prevStripe = usePrevious(rawStripeProp);
77 | React.useEffect(() => {
78 | if (prevStripe !== null && prevStripe !== rawStripeProp) {
79 | console.warn(
80 | 'Unsupported prop change on FinancialAccountDisclosure: You cannot change the `stripe` prop after setting it.'
81 | );
82 | }
83 | }, [prevStripe, rawStripeProp]);
84 |
85 | // Attach react-stripe-js version to stripe.js instance
86 | React.useEffect(() => {
87 | registerWithStripeJs(stripeState);
88 | }, [stripeState]);
89 |
90 | React.useEffect(() => {
91 | const createDisclosure = async () => {
92 | if (!stripeState || !containerRef.current) {
93 | return;
94 | }
95 |
96 | const {
97 | htmlElement: disclosureContent,
98 | error,
99 | } = await (stripeState as any).createFinancialAccountDisclosure({
100 | businessName,
101 | learnMoreLink,
102 | });
103 |
104 | if (error && onError) {
105 | onError(error);
106 | } else if (disclosureContent) {
107 | const container = containerRef.current;
108 | container.innerHTML = '';
109 | container.appendChild(disclosureContent);
110 | if (onLoad) {
111 | onLoad();
112 | }
113 | }
114 | };
115 |
116 | createDisclosure();
117 | }, [stripeState, businessName, learnMoreLink, onLoad, onError]);
118 |
119 | return React.createElement('div', {ref: containerRef});
120 | };
121 |
--------------------------------------------------------------------------------
/src/components/IssuingDisclosure.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from '@testing-library/react';
3 | import {IssuingDisclosure} from './IssuingDisclosure';
4 | import {StripeErrorType} from '@stripe/stripe-js';
5 | import {mockStripe as baseMockStripe} from '../../test/mocks';
6 |
7 | const apiError: StripeErrorType = 'api_error';
8 |
9 | const mockSuccessfulStripeJsCall = () => {
10 | return {
11 | ...baseMockStripe(),
12 | createIssuingDisclosure: jest.fn(() =>
13 | Promise.resolve({
14 | htmlElement: document.createElement('div'),
15 | })
16 | ),
17 | };
18 | };
19 |
20 | const mockStripeJsWithError = () => {
21 | return {
22 | ...baseMockStripe(),
23 | createIssuingDisclosure: jest.fn(() =>
24 | Promise.resolve({
25 | error: {
26 | type: apiError,
27 | message: 'This is a test error',
28 | },
29 | })
30 | ),
31 | };
32 | };
33 |
34 | describe('IssuingDisclosure', () => {
35 | let mockStripe: any;
36 |
37 | beforeEach(() => {
38 | mockStripe = mockSuccessfulStripeJsCall();
39 | });
40 |
41 | afterEach(() => {
42 | jest.restoreAllMocks();
43 | });
44 |
45 | it('should render', () => {
46 | render();
47 | });
48 |
49 | it('should render with options', () => {
50 | const options = {
51 | issuingProgramID: 'iprg_123',
52 | publicCardProgramName: 'My Cool Card Program',
53 | learnMoreLink: 'https://test.com',
54 | };
55 | render();
56 | });
57 |
58 | it('should render when there is an error', () => {
59 | mockStripe = mockStripeJsWithError();
60 | render();
61 | });
62 |
63 | it('should render with an onLoad callback', async () => {
64 | const onLoad = jest.fn();
65 | render();
66 | await new Promise((resolve) => setTimeout(resolve, 0));
67 | expect(onLoad).toHaveBeenCalled();
68 | });
69 |
70 | it('should not call onLoad if there is an error', async () => {
71 | const onLoad = jest.fn();
72 | mockStripe = mockStripeJsWithError();
73 | render();
74 | await new Promise((resolve) => setTimeout(resolve, 0));
75 | expect(onLoad).not.toHaveBeenCalled();
76 | });
77 |
78 | it('should render with an onError callback', async () => {
79 | const onError = jest.fn();
80 | mockStripe = mockStripeJsWithError();
81 | render();
82 | await new Promise((resolve) => setTimeout(resolve, 0));
83 | expect(onError).toHaveBeenCalled();
84 | });
85 |
86 | it('should not call onError if there is no error', async () => {
87 | const onError = jest.fn();
88 | render();
89 | await new Promise((resolve) => setTimeout(resolve, 0));
90 | expect(onError).not.toHaveBeenCalled();
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/src/components/IssuingDisclosure.tsx:
--------------------------------------------------------------------------------
1 | import * as stripeJs from '@stripe/stripe-js';
2 | import React, {FunctionComponent} from 'react';
3 | import {parseStripeProp} from '../utils/parseStripeProp';
4 | import {registerWithStripeJs} from '../utils/registerWithStripeJs';
5 | import {StripeError} from '@stripe/stripe-js';
6 | import {usePrevious} from '../utils/usePrevious';
7 |
8 | interface IssuingDisclosureProps {
9 | /**
10 | * A [Stripe object](https://stripe.com/docs/js/initializing) or a `Promise` resolving to a `Stripe` object.
11 | * The easiest way to initialize a `Stripe` object is with the the [Stripe.js wrapper module](https://github.com/stripe/stripe-js/blob/master/README.md#readme).
12 | * Once this prop has been set, it can not be changed.
13 | *
14 | * You can also pass in `null` or a `Promise` resolving to `null` if you are performing an initial server-side render or when generating a static site.
15 | */
16 | stripe: PromiseLike | stripeJs.Stripe | null;
17 |
18 | /**
19 | * Callback function called after the disclosure content loads.
20 | */
21 | onLoad?: () => void;
22 |
23 | /**
24 | * Callback function called when an error occurs during disclosure creation.
25 | */
26 | onError?: (error: StripeError) => void;
27 |
28 | /**
29 | * Optional Issuing Disclosure configuration options.
30 | *
31 | * issuingProgramID: The ID of the issuing program you want to display the disclosure for.
32 | * publicCardProgramName: The public name of the Issuing card program you want to display the disclosure for.
33 | * learnMoreLink: A supplemental link to for your users to learn more about Issuing or any other relevant information included in the disclosure.
34 | */
35 | options?: {
36 | issuingProgramID?: string;
37 | publicCardProgramName?: string;
38 | learnMoreLink?: string;
39 | };
40 | }
41 |
42 | export const IssuingDisclosure: FunctionComponent = ({
43 | stripe: rawStripeProp,
44 | onLoad,
45 | onError,
46 | options,
47 | }) => {
48 | const issuingProgramID = options?.issuingProgramID;
49 | const publicCardProgramName = options?.publicCardProgramName;
50 | const learnMoreLink = options?.learnMoreLink;
51 |
52 | const containerRef = React.useRef(null);
53 | const parsed = React.useMemo(() => parseStripeProp(rawStripeProp), [
54 | rawStripeProp,
55 | ]);
56 | const [stripeState, setStripeState] = React.useState(
57 | parsed.tag === 'sync' ? parsed.stripe : null
58 | );
59 |
60 | React.useEffect(() => {
61 | let isMounted = true;
62 |
63 | if (parsed.tag === 'async') {
64 | parsed.stripePromise.then((stripePromise: stripeJs.Stripe | null) => {
65 | if (stripePromise && isMounted) {
66 | setStripeState(stripePromise);
67 | }
68 | });
69 | } else if (parsed.tag === 'sync') {
70 | setStripeState(parsed.stripe);
71 | }
72 |
73 | return () => {
74 | isMounted = false;
75 | };
76 | }, [parsed]);
77 |
78 | // Warn on changes to stripe prop
79 | const prevStripe = usePrevious(rawStripeProp);
80 | React.useEffect(() => {
81 | if (prevStripe !== null && prevStripe !== rawStripeProp) {
82 | console.warn(
83 | 'Unsupported prop change on IssuingDisclosure: You cannot change the `stripe` prop after setting it.'
84 | );
85 | }
86 | }, [prevStripe, rawStripeProp]);
87 |
88 | // Attach react-stripe-js version to stripe.js instance
89 | React.useEffect(() => {
90 | registerWithStripeJs(stripeState);
91 | }, [stripeState]);
92 |
93 | React.useEffect(() => {
94 | const createDisclosure = async () => {
95 | if (!stripeState || !containerRef.current) {
96 | return;
97 | }
98 |
99 | const {
100 | htmlElement: disclosureContent,
101 | error,
102 | } = await (stripeState as any).createIssuingDisclosure({
103 | issuingProgramID,
104 | publicCardProgramName,
105 | learnMoreLink,
106 | });
107 |
108 | if (error && onError) {
109 | onError(error);
110 | } else if (disclosureContent) {
111 | const container = containerRef.current;
112 | container.innerHTML = '';
113 | container.appendChild(disclosureContent);
114 | if (onLoad) {
115 | onLoad();
116 | }
117 | }
118 | };
119 |
120 | createDisclosure();
121 | }, [
122 | stripeState,
123 | issuingProgramID,
124 | publicCardProgramName,
125 | learnMoreLink,
126 | onLoad,
127 | onError,
128 | ]);
129 |
130 | return React.createElement('div', {ref: containerRef});
131 | };
132 |
--------------------------------------------------------------------------------
/src/components/createElementComponent.tsx:
--------------------------------------------------------------------------------
1 | // Must use `import *` or named imports for React's types
2 | import {FunctionComponent} from 'react';
3 | import * as stripeJs from '@stripe/stripe-js';
4 |
5 | import React from 'react';
6 |
7 | import PropTypes from 'prop-types';
8 |
9 | import {useAttachEvent} from '../utils/useAttachEvent';
10 | import {ElementProps} from '../types';
11 | import {usePrevious} from '../utils/usePrevious';
12 | import {
13 | extractAllowedOptionsUpdates,
14 | UnknownOptions,
15 | } from '../utils/extractAllowedOptionsUpdates';
16 | import {useElementsOrCheckoutContextWithUseCase} from '../checkout/components/CheckoutProvider';
17 |
18 | type UnknownCallback = (...args: unknown[]) => any;
19 |
20 | interface PrivateElementProps {
21 | id?: string;
22 | className?: string;
23 | onChange?: UnknownCallback;
24 | onBlur?: UnknownCallback;
25 | onFocus?: UnknownCallback;
26 | onEscape?: UnknownCallback;
27 | onReady?: UnknownCallback;
28 | onClick?: UnknownCallback;
29 | onLoadError?: UnknownCallback;
30 | onLoaderStart?: UnknownCallback;
31 | onNetworksChange?: UnknownCallback;
32 | onConfirm?: UnknownCallback;
33 | onCancel?: UnknownCallback;
34 | onShippingAddressChange?: UnknownCallback;
35 | onShippingRateChange?: UnknownCallback;
36 | onSavedPaymentMethodRemove?: UnknownCallback;
37 | onSavedPaymentMethodUpdate?: UnknownCallback;
38 | options?: UnknownOptions;
39 | }
40 |
41 | const capitalized = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
42 |
43 | const createElementComponent = (
44 | type: stripeJs.StripeElementType,
45 | isServer: boolean
46 | ): FunctionComponent => {
47 | const displayName = `${capitalized(type)}Element`;
48 |
49 | const ClientElement: FunctionComponent = ({
50 | id,
51 | className,
52 | options = {},
53 | onBlur,
54 | onFocus,
55 | onReady,
56 | onChange,
57 | onEscape,
58 | onClick,
59 | onLoadError,
60 | onLoaderStart,
61 | onNetworksChange,
62 | onConfirm,
63 | onCancel,
64 | onShippingAddressChange,
65 | onShippingRateChange,
66 | onSavedPaymentMethodRemove,
67 | onSavedPaymentMethodUpdate,
68 | }) => {
69 | const ctx = useElementsOrCheckoutContextWithUseCase(
70 | `mounts <${displayName}>`
71 | );
72 | const elements = 'elements' in ctx ? ctx.elements : null;
73 | const checkoutState = 'checkoutState' in ctx ? ctx.checkoutState : null;
74 | const checkoutSdk =
75 | checkoutState?.type === 'success' || checkoutState?.type === 'loading'
76 | ? checkoutState.sdk
77 | : null;
78 | const [element, setElement] = React.useState(
79 | null
80 | );
81 | const elementRef = React.useRef(null);
82 | const domNode = React.useRef(null);
83 |
84 | // For every event where the merchant provides a callback, call element.on
85 | // with that callback. If the merchant ever changes the callback, removes
86 | // the old callback with element.off and then call element.on with the new one.
87 | useAttachEvent(element, 'blur', onBlur);
88 | useAttachEvent(element, 'focus', onFocus);
89 | useAttachEvent(element, 'escape', onEscape);
90 | useAttachEvent(element, 'click', onClick);
91 | useAttachEvent(element, 'loaderror', onLoadError);
92 | useAttachEvent(element, 'loaderstart', onLoaderStart);
93 | useAttachEvent(element, 'networkschange', onNetworksChange);
94 | useAttachEvent(element, 'confirm', onConfirm);
95 | useAttachEvent(element, 'cancel', onCancel);
96 | useAttachEvent(element, 'shippingaddresschange', onShippingAddressChange);
97 | useAttachEvent(element, 'shippingratechange', onShippingRateChange);
98 | useAttachEvent(
99 | element,
100 | 'savedpaymentmethodremove',
101 | onSavedPaymentMethodRemove
102 | );
103 | useAttachEvent(
104 | element,
105 | 'savedpaymentmethodupdate',
106 | onSavedPaymentMethodUpdate
107 | );
108 | useAttachEvent(element, 'change', onChange);
109 |
110 | let readyCallback: UnknownCallback | undefined;
111 | if (onReady) {
112 | if (type === 'expressCheckout') {
113 | // Passes through the event, which includes visible PM types
114 | readyCallback = onReady;
115 | } else {
116 | // For other Elements, pass through the Element itself.
117 | readyCallback = () => {
118 | onReady(element);
119 | };
120 | }
121 | }
122 |
123 | useAttachEvent(element, 'ready', readyCallback);
124 |
125 | React.useLayoutEffect(() => {
126 | if (
127 | elementRef.current === null &&
128 | domNode.current !== null &&
129 | (elements || checkoutSdk)
130 | ) {
131 | let newElement: stripeJs.StripeElement | null = null;
132 | if (checkoutSdk) {
133 | switch (type) {
134 | case 'paymentForm':
135 | newElement = checkoutSdk.createPaymentFormElement();
136 | break;
137 | case 'payment':
138 | newElement = checkoutSdk.createPaymentElement(options);
139 | break;
140 | case 'address':
141 | if ('mode' in options) {
142 | const {mode, ...restOptions} = options;
143 | if (mode === 'shipping') {
144 | newElement = checkoutSdk.createShippingAddressElement(
145 | restOptions
146 | );
147 | } else if (mode === 'billing') {
148 | newElement = checkoutSdk.createBillingAddressElement(
149 | restOptions
150 | );
151 | } else {
152 | throw new Error(
153 | "Invalid options.mode. mode must be 'billing' or 'shipping'."
154 | );
155 | }
156 | } else {
157 | throw new Error(
158 | "You must supply options.mode. mode must be 'billing' or 'shipping'."
159 | );
160 | }
161 | break;
162 | case 'expressCheckout':
163 | newElement = checkoutSdk.createExpressCheckoutElement(
164 | options as any
165 | ) as stripeJs.StripeExpressCheckoutElement;
166 | break;
167 | case 'currencySelector':
168 | newElement = checkoutSdk.createCurrencySelectorElement();
169 | break;
170 | case 'taxId':
171 | newElement = checkoutSdk.createTaxIdElement(options);
172 | break;
173 | default:
174 | throw new Error(
175 | `Invalid Element type ${displayName}. You must use either the , , , or .`
176 | );
177 | }
178 | } else if (elements) {
179 | newElement = elements.create(type as any, options);
180 | }
181 |
182 | // Store element in a ref to ensure it's _immediately_ available in cleanup hooks in StrictMode
183 | elementRef.current = newElement;
184 | // Store element in state to facilitate event listener attachment
185 | setElement(newElement);
186 |
187 | if (newElement) {
188 | newElement.mount(domNode.current);
189 | }
190 | }
191 | }, [elements, checkoutSdk, options]);
192 |
193 | const prevOptions = usePrevious(options);
194 | React.useEffect(() => {
195 | if (!elementRef.current) {
196 | return;
197 | }
198 |
199 | const updates = extractAllowedOptionsUpdates(options, prevOptions, [
200 | 'paymentRequest',
201 | ]);
202 |
203 | if (updates && 'update' in elementRef.current) {
204 | elementRef.current.update(updates);
205 | }
206 | }, [options, prevOptions]);
207 |
208 | React.useLayoutEffect(() => {
209 | return () => {
210 | if (
211 | elementRef.current &&
212 | typeof elementRef.current.destroy === 'function'
213 | ) {
214 | try {
215 | elementRef.current.destroy();
216 | elementRef.current = null;
217 | } catch (error) {
218 | // Do nothing
219 | }
220 | }
221 | };
222 | }, []);
223 |
224 | return ;
225 | };
226 |
227 | // Only render the Element wrapper in a server environment.
228 | const ServerElement: FunctionComponent = (props) => {
229 | useElementsOrCheckoutContextWithUseCase(`mounts <${displayName}>`);
230 | const {id, className} = props;
231 | return ;
232 | };
233 |
234 | const Element = isServer ? ServerElement : ClientElement;
235 |
236 | Element.propTypes = {
237 | id: PropTypes.string,
238 | className: PropTypes.string,
239 | onChange: PropTypes.func,
240 | onBlur: PropTypes.func,
241 | onFocus: PropTypes.func,
242 | onReady: PropTypes.func,
243 | onEscape: PropTypes.func,
244 | onClick: PropTypes.func,
245 | onLoadError: PropTypes.func,
246 | onLoaderStart: PropTypes.func,
247 | onNetworksChange: PropTypes.func,
248 | onConfirm: PropTypes.func,
249 | onCancel: PropTypes.func,
250 | onShippingAddressChange: PropTypes.func,
251 | onShippingRateChange: PropTypes.func,
252 | onSavedPaymentMethodRemove: PropTypes.func,
253 | onSavedPaymentMethodUpdate: PropTypes.func,
254 | options: PropTypes.object as any,
255 | };
256 |
257 | Element.displayName = displayName;
258 | (Element as any).__elementType = type;
259 |
260 | return Element as FunctionComponent;
261 | };
262 |
263 | export default createElementComponent;
264 |
--------------------------------------------------------------------------------
/src/components/useStripe.tsx:
--------------------------------------------------------------------------------
1 | import * as stripeJs from '@stripe/stripe-js';
2 | import {useElementsOrCheckoutContextWithUseCase} from '../checkout/components/CheckoutProvider';
3 |
4 | /**
5 | * @docs https://stripe.com/docs/stripe-js/react#usestripe-hook
6 | */
7 | export const useStripe = (): stripeJs.Stripe | null => {
8 | const {stripe} = useElementsOrCheckoutContextWithUseCase('calls useStripe()');
9 | return stripe;
10 | };
11 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | // A magic global reflecting the current package version defined in
2 | // `package.json`. This will be rewritten at build time as a string literal
3 | // when rollup is run (via `@plugin/rollup-replace`).
4 | declare const _VERSION: string;
5 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import createElementComponent from './components/createElementComponent';
2 | import {
3 | AuBankAccountElementComponent,
4 | CardElementComponent,
5 | CardNumberElementComponent,
6 | CardExpiryElementComponent,
7 | CardCvcElementComponent,
8 | ExpressCheckoutElementComponent,
9 | IbanElementComponent,
10 | LinkAuthenticationElementComponent,
11 | PaymentElementComponent,
12 | PaymentRequestButtonElementComponent,
13 | ShippingAddressElementComponent,
14 | AddressElementComponent,
15 | PaymentMethodMessagingElementComponent,
16 | TaxIdElementComponent,
17 | } from './types';
18 | import {isServer} from './utils/isServer';
19 |
20 | export * from './types';
21 |
22 | export {useElements, Elements, ElementsConsumer} from './components/Elements';
23 |
24 | export {EmbeddedCheckout} from './components/EmbeddedCheckout';
25 | export {EmbeddedCheckoutProvider} from './components/EmbeddedCheckoutProvider';
26 | export {FinancialAccountDisclosure} from './components/FinancialAccountDisclosure';
27 | export {IssuingDisclosure} from './components/IssuingDisclosure';
28 | export {useStripe} from './components/useStripe';
29 |
30 | /**
31 | * Requires beta access:
32 | * Contact [Stripe support](https://support.stripe.com/) for more information.
33 | *
34 | * @docs https://stripe.com/docs/stripe-js/react#element-components
35 | */
36 | export const AuBankAccountElement: AuBankAccountElementComponent = createElementComponent(
37 | 'auBankAccount',
38 | isServer
39 | );
40 |
41 | /**
42 | * @docs https://stripe.com/docs/stripe-js/react#element-components
43 | */
44 | export const CardElement: CardElementComponent = createElementComponent(
45 | 'card',
46 | isServer
47 | );
48 |
49 | /**
50 | * @docs https://stripe.com/docs/stripe-js/react#element-components
51 | */
52 | export const CardNumberElement: CardNumberElementComponent = createElementComponent(
53 | 'cardNumber',
54 | isServer
55 | );
56 |
57 | /**
58 | * @docs https://stripe.com/docs/stripe-js/react#element-components
59 | */
60 | export const CardExpiryElement: CardExpiryElementComponent = createElementComponent(
61 | 'cardExpiry',
62 | isServer
63 | );
64 |
65 | /**
66 | * @docs https://stripe.com/docs/stripe-js/react#element-components
67 | */
68 | export const CardCvcElement: CardCvcElementComponent = createElementComponent(
69 | 'cardCvc',
70 | isServer
71 | );
72 |
73 | /**
74 | * @docs https://stripe.com/docs/stripe-js/react#element-components
75 | */
76 | export const IbanElement: IbanElementComponent = createElementComponent(
77 | 'iban',
78 | isServer
79 | );
80 |
81 | export const PaymentElement: PaymentElementComponent = createElementComponent(
82 | 'payment',
83 | isServer
84 | );
85 |
86 | /**
87 | * @docs https://stripe.com/docs/stripe-js/react#element-components
88 | */
89 | export const ExpressCheckoutElement: ExpressCheckoutElementComponent = createElementComponent(
90 | 'expressCheckout',
91 | isServer
92 | );
93 |
94 | /**
95 | * @docs https://stripe.com/docs/stripe-js/react#element-components
96 | */
97 | export const PaymentRequestButtonElement: PaymentRequestButtonElementComponent = createElementComponent(
98 | 'paymentRequestButton',
99 | isServer
100 | );
101 |
102 | /**
103 | * @docs https://stripe.com/docs/stripe-js/react#element-components
104 | */
105 | export const LinkAuthenticationElement: LinkAuthenticationElementComponent = createElementComponent(
106 | 'linkAuthentication',
107 | isServer
108 | );
109 |
110 | /**
111 | * @docs https://stripe.com/docs/stripe-js/react#element-components
112 | */
113 | export const AddressElement: AddressElementComponent = createElementComponent(
114 | 'address',
115 | isServer
116 | );
117 |
118 | /**
119 | * @deprecated
120 | * Use `AddressElement` instead.
121 | *
122 | * @docs https://stripe.com/docs/stripe-js/react#element-components
123 | */
124 | export const ShippingAddressElement: ShippingAddressElementComponent = createElementComponent(
125 | 'shippingAddress',
126 | isServer
127 | );
128 |
129 | /**
130 | * @docs https://stripe.com/docs/stripe-js/react#element-components
131 | */
132 | export const PaymentMethodMessagingElement: PaymentMethodMessagingElementComponent = createElementComponent(
133 | 'paymentMethodMessaging',
134 | isServer
135 | );
136 |
137 | /**
138 | * Requires beta access:
139 | * Contact [Stripe support](https://support.stripe.com/) for more information.
140 | */
141 | export const TaxIdElement: TaxIdElementComponent = createElementComponent(
142 | 'taxId',
143 | isServer
144 | );
145 |
--------------------------------------------------------------------------------
/src/utils/extractAllowedOptionsUpdates.test.ts:
--------------------------------------------------------------------------------
1 | import {extractAllowedOptionsUpdates} from './extractAllowedOptionsUpdates';
2 |
3 | describe('extractAllowedOptionsUpdates', () => {
4 | it('drops unchanged keys', () => {
5 | expect(
6 | extractAllowedOptionsUpdates(
7 | {foo: 'foo2', bar: {buz: 'buz'}},
8 | {foo: 'foo1', bar: {buz: 'buz'}},
9 | []
10 | )
11 | ).toEqual({foo: 'foo2'});
12 | });
13 |
14 | it('works with a null previous value', () => {
15 | expect(extractAllowedOptionsUpdates({foo: 'foo2'}, null, [])).toEqual({
16 | foo: 'foo2',
17 | });
18 | });
19 |
20 | it('warns about and drops updates to immutable keys', () => {
21 | const consoleSpy = jest.spyOn(window.console, 'warn');
22 |
23 | // Silence console output so test output is less noisy
24 | consoleSpy.mockImplementation(() => {});
25 |
26 | expect(
27 | extractAllowedOptionsUpdates(
28 | {foo: 'foo2', bar: 'bar'},
29 | {foo: 'foo1', bar: 'bar'},
30 | ['bar', 'foo']
31 | )
32 | ).toEqual(null);
33 | expect(consoleSpy).toHaveBeenCalledWith(
34 | 'Unsupported prop change: options.foo is not a mutable property.'
35 | );
36 | expect(consoleSpy).toHaveBeenCalledTimes(1);
37 |
38 | consoleSpy.mockRestore();
39 | });
40 |
41 | it('does not warn on properties that do not change', () => {
42 | const consoleSpy = jest.spyOn(window.console, 'warn');
43 |
44 | // Silence console output so test output is less noisy
45 | consoleSpy.mockImplementation(() => {});
46 |
47 | const obj = {
48 | num: 0,
49 | obj: {
50 | num: 0,
51 | },
52 | emptyObj: {},
53 | regex: /foo/,
54 | func: () => {},
55 | null: null,
56 | undefined: undefined,
57 | array: [1, 2, 3],
58 | };
59 |
60 | expect(extractAllowedOptionsUpdates(obj, obj, Object.keys(obj))).toEqual(
61 | null
62 | );
63 |
64 | expect(consoleSpy).not.toHaveBeenCalled();
65 | consoleSpy.mockRestore();
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/utils/extractAllowedOptionsUpdates.ts:
--------------------------------------------------------------------------------
1 | import {isUnknownObject} from './guards';
2 | import {isEqual} from './isEqual';
3 |
4 | export type UnknownOptions = {[k: string]: unknown};
5 |
6 | export const extractAllowedOptionsUpdates = (
7 | options: unknown | void,
8 | prevOptions: unknown | void,
9 | immutableKeys: string[]
10 | ): UnknownOptions | null => {
11 | if (!isUnknownObject(options)) {
12 | return null;
13 | }
14 |
15 | return Object.keys(options).reduce(
16 | (newOptions: null | UnknownOptions, key) => {
17 | const isUpdated =
18 | !isUnknownObject(prevOptions) ||
19 | !isEqual(options[key], prevOptions[key]);
20 |
21 | if (immutableKeys.includes(key)) {
22 | if (isUpdated) {
23 | console.warn(
24 | `Unsupported prop change: options.${key} is not a mutable property.`
25 | );
26 | }
27 |
28 | return newOptions;
29 | }
30 |
31 | if (!isUpdated) {
32 | return newOptions;
33 | }
34 |
35 | return {...(newOptions || {}), [key]: options[key]};
36 | },
37 | null
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/utils/guards.ts:
--------------------------------------------------------------------------------
1 | import {Stripe} from '@stripe/stripe-js';
2 |
3 | export const isUnknownObject = (
4 | raw: unknown
5 | ): raw is {[key in PropertyKey]: unknown} => {
6 | return raw !== null && typeof raw === 'object';
7 | };
8 |
9 | export const isPromise = (raw: unknown): raw is PromiseLike => {
10 | return isUnknownObject(raw) && typeof raw.then === 'function';
11 | };
12 |
13 | // We are using types to enforce the `stripe` prop in this lib,
14 | // but in an untyped integration `stripe` could be anything, so we need
15 | // to do some sanity validation to prevent type errors.
16 | export const isStripe = (raw: unknown): raw is Stripe => {
17 | return (
18 | isUnknownObject(raw) &&
19 | typeof raw.elements === 'function' &&
20 | typeof raw.createToken === 'function' &&
21 | typeof raw.createPaymentMethod === 'function' &&
22 | typeof raw.confirmCardPayment === 'function'
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/utils/isEqual.test.ts:
--------------------------------------------------------------------------------
1 | import {isEqual} from './isEqual';
2 |
3 | describe('isEqual', () => {
4 | [
5 | ['a', 'a'],
6 | [100, 100],
7 | [false, false],
8 | [undefined, undefined],
9 | [null, null],
10 | [{}, {}],
11 | [{a: 10}, {a: 10}],
12 | [{a: null}, {a: null}],
13 | [{a: undefined}, {a: undefined}],
14 | [[], []],
15 | [
16 | ['a', 'b', 'c'],
17 | ['a', 'b', 'c'],
18 | ],
19 | [
20 | ['a', {inner: [12]}, 'c'],
21 | ['a', {inner: [12]}, 'c'],
22 | ],
23 | [{a: {nested: {more: [1, 2, 3]}}}, {a: {nested: {more: [1, 2, 3]}}}],
24 | ].forEach(([left, right]) => {
25 | it(`should should return true for isEqual(${JSON.stringify(
26 | left
27 | )}, ${JSON.stringify(right)})`, () => {
28 | expect(isEqual(left, right)).toBe(true);
29 | expect(isEqual(right, left)).toBe(true);
30 | });
31 | });
32 |
33 | [
34 | ['a', 'b'],
35 | ['0', 0],
36 | [new Date(1), {}],
37 | [false, ''],
38 | [false, true],
39 | [null, undefined],
40 | [{}, []],
41 | [/foo/, /foo/],
42 | [new Date(1), new Date(1)],
43 | [{a: 10}, {a: 11}],
44 | [
45 | ['a', 'b', 'c'],
46 | ['a', 'b', 'c', 'd'],
47 | ],
48 | [
49 | ['a', 'b', 'c', 'd'],
50 | ['a', 'b', 'c'],
51 | ],
52 | [
53 | ['a', {inner: [12]}, 'c'],
54 | ['a', {inner: [null]}, 'c'],
55 | ],
56 | [{a: {nested: {more: [1, 2, 3]}}}, {b: {nested: {more: [1, 2, 3]}}}],
57 | ].forEach(([left, right]) => {
58 | it(`should should return false for isEqual(${JSON.stringify(
59 | left
60 | )}, ${JSON.stringify(right)})`, () => {
61 | expect(isEqual(left, right)).toBe(false);
62 | expect(isEqual(right, left)).toBe(false);
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/utils/isEqual.ts:
--------------------------------------------------------------------------------
1 | import {isUnknownObject} from './guards';
2 |
3 | const PLAIN_OBJECT_STR = '[object Object]';
4 |
5 | export const isEqual = (left: unknown, right: unknown): boolean => {
6 | if (!isUnknownObject(left) || !isUnknownObject(right)) {
7 | return left === right;
8 | }
9 |
10 | const leftArray = Array.isArray(left);
11 | const rightArray = Array.isArray(right);
12 |
13 | if (leftArray !== rightArray) return false;
14 |
15 | const leftPlainObject =
16 | Object.prototype.toString.call(left) === PLAIN_OBJECT_STR;
17 | const rightPlainObject =
18 | Object.prototype.toString.call(right) === PLAIN_OBJECT_STR;
19 |
20 | if (leftPlainObject !== rightPlainObject) return false;
21 |
22 | // not sure what sort of special object this is (regexp is one option), so
23 | // fallback to reference check.
24 | if (!leftPlainObject && !leftArray) return left === right;
25 |
26 | const leftKeys = Object.keys(left);
27 | const rightKeys = Object.keys(right);
28 |
29 | if (leftKeys.length !== rightKeys.length) return false;
30 |
31 | const keySet: {[key: string]: boolean} = {};
32 | for (let i = 0; i < leftKeys.length; i += 1) {
33 | keySet[leftKeys[i]] = true;
34 | }
35 | for (let i = 0; i < rightKeys.length; i += 1) {
36 | keySet[rightKeys[i]] = true;
37 | }
38 | const allKeys = Object.keys(keySet);
39 | if (allKeys.length !== leftKeys.length) {
40 | return false;
41 | }
42 |
43 | const l = left;
44 | const r = right;
45 | const pred = (key: string): boolean => {
46 | return isEqual(l[key], r[key]);
47 | };
48 |
49 | return allKeys.every(pred);
50 | };
51 |
--------------------------------------------------------------------------------
/src/utils/isServer.ts:
--------------------------------------------------------------------------------
1 | export const isServer = typeof window === 'undefined';
2 |
--------------------------------------------------------------------------------
/src/utils/parseStripeProp.ts:
--------------------------------------------------------------------------------
1 | import * as stripeJs from '@stripe/stripe-js';
2 | import {isStripe, isPromise} from '../utils/guards';
3 |
4 | const INVALID_STRIPE_ERROR =
5 | 'Invalid prop `stripe` supplied to `Elements`. We recommend using the `loadStripe` utility from `@stripe/stripe-js`. See https://stripe.com/docs/stripe-js/react#elements-props-stripe for details.';
6 |
7 | // We are using types to enforce the `stripe` prop in this lib, but in a real
8 | // integration `stripe` could be anything, so we need to do some sanity
9 | // validation to prevent type errors.
10 | const validateStripe = (
11 | maybeStripe: unknown,
12 | errorMsg = INVALID_STRIPE_ERROR
13 | ): null | stripeJs.Stripe => {
14 | if (maybeStripe === null || isStripe(maybeStripe)) {
15 | return maybeStripe;
16 | }
17 |
18 | throw new Error(errorMsg);
19 | };
20 |
21 | type ParsedStripeProp =
22 | | {tag: 'empty'}
23 | | {tag: 'sync'; stripe: stripeJs.Stripe}
24 | | {tag: 'async'; stripePromise: Promise};
25 |
26 | export const parseStripeProp = (
27 | raw: unknown,
28 | errorMsg = INVALID_STRIPE_ERROR
29 | ): ParsedStripeProp => {
30 | if (isPromise(raw)) {
31 | return {
32 | tag: 'async',
33 | stripePromise: Promise.resolve(raw).then((result) =>
34 | validateStripe(result, errorMsg)
35 | ),
36 | };
37 | }
38 |
39 | const stripe = validateStripe(raw, errorMsg);
40 |
41 | if (stripe === null) {
42 | return {tag: 'empty'};
43 | }
44 |
45 | return {tag: 'sync', stripe};
46 | };
47 |
--------------------------------------------------------------------------------
/src/utils/registerWithStripeJs.ts:
--------------------------------------------------------------------------------
1 | export const registerWithStripeJs = (stripe: any) => {
2 | if (!stripe || !stripe._registerWrapper || !stripe.registerAppInfo) {
3 | return;
4 | }
5 |
6 | stripe._registerWrapper({name: 'react-stripe-js', version: _VERSION});
7 |
8 | stripe.registerAppInfo({
9 | name: 'react-stripe-js',
10 | version: _VERSION,
11 | url: 'https://stripe.com/docs/stripe-js/react',
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/src/utils/useAttachEvent.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as stripeJs from '@stripe/stripe-js';
3 |
4 | export const useAttachEvent = (
5 | element: stripeJs.StripeElement | null,
6 | event: string,
7 | cb?: (...args: A) => any
8 | ) => {
9 | const cbDefined = !!cb;
10 | const cbRef = React.useRef(cb);
11 |
12 | // In many integrations the callback prop changes on each render.
13 | // Using a ref saves us from calling element.on/.off every render.
14 | React.useEffect(() => {
15 | cbRef.current = cb;
16 | }, [cb]);
17 |
18 | React.useEffect(() => {
19 | if (!cbDefined || !element) {
20 | return () => {};
21 | }
22 |
23 | const decoratedCb = (...args: A): void => {
24 | if (cbRef.current) {
25 | cbRef.current(...args);
26 | }
27 | };
28 |
29 | (element as any).on(event, decoratedCb);
30 |
31 | return () => {
32 | (element as any).off(event, decoratedCb);
33 | };
34 | }, [cbDefined, event, element, cbRef]);
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/usePrevious.test.tsx:
--------------------------------------------------------------------------------
1 | import {renderHook} from '@testing-library/react-hooks';
2 |
3 | import {usePrevious} from './usePrevious';
4 |
5 | describe('usePrevious', () => {
6 | it('returns the initial value if it has not yet been changed', () => {
7 | const {result} = renderHook(() => usePrevious('foo'));
8 |
9 | expect(result.current).toEqual('foo');
10 | });
11 |
12 | it('returns the previous value after the it has been changed', () => {
13 | let val = 'foo';
14 | const {result, rerender} = renderHook(() => usePrevious(val));
15 |
16 | expect(result.current).toEqual('foo');
17 |
18 | val = 'bar';
19 | rerender();
20 | expect(result.current).toEqual('foo');
21 |
22 | val = 'baz';
23 | rerender();
24 | expect(result.current).toEqual('bar');
25 |
26 | val = 'buz';
27 | rerender();
28 | expect(result.current).toEqual('baz');
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/utils/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const usePrevious = (value: T): T => {
4 | const ref = React.useRef(value);
5 |
6 | React.useEffect(() => {
7 | ref.current = value;
8 | }, [value]);
9 |
10 | return ref.current;
11 | };
12 |
--------------------------------------------------------------------------------
/test/makeDeferred.ts:
--------------------------------------------------------------------------------
1 | const makeDeferred = () => {
2 | let resolve!: (arg: T) => void;
3 | let reject!: (arg: any) => void;
4 | const promise: Promise = new Promise((res: any, rej: any) => {
5 | resolve = jest.fn(res);
6 | reject = jest.fn(rej);
7 | });
8 | return {
9 | resolve: async (arg: T) => {
10 | resolve(arg);
11 | await new Promise(process.nextTick);
12 | },
13 | reject: async (failure: any) => {
14 | reject(failure);
15 | await new Promise(process.nextTick);
16 | },
17 | promise,
18 | getPromise: jest.fn(() => promise),
19 | };
20 | };
21 | export default makeDeferred;
22 |
--------------------------------------------------------------------------------
/test/mocks.js:
--------------------------------------------------------------------------------
1 | export const mockElement = () => ({
2 | mount: jest.fn(),
3 | destroy: jest.fn(),
4 | on: jest.fn(),
5 | update: jest.fn(),
6 | });
7 |
8 | export const mockElements = () => {
9 | const elements = {};
10 | return {
11 | create: jest.fn((type) => {
12 | elements[type] = mockElement();
13 | return elements[type];
14 | }),
15 | getElement: jest.fn((type) => {
16 | return elements[type] || null;
17 | }),
18 | update: jest.fn(),
19 | };
20 | };
21 |
22 | export const mockCheckoutSession = () => {
23 | return {
24 | lineItems: [],
25 | currency: 'usd',
26 | shippingOptions: [],
27 | total: {
28 | subtotal: 1099,
29 | taxExclusive: 0,
30 | taxInclusive: 0,
31 | shippingRate: 0,
32 | discount: 0,
33 | total: 1099,
34 | },
35 | confirmationRequirements: [],
36 | canConfirm: true,
37 | };
38 | };
39 |
40 | export const mockCheckoutActions = () => {
41 | return {
42 | getSession: jest.fn(() => mockCheckoutSession()),
43 | applyPromotionCode: jest.fn(),
44 | removePromotionCode: jest.fn(),
45 | updateShippingAddress: jest.fn(),
46 | updateBillingAddress: jest.fn(),
47 | updatePhoneNumber: jest.fn(),
48 | updateEmail: jest.fn(),
49 | updateLineItemQuantity: jest.fn(),
50 | updateShippingOption: jest.fn(),
51 | confirm: jest.fn(),
52 | };
53 | };
54 |
55 | export const mockCheckoutSdk = () => {
56 | const elements = {};
57 |
58 | return {
59 | changeAppearance: jest.fn(),
60 | loadFonts: jest.fn(),
61 | createPaymentElement: jest.fn(() => {
62 | elements.payment = mockElement();
63 | return elements.payment;
64 | }),
65 | createPaymentFormElement: jest.fn(() => {
66 | elements.paymentForm = mockElement();
67 | return elements.paymentForm;
68 | }),
69 | createBillingAddressElement: jest.fn(() => {
70 | elements.billingAddress = mockElement();
71 | return elements.billingAddress;
72 | }),
73 | createShippingAddressElement: jest.fn(() => {
74 | elements.shippingAddress = mockElement();
75 | return elements.shippingAddress;
76 | }),
77 | createExpressCheckoutElement: jest.fn(() => {
78 | elements.expressCheckout = mockElement();
79 | return elements.expressCheckout;
80 | }),
81 | getPaymentElement: jest.fn(() => {
82 | return elements.payment || null;
83 | }),
84 | getPaymentFormElement: jest.fn(() => {
85 | return elements.paymentForm || null;
86 | }),
87 | getBillingAddressElement: jest.fn(() => {
88 | return elements.billingAddress || null;
89 | }),
90 | getShippingAddressElement: jest.fn(() => {
91 | return elements.shippingAddress || null;
92 | }),
93 | getExpressCheckoutElement: jest.fn(() => {
94 | return elements.expressCheckout || null;
95 | }),
96 |
97 | on: jest.fn((event, callback) => {
98 | if (event === 'change') {
99 | // Simulate initial session call
100 | setTimeout(() => callback(mockCheckoutSession()), 0);
101 | }
102 | }),
103 | loadActions: jest.fn().mockResolvedValue({
104 | type: 'success',
105 | actions: mockCheckoutActions(),
106 | }),
107 | };
108 | };
109 |
110 | export const mockEmbeddedCheckout = () => ({
111 | mount: jest.fn(),
112 | unmount: jest.fn(),
113 | destroy: jest.fn(),
114 | });
115 |
116 | export const mockStripe = () => {
117 | const checkoutSdk = mockCheckoutSdk();
118 |
119 | return {
120 | elements: jest.fn(() => mockElements()),
121 | createToken: jest.fn(),
122 | createSource: jest.fn(),
123 | createPaymentMethod: jest.fn(),
124 | confirmCardPayment: jest.fn(),
125 | confirmCardSetup: jest.fn(),
126 | paymentRequest: jest.fn(),
127 | registerAppInfo: jest.fn(),
128 | _registerWrapper: jest.fn(),
129 | initCheckout: jest.fn(() => checkoutSdk),
130 | initEmbeddedCheckout: jest.fn(() =>
131 | Promise.resolve(mockEmbeddedCheckout())
132 | ),
133 | };
134 | };
135 |
--------------------------------------------------------------------------------
/test/setupJest.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext", // Let Babel deal with transpiling new language features
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "jsx": "react",
7 | "noEmit": true,
8 | "declaration": true,
9 | "allowJs": true,
10 | "removeComments": false,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "esModuleInterop": true
14 | },
15 | "include": ["./src"]
16 | }
17 |
--------------------------------------------------------------------------------