├── .babelrc ├── .eslintignore ├── .eslintrc.yml ├── .github ├── API_REVIEW.md ├── ISSUE_TEMPLATE │ ├── BUG.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── stale.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── google-java-format.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml ├── react-stripe-js.iml └── vcs.xml ├── .prettierignore ├── .prettierrc.yml ├── .storybook ├── example.stories.js ├── main.js └── preview-head.html ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── checkout.d.ts ├── checkout.js ├── docs └── migrating.md ├── examples ├── .eslintrc.yml ├── class-components │ ├── 0-Card-Minimal.js │ ├── 1-Card-Detailed.js │ ├── 2-Split-Card.js │ ├── 3-Payment-Request-Button.js │ └── 4-IBAN.js ├── hooks │ ├── 0-Card-Minimal.js │ ├── 1-Card-Detailed.js │ ├── 11-Custom-Checkout.js │ ├── 12-Embedded-Checkout.js │ ├── 13-Payment-Form-Element.js │ ├── 2-Split-Card.js │ ├── 3-Payment-Request-Button.js │ ├── 4-IBAN.js │ └── 9-Payment-Element.js ├── styles │ ├── 2-Card-Detailed.css │ └── common.css └── util.js ├── package.json ├── rollup.config.js ├── scripts ├── check-imports ├── is_release_candidate.js └── publish ├── src ├── checkout │ ├── components │ │ ├── CheckoutProvider.test.tsx │ │ └── CheckoutProvider.tsx │ ├── index.ts │ └── types │ │ └── index.ts ├── components │ ├── Elements.test.tsx │ ├── Elements.tsx │ ├── EmbeddedCheckout.client.test.tsx │ ├── EmbeddedCheckout.server.test.tsx │ ├── EmbeddedCheckout.tsx │ ├── EmbeddedCheckoutProvider.test.tsx │ ├── EmbeddedCheckoutProvider.tsx │ ├── FinancialAccountDisclosure.test.tsx │ ├── FinancialAccountDisclosure.tsx │ ├── IssuingDisclosure.test.tsx │ ├── IssuingDisclosure.tsx │ ├── createElementComponent.test.tsx │ ├── createElementComponent.tsx │ └── useStripe.tsx ├── env.d.ts ├── index.ts ├── types │ └── index.ts └── utils │ ├── extractAllowedOptionsUpdates.test.ts │ ├── extractAllowedOptionsUpdates.ts │ ├── guards.ts │ ├── isEqual.test.ts │ ├── isEqual.ts │ ├── isServer.ts │ ├── parseStripeProp.ts │ ├── registerWithStripeJs.ts │ ├── useAttachEvent.ts │ ├── usePrevious.test.tsx │ └── usePrevious.ts ├── test ├── makeDeferred.ts ├── mocks.js └── setupJest.js ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: 4 | - eslint:recommended 5 | - plugin:@typescript-eslint/eslint-recommended 6 | - plugin:@typescript-eslint/recommended 7 | - plugin:react/recommended 8 | parser: '@typescript-eslint/parser' 9 | parserOptions: 10 | # project: './tsconfig.json' 11 | ecmaFeatures: 12 | jsx: true 13 | plugins: 14 | - '@typescript-eslint' 15 | - jest 16 | - react 17 | - react-hooks 18 | settings: 19 | react: 20 | version: detect 21 | env: 22 | jest/globals: true 23 | browser: true 24 | es6: true 25 | rules: 26 | no-console: 0 27 | func-style: 2 28 | consistent-return: 2 29 | prefer-arrow-callback: 30 | - 2 31 | - allowNamedFunctions: false 32 | allowUnboundThis: false 33 | jest/no-disabled-tests: 2 34 | jest/no-focused-tests: 2 35 | react/prop-types: 0 36 | react/forbid-prop-types: 0 37 | react/no-unused-prop-types: 0 38 | react-hooks/rules-of-hooks: 2 39 | react-hooks/exhaustive-deps: 1 40 | '@typescript-eslint/no-explicit-any': 0 41 | '@typescript-eslint/no-empty-interface': 0 42 | '@typescript-eslint/explicit-function-return-type': 0 43 | '@typescript-eslint/camelcase': 0 44 | '@typescript-eslint/no-empty-function': 0 45 | '@typescript-eslint/no-unused-vars': 46 | - 2 47 | - varsIgnorePattern: ^_ 48 | -------------------------------------------------------------------------------- /.github/API_REVIEW.md: -------------------------------------------------------------------------------- 1 | # API Review 2 | 3 | All API changes should go through API review, in addition to our normal code 4 | review process. We define an API change as 5 | 6 | - a change large enough to warrant updating documentation, or 7 | - a change that increases the maintenance burden of features we offer to our 8 | users (i.e., the "API surface area") 9 | 10 | For small changes, some or all of these changes can be omitted, but it's best to 11 | **err on the side of being thorough**. Especially for large changes, you might 12 | even consider drafting a full-fledged design document. 13 | 14 | It's best to go through an API review **before** you start changing the code, so 15 | that we can offer guidance on how to proceed before getting too caught in the 16 | weeds. 17 | 18 | ## Template 19 | 20 | Copy/paste this template into a new issue and fill it in to request an API 21 | review from a maintainer. Remember: depending on the size of your change, it's 22 | possible to omit some of the sections below. 23 | 24 | ```md 25 | #### Summary 26 | 27 | > A brief of the new API, including a code sample. Consider where this feature 28 | > would fit into our documentation, and what the updated documentation would 29 | > look like. 30 | 31 | 32 | 33 | #### Motivation 34 | 35 | > Describe the problem you are trying to solve with this API change. What does 36 | > this API enable that was previously not possible? 37 | 38 | 39 | 40 | #### Similar APIs 41 | 42 | > Is this new API similar to an existing Stripe API? Are there similar APIs or 43 | > prior art in other popular projects? 44 | 45 | 46 | 47 | #### Alternatives 48 | 49 | > How else could we implement this feature? Are there any existing workarounds 50 | > that would offer the same functionality? Why should we chose this 51 | > implementation over another? 52 | 53 | 54 | 55 | #### Scope 56 | 57 | > Which interfaces will this apply to? For example, is this specific to one 58 | > component, or does it affect all Element components? Does this set a precedent 59 | > for future interfaces we'll add? 60 | 61 | 62 | 63 | #### Risks 64 | 65 | > Are there any security implications (for example, XSS)? What are some ways 66 | > users might get confused or misuse this feature? 67 | 68 | 69 | ``` 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report relating to the React Stripe.js wrapper library. 3 | title: '[BUG]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! This project is a thin wrapper around [Stripe.js](https://stripe.com/docs/js). Please only file issues here that you believe represent bugs with the wrapper, not Stripe.js itself. 10 | 11 | If you're having general trouble with Stripe.js or your Stripe integration, please reach out to us using the form at https://support.stripe.com/email or come chat with us on the [Stripe Discord](https://discord.com/invite/stripe) server. We're very proud of our level of service, and we're more than happy to help you out with your integration. 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: What happened? 16 | description: 17 | Please include what you were trying to accomplish, and what happened 18 | instead. 19 | validations: 20 | required: true 21 | - type: input 22 | id: env 23 | attributes: 24 | label: Environment 25 | description: 26 | What browser and operating system are you seeing this issue on? What 27 | versions? 28 | placeholder: Chrome 99.0.4844.51 on macOS 12.2.1 29 | - type: input 30 | id: repro 31 | attributes: 32 | label: Reproduction 33 | description: 34 | Please include a link to a runnable reproduction, if you can. This will 35 | greatly increase the likelihood we are able to help. A Glitch or 36 | CodeSandbox link is perfect. 37 | placeholder: https://glitch.com/edit/#!/your-project 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Stripe Support 4 | url: https://support.stripe.com/contact 5 | about: If you are having general trouble with Stripe.js or your Stripe integration, please reach out to Stripe Support instead. 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary & motivation 2 | 3 | 4 | 5 | ### API review 6 | 7 | 8 | 9 | Copy [this template] **or** link to an API review issue. 10 | 11 | [this template]: 12 | https://github.com/stripe/react-stripe-js/tree/master/.github/API_REVIEW.md 13 | 14 | ### Testing & documentation 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 20.x 15 | - run: yarn install --frozen-lockfile 16 | - run: yarn run lint:prettier 17 | - run: yarn run lint 18 | - run: yarn run typecheck 19 | - run: yarn run test:unit 20 | - run: yarn run build 21 | - run: yarn run test:package-types 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' # Run daily at 1:30 AM UTC 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | # Number of days of inactivity before an issue becomes stale 16 | days-before-stale: 20 17 | # Number of days of inactivity before a stale issue is closed 18 | days-before-close: 7 19 | # Never mark PRs as stale or close them 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | # Issues with these labels will never be considered stale 23 | exempt-issue-labels: 'pinned,security' 24 | # Comment to post when marking an issue as stale 25 | stale-issue-message: > 26 | This issue has been automatically marked as stale because it has not 27 | had recent activity. It will be closed if no further activity 28 | occurs. Thank you for your contributions. 29 | # Close comment is disabled 30 | # close-issue-message and close-pr-message are omitted to disable close comments 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | node_modules 4 | dist 5 | es 6 | lib 7 | *.log 8 | .vim 9 | .vscode/ 10 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 19 | 20 | 27 | 28 | 35 | 36 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/google-java-format.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/react-stripe-js.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package.json 4 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: es5 3 | bracketSpacing: false 4 | arrowParens: always 5 | proseWrap: always 6 | -------------------------------------------------------------------------------- /.storybook/example.stories.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | import {storiesOf, module} from '@storybook/react'; 4 | import React, {useEffect, useState} from 'react'; 5 | 6 | const ExampleComponent = ({file}) => { 7 | const [example, setExample] = useState(null); 8 | 9 | useEffect(() => { 10 | import(`../examples/${file}`).then(({default: Example}) => { 11 | setExample(); 12 | }); 13 | }, []); 14 | 15 | return example; 16 | }; 17 | 18 | const addDemo = (directory, file, stories) => { 19 | const name = file 20 | .replace('.js', '') 21 | .split('-') 22 | .slice(1) 23 | .join(' '); 24 | 25 | stories.add(name, () => ); 26 | }; 27 | 28 | const hooksStories = storiesOf('react-stripe-js/Hooks', module); 29 | require 30 | .context('../examples/hooks/', false, /\/\d+-(.*).js$/) 31 | .keys() 32 | .forEach((key) => { 33 | addDemo('hooks', key.slice(2), hooksStories); 34 | }); 35 | 36 | const classStories = storiesOf('react-stripe-js/Class Components', module); 37 | require 38 | .context('../examples/class-components/', false, /\/\d+-(.*).js$/) 39 | .keys() 40 | .forEach((key) => { 41 | addDemo('class-components', key.slice(2), classStories); 42 | }); 43 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['./example.stories.js'], 3 | reactOptions: { 4 | strictMode: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See the [releases page](https://github.com/stripe/react-stripe-js/releases) on 4 | GitHub for release notes. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React Stripe.js 2 | 3 | Thanks for contributing to React Stripe.js! 4 | 5 | ## Issues 6 | 7 | React Stripe is a thin wrapper around [Stripe.js] and [Stripe 8 | Elements][elements] for React. Please only file issues here that you believe 9 | represent bugs with React Stripe.js, not Stripe.js itself. 10 | 11 | If you're having general trouble with Stripe.js or your Stripe integration, 12 | please reach out to us using the form at or 13 | come chat with us on the [Stripe Discord server][developer-chat]. We're very 14 | proud of our level of service, and we're more than happy to help you out with 15 | your integration. 16 | 17 | If you've found a bug in React Stripe.js, please [let us know][issue]! You may 18 | also want to check out our [issue template][issue-template]. 19 | 20 | ## API review 21 | 22 | At Stripe, we scrutinize changes that affect the developer API more so than 23 | implementation changes. If your code change involves adding, removing, or 24 | modifying the surface area of the API, we ask that you go through an API review 25 | by following [this guide][api-review]. It's best to go through API review before 26 | implementing a feature. If you've already implemented a feature, address the 27 | [API review][api-review] considerations within your pull request. 28 | 29 | Going through an API review is not required, but it helps us to understand the 30 | problem you are trying to solve, and enables us to collaborate and solve it 31 | together. 32 | 33 | ## Code review 34 | 35 | All pull requests will be reviewed by someone from Stripe before merging. At 36 | Stripe, we believe that code review is for explaining and having a discussion 37 | around code. For those new to code review, we strongly recommend [this 38 | video][code-review] on "code review culture." 39 | 40 | ## Developing 41 | 42 | Install dependencies: 43 | 44 | ```sh 45 | yarn install 46 | ``` 47 | 48 | Run the examples using [Storybook](https://storybook.js.org/): 49 | 50 | ```sh 51 | yarn storybook 52 | ``` 53 | 54 | We use a number of automated checks: 55 | 56 | - Flow, for adding types to JavaScript 57 | - `yarn run flow` 58 | - Jest, for testing 59 | - `yarn test` 60 | - ESLint, for assorted warnings 61 | - `yarn run lint` 62 | - Prettier, for code formatting 63 | - `yarn run prettier` 64 | 65 | You might want to configure your editor to automatically run these checks. Not 66 | passing any of these checks will cause the CI build to fail. 67 | 68 | [code-review]: https://www.youtube.com/watch?v=PJjmw9TRB7s 69 | [api-review]: .github/API_REVIEW.md 70 | [stripe.js]: https://stripe.com/docs/stripe.js 71 | [elements]: https://stripe.com/elements 72 | [issue]: https://github.com/stripe/react-stripe-js/issues/new 73 | [issue-template]: .github/ISSUE_TEMPLATE.md 74 | [developer-chat]: https://stripe.com/go/developer-chat 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stripe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Stripe.js 2 | 3 | React components for 4 | [Stripe.js and Elements](https://stripe.com/docs/stripe-js). 5 | 6 | [![npm version](https://img.shields.io/npm/v/@stripe/react-stripe-js.svg?style=flat-square)](https://www.npmjs.com/package/@stripe/react-stripe-js) 7 | 8 | ## Requirements 9 | 10 | The minimum supported version of React is v16.8. If you use an older version, 11 | upgrade React to use this library. If you prefer not to upgrade your React 12 | version, we recommend using legacy 13 | [`react-stripe-elements`](https://github.com/stripe/react-stripe-elements). 14 | 15 | ## Getting started 16 | 17 | - [Learn how to accept a payment](https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements) 18 | - [Add React Stripe.js to your React app](https://stripe.com/docs/stripe-js/react#setup) 19 | - [Try it out using CodeSandbox](https://codesandbox.io/s/react-stripe-official-q1loc?fontsize=14&hidenavigation=1&theme=dark) 20 | 21 | ## Documentation 22 | 23 | - [React Stripe.js reference](https://stripe.com/docs/stripe-js/react) 24 | - [Migrate from `react-stripe-elements`](docs/migrating.md) 25 | - [Legacy `react-stripe-elements` docs](https://github.com/stripe/react-stripe-elements/#react-stripe-elements) 26 | - [Examples](examples) 27 | 28 | ### Minimal example 29 | 30 | First, install React Stripe.js and 31 | [Stripe.js](https://github.com/stripe/stripe-js). 32 | 33 | ```sh 34 | npm install @stripe/react-stripe-js @stripe/stripe-js 35 | ``` 36 | 37 | #### Using hooks 38 | 39 | ```jsx 40 | import React, {useState} from 'react'; 41 | import ReactDOM from 'react-dom'; 42 | import {loadStripe} from '@stripe/stripe-js'; 43 | import { 44 | PaymentElement, 45 | Elements, 46 | useStripe, 47 | useElements, 48 | } from '@stripe/react-stripe-js'; 49 | 50 | const CheckoutForm = () => { 51 | const stripe = useStripe(); 52 | const elements = useElements(); 53 | 54 | const [errorMessage, setErrorMessage] = useState(null); 55 | 56 | const handleSubmit = async (event) => { 57 | event.preventDefault(); 58 | 59 | if (elements == null) { 60 | return; 61 | } 62 | 63 | // Trigger form validation and wallet collection 64 | const {error: submitError} = await elements.submit(); 65 | if (submitError) { 66 | // Show error to your customer 67 | setErrorMessage(submitError.message); 68 | return; 69 | } 70 | 71 | // Create the PaymentIntent and obtain clientSecret from your server endpoint 72 | const res = await fetch('/create-intent', { 73 | method: 'POST', 74 | }); 75 | 76 | const {client_secret: clientSecret} = await res.json(); 77 | 78 | const {error} = await stripe.confirmPayment({ 79 | //`Elements` instance that was used to create the Payment Element 80 | elements, 81 | clientSecret, 82 | confirmParams: { 83 | return_url: 'https://example.com/order/123/complete', 84 | }, 85 | }); 86 | 87 | if (error) { 88 | // This point will only be reached if there is an immediate error when 89 | // confirming the payment. Show error to your customer (for example, payment 90 | // details incomplete) 91 | setErrorMessage(error.message); 92 | } else { 93 | // Your customer will be redirected to your `return_url`. For some payment 94 | // methods like iDEAL, your customer will be redirected to an intermediate 95 | // site first to authorize the payment, then redirected to the `return_url`. 96 | } 97 | }; 98 | 99 | return ( 100 |
101 | 102 | 105 | {/* Show error message to your customers */} 106 | {errorMessage &&
{errorMessage}
} 107 | 108 | ); 109 | }; 110 | 111 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'); 112 | 113 | const options = { 114 | mode: 'payment', 115 | amount: 1099, 116 | currency: 'usd', 117 | // Fully customizable with appearance API. 118 | appearance: { 119 | /*...*/ 120 | }, 121 | }; 122 | 123 | const App = () => ( 124 | 125 | 126 | 127 | ); 128 | 129 | ReactDOM.render(, document.body); 130 | ``` 131 | 132 | #### Using class components 133 | 134 | ```jsx 135 | import React from 'react'; 136 | import ReactDOM from 'react-dom'; 137 | import {loadStripe} from '@stripe/stripe-js'; 138 | import { 139 | PaymentElement, 140 | Elements, 141 | ElementsConsumer, 142 | } from '@stripe/react-stripe-js'; 143 | 144 | class CheckoutForm extends React.Component { 145 | handleSubmit = async (event) => { 146 | event.preventDefault(); 147 | const {stripe, elements} = this.props; 148 | 149 | if (elements == null) { 150 | return; 151 | } 152 | 153 | // Trigger form validation and wallet collection 154 | const {error: submitError} = await elements.submit(); 155 | if (submitError) { 156 | // Show error to your customer 157 | return; 158 | } 159 | 160 | // Create the PaymentIntent and obtain clientSecret 161 | const res = await fetch('/create-intent', { 162 | method: 'POST', 163 | }); 164 | 165 | const {client_secret: clientSecret} = await res.json(); 166 | 167 | const {error} = await stripe.confirmPayment({ 168 | //`Elements` instance that was used to create the Payment Element 169 | elements, 170 | clientSecret, 171 | confirmParams: { 172 | return_url: 'https://example.com/order/123/complete', 173 | }, 174 | }); 175 | 176 | if (error) { 177 | // This point will only be reached if there is an immediate error when 178 | // confirming the payment. Show error to your customer (for example, payment 179 | // details incomplete) 180 | } else { 181 | // Your customer will be redirected to your `return_url`. For some payment 182 | // methods like iDEAL, your customer will be redirected to an intermediate 183 | // site first to authorize the payment, then redirected to the `return_url`. 184 | } 185 | }; 186 | 187 | render() { 188 | const {stripe} = this.props; 189 | return ( 190 |
191 | 192 | 195 | 196 | ); 197 | } 198 | } 199 | 200 | const InjectedCheckoutForm = () => ( 201 | 202 | {({stripe, elements}) => ( 203 | 204 | )} 205 | 206 | ); 207 | 208 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'); 209 | 210 | const options = { 211 | mode: 'payment', 212 | amount: 1099, 213 | currency: 'usd', 214 | // Fully customizable with appearance API. 215 | appearance: { 216 | /*...*/ 217 | }, 218 | }; 219 | 220 | const App = () => ( 221 | 222 | 223 | 224 | ); 225 | 226 | ReactDOM.render(, document.body); 227 | ``` 228 | 229 | ### TypeScript support 230 | 231 | React Stripe.js is packaged with TypeScript declarations. Some types are pulled 232 | from [`@stripe/stripe-js`](https://github.com/stripe/stripe-js)—be sure to add 233 | `@stripe/stripe-js` as a dependency to your project for full TypeScript support. 234 | 235 | Typings in React Stripe.js follow the same 236 | [versioning policy](https://github.com/stripe/stripe-js#typescript-support) as 237 | `@stripe/stripe-js`. 238 | 239 | ### Contributing 240 | 241 | If you would like to contribute to React Stripe.js, please make sure to read our 242 | [contributor guidelines](CONTRIBUTING.md). 243 | -------------------------------------------------------------------------------- /checkout.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/checkout'; 2 | -------------------------------------------------------------------------------- /checkout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = require('./dist/checkout.js'); 3 | -------------------------------------------------------------------------------- /docs/migrating.md: -------------------------------------------------------------------------------- 1 | # Migrating from `react-stripe-elements` 2 | 3 | This guide will walk you through migrating your Stripe integration from 4 | [`react-stripe-elements`](https://github.com/stripe/react-stripe-elements) to 5 | React Stripe.js. 6 | 7 | - Prefer something a little more comprehensive? Check out the official 8 | [React Stripe.js docs](https://stripe.com/docs/stripe-js/react). 9 | - Or take a look at some 10 | [example integrations](https://github.com/stripe/react-stripe-js/tree/master/examples). 11 | 12 | ### Prerequisites 13 | 14 | React Stripe.js depends on the 15 | [React Hooks API](https://reactjs.org/docs/hooks-intro.html). The minimum 16 | supported version of React is v16.8. If you use an older version, upgrade React 17 | to use this library. If you prefer not to upgrade your React version, feel free 18 | to continue using legacy 19 | [`react-stripe-elements`](https://github.com/stripe/react-stripe-elements). 20 | 21 |
22 | 23 | ## 1. Install and fix imports 24 | 25 | First, use `npm` or `yarn` to remove `react-stripe-elements` and install 26 | `@stripe/react-stripe-js` and `@stripe/stripe-js`. 27 | 28 | ```sh 29 | npm uninstall react-stripe-elements 30 | npm install @stripe/react-stripe-js @stripe/stripe-js 31 | ``` 32 | 33 | After installing React Stripe.js, update your import statements. In places where 34 | you used to import from `react-stripe-elements`, adjust your code to import from 35 | `@stripe/react-stripe-js`. 36 | 37 | #### Before 38 | 39 | ```js 40 | import {CardElement} from 'react-stripe-elements'; 41 | ``` 42 | 43 | #### After 44 | 45 | ```js 46 | import {CardElement} from '@stripe/react-stripe-js'; 47 | ``` 48 | 49 |
50 | 51 | ## 2. Remove `` 52 | 53 | React Stripe.js no longer has a `` component. Instead you will 54 | instantiate the [Stripe object](https://stripe.com/docs/js/initializing) 55 | yourself and pass it directly to ``. We've prefilled the examples 56 | below with a sample test [API key](https://stripe.com/docs/keys). Replace it 57 | with your own publishable key. 58 | 59 | #### Before 60 | 61 | ```jsx 62 | import {StripeProvider, Elements} from 'react-stripe-elements'; 63 | 64 | // Pass your API key to which creates and 65 | // provides the Stripe object to . 66 | const App = () => ( 67 | 68 | {/* Somewhere in the StripeProvider component tree... */} 69 | {/* Your checkout form */} 70 | 71 | ); 72 | ``` 73 | 74 | #### After 75 | 76 | ```jsx 77 | import {loadStripe} from '@stripe/stripe-js'; 78 | import {Elements} from '@stripe/react-stripe-js'; 79 | 80 | // Create the Stripe object yourself... 81 | const stripePromise = loadStripe('pk_test_6pRNASCoBOKtIshFeQd4XMUh'); 82 | 83 | const App = () => ( 84 | // ...and pass it directly to . 85 | {/* Your checkout form */} 86 | ); 87 | ``` 88 | 89 |
90 | 91 | ## 3. Update Element component options 92 | 93 | The way you pass in 94 | [Element options](https://stripe.com/docs/js/elements_object/create_element?type=card#elements_create-options) 95 | is different in React Stripe.js. 96 | 97 | #### Before 98 | 99 | ```jsx 100 | import {CardElement} from 'react-stripe-elements'; 101 | 102 | ; 119 | ``` 120 | 121 | #### After 122 | 123 | ```jsx 124 | import {CardElement} from '@stripe/react-stripe-js'; 125 | 126 | 127 | ; 146 | ``` 147 | 148 |
149 | 150 | ## 4. `useStripe` and `useElements` instead of `injectStripe`. 151 | 152 | React Stripe.js uses hooks and consumers rather than higher order components. 153 | 154 | #### Before 155 | 156 | ```jsx 157 | import {injectStripe} from 'react-stripe-elements'; 158 | 159 | const CheckoutForm = (props) => { 160 | const {stripe, elements} = props; 161 | 162 | // the rest of CheckoutForm... 163 | }; 164 | 165 | // Inject Stripe and Elements with `injectStripe`. 166 | const InjectedCheckoutForm = injectStripe(CheckoutForm); 167 | ``` 168 | 169 | #### After 170 | 171 | ```jsx 172 | import {useStripe, useElements} from '@stripe/react-stripe-js'; 173 | 174 | const CheckoutForm = (props) => { 175 | // Get a reference to Stripe or Elements using hooks. 176 | const stripe = useStripe(); 177 | const elements = useElements(); 178 | 179 | // the rest of CheckoutForm... 180 | }; 181 | 182 | // Or use `` 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 |
225 | 226 | 227 | 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 |
269 | 270 | 271 | 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 |
48 | 64 | 67 | 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 | 82 | 86 | 90 | 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 |
188 |
189 | { 198 | this.setState({name: event.target.value}); 199 | }} 200 | /> 201 | { 210 | this.setState({email: event.target.value}); 211 | }} 212 | /> 213 | { 222 | this.setState({phone: event.target.value}); 223 | }} 224 | /> 225 |
226 |
227 | { 229 | this.setState({ 230 | error: event.error, 231 | cardComplete: event.complete, 232 | }); 233 | }} 234 | /> 235 |
236 | {error && {error.message}} 237 | 238 | Pay $25 239 | 240 |
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 |
94 | 95 | { 101 | this.setState({name: event.target.value}); 102 | }} 103 | /> 104 | 105 | 113 | 114 | 122 | 123 | 131 | 132 | { 138 | this.setState({postal: event.target.value}); 139 | }} 140 | /> 141 | {errorMessage && {errorMessage}} 142 | {paymentMethod && ( 143 | Got PaymentMethod: {paymentMethod.id} 144 | )} 145 | 148 | 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 |
93 | {canMakePayment && ( 94 | { 96 | if (paymentMethod) { 97 | event.preventDefault(); 98 | this.setState({ 99 | errorMessage: 100 | 'You can only use the PaymentRequest button once. Refresh the page to start over.', 101 | }); 102 | } 103 | }} 104 | options={{ 105 | ...ELEMENT_OPTIONS, 106 | paymentRequest: this.paymentRequest, 107 | }} 108 | /> 109 | )} 110 | {!canMakePayment && hasCheckedAvailability && } 111 | {errorMessage && {errorMessage}} 112 | {paymentMethod && ( 113 | Got PaymentMethod: {paymentMethod.id} 114 | )} 115 | 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 |
82 | 83 | { 89 | this.setState({name: event.target.value}); 90 | }} 91 | /> 92 | 93 | { 100 | this.setState({email: event.target.value}); 101 | }} 102 | /> 103 | 104 | 112 | {errorMessage && {errorMessage}} 113 | {paymentMethod && ( 114 | Got PaymentMethod: {paymentMethod.id} 115 | )} 116 | 119 | 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 |
49 | 65 | 68 | 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 | 82 | 86 | 90 | 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 |
182 |
183 | { 192 | setBillingDetails({...billingDetails, name: e.target.value}); 193 | }} 194 | /> 195 | { 204 | setBillingDetails({...billingDetails, email: e.target.value}); 205 | }} 206 | /> 207 | { 216 | setBillingDetails({...billingDetails, phone: e.target.value}); 217 | }} 218 | /> 219 |
220 |
221 | { 223 | setError(e.error); 224 | setCardComplete(e.complete); 225 | }} 226 | /> 227 |
228 | {error && {error.message}} 229 | 230 | Pay $25 231 | 232 |
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 |
83 | 89 |

Payment Details

90 | 91 |

Billing Details

92 | 93 | 96 | {status &&

{status}

} 97 | 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 |
133 | 140 | 144 | 147 | 150 | 160 |
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 |
45 | 52 | 56 | 59 | 62 |
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 |
28 | 29 | 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 |
71 | 78 | 82 | 85 | 88 | 98 |
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 |
82 | 83 | { 89 | setName(e.target.value); 90 | }} 91 | /> 92 | 93 | 101 | 102 | 110 | 111 | 119 | 120 | { 126 | setPostal(e.target.value); 127 | }} 128 | /> 129 | {errorMessage && {errorMessage}} 130 | {paymentMethod && Got PaymentMethod: {paymentMethod.id}} 131 | 134 | 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 |
76 | {paymentRequest && ( 77 | { 79 | if (paymentMethod) { 80 | event.preventDefault(); 81 | setErrorMessage( 82 | 'You can only use the PaymentRequest button once. Refresh the page to start over.' 83 | ); 84 | } 85 | }} 86 | options={{ 87 | ...ELEMENT_OPTIONS, 88 | paymentRequest, 89 | }} 90 | /> 91 | )} 92 | {notAvailable && } 93 | {errorMessage && {errorMessage}} 94 | {paymentMethod && Got PaymentMethod: {paymentMethod.id}} 95 | 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 |
74 | 75 | { 81 | setName(e.target.value); 82 | }} 83 | /> 84 | 85 | { 92 | setEmail(e.target.value); 93 | }} 94 | /> 95 | 96 | 104 | {errorMessage && {errorMessage}} 105 | {paymentMethod && Got PaymentMethod: {paymentMethod.id}} 106 | 109 | 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 |
48 | 49 | 52 | {status &&

{status}

} 53 | 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 |
89 | 96 | 100 | 103 | 106 | 116 |
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 | --------------------------------------------------------------------------------