├── .babelrc ├── .eslintrc.yml ├── .flowconfig ├── .github ├── API_REVIEW.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo ├── async │ ├── async.js │ ├── async.test.js │ ├── index.html │ └── main.js ├── demo.png ├── demo │ ├── index.html │ └── index.js └── intents │ ├── api.js │ ├── index.html │ └── index.js ├── package.json ├── src ├── components │ ├── Element.js │ ├── Element.test.js │ ├── Elements.js │ ├── Elements.test.js │ ├── PaymentRequestButtonElement.js │ ├── PaymentRequestButtonElement.test.js │ ├── Provider.js │ ├── Provider.test.js │ ├── inject.js │ └── inject.test.js ├── decls │ └── Stripe.js ├── index.js ├── index.test.js └── utils │ ├── isEqual.js │ ├── isEqual.test.js │ ├── shallowEqual.js │ └── shallowEqual.test.js ├── test └── setupJest.js ├── webpack.config.js ├── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "env"], 3 | "plugins": [ 4 | "transform-class-properties", 5 | "transform-object-rest-spread", 6 | [ "transform-es2015-classes", { "loose": true } ], 7 | "transform-inline-environment-variables" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - stripe 4 | - plugin:flowtype/recommended 5 | # overwrite any rules that may conflict with prettier 6 | - "prettier" 7 | - "prettier/flowtype" 8 | - "prettier/react" 9 | parser: 'babel-eslint' 10 | plugins: 11 | - flowtype 12 | - jest 13 | env: 14 | jest/globals: true 15 | rules: 16 | react/jsx-filename-extension: 0 17 | no-duplicate-imports: 2 # doesn't support flow imports. 18 | no-console: 0 19 | func-style: 2 20 | consistent-return: 2 21 | prefer-arrow-callback: 22 | - 2 23 | - allowNamedFunctions: false 24 | allowUnboundThis: false 25 | flowtype/no-primitive-constructor-types: 2 26 | flowtype/require-valid-file-annotation: 27 | - 2 28 | - 'always' 29 | - annotationStyle: 'line' 30 | no-unused-vars: 31 | - 2 32 | - ignoreRestSiblings: true 33 | jest/no-disabled-tests: 2 34 | jest/no-focused-tests: 2 35 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [libs] 2 | src/decls/ 3 | -------------------------------------------------------------------------------- /.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.md: -------------------------------------------------------------------------------- 1 | Feature request or idea? Consider opening an 2 | [API review](https://github.com/stripe/react-stripe-elements/tree/master/.github/API_REVIEW.md)! 3 | 4 | 14 | 15 | ### Summary 16 | 17 | 18 | 19 | ### Other information 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.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-elements/tree/master/.github/API_REVIEW.md 13 | 14 | ### Testing & documentation 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | es 5 | lib 6 | *.log 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | es 4 | lib 5 | package.json 6 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | trailingComma: es5 3 | bracketSpacing: false 4 | arrowParens: always 5 | proseWrap: always 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run lint 6 | - npm run flow 7 | - npm run test 8 | - npm run prettier-list-different 9 | cache: yarn 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | `react-stripe-elements` adheres to 4 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## v6.1.1 - 2020-04-01 7 | 8 | ### Changes 9 | 10 | - Register package version with Stripe instance (#512) 11 | 12 | ## v6.1.0 - 2020-02-14 13 | 14 | ### New Features 15 | 16 | Added the `auBankAccount` and `fpxBank` elements. These elements will not have 17 | automatic Element detection/insertion. To use them you will need to use 18 | `elements.getElement` and pass them directly to other Stripe.js methods (e.g. 19 | `stripe.confirmFpxPayment`): 20 | 21 | ```jsx 22 | const FpxForm = injectStripe(({stripe, elements}) => { 23 | const handleSubmit = async (event) => { 24 | event.preventDefault(); 25 | const {error} = await stripe.confirmFpxPayment('{{CLIENT_SECRET}}', { 26 | payment_method: { 27 | fpx: elements.getElement('fpxBank'), 28 | }, 29 | }); 30 | }; 31 | 32 | return ( 33 |
34 | 35 | 36 | 37 | ); 38 | }); 39 | ``` 40 | 41 | ## v6.0.1 - 2019-11-13 42 | 43 | Version bump that fixes some typos, no changes. 44 | 45 | ## v6.0.0 - 2019-11-13 46 | 47 | ### New Features 48 | 49 | - `injectStripe` now injects a reference to the Elements instance created by 50 | `` as the prop `elements`. 51 | 52 | The primary reason you would want an Elements instance is to use 53 | [`elements.getElement()`](https://stripe.com/docs/stripe-js/reference#elements-get-element). 54 | which provides an easy way to get a reference to an Element. You will need to 55 | get a reference to an Element to use 56 | [`confirmCardPayment`](https://stripe.com/docs/stripe-js/reference#stripe-confirm-card-payment), 57 | [`confirmCardSetup()`](https://stripe.com/docs/stripe-js/reference#stripe-confirm-card-setup), 58 | or 59 | [`createPaymentMethod()`](https://stripe.com/docs/stripe-js/reference#stripe-create-payment-method). 60 | 61 | Note that the old API for `createPaymentMethod` will continue to work and 62 | provide automatic element injection, but we are updating documentation and 63 | examples to use the new argument shape: 64 | 65 | ```js 66 | // old shape with automatic element detection - still works 67 | this.props.stripe.createPaymentMethod('card').then(/* ... */); 68 | 69 | // new shape without automatic element detection - recommended and will work with new non-card PaymentMethods 70 | this.props.stripe 71 | .createPaymentMethod({ 72 | type: 'card', 73 | card: this.props.elements.getElement('card'), 74 | }) 75 | .then(/* ... */); 76 | ``` 77 | 78 | ### Breaking Changes 79 | 80 | - We have removed the `getElement` method on RSE components that we introduced 81 | in v5.1.0 in favor of the above change. Sorry for the churn. 82 | 83 | ## v5.1.0 - 2019-10-22 84 | 85 | ### New Features 86 | 87 | - Add support for accessing the underlying Element using refs via `getElement`. 88 | 89 | ### Bug Fixes 90 | 91 | - Fix crash when trying to create element while unmounting. Thanks @CarsonF! 92 | 93 | ## v5.0.1 - 2019-09-18 94 | 95 | ### Bug Fixes 96 | 97 | - Fixes a bug where calling `stripe.createPaymentMethod` would error in IE. 98 | 99 | ## v5.0.0 - 2019-08-27 100 | 101 | ### New Features 102 | 103 | - React 16.9 compatibility. 104 | 105 | ### Breaking Changes 106 | 107 | - We replaced the internal use of deprecated `componentWillReceiveProps`. This 108 | internal movement of logic between lifecycle methods is likely safe for almost 109 | all apps and should not require any changes. 110 | 111 | ## v4.0.1 - 2019-08-14 112 | 113 | ### Bug Fixes 114 | 115 | - Fixes a bug where calling `stripe.handleCardPayment` with only a client secret 116 | caused an error to be thrown. 117 | 118 | ## v4.0.0 - 2019-07-05 119 | 120 | ### New Features 121 | 122 | - Renamed `CardCVCElement` to `CardCvcElement` which better mirrors the Elements 123 | API. We will keep the old component name around as an alias until 5.0.0. 124 | - Added support for `stripe.handleCardSetup` 125 | 126 | ```js 127 | stripe.handleCardSetup( 128 | clientSecret: string, 129 | data?: Object 130 | ): Promise<{error?: Object, setupIntent?: Object}> 131 | ``` 132 | 133 | For more information, please review the Stripe Docs: 134 | 135 | - [`stripe.handleCardSetup`](https://stripe.com/docs/stripe-js/reference#stripe-handle-card-setup) 136 | 137 | ### Deprecations 138 | 139 | - `CardCVCElement` has been renamed to `CardCvcElement`. `CardCVCElement` will 140 | be removed in version 5.0.0. 141 | 142 | ### Breaking Changes 143 | 144 | - If you were already using `handleCardSetup` with `react-stripe-elements`, you 145 | should upgrade your integration. This method will now automatically find and 146 | use valid Elements. 147 | 148 | #### Old Way 149 | 150 | ```js 151 | 155 | 156 | handleReady = (element) => { 157 | this.setState({cardElement: element}) ; 158 | }; 159 | 160 | const {setupIntent, error} = await this.props.stripe.handleCardSetup( 161 | intent.client_secret, this.state.cardElement, {} 162 | ); 163 | ``` 164 | 165 | #### New Way 166 | 167 | ```js 168 | ; 169 | 170 | const {setupIntent, error} = await this.props.stripe.handleCardSetup( 171 | intent.client_secret, 172 | {} 173 | ); 174 | ``` 175 | 176 | ## v3.0.0 - 2019-04-17 177 | 178 | ### New Features 179 | 180 | - added a [changelog](/CHANGELOG.md) 181 | - added support for `stripe.handleCardPayment` and `stripe.createPaymentMethod`. 182 | These methods allow you to easily integrate Stripe's new Payment Intents API. 183 | Like `createToken` and `createSource`, these new methods will automatically 184 | find and use a corresponding Element when they are called. 185 | 186 | ```js 187 | stripe.createPaymentMethod( 188 | paymentMethodType: string, 189 | paymentMethodDetails: Object 190 | ): Promise<{error?: Object, paymentIntent?: Object}> 191 | 192 | stripe.handleCardPayment( 193 | clientSecret: string, 194 | data?: Object 195 | ): Promise<{error?: Object, paymentIntent?: Object}> 196 | ``` 197 | 198 | For more information, please review the Stripe Docs: 199 | 200 | - [`stripe.createPaymentMethod`](https://stripe.com/docs/stripe-js/reference#stripe-create-payment-method) 201 | - [`stripe.handleCardPayment`](https://stripe.com/docs/stripe-js/reference#stripe-handle-card-payment) 202 | - [Payment Intents API](https://stripe.com/docs/payments/payment-intents) 203 | - [Payment Methods API](https://stripe.com/docs/payments/payment-methods) 204 | 205 | ### Breaking Changes: 206 | 207 | - If you were already using `handleCardPayment` or `createPaymentMethod` with 208 | `react-stripe-elements`, you should upgrade your integration. These methods 209 | will now automatically find and use valid Elements. 210 | 211 | #### Old Way 212 | 213 | ```js 214 | 218 | 219 | handleReady = (element) => { 220 | this.setState({cardElement: element}) ; 221 | }; 222 | 223 | let { paymentIntent, error } = await this.props.stripe.handleCardPayment( 224 | intent.client_secret, this.state.cardElement, {} 225 | ); 226 | ``` 227 | 228 | #### New Way 229 | 230 | ```js 231 | ; 232 | 233 | let {paymentIntent, error} = await this.props.stripe.handleCardPayment( 234 | intent.client_secret, 235 | {} 236 | ); 237 | ``` 238 | 239 | - Passing a beta flag to Stripe.js to use one of the PaymentIntents betas is not 240 | supported. 241 | 242 | #### Old Way 243 | 244 | ```js 245 | const stripe = window.Stripe( 246 | publicKey, 247 | {betas: ['payment_intent_beta_3']}, 248 | ); 249 | 250 | 251 | 252 | 253 | ``` 254 | 255 | #### New Way 256 | 257 | ```js 258 | 259 | 260 | 261 | ``` 262 | 263 | - `PostalCodeElement` has been removed. We suggest that you build your own 264 | postal code input. 265 | 266 | ## v2.0.3 - 2019-01-25 267 | 268 | ### Bug Fixes 269 | 270 | - Fixes a bug where the elements.update event was triggered far too often, 271 | incorrectly, when an Element was repeatedly rendered with the same options. 272 | 273 | ## v2.0.1 - 2018-07-11 274 | 275 | ### Bug Fixes 276 | 277 | - The Element higher-order component now reports a proper displayName, which is 278 | more useful for debugging. (Thanks @emilrose!) 279 | 280 | ## v2.0.0 - 2018-06-01 281 | 282 | ### New Features 283 | 284 | - Support for the `IbanElement` and `IdealBankElement`. 285 | 286 | ### Breaking Changes 287 | 288 | - `stripe.createSource` now requires the Source type be passed in. 289 | - For example, if you previously called 290 | `stripe.createSource({ name: 'Jenny Rosen' })`, you now must use 291 | `stripe.createSource({ type: 'card', name: 'Jenny Rosen' })`. 292 | - elementRef is no longer a valid prop you can pass to an ``. Use 293 | onReady instead to get a reference to the underlying Element instance. 294 | 295 | ## v1.7.0 - 2018-05-31 296 | 297 | ### Deprecations 298 | 299 | - `createSource` automatically infers the type of Source to create based on 300 | which Elements are in use. This behavior is now deprecated, and the Source 301 | type will be required in version 2.0.0. 302 | 303 | ## v1.6.0 - 2018-03-05 304 | 305 | ### Deprecations 306 | 307 | - The elementRef callback is deprecated and will be removed in version 2.0.0. 308 | Use onReady instead, which is the exact same. 309 | 310 | ### Bug Fixes 311 | 312 | - The id prop from v1.5.0 was absent from the `PaymentRequestButtonElement`. 313 | Now, the `PaymentRequestButtonElement` behaves like all the other \*Element 314 | components. 315 | 316 | ## v1.5.0 - 2018-03-02 317 | 318 | ### New Features 319 | 320 | - (#177 / #178) The \*Element classes learned a new id prop. This can be used to 321 | set the ID of the underlying DOM Element. 322 | 323 | ## v1.4.1 - 2018-01-22 324 | 325 | ### Bug Fixes 326 | 327 | - Fixed a TODO in an error message emitted by Provider.js. 328 | 329 | ## v1.4.0 - 2018-01-17 330 | 331 | ### Bug Fixes 332 | 333 | - Modify build pipeline to fix issues with IE9 and IE10. 334 | 335 | ## v1.3.2 - 2018-01-11 336 | 337 | ### Bug Fixes 338 | 339 | - Fix split Element token creation for async codepath. (#148) 340 | 341 | ## v1.3.1 - 2018-01-10 342 | 343 | ### Bug Fixes 344 | 345 | - Fixes a regression introduced by v1.3.0 (#146). 346 | 347 | ## v1.3.0 - 2018-01-09 348 | 349 | ### New Features 350 | 351 | - Loading Stripe.js and react-stripe-elements asynchronously 352 | - Rendering react-stripe-elements on the server 353 | - Passing a custom stripe instance to `StripeProvider` 354 | - For an overview of how this works, see the Advanced integrations section. 355 | 356 | ## v1.2.1 - 2017-11-21 357 | 358 | ### Bug Fixes 359 | 360 | - Fixed a bug where using pure components under the `` component would 361 | lead to an error. 362 | 363 | ## v1.2.0 - 2017-10-12 364 | 365 | ### New Features 366 | 367 | - The PaymentRequestButtonElement now accepts an onClick prop that maps to the 368 | `element.on('click')` event. 369 | 370 | ## v1.1.1 - 2017-10-11 371 | 372 | ### Bug Fixes 373 | 374 | - The instance of Stripe provided by StripeProvider is now consistent across 375 | StripeProvider usages across your application, as long as you're passing in 376 | the same API key & configuration. 377 | 378 | ## v1.1.0 - 2017-10-05 379 | 380 | ### New Features 381 | 382 | - We've added a new component! You can now use `` 383 | which wraps up `elements.create('paymentRequestButton')` in a React component. 384 | 385 | ## v1.0.0 - 2017-09-18 386 | 387 | ### New Features 388 | 389 | - Update dependencies 390 | - Improve test coverage 391 | - Allow React 16 as peer dependency 392 | 393 | ## v0.1.0 - 2017-09-13 394 | 395 | ### New Features 396 | 397 | - You can now pass the `withRef` option to `injectStripe` to make the wrapped 398 | component instance available via `getWrappedInstance()` 399 | 400 | ## v0.0.8 - 2017-08-21 401 | 402 | ### New Features 403 | 404 | - Render \*Element components with div instead of span (#61) 405 | 406 | ## v0.0.7 - 2018-08-03 407 | 408 | ### New Features 409 | 410 | - You can now pass `className` to `<*Element>` (e.g. 411 | and it will be passed down to the element 412 | container DOM element. 413 | 414 | ## v0.0.6 - 2017-07-25 415 | 416 | ### Bug Fixes 417 | 418 | - Bugfix for collapsed Elements: #45 #48 419 | 420 | ## v0.0.5 - 2017-07-20 421 | 422 | ### Bug Fixes 423 | 424 | - Same as v0.0.3 but fixed corrupted npm upload. 425 | 426 | ## v0.0.3 - 2017-07-20 427 | 428 | ### Bug Fixes 429 | 430 | - Bug fixes for: #29, #40 431 | 432 | ## v0.0.2 - 05-04-2017 433 | 434 | ### New Features 435 | 436 | Initial release! Support for: 437 | 438 | - StripeProvider 439 | - Elements 440 | - injectStripe 441 | - Individual elements: 442 | - CardElement 443 | - CardNumberElement 444 | - CardExpiryElement 445 | - CardCVCElement 446 | - PostalCodeElement 447 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `react-stripe-elements` 2 | 3 | Thanks for contributing to react-stripe-elements! 4 | 5 | ## Issues 6 | 7 | `react-stripe-elements` 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-elements, 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 at #stripe on freenode. We're very proud of our level of 14 | service, and we're more than happy to help you out with your integration. 15 | 16 | If you've found a bug in `react-stripe-elements`, please [let us know][issue]! 17 | You may also want to check out our [issue template][issue-template]. 18 | 19 | ## API review 20 | 21 | At Stripe, we scrutinize changes that affect the developer API more so than 22 | implementation changes. If your code change involves adding, removing, or 23 | modifying the surface area of the API, we ask that you go through an API review 24 | by following [this guide][api-review]. It's best to go through API review before 25 | implementing a feature. If you've already implemented a feature, address the 26 | [API review][api-review] considerations within your pull request. 27 | 28 | Going through an API review is not required, but it helps us to understand the 29 | problem you are trying to solve, and enables us to collaborate and solve it 30 | together. 31 | 32 | ## Code review 33 | 34 | All pull requests will be reviewed by someone from Stripe before merging. At 35 | Stripe, we believe that code review is for explaining and having a discussion 36 | around code. For those new to code review, we strongly recommend [this 37 | video][code-review] on "code review culture." 38 | 39 | ## Developing 40 | 41 | We use a number of automated checks: 42 | 43 | - Flow, for adding types to JavaScript 44 | - `yarn run flow` 45 | - Jest, for testing 46 | - `yarn test` 47 | - ESLint, for assorted warnings 48 | - `yarn run lint` 49 | - Prettier, for code formatting 50 | - `yarn run prettier` 51 | 52 | You might want to configure your editor to automatically run these checks. Not 53 | passing any of these checks will cause the CI build to fail. 54 | 55 | [code-review]: https://www.youtube.com/watch?v=PJjmw9TRB7s 56 | [api-review]: .github/API_REVIEW.md 57 | [stripe.js]: https://stripe.com/docs/stripe.js 58 | [elements]: https://stripe.com/elements 59 | [issue]: https://github.com/stripe/react-stripe-elements/issues/new 60 | [issue-template]: .github/ISSUE_TEMPLATE.md 61 | -------------------------------------------------------------------------------- /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 | ## We’ve moved to [@stripe/react-stripe-js](https://github.com/stripe/react-stripe-js)! 2 | 3 | We have decided to rename, rework, and move this project. We have no plans for 4 | any additional major releases of `react-stripe-elements`. If you have an issue with this package, please open it on the [react-stripe-js repo](https://github.com/stripe/react-stripe-js). 5 | 6 | If you are starting a new Stripe integration or are looking to update your 7 | existing integration, use 8 | [React Stripe.js](https://github.com/stripe/react-stripe-js). 9 | 10 | - [Learn to accept a payment (with React Stripe.js!)](https://stripe.com/docs/payments/accept-a-payment#web) 11 | - [Migrate from `react-stripe-elements` to React Stripe.js](https://github.com/stripe/react-stripe-js/blob/master/docs/migrating.md) 12 | 13 | --- 14 | 15 | # react-stripe-elements 16 | 17 | [![build status](https://img.shields.io/travis/stripe/react-stripe-elements/master.svg?style=flat-square)](https://travis-ci.org/stripe/react-stripe-elements) 18 | [![npm version](https://img.shields.io/npm/v/react-stripe-elements.svg?style=flat-square)](https://www.npmjs.com/package/react-stripe-elements) 19 | 20 | > React components for Stripe.js and Stripe Elements 21 | 22 | This project is a thin React wrapper around 23 | [Stripe.js](https://stripe.com/docs/stripe.js) and 24 | [Stripe Elements](https://stripe.com/docs/elements). It allows you to add 25 | Elements to any React app, and manages the state and lifecycle of Elements for 26 | you. 27 | 28 | The 29 | [Stripe.js / Stripe Elements API reference](https://stripe.com/docs/elements/reference) 30 | goes into more detail on the various customization options for Elements (e.g. 31 | styles, fonts). 32 | 33 | 34 | 35 | 36 | ## Table of Contents 37 | 38 | - [Demo](#demo) 39 | - [Installation](#installation) 40 | - [First, install `react-stripe-elements`.](#first-install-react-stripe-elements) 41 | - [Then, load Stripe.js in your application:](#then-load-stripejs-in-your-application) 42 | - [Getting started](#getting-started) 43 | - [The Stripe context (`StripeProvider`)](#the-stripe-context-stripeprovider) 44 | - [Element groups (`Elements`)](#element-groups-elements) 45 | - [Setting up your payment form (`injectStripe`)](#setting-up-your-payment-form-injectstripe) 46 | - [Using individual `*Element` components](#using-individual-element-components) 47 | - [Using the `PaymentRequestButtonElement`](#using-the-paymentrequestbuttonelement) 48 | - [Advanced integrations](#advanced-integrations) 49 | - [Loading Stripe.js asynchronously](#loading-stripejs-asynchronously) 50 | - [Server-side rendering (SSR)](#server-side-rendering-ssr) 51 | - [Using an existing Stripe instance](#using-an-existing-stripe-instance) 52 | - [Component reference](#component-reference) 53 | - [``](#stripeprovider) 54 | - [Props shape](#props-shape) 55 | - [``](#elements) 56 | - [Props shape](#props-shape-1) 57 | - [`<*Element>` components](#element-components) 58 | - [Available components](#available-components) 59 | - [Props shape](#props-shape-2) 60 | - [Using `onReady`](#using-onready) 61 | - [`injectStripe` HOC](#injectstripe-hoc) 62 | - [Example](#example) 63 | - [Troubleshooting](#troubleshooting) 64 | - [Development](#development) 65 | 66 | 67 | 68 | 69 | ## Demo 70 | 71 | The fastest way to start playing around with `react-stripe-elements` is with 72 | this JSFiddle: . 73 | 74 | You can also play around with the demo locally. The source code is in 75 | [demo/](demo/). To run it: 76 | 77 | ```shell 78 | git clone https://github.com/stripe/react-stripe-elements 79 | cd react-stripe-elements 80 | 81 | # (make sure you have yarn installed: https://yarnpkg.com/) 82 | 83 | yarn install 84 | yarn run demo 85 | ``` 86 | 87 | Now go to to try it out! 88 | 89 | > :warning: `PaymentRequestButtonElement` will not render unless the page is 90 | > served over HTTPS. To demo `PaymentRequestButtonElement`, you can tunnel over 91 | > HTTPS to the local server using ngrok or a similar service. 92 | 93 | ![Screenshot of the demo running](demo/demo.png) 94 | 95 | ## Installation 96 | 97 | ### First, install `react-stripe-elements`. 98 | 99 | Install with `yarn`: 100 | 101 | ``` 102 | yarn add react-stripe-elements 103 | ``` 104 | 105 | OR with `npm`: 106 | 107 | ``` 108 | npm install --save react-stripe-elements 109 | ``` 110 | 111 | OR using UMD build (exports a global `ReactStripeElements` object); 112 | 113 | ```html 114 | 115 | ``` 116 | 117 | ### Then, load Stripe.js in your application: 118 | 119 | ```html 120 | 121 | ``` 122 | 123 | ## Getting started 124 | 125 | ### The Stripe context (`StripeProvider`) 126 | 127 | In order for your application to have access to 128 | [the Stripe object](https://stripe.com/docs/elements/reference#the-stripe-object), 129 | let's add `StripeProvider` to our root React App component: 130 | 131 | ```jsx 132 | // index.js 133 | import React from 'react'; 134 | import {render} from 'react-dom'; 135 | import {StripeProvider} from 'react-stripe-elements'; 136 | 137 | import MyStoreCheckout from './MyStoreCheckout'; 138 | 139 | const App = () => { 140 | return ( 141 | 142 | 143 | 144 | ); 145 | }; 146 | 147 | render(, document.getElementById('root')); 148 | ``` 149 | 150 | ### Element groups (`Elements`) 151 | 152 | Next, when you're building components for your checkout form, you'll want to 153 | wrap the `Elements` component around your `form`. This groups the set of Stripe 154 | Elements you're using together, so that we're able to pull data from groups of 155 | Elements when you're tokenizing. 156 | 157 | ```jsx 158 | // MyStoreCheckout.js 159 | import React from 'react'; 160 | import {Elements} from 'react-stripe-elements'; 161 | 162 | import InjectedCheckoutForm from './CheckoutForm'; 163 | 164 | class MyStoreCheckout extends React.Component { 165 | render() { 166 | return ( 167 | 168 | 169 | 170 | ); 171 | } 172 | } 173 | 174 | export default MyStoreCheckout; 175 | ``` 176 | 177 | ### Setting up your payment form (`injectStripe`) 178 | 179 | Use the `injectStripe` [Higher-Order Component][hoc] (HOC) to build your payment 180 | form components in the `Elements` tree. The [Higher-Order Component][hoc] 181 | pattern in React can be unfamiliar to those who've never seen it before, so 182 | consider reading up before continuing. The `injectStripe` HOC provides the 183 | `this.props.stripe` and `this.props.elements` properties that manage your 184 | `Elements` groups. Within an injected component, you can call any of the methods 185 | on the [Stripe][stripe] or [Elements][elements] objects. 186 | 187 | [hoc]: https://facebook.github.io/react/docs/higher-order-components.html 188 | [stripe]: https://stripe.com/docs/stripe-js/reference#the-stripe-object 189 | [elements]: https://stripe.com/docs/stripe-js/reference#the-elements-object 190 | 191 | > :warning: NOTE `injectStripe` cannot be used on the same element that renders 192 | > the `Elements` component; it must be used on the child component of 193 | > `Elements`. `injectStripe` _returns a wrapped component_ that needs to sit 194 | > under `` but above any code where you'd like to access 195 | > `this.props.stripe`. 196 | 197 | ```jsx 198 | // CheckoutForm.js 199 | import React from 'react'; 200 | import {injectStripe} from 'react-stripe-elements'; 201 | 202 | import AddressSection from './AddressSection'; 203 | import CardSection from './CardSection'; 204 | 205 | class CheckoutForm extends React.Component { 206 | handleSubmit = (ev) => { 207 | // We don't want to let default form submission happen here, which would refresh the page. 208 | ev.preventDefault(); 209 | 210 | // Use Elements to get a reference to the Card Element mounted somewhere 211 | // in your tree. Elements will know how to find your Card Element 212 | // because only one is allowed. 213 | // See our getElement documentation for more: 214 | // https://stripe.com/docs/stripe-js/reference#elements-get-element 215 | const cardElement = this.props.elements.getElement('card'); 216 | 217 | // From here we can call createPaymentMethod to create a PaymentMethod 218 | // See our createPaymentMethod documentation for more: 219 | // https://stripe.com/docs/stripe-js/reference#stripe-create-payment-method 220 | this.props.stripe 221 | .createPaymentMethod({ 222 | type: 'card', 223 | card: cardElement, 224 | billing_details: {name: 'Jenny Rosen'}, 225 | }) 226 | .then(({paymentMethod}) => { 227 | console.log('Received Stripe PaymentMethod:', paymentMethod); 228 | }); 229 | 230 | // You can also use confirmCardPayment with the PaymentIntents API automatic confirmation flow. 231 | // See our confirmCardPayment documentation for more: 232 | // https://stripe.com/docs/stripe-js/reference#stripe-confirm-card-payment 233 | this.props.stripe.confirmCardPayment('{PAYMENT_INTENT_CLIENT_SECRET}', { 234 | payment_method: { 235 | card: cardElement, 236 | }, 237 | }); 238 | 239 | // You can also use confirmCardSetup with the SetupIntents API. 240 | // See our confirmCardSetup documentation for more: 241 | // https://stripe.com/docs/stripe-js/reference#stripe-confirm-card-setup 242 | this.props.stripe.confirmCardSetup('{PAYMENT_INTENT_CLIENT_SECRET}', { 243 | payment_method: { 244 | card: cardElement, 245 | }, 246 | }); 247 | 248 | // You can also use createToken to create tokens. 249 | // See our tokens documentation for more: 250 | // https://stripe.com/docs/stripe-js/reference#stripe-create-token 251 | // With createToken, you will not need to pass in the reference to 252 | // the Element. It will be inferred automatically. 253 | this.props.stripe.createToken({type: 'card', name: 'Jenny Rosen'}); 254 | // token type can optionally be inferred if there is only one Element 255 | // with which to create tokens 256 | // this.props.stripe.createToken({name: 'Jenny Rosen'}); 257 | 258 | // You can also use createSource to create Sources. 259 | // See our Sources documentation for more: 260 | // https://stripe.com/docs/stripe-js/reference#stripe-create-source 261 | // With createSource, you will not need to pass in the reference to 262 | // the Element. It will be inferred automatically. 263 | this.props.stripe.createSource({ 264 | type: 'card', 265 | owner: { 266 | name: 'Jenny Rosen', 267 | }, 268 | }); 269 | }; 270 | 271 | render() { 272 | return ( 273 |
274 | 275 | 276 | 277 | 278 | ); 279 | } 280 | } 281 | 282 | export default injectStripe(CheckoutForm); 283 | ``` 284 | 285 | ### Using individual `*Element` components 286 | 287 | Now, you can use individual `*Element` components, such as `CardElement`, to 288 | build your form. 289 | 290 | ```jsx 291 | // CardSection.js 292 | import React from 'react'; 293 | import {CardElement} from 'react-stripe-elements'; 294 | 295 | class CardSection extends React.Component { 296 | render() { 297 | return ( 298 | 302 | ); 303 | } 304 | } 305 | 306 | export default CardSection; 307 | ``` 308 | 309 | ### Using the `PaymentRequestButtonElement` 310 | 311 | The 312 | [Payment Request Button](https://stripe.com/docs/elements/payment-request-button) 313 | lets you collect payment and address information from your customers using Apple 314 | Pay and the Payment Request API. 315 | 316 | To use the `PaymentRequestButtonElement` you need to first create a 317 | [`PaymentRequest` object](https://stripe.com/docs/stripe.js#the-payment-request-object). 318 | You can then conditionally render the `PaymentRequestButtonElement` based on the 319 | result of `paymentRequest.canMakePayment` and pass the `PaymentRequest` Object 320 | as a prop. 321 | 322 | ```jsx 323 | class PaymentRequestForm extends React.Component { 324 | constructor(props) { 325 | super(props); 326 | 327 | // For full documentation of the available paymentRequest options, see: 328 | // https://stripe.com/docs/stripe.js#the-payment-request-object 329 | const paymentRequest = props.stripe.paymentRequest({ 330 | country: 'US', 331 | currency: 'usd', 332 | total: { 333 | label: 'Demo total', 334 | amount: 1000, 335 | }, 336 | }); 337 | 338 | paymentRequest.on('token', ({complete, token, ...data}) => { 339 | console.log('Received Stripe token: ', token); 340 | console.log('Received customer information: ', data); 341 | complete('success'); 342 | }); 343 | 344 | paymentRequest.canMakePayment().then((result) => { 345 | this.setState({canMakePayment: !!result}); 346 | }); 347 | 348 | this.state = { 349 | canMakePayment: false, 350 | paymentRequest, 351 | }; 352 | } 353 | 354 | render() { 355 | return this.state.canMakePayment ? ( 356 | 368 | ) : null; 369 | } 370 | } 371 | export default injectStripe(PaymentRequestForm); 372 | ``` 373 | 374 | ## Advanced integrations 375 | 376 | The above [Getting started](#getting-started) section outlines the most common 377 | integration, which makes the following assumptions: 378 | 379 | - The Stripe.js script is loaded before your application's code. 380 | - Your code is only run in a browser environment. 381 | - You don't need fine-grained control over the Stripe instance that 382 | `react-stripe-elements` uses under the hood. 383 | 384 | When all of these assumptions are true, you can pass the `apiKey` prop to 385 | `` and let `react-stripe-elements` handle the rest. 386 | 387 | When one or more of these assumptions doesn't hold true for your integration, 388 | you have another option: pass a Stripe instance as the `stripe` prop to 389 | `` directly. The `stripe` prop can be either `null` or the 390 | result of using `Stripe(apiKey, options)` to construct a [Stripe instance]. 391 | 392 | [stripe-function]: https://stripe.com/docs/stripe-js/reference#stripe-function 393 | 394 | We'll now cover a couple of use cases which break at least one of the 395 | assumptions listed above. 396 | 397 | ### Loading Stripe.js asynchronously 398 | 399 | Loading Stripe.js asynchronously can speed up your initial page load, especially 400 | if you don't show the payment form until the user interacts with your 401 | application in some way. 402 | 403 | ```html 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | ``` 417 | 418 | Initialize `this.state.stripe` to `null` in the `constructor`, then update it in 419 | `componentDidMount` when the script tag has loaded. 420 | 421 | ```jsx 422 | class App extends React.Component { 423 | constructor() { 424 | super(); 425 | this.state = {stripe: null}; 426 | } 427 | componentDidMount() { 428 | if (window.Stripe) { 429 | this.setState({stripe: window.Stripe('pk_test_12345')}); 430 | } else { 431 | document.querySelector('#stripe-js').addEventListener('load', () => { 432 | // Create Stripe instance once Stripe.js loads 433 | this.setState({stripe: window.Stripe('pk_test_12345')}); 434 | }); 435 | } 436 | } 437 | render() { 438 | // this.state.stripe will either be null or a Stripe instance 439 | // depending on whether Stripe.js has loaded. 440 | return ( 441 | 442 | 443 | 444 | 445 | 446 | ); 447 | } 448 | } 449 | ``` 450 | 451 | When loading Stripe.js asynchronously, the `stripe` prop provided by 452 | `injectStripe` will initially be `null`, and will update to the Stripe instance 453 | once you pass it in to your `StripeProvider`. You can find a working demo of 454 | this strategy in [async.js](demo/async/async.js). If you run the demo locally, 455 | you can view it at . 456 | 457 | For alternatives to calling `setState`in `componentDidMount`, consider using a 458 | `setTimeout()`, moving the `if/else` statement to the `constructor`, or 459 | dynamically injecting a script tag in `componentDidMount`. For more information, 460 | see [stripe/react-stripe-elements][issue-154]. 461 | 462 | [issue-154]: https://github.com/stripe/react-stripe-elements/issues/154 463 | 464 | ### Server-side rendering (SSR) 465 | 466 | If you're using `react-stripe-elements` in a non-browser environment 467 | (`React.renderToString`, etc.), Stripe.js is not available. To use 468 | `react-stripe-elements` with SSR frameworks, use the following instructions. 469 | 470 | The general idea is similar to the async loading snippet from the previous 471 | section (initialize `this.state.stripe` to `null` in `constructor`, update in 472 | `componentDidMount`), but this time we don't have to wait for the script tag to 473 | load in `componentDidMount`; we can use `window.Stripe` directly. 474 | 475 | ```jsx 476 | class App extends React.Component { 477 | constructor() { 478 | super(); 479 | this.state = {stripe: null}; 480 | } 481 | componentDidMount() { 482 | // Create Stripe instance in componentDidMount 483 | // (componentDidMount only fires in browser/DOM environment) 484 | this.setState({stripe: window.Stripe('pk_test_12345')}); 485 | } 486 | render() { 487 | return ( 488 | 489 | 490 | 491 | 492 | 493 | ); 494 | } 495 | } 496 | ``` 497 | 498 | Inside your form, ``, `this.props.stripe` will either be 499 | `null` or a valid Stripe instance. This means that it will be `null` when 500 | rendered server-side, but set when rendered in a browser. 501 | 502 | ### Using an existing Stripe instance 503 | 504 | In some projects, part of the project uses React, while another part doesn't. 505 | For example, maybe you have business logic and view logic separate. Or maybe you 506 | use `react-stripe-elements` for your credit card form, but use Stripe.js APIs 507 | directly for tokenizing bank account information. 508 | 509 | You can use the `stripe` prop to get more fine-grained control over the Stripe 510 | instance that `` uses. For example, if you have a `stripe` 511 | instance in a Redux store that you pass to your `` as a prop, you can 512 | pass that instance directly into ``: 513 | 514 | ```jsx 515 | class App extends React.Component { 516 | render() { 517 | return ( 518 | 519 | 520 | 521 | 522 | 523 | ); 524 | } 525 | } 526 | ``` 527 | 528 | As long as `` is provided a non-`null` `stripe` prop, `this.props.stripe` 529 | will always be available within your `InjectedCheckoutForm`. 530 | 531 | ## Component reference 532 | 533 | ### `` 534 | 535 | All applications using `react-stripe-elements` must use the `` 536 | component, which sets up the Stripe context for a component tree. 537 | `react-stripe-elements` uses the provider pattern (which is also adopted by 538 | tools like [`react-redux`](https://github.com/reactjs/react-redux) and 539 | [`react-intl`](https://github.com/yahoo/react-intl)) to scope a Stripe context 540 | to a tree of components. 541 | 542 | This allows configuration like your API key to be provided at the root of a 543 | component tree. This context is then made available to the `` 544 | component and individual `<*Element>` components that we provide. 545 | 546 | An integration usually wraps the `` around the application’s 547 | root component. This way, your entire application has the configured Stripe 548 | context. 549 | 550 | #### Props shape 551 | 552 | There are two _distinct_ props shapes you can pass to ``. 553 | 554 | ```jsx 555 | type StripeProviderProps = 556 | | {apiKey: string, ...} 557 | | {stripe: StripeObject | null}; 558 | ``` 559 | 560 | See [Advanced integrations](#advanced-integrations) for more information on when 561 | to use each. 562 | 563 | The `...` above represents that this component accepts props for any option that 564 | can be passed into `Stripe(apiKey, options)`. For example, if you are using 565 | [Stripe Connect](https://stripe.com/connect) and want to act on behalf of a 566 | connected account, you can pass `stripeAccount="acct_123"` as a property to 567 | ``. This will get used just like passing `stripeAccount` in the 568 | options of the `Stripe` constructor or like using `stripe_account` when your 569 | backend calls the Stripe API directly 570 | 571 | ### `` 572 | 573 | The `Elements` component wraps groups of Elements that belong together. In most 574 | cases, you want to wrap this around your checkout form. 575 | 576 | #### Props shape 577 | 578 | This component accepts all `options` that can be passed into 579 | `stripe.elements(options)` as props. 580 | 581 | ```jsx 582 | type ElementsProps = { 583 | locale?: string, 584 | fonts?: Array, 585 | // The full specification for `elements()` options is here: https://stripe.com/docs/elements/reference#elements-options 586 | }; 587 | ``` 588 | 589 | ### `<*Element>` components 590 | 591 | These components display the UI for Elements, and must be used within 592 | `StripeProvider` and `Elements`. 593 | 594 | #### Available components 595 | 596 | (More to come!) 597 | 598 | - `CardElement` 599 | - `CardNumberElement` 600 | - `CardExpiryElement` 601 | - `CardCvcElement` 602 | - `PaymentRequestButtonElement` 603 | - `IbanElement` 604 | - `IdealBankElement` 605 | 606 | #### Props shape 607 | 608 | These components accept all `options` that can be passed into 609 | `elements.create(type, options)` as props. 610 | 611 | ```jsx 612 | type ElementProps = { 613 | id?: string, 614 | className?: string, 615 | 616 | // For full documentation on the events and payloads below, see: 617 | // https://stripe.com/docs/elements/reference#element-on 618 | onBlur?: () => void, 619 | onChange?: (changeObject: Object) => void, 620 | onFocus?: () => void, 621 | onReady?: (StripeElement) => void, 622 | }; 623 | ``` 624 | 625 | The props for the `PaymentRequestButtonElement` are: 626 | 627 | ```jsx 628 | type PaymentRequestButtonProps = { 629 | id?: string, 630 | className?: string, 631 | 632 | paymentRequest: StripePaymentRequest, 633 | 634 | onBlur?: () => void, 635 | onClick?: () => void, 636 | onFocus?: () => void, 637 | onReady?: (StripeElement) => void, 638 | }; 639 | ``` 640 | 641 | #### Using `onReady` 642 | 643 | Note that the `onReady` callback gives you access to the underlying [Element] 644 | created with Stripe.js. You can use this to get access to all the underlying 645 | methods that a Stripe Element supports. 646 | 647 | For example, you can use `onReady` to force your element to focus: 648 | 649 | ```jsx 650 | // CardSection.js 651 | import React from 'react'; 652 | import {CardElement} from 'react-stripe-elements'; 653 | 654 | class CardSection extends React.Component { 655 | render = () => { 656 | return ( 657 | 661 | ); 662 | }; 663 | } 664 | 665 | export default CardSection; 666 | ``` 667 | 668 | (Note that this functionality is new as of react-stripe-elements v1.6.0.) 669 | 670 | [element]: https://stripe.com/docs/stripe-js/reference#other-methods 671 | 672 | ### `injectStripe` HOC 673 | 674 | ```jsx 675 | function injectStripe( 676 | WrappedComponent: ReactClass, 677 | options?: { 678 | withRef?: boolean = false, 679 | } 680 | ): ReactClass; 681 | ``` 682 | 683 | Use `injectStripe` to wrap a component that needs to interact with `Stripe.js` 684 | to create sources or tokens. 685 | 686 | 1. First, create a component that accepts the `stripe` prop and calls one of 687 | the Stripe or Elements methods when necessary. 688 | 2. Wrap that component by passing it to `injectStripe` so that it actually 689 | receives the `stripe` and `elements` props. 690 | 3. Render the component that `injectStripe` returns. 691 | 692 | #### Example 693 | 694 | ```jsx 695 | // 1. Create a component that uses this.props.stripe: 696 | class CheckoutForm extends React.Component { 697 | render() { 698 | /* ... */ 699 | } 700 | onCompleteCheckout() { 701 | this.props.stripe 702 | .createPaymentMethod({ 703 | type: 'card', 704 | card: this.props.stripe.getElement('card'), 705 | }) 706 | .then(/* ... */); 707 | } 708 | } 709 | 710 | // 2. Wrap it in a higher-order component that provides the `stripe` prop: 711 | const InjectedCheckoutForm = injectStripe(CheckoutForm); 712 | 713 | // 3. Render the wrapped component in your app: 714 | const CheckoutRoute = (props) => ( 715 |
716 | 717 |
718 | ); 719 | ``` 720 | 721 | `injectStripe` will work with any method of providing the actual Stripe instance 722 | with `StripeProvider`, whether you just give it an api key, 723 | [load Stripe.js asynchronously](#loading-stripejs-asynchronously), or 724 | [pass in an existing instance](#using-an-existing-stripe-instance). 725 | 726 | Within the context of `Elements`, `stripe.createToken` and `stripe.createSource` 727 | wrap methods of the same name in 728 | [Stripe.js](https://stripe.com/docs/stripe-js/reference#stripe-create-Token). 729 | Calls to them automatically infer and pass the `Element` object as the first 730 | argument. 731 | 732 | If the `withRef` option is set to `true`, the wrapped component instance will be 733 | available with the `getWrappedInstance()` method of the wrapper component. This 734 | feature can not be used if the wrapped component is a stateless function 735 | component. 736 | 737 | Within the wrapped component, the `stripe` and `elements` props have the type: 738 | 739 | ```jsx 740 | type FactoryProps = { 741 | elements: null | { 742 | getElement: (type: string) => Element | null, 743 | // For more detail and documentation on other methods available on 744 | // the `elements` object, please refer to our official documentation: 745 | // https://stripe.com/docs/elements/reference#the-elements-object 746 | }, 747 | stripe: null | { 748 | createToken: (tokenData: {type?: string}) => Promise<{ 749 | token?: Object, 750 | error?: Object, 751 | }>, 752 | createSource: (sourceData: {type: string}) => Promise<{ 753 | source?: Object, 754 | error?: Object, 755 | }>, 756 | createPaymentMethod: ( 757 | paymentMethodData: Object 758 | ) => Promise<{ 759 | paymentMethod?: Object, 760 | error?: Object, 761 | }>, 762 | confirmCardPayment: ( 763 | clientSecret: string, 764 | paymentIntentData?: Object 765 | ) => Promise<{ 766 | paymentIntent?: Object, 767 | error?: Object, 768 | }>, 769 | confirmCardSetup: ( 770 | clientSecret: string, 771 | paymentIntentData?: Object 772 | ) => Promise<{ 773 | setupIntent?: Object, 774 | error?: Object, 775 | }>, 776 | // For more detail and documentation on other methods available on 777 | // the `stripe` object, please refer to our official documentation: 778 | // https://stripe.com/docs/elements/reference#the-stripe-object 779 | }, 780 | }; 781 | ``` 782 | 783 | The `stripe` and `elements` props can only be `null` if you are using one of the 784 | [Advanced integrations](#advanced-integrations) mentioned above, like loading 785 | Stripe.js asynchronously or providing an existing instance. If you are using a 786 | basic integration where you pass in an api key to ``, they will 787 | always be present. 788 | 789 | ## Troubleshooting 790 | 791 | `react-stripe-elements` may not work properly when used with components that 792 | implement `shouldComponentUpdate`. `react-stripe-elements` relies heavily on 793 | React's `context` feature and `shouldComponentUpdate` does not provide a way to 794 | take context updates into account when deciding whether to allow a re-render. 795 | These components can block context updates from reaching `react-stripe-element` 796 | components in the tree. 797 | 798 | For example, when using `react-stripe-elements` together with 799 | [`react-redux`](https://github.com/reactjs/react-redux) doing the following will 800 | not work: 801 | 802 | ```jsx 803 | const Component = connect()(injectStripe(_Component)); 804 | ``` 805 | 806 | In this case, the context updates originating from the `StripeProvider` are not 807 | reaching the components wrapped inside the `connect` function. Therefore, 808 | `react-stripe-elements` components deeper in the tree break. The reason is that 809 | the `connect` function of `react-redux` 810 | [implements `shouldComponentUpdate`](https://github.com/reactjs/react-redux/blob/master/docs/troubleshooting.md#my-views-arent-updating-when-something-changes-outside-of-redux) 811 | and blocks re-renders that are triggered by context changes outside of the 812 | connected component. 813 | 814 | There are two ways to prevent this issue: 815 | 816 | 1. Change the order of the functions to have `injectStripe` be the outermost 817 | one: 818 | 819 | ```jsx 820 | const Component = injectStripe(connect()(_CardForm)); 821 | ``` 822 | 823 | This works, because `injectStripe` does not implement `shouldComponentUpdate` 824 | itself, so context updates originating from the `redux` `Provider` will still 825 | reach all components. 826 | 827 | 2. You can use the [`pure: false`][pure-false] option for redux-connect: 828 | 829 | ```jsx 830 | const Component = connect( 831 | mapStateToProps, 832 | mapDispatchToProps, 833 | mergeProps, 834 | { 835 | pure: false, 836 | } 837 | )(injectStripe(_CardForm)); 838 | ``` 839 | 840 | [pure-false]: 841 | https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options 842 | 843 | ## Development 844 | 845 | Install dependencies: 846 | 847 | yarn install 848 | 849 | Run the demo: 850 | 851 | yarn run demo 852 | 853 | Run the tests: 854 | 855 | yarn run test 856 | 857 | Build: 858 | 859 | yarn run build 860 | 861 | We use [prettier](https://github.com/prettier/prettier) for code formatting: 862 | 863 | yarn run prettier 864 | 865 | To update the ToC in the README if any of the headers changed: 866 | 867 | yarn run doctoc 868 | 869 | Checks: 870 | 871 | yarn test 872 | yarn run lint 873 | yarn run flow 874 | -------------------------------------------------------------------------------- /demo/async/async.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-console, react/no-multi-comp */ 3 | import React from 'react'; 4 | 5 | import type {InjectedProps} from '../../src/components/inject'; 6 | 7 | import { 8 | CardElement, 9 | StripeProvider, 10 | Elements, 11 | injectStripe, 12 | } from '../../src/index'; 13 | 14 | const handleBlur = () => { 15 | console.log('[blur]'); 16 | }; 17 | const handleChange = (change) => { 18 | console.log('[change]', change); 19 | }; 20 | const handleFocus = () => { 21 | console.log('[focus]'); 22 | }; 23 | const handleReady = () => { 24 | console.log('[ready]'); 25 | }; 26 | 27 | const CARD_ELEMENT_OPTIONS = { 28 | style: { 29 | base: { 30 | fontSize: '18px', 31 | color: '#424770', 32 | letterSpacing: '0.025em', 33 | '::placeholder': { 34 | color: '#aab7c4', 35 | }, 36 | }, 37 | invalid: { 38 | color: '#9e2146', 39 | }, 40 | }, 41 | }; 42 | 43 | class _CardForm extends React.Component { 44 | handleSubmit = (ev) => { 45 | ev.preventDefault(); 46 | if (this.props.stripe) { 47 | this.props.stripe 48 | .createToken() 49 | .then((payload) => console.log('[token]', payload)); 50 | } else { 51 | console.log('Form submitted before Stripe.js loaded.'); 52 | } 53 | }; 54 | render() { 55 | return ( 56 |
57 | 58 | {this.props.stripe ? ( 59 | 66 | ) : ( 67 |
68 | )} 69 | 70 | 71 | ); 72 | } 73 | } 74 | const CardForm = injectStripe(_CardForm); 75 | 76 | const Checkout = () => { 77 | return ( 78 |
79 | 80 | 81 | 82 |
83 | ); 84 | }; 85 | 86 | export class App extends React.Component<{}, {stripe: null | StripeShape}> { 87 | constructor() { 88 | super(); 89 | 90 | this.state = { 91 | stripe: null, 92 | }; 93 | } 94 | 95 | componentDidMount() { 96 | // componentDidMount only runs in a browser environment. 97 | // In addition to loading asynchronously, this code is safe to server-side render. 98 | 99 | // You can inject a script tag manually like this, 100 | // or you can use the 'async' attribute on the Stripe.js v3 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /demo/async/main.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from 'react'; 4 | import {render} from 'react-dom'; 5 | 6 | import {App} from './async'; 7 | 8 | const appElement = document.querySelector('.App'); 9 | if (appElement) { 10 | render(, appElement); 11 | } else { 12 | console.error( 13 | 'We could not find an HTML element with a class name of "App" in the DOM. Please make sure you copy index.html as well for this demo to work.' 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-archive/react-stripe-elements/99ab31d9188b6b1a12daa1675f8ee434e07ef2cc/demo/demo.png -------------------------------------------------------------------------------- /demo/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo | react-stripe-elements 6 | 7 | 108 | 109 | 110 | 111 |
112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /demo/demo/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-console, react/no-multi-comp */ 3 | import React from 'react'; 4 | import {render} from 'react-dom'; 5 | 6 | import type {InjectedProps} from '../../src/components/inject'; 7 | 8 | import { 9 | CardElement, 10 | CardNumberElement, 11 | CardExpiryElement, 12 | CardCvcElement, 13 | PaymentRequestButtonElement, 14 | IbanElement, 15 | IdealBankElement, 16 | StripeProvider, 17 | Elements, 18 | injectStripe, 19 | } from '../../src/index'; 20 | 21 | const handleBlur = () => { 22 | console.log('[blur]'); 23 | }; 24 | const handleChange = (change) => { 25 | console.log('[change]', change); 26 | }; 27 | const handleClick = () => { 28 | console.log('[click]'); 29 | }; 30 | const handleFocus = () => { 31 | console.log('[focus]'); 32 | }; 33 | const handleReady = () => { 34 | console.log('[ready]'); 35 | }; 36 | 37 | const createOptions = (fontSize: string, padding: ?string) => { 38 | return { 39 | style: { 40 | base: { 41 | fontSize, 42 | color: '#424770', 43 | letterSpacing: '0.025em', 44 | fontFamily: 'Source Code Pro, monospace', 45 | '::placeholder': { 46 | color: '#aab7c4', 47 | }, 48 | ...(padding ? {padding} : {}), 49 | }, 50 | invalid: { 51 | color: '#9e2146', 52 | }, 53 | }, 54 | }; 55 | }; 56 | 57 | class _CardForm extends React.Component { 58 | handleSubmit = (ev) => { 59 | ev.preventDefault(); 60 | if (this.props.stripe) { 61 | this.props.stripe 62 | .createToken() 63 | .then((payload) => console.log('[token]', payload)); 64 | } else { 65 | console.log("Stripe.js hasn't loaded yet."); 66 | } 67 | }; 68 | render() { 69 | return ( 70 |
71 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | const CardForm = injectStripe(_CardForm); 87 | 88 | class _SplitForm extends React.Component { 89 | handleSubmit = (ev) => { 90 | ev.preventDefault(); 91 | if (this.props.stripe) { 92 | this.props.stripe 93 | .createToken() 94 | .then((payload) => console.log('[token]', payload)); 95 | } else { 96 | console.log("Stripe.js hasn't loaded yet."); 97 | } 98 | }; 99 | render() { 100 | return ( 101 |
102 | 112 | 122 | 132 | 133 |
134 | ); 135 | } 136 | } 137 | const SplitForm = injectStripe(_SplitForm); 138 | 139 | class _PaymentRequestForm extends React.Component< 140 | InjectedProps, 141 | { 142 | canMakePayment: boolean, 143 | paymentRequest: Object, 144 | } 145 | > { 146 | constructor(props) { 147 | super(props); 148 | 149 | const paymentRequest = props.stripe.paymentRequest({ 150 | country: 'US', 151 | currency: 'usd', 152 | total: { 153 | label: 'Demo total', 154 | amount: 1000, 155 | }, 156 | }); 157 | 158 | this.state = { 159 | canMakePayment: false, 160 | paymentRequest, 161 | }; 162 | } 163 | 164 | state: { 165 | canMakePayment: boolean, 166 | paymentRequest: Object, 167 | }; 168 | 169 | componentDidMount() { 170 | this.state.paymentRequest.on('token', ({complete, token, ...data}) => { 171 | console.log('Received Stripe token: ', token); 172 | console.log('Received customer information: ', data); 173 | complete('success'); 174 | }); 175 | 176 | this.state.paymentRequest.canMakePayment().then((result) => { 177 | this.setState({canMakePayment: !!result}); 178 | }); 179 | } 180 | 181 | render() { 182 | return this.state.canMakePayment ? ( 183 | 198 | ) : null; 199 | } 200 | } 201 | const PaymentRequestForm = injectStripe(_PaymentRequestForm); 202 | 203 | class _IbanForm extends React.Component { 204 | handleSubmit = (ev) => { 205 | ev.preventDefault(); 206 | if (this.props.stripe) { 207 | this.props.stripe 208 | .createSource({ 209 | type: 'sepa_debit', 210 | currency: 'eur', 211 | owner: { 212 | name: ev.target.name.value, 213 | email: ev.target.email.value, 214 | }, 215 | mandate: { 216 | notification_method: 'email', 217 | }, 218 | }) 219 | .then((payload) => console.log('[source]', payload)); 220 | } else { 221 | console.log("Stripe.js hasn't loaded yet."); 222 | } 223 | }; 224 | render() { 225 | return ( 226 |
227 | 231 | 240 | 251 | 252 |
253 | ); 254 | } 255 | } 256 | const IbanForm = injectStripe(_IbanForm); 257 | 258 | class _IdealBankForm extends React.Component< 259 | InjectedProps & {fontSize: string} 260 | > { 261 | handleSubmit = (ev) => { 262 | ev.preventDefault(); 263 | if (this.props.stripe) { 264 | this.props.stripe 265 | .createSource({ 266 | type: 'ideal', 267 | amount: 1099, 268 | currency: 'eur', 269 | owner: { 270 | name: ev.target.name.value, 271 | }, 272 | redirect: { 273 | return_url: 'https://example.com', 274 | }, 275 | }) 276 | .then((payload) => console.log('[source]', payload)); 277 | } else { 278 | console.log("Stripe.js hasn't loaded yet."); 279 | } 280 | }; 281 | render() { 282 | return ( 283 |
284 | 288 | 299 | 300 |
301 | ); 302 | } 303 | } 304 | const IdealBankForm = injectStripe(_IdealBankForm); 305 | 306 | class Checkout extends React.Component<{}, {elementFontSize: string}> { 307 | constructor() { 308 | super(); 309 | this.state = { 310 | elementFontSize: window.innerWidth < 450 ? '14px' : '18px', 311 | }; 312 | window.addEventListener('resize', () => { 313 | if (window.innerWidth < 450 && this.state.elementFontSize !== '14px') { 314 | this.setState({elementFontSize: '14px'}); 315 | } else if ( 316 | window.innerWidth >= 450 && 317 | this.state.elementFontSize !== '18px' 318 | ) { 319 | this.setState({elementFontSize: '18px'}); 320 | } 321 | }); 322 | } 323 | 324 | render() { 325 | const {elementFontSize} = this.state; 326 | return ( 327 |
328 |

Available Elements

329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 |
345 | ); 346 | } 347 | } 348 | const App = () => { 349 | return ( 350 | 351 | 352 | 353 | ); 354 | }; 355 | 356 | const appElement = document.querySelector('.App'); 357 | if (appElement) { 358 | render(, appElement); 359 | } else { 360 | console.error( 361 | 'We could not find an HTML element with a class name of "App" in the DOM. Please make sure you copy index.html as well for this demo to work.' 362 | ); 363 | } 364 | -------------------------------------------------------------------------------- /demo/intents/api.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const createPaymentIntent = (options: {}): Promise => { 4 | // To use this demo with your own Stripe account, clone this Runkit backend: 5 | // https://runkit.com/stripe/create-intents 6 | return window 7 | .fetch(`https://create-intents-35aylzrcx0ej.runkit.sh/payment_intents`, { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | body: JSON.stringify({options}), 13 | }) 14 | .then((res) => { 15 | if (res.status === 200) { 16 | return res.json(); 17 | } else { 18 | return null; 19 | } 20 | }) 21 | .then((data) => { 22 | if (!data || data.error) { 23 | console.log('API error:', {data}); 24 | throw new Error('PaymentIntent API Error'); 25 | } else { 26 | return data.client_secret; 27 | } 28 | }); 29 | }; 30 | 31 | const createSetupIntent = (options: {}): Promise => { 32 | // To use this demo with your own Stripe account, clone this Runkit backend: 33 | // https://runkit.com/stripe/create-intents 34 | return window 35 | .fetch(`https://create-intents-35aylzrcx0ej.runkit.sh/setup_intents`, { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | body: JSON.stringify({options}), 41 | }) 42 | .then((res) => { 43 | if (res.status === 200) { 44 | return res.json(); 45 | } else { 46 | return null; 47 | } 48 | }) 49 | .then((data) => { 50 | if (!data || data.error) { 51 | console.log('API error:', {data}); 52 | throw new Error('SetupIntents API Error'); 53 | } else { 54 | return data.client_secret; 55 | } 56 | }); 57 | }; 58 | 59 | const api = { 60 | createPaymentIntent, 61 | createSetupIntent, 62 | }; 63 | 64 | export default api; 65 | -------------------------------------------------------------------------------- /demo/intents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | demo | react-stripe-elements 5 | 6 | 130 | 131 | 132 | 133 |
134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /demo/intents/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-console, react/no-multi-comp */ 3 | import React from 'react'; 4 | import {render} from 'react-dom'; 5 | 6 | import type {InjectedProps} from '../../src/components/inject'; 7 | 8 | import { 9 | CardElement, 10 | StripeProvider, 11 | Elements, 12 | injectStripe, 13 | } from '../../src/index'; 14 | 15 | import api from './api'; 16 | 17 | const handleBlur = () => { 18 | console.log('[blur]'); 19 | }; 20 | const handleChange = (change) => { 21 | console.log('[change]', change); 22 | }; 23 | const handleFocus = () => { 24 | console.log('[focus]'); 25 | }; 26 | const handleReady = () => { 27 | console.log('[ready]'); 28 | }; 29 | 30 | const createOptions = (fontSize: string, padding: ?string) => { 31 | return { 32 | style: { 33 | base: { 34 | fontSize, 35 | color: '#424770', 36 | letterSpacing: '0.025em', 37 | fontFamily: 'Source Code Pro, monospace', 38 | '::placeholder': { 39 | color: '#aab7c4', 40 | }, 41 | ...(padding ? {padding} : {}), 42 | }, 43 | invalid: { 44 | color: '#9e2146', 45 | }, 46 | }, 47 | }; 48 | }; 49 | 50 | class _CreatePaymentMethod extends React.Component< 51 | InjectedProps & {fontSize: string}, 52 | { 53 | error: string | null, 54 | processing: boolean, 55 | message: string | null, 56 | } 57 | > { 58 | state = { 59 | error: null, 60 | processing: false, 61 | message: null, 62 | }; 63 | 64 | handleSubmit = (ev) => { 65 | ev.preventDefault(); 66 | if (this.props.stripe && this.props.elements) { 67 | this.props.stripe 68 | .createPaymentMethod({ 69 | type: 'card', 70 | card: this.props.elements.getElement('card'), 71 | }) 72 | .then((payload) => { 73 | if (payload.error) { 74 | this.setState({ 75 | error: `Failed to create PaymentMethod: ${payload.error.message}`, 76 | processing: false, 77 | }); 78 | console.log('[error]', payload.error); 79 | } else { 80 | this.setState({ 81 | message: `Created PaymentMethod: ${payload.paymentMethod.id}`, 82 | processing: false, 83 | }); 84 | console.log('[paymentMethod]', payload.paymentMethod); 85 | } 86 | }); 87 | this.setState({processing: true}); 88 | } else { 89 | console.log("Stripe.js hasn't loaded yet."); 90 | } 91 | }; 92 | 93 | render() { 94 | return ( 95 |
96 | 106 | {this.state.error &&
{this.state.error}
} 107 | {this.state.message && ( 108 |
{this.state.message}
109 | )} 110 | 113 |
114 | ); 115 | } 116 | } 117 | 118 | const CreatePaymentMethod = injectStripe(_CreatePaymentMethod); 119 | 120 | class _HandleCardPayment extends React.Component< 121 | InjectedProps & {fontSize: string}, 122 | { 123 | clientSecret: string | null, 124 | error: string | null, 125 | disabled: boolean, 126 | succeeded: boolean, 127 | processing: boolean, 128 | message: string | null, 129 | } 130 | > { 131 | state = { 132 | clientSecret: null, 133 | error: null, 134 | disabled: true, 135 | succeeded: false, 136 | processing: false, 137 | message: null, 138 | }; 139 | 140 | componentDidMount() { 141 | api 142 | .createPaymentIntent({ 143 | amount: 1099, 144 | currency: 'usd', 145 | payment_method_types: ['card'], 146 | }) 147 | .then((clientSecret) => { 148 | this.setState({clientSecret, disabled: false}); 149 | }) 150 | .catch((err) => { 151 | this.setState({error: err.message}); 152 | }); 153 | } 154 | 155 | handleSubmit = (ev) => { 156 | ev.preventDefault(); 157 | if (this.props.stripe && this.props.elements) { 158 | this.props.stripe 159 | .confirmCardPayment(this.state.clientSecret, { 160 | payment_method: { 161 | card: this.props.elements.getElement('card'), 162 | }, 163 | }) 164 | .then((payload) => { 165 | if (payload.error) { 166 | this.setState({ 167 | error: `Charge failed: ${payload.error.message}`, 168 | disabled: false, 169 | }); 170 | console.log('[error]', payload.error); 171 | } else { 172 | this.setState({ 173 | succeeded: true, 174 | message: `Charge succeeded! PaymentIntent is in state: ${ 175 | payload.paymentIntent.status 176 | }`, 177 | }); 178 | console.log('[PaymentIntent]', payload.paymentIntent); 179 | } 180 | }); 181 | this.setState({disabled: true, processing: true}); 182 | } else { 183 | console.log("Stripe.js hasn't loaded yet."); 184 | } 185 | }; 186 | 187 | render() { 188 | return ( 189 |
190 | 200 | {this.state.error &&
{this.state.error}
} 201 | {this.state.message && ( 202 |
{this.state.message}
203 | )} 204 | {!this.state.succeeded && ( 205 | 208 | )} 209 |
210 | ); 211 | } 212 | } 213 | 214 | const HandleCardPayment = injectStripe(_HandleCardPayment); 215 | 216 | class _HandleCardSetup extends React.Component< 217 | InjectedProps & {fontSize: string}, 218 | { 219 | clientSecret: string | null, 220 | error: string | null, 221 | disabled: boolean, 222 | succeeded: boolean, 223 | processing: boolean, 224 | message: string | null, 225 | } 226 | > { 227 | state = { 228 | clientSecret: null, 229 | error: null, 230 | disabled: true, 231 | succeeded: false, 232 | processing: false, 233 | message: null, 234 | }; 235 | 236 | componentDidMount() { 237 | api 238 | .createSetupIntent({ 239 | payment_method_types: ['card'], 240 | }) 241 | .then((clientSecret) => { 242 | this.setState({clientSecret, disabled: false}); 243 | }) 244 | .catch((err) => { 245 | this.setState({error: err.message}); 246 | }); 247 | } 248 | 249 | handleSubmit = (ev) => { 250 | ev.preventDefault(); 251 | if (this.props.stripe && this.props.elements) { 252 | this.props.stripe 253 | .confirmCardSetup(this.state.clientSecret, { 254 | payment_method: { 255 | card: this.props.elements.getElement('card'), 256 | }, 257 | }) 258 | .then((payload) => { 259 | if (payload.error) { 260 | this.setState({ 261 | error: `Setup failed: ${payload.error.message}`, 262 | disabled: false, 263 | }); 264 | console.log('[error]', payload.error); 265 | } else { 266 | this.setState({ 267 | succeeded: true, 268 | message: `Setup succeeded! SetupIntent is in state: ${ 269 | payload.setupIntent.status 270 | }`, 271 | }); 272 | console.log('[SetupIntent]', payload.setupIntent); 273 | } 274 | }); 275 | this.setState({disabled: true, processing: true}); 276 | } else { 277 | console.log("Stripe.js hasn't loaded yet."); 278 | } 279 | }; 280 | 281 | render() { 282 | return ( 283 |
284 | 294 | {this.state.error &&
{this.state.error}
} 295 | {this.state.message && ( 296 |
{this.state.message}
297 | )} 298 | {!this.state.succeeded && ( 299 | 302 | )} 303 |
304 | ); 305 | } 306 | } 307 | 308 | const HandleCardSetup = injectStripe(_HandleCardSetup); 309 | 310 | class Checkout extends React.Component<{}, {elementFontSize: string}> { 311 | constructor() { 312 | super(); 313 | this.state = { 314 | elementFontSize: window.innerWidth < 450 ? '14px' : '18px', 315 | }; 316 | window.addEventListener('resize', () => { 317 | if (window.innerWidth < 450 && this.state.elementFontSize !== '14px') { 318 | this.setState({elementFontSize: '14px'}); 319 | } else if ( 320 | window.innerWidth >= 450 && 321 | this.state.elementFontSize !== '18px' 322 | ) { 323 | this.setState({elementFontSize: '18px'}); 324 | } 325 | }); 326 | } 327 | 328 | render() { 329 | const {elementFontSize} = this.state; 330 | return ( 331 |
332 |

React Stripe Elements with PaymentIntents

333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 |
343 | ); 344 | } 345 | } 346 | 347 | const App = () => { 348 | return ( 349 | 350 | 351 | 352 | ); 353 | }; 354 | 355 | const appElement = document.querySelector('.App'); 356 | if (appElement) { 357 | render(, appElement); 358 | } else { 359 | console.error( 360 | 'We could not find an HTML element with a class name of "App" in the DOM. Please make sure you copy index.html as well for this demo to work.' 361 | ); 362 | } 363 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-stripe-elements", 3 | "version": "6.1.2", 4 | "description": "React components for Stripe.js and Stripe Elements", 5 | "main": "./lib/index.js", 6 | "module": "es/index.js", 7 | "jsnext:main": "es/index.js", 8 | "scripts": { 9 | "test": "node_modules/.bin/jest", 10 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 11 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 12 | "build:umd": "cross-env BABEL_ENV=commonjs webpack src/index.js -o dist/react-stripe-elements.js --config webpack.config.prod.js --env.noMinimize", 13 | "build:umd:min": "cross-env BABEL_ENV=commonjs webpack src/index.js -o dist/react-stripe-elements.min.js --config webpack.config.prod.js", 14 | "lint": "eslint src demo", 15 | "flow": "flow", 16 | "build": "yarn run lint && yarn run flow && yarn run build:commonjs && yarn run build:es && yarn run build:umd && yarn run build:umd:min", 17 | "clean": "rimraf lib dist es", 18 | "prettier": "prettier './**/*.js' './**/*.css' './**/*.md' --write", 19 | "prettier-list-different": "prettier './**/*.js' './**/*.css' './**/*.md' --list-different", 20 | "prepublish": "yarn run clean && yarn run build", 21 | "demo": "webpack-dev-server --content-base dist", 22 | "doctoc": "doctoc README.md" 23 | }, 24 | "keywords": [], 25 | "author": "Stripe (https://www.stripe.com)", 26 | "license": "MIT", 27 | "dependencies": { 28 | "prop-types": "15.7.2" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/stripe/react-stripe-elements.git" 33 | }, 34 | "files": [ 35 | "dist", 36 | "lib", 37 | "src", 38 | "es" 39 | ], 40 | "jest": { 41 | "setupTestFrameworkScriptFile": "/test/setupJest.js" 42 | }, 43 | "devDependencies": { 44 | "babel-cli": "^6.26.0", 45 | "babel-core": "^6.26.0", 46 | "babel-eslint": "^10.0.2", 47 | "babel-jest": "^23.6.0", 48 | "babel-loader": "^7.1.2", 49 | "babel-plugin-transform-class-properties": "^6.24.1", 50 | "babel-plugin-transform-es2015-classes": "^6.24.1", 51 | "babel-plugin-transform-inline-environment-variables": "^0.4.3", 52 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 53 | "babel-preset-env": "^1.6.1", 54 | "babel-preset-react": "^6.24.1", 55 | "cross-env": "^5.0.5", 56 | "doctoc": "^1.3.0", 57 | "enzyme": "3.10", 58 | "enzyme-adapter-react-16": "^1.14.0", 59 | "eslint": "4", 60 | "eslint-config-prettier": "2.9.0", 61 | "eslint-config-stripe": "^1.0.13", 62 | "eslint-plugin-flowtype": "^2.35.1", 63 | "eslint-plugin-import": "^2.7.0", 64 | "eslint-plugin-jest": "21.17.0", 65 | "eslint-plugin-jsx-a11y": "^6.0.2", 66 | "eslint-plugin-react": "^7.4.0", 67 | "flow-bin": "^0.90.0", 68 | "html-webpack-plugin": "^3.2.0", 69 | "jest": "23.6.0", 70 | "prettier": "1.15.3", 71 | "react": "16.9", 72 | "react-dom": "16.9", 73 | "react-test-renderer": "16.9", 74 | "rimraf": "^2.6.2", 75 | "webpack": "^4.41.2", 76 | "webpack-cli": "^3.2.1", 77 | "webpack-dev-server": "^3.1.14" 78 | }, 79 | "peerDependencies": { 80 | "react": "^15.5.4 || ^16.0.0-0", 81 | "react-dom": "^15.5.4 || ^16.0.0-0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import isEqual from '../utils/isEqual'; 5 | import {type ElementContext, elementContextTypes} from './Elements'; 6 | 7 | type Props = { 8 | id?: string, 9 | className?: string, 10 | onChange: Function, 11 | onBlur: Function, 12 | onFocus: Function, 13 | onReady: Function, 14 | }; 15 | 16 | const noop = () => {}; 17 | 18 | const _extractOptions = (props: Props): Object => { 19 | const {id, className, onChange, onFocus, onBlur, onReady, ...options} = props; 20 | return options; 21 | }; 22 | 23 | const capitalized = (str: string) => { 24 | return str.charAt(0).toUpperCase() + str.slice(1); 25 | }; 26 | 27 | const Element = ( 28 | type: string, 29 | hocOptions: { 30 | impliedTokenType?: string, 31 | impliedSourceType?: string, 32 | impliedPaymentMethodType?: string, 33 | } = {} 34 | ) => 35 | class extends React.Component { 36 | static propTypes = { 37 | id: PropTypes.string, 38 | className: PropTypes.string, 39 | onChange: PropTypes.func, 40 | onBlur: PropTypes.func, 41 | onFocus: PropTypes.func, 42 | onReady: PropTypes.func, 43 | }; 44 | static defaultProps = { 45 | id: undefined, 46 | className: undefined, 47 | onChange: noop, 48 | onBlur: noop, 49 | onFocus: noop, 50 | onReady: noop, 51 | }; 52 | 53 | static contextTypes = elementContextTypes; 54 | 55 | static displayName = `${capitalized(type)}Element`; 56 | 57 | constructor(props: Props, context: ElementContext) { 58 | super(props, context); 59 | 60 | this._element = null; 61 | 62 | const options = _extractOptions(this.props); 63 | // We keep track of the extracted options on this._options to avoid re-rendering. 64 | // (We would unnecessarily re-render if we were tracking them with state.) 65 | this._options = options; 66 | } 67 | 68 | componentDidMount() { 69 | this.context.addElementsLoadListener((elements: ElementsShape) => { 70 | if (!this._ref) { 71 | return; 72 | } 73 | 74 | const element = elements.create(type, this._options); 75 | this._element = element; 76 | 77 | this._setupEventListeners(element); 78 | 79 | element.mount(this._ref); 80 | 81 | // Register Element for automatic token / source / paymentMethod creation 82 | this.context.registerElement( 83 | element, 84 | hocOptions.impliedTokenType, 85 | hocOptions.impliedSourceType, 86 | hocOptions.impliedPaymentMethodType 87 | ); 88 | }); 89 | } 90 | 91 | componentDidUpdate() { 92 | const options = _extractOptions(this.props); 93 | if ( 94 | Object.keys(options).length !== 0 && 95 | !isEqual(options, this._options) 96 | ) { 97 | this._options = options; 98 | if (this._element) { 99 | this._element.update(options); 100 | } 101 | } 102 | } 103 | 104 | componentWillUnmount() { 105 | if (this._element) { 106 | const element = this._element; 107 | element.destroy(); 108 | this.context.unregisterElement(element); 109 | } 110 | } 111 | 112 | context: ElementContext; 113 | _element: ElementShape | null; 114 | _ref: ?HTMLElement; 115 | _options: Object; 116 | 117 | _setupEventListeners(element: ElementShape) { 118 | element.on('ready', () => { 119 | this.props.onReady(this._element); 120 | }); 121 | 122 | element.on('change', (change) => { 123 | this.props.onChange(change); 124 | }); 125 | 126 | element.on('blur', (...args) => this.props.onBlur(...args)); 127 | element.on('focus', (...args) => this.props.onFocus(...args)); 128 | } 129 | 130 | handleRef = (ref: ?HTMLElement) => { 131 | this._ref = ref; 132 | }; 133 | 134 | render() { 135 | return ( 136 |
141 | ); 142 | } 143 | }; 144 | 145 | export default Element; 146 | -------------------------------------------------------------------------------- /src/components/Element.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount, shallow} from 'enzyme'; 4 | 5 | import Element from './Element'; 6 | 7 | describe('Element', () => { 8 | let elementMock; 9 | let elementsMock; 10 | let context; 11 | beforeEach(() => { 12 | elementMock = { 13 | mount: jest.fn(), 14 | destroy: jest.fn(), 15 | on: jest.fn((event, cb) => { 16 | if (event === 'ready') { 17 | cb(); 18 | } 19 | }), 20 | update: jest.fn(), 21 | }; 22 | elementsMock = { 23 | create: jest.fn().mockReturnValue(elementMock), 24 | }; 25 | context = { 26 | addElementsLoadListener: (fn) => fn(elementsMock), 27 | registerElement: jest.fn(), 28 | unregisterElement: jest.fn(), 29 | }; 30 | }); 31 | 32 | it('should pass id to the DOM element', () => { 33 | const id = 'my-id'; 34 | const CardElement = Element('card', { 35 | impliedTokenType: 'card', 36 | impliedSourceType: 'card', 37 | impliedPaymentMethodType: 'card', 38 | }); 39 | const element = shallow(, {context}); 40 | expect(element.find('#my-id').length).toBe(1); 41 | }); 42 | 43 | it('should pass className to the DOM element', () => { 44 | const className = 'my-class'; 45 | const CardElement = Element('card', { 46 | impliedTokenType: 'card', 47 | impliedSourceType: 'card', 48 | impliedPaymentMethodType: 'card', 49 | }); 50 | const element = shallow(, {context}); 51 | expect(element.first().hasClass(className)).toBeTruthy(); 52 | }); 53 | 54 | it('should call the right hooks for a registered Element', () => { 55 | const TestElement = Element('test', { 56 | impliedTokenType: 'foo', 57 | impliedSourceType: 'bar', 58 | impliedPaymentMethodType: 'baz', 59 | }); 60 | const element = mount(, {context}); 61 | 62 | expect(context.registerElement).toHaveBeenCalledTimes(1); 63 | expect(context.registerElement).toHaveBeenCalledWith( 64 | elementMock, 65 | 'foo', 66 | 'bar', 67 | 'baz' 68 | ); 69 | 70 | element.unmount(); 71 | expect(elementMock.destroy).toHaveBeenCalledTimes(1); 72 | expect(context.unregisterElement).toHaveBeenCalledTimes(1); 73 | expect(context.unregisterElement).toHaveBeenCalledWith(elementMock); 74 | }); 75 | 76 | it('should call the right hooks for a non-auto-detected Element', () => { 77 | const TestElement = Element('test'); 78 | const element = mount(, {context}); 79 | 80 | expect(context.registerElement).toHaveBeenCalledTimes(1); 81 | expect(context.registerElement).toHaveBeenCalledWith( 82 | elementMock, 83 | undefined, 84 | undefined, 85 | undefined 86 | ); 87 | 88 | element.unmount(); 89 | expect(elementMock.destroy).toHaveBeenCalledTimes(1); 90 | expect(context.unregisterElement).toHaveBeenCalledTimes(1); 91 | expect(context.unregisterElement).toHaveBeenCalledWith(elementMock); 92 | }); 93 | 94 | it('should call onReady', () => { 95 | const CardElement = Element('card', { 96 | impliedTokenType: 'card', 97 | impliedSourceType: 'card', 98 | impliedPaymentMethodType: 'card', 99 | }); 100 | const onReadyMock = jest.fn(); 101 | 102 | mount(, { 103 | context, 104 | }); 105 | 106 | expect(elementMock.on.mock.calls[0][0]).toBe('ready'); 107 | expect(onReadyMock).toHaveBeenCalledWith(elementMock); 108 | }); 109 | 110 | it('should update the Element when props change', () => { 111 | const style = { 112 | base: { 113 | fontSize: '16px', 114 | }, 115 | }; 116 | const TestElement = Element('test', { 117 | impliedTokenType: 'foo', 118 | impliedSourceType: 'bar', 119 | impliedPaymentMethodType: 'card', 120 | }); 121 | const element = mount(, { 122 | context, 123 | }); 124 | 125 | expect(elementMock.update).toHaveBeenCalledTimes(0); 126 | element.setProps({style, onChange: jest.fn()}); 127 | expect(elementMock.update).toHaveBeenCalledTimes(0); 128 | 129 | element.setProps({ 130 | style: {base: {fontSize: '20px'}}, 131 | onChange: jest.fn(), 132 | }); 133 | expect(elementMock.update).toHaveBeenCalledTimes(1); 134 | expect(elementMock.update).toHaveBeenCalledWith({ 135 | style: {base: {fontSize: '20px'}}, 136 | }); 137 | }); 138 | 139 | it("re-rendering with new props should still work if addElementsLoadListener hasn't fired yet", () => { 140 | // no-op function so that any registered listeners are never woken up 141 | context.addElementsLoadListener = () => {}; 142 | 143 | const placeholder = 'hello'; 144 | const CardElement = Element('card', { 145 | impliedTokenType: 'card', 146 | impliedSourceType: 'card', 147 | impliedPaymentMethodType: 'card', 148 | }); 149 | const element = shallow(, { 150 | context, 151 | }); 152 | 153 | expect(() => element.setProps({placeholder: 'placeholder'})).not.toThrow(); 154 | }); 155 | 156 | it('should have a displayName based on the type argument', () => { 157 | const TestElement = Element('test'); 158 | expect(TestElement.displayName).toEqual('TestElement'); 159 | }); 160 | 161 | it('Do not create element if component is not mounted', () => { 162 | const listeners = []; 163 | context.addElementsLoadListener = (fn) => listeners.push(fn); 164 | 165 | const CardElement = Element('card', { 166 | impliedTokenType: 'card', 167 | impliedSourceType: 'card', 168 | impliedPaymentMethodType: 'card', 169 | }); 170 | const element = mount(, {context}); 171 | element.unmount(); 172 | 173 | // ensure listener was called on mount 174 | expect(listeners).toHaveLength(1); 175 | // simulate load complete after unmount 176 | listeners[0](elementsMock); 177 | // listener should do nothing since it's unmounted 178 | expect(elementsMock.create).toHaveBeenCalledTimes(0); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /src/components/Elements.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import {type ProviderContext, providerContextTypes} from './Provider'; 5 | 6 | export type ElementsList = Array<{ 7 | element: ElementShape, 8 | impliedTokenType?: string, 9 | impliedSourceType?: string, 10 | impliedPaymentMethodType?: string, 11 | }>; 12 | export type ElementsLoadListener = (ElementsShape) => void; 13 | 14 | type Props = { 15 | children?: any, 16 | }; 17 | 18 | type State = { 19 | registeredElements: ElementsList, 20 | }; 21 | 22 | export type InjectContext = { 23 | getRegisteredElements: () => ElementsList, 24 | elements: ElementsShape | null, 25 | }; 26 | 27 | export const injectContextTypes = { 28 | getRegisteredElements: PropTypes.func.isRequired, 29 | elements: PropTypes.object, 30 | }; 31 | 32 | export type ElementContext = { 33 | addElementsLoadListener: (ElementsLoadListener) => void, 34 | registerElement: ( 35 | element: ElementShape, 36 | impliedTokenType: ?string, 37 | impliedSourceType: ?string, 38 | impliedPaymentMethodType: ?string 39 | ) => void, 40 | unregisterElement: (element: ElementShape) => void, 41 | }; 42 | 43 | export const elementContextTypes = { 44 | addElementsLoadListener: PropTypes.func.isRequired, 45 | registerElement: PropTypes.func.isRequired, 46 | unregisterElement: PropTypes.func.isRequired, 47 | }; 48 | 49 | type ChildContext = InjectContext & ElementContext; 50 | 51 | export default class Elements extends React.Component { 52 | static childContextTypes = { 53 | ...injectContextTypes, 54 | ...elementContextTypes, 55 | }; 56 | static contextTypes = providerContextTypes; 57 | static defaultProps = { 58 | children: null, 59 | }; 60 | 61 | constructor(props: Props, context: ProviderContext) { 62 | super(props, context); 63 | 64 | const {children, ...options} = this.props; 65 | 66 | if (this.context.tag === 'sync') { 67 | this._elements = this.context.stripe.elements(options); 68 | } 69 | 70 | this.state = { 71 | registeredElements: [], 72 | }; 73 | } 74 | 75 | getChildContext(): ChildContext { 76 | return { 77 | addElementsLoadListener: (fn: ElementsLoadListener) => { 78 | // Return the existing elements instance if we already have one. 79 | if (this.context.tag === 'sync') { 80 | // This is impossible, but we need to make flow happy. 81 | if (!this._elements) { 82 | throw new Error( 83 | 'Expected elements to be instantiated but it was not.' 84 | ); 85 | } 86 | 87 | fn(this._elements); 88 | } else { 89 | this.context.addStripeLoadListener((stripe: StripeShape) => { 90 | if (this._elements) { 91 | fn(this._elements); 92 | } else { 93 | const {children, ...options} = this.props; 94 | this._elements = stripe.elements(options); 95 | fn(this._elements); 96 | } 97 | }); 98 | } 99 | }, 100 | registerElement: this.handleRegisterElement, 101 | unregisterElement: this.handleUnregisterElement, 102 | getRegisteredElements: () => this.state.registeredElements, 103 | elements: this._elements, 104 | }; 105 | } 106 | 107 | props: Props; 108 | context: ProviderContext; 109 | _elements: ElementsShape | null = null; 110 | 111 | handleRegisterElement = ( 112 | element: Object, 113 | impliedTokenType: ?string, 114 | impliedSourceType: ?string, 115 | impliedPaymentMethodType: ?string 116 | ) => { 117 | this.setState((prevState) => ({ 118 | registeredElements: [ 119 | ...prevState.registeredElements, 120 | { 121 | element, 122 | ...(impliedTokenType ? {impliedTokenType} : {}), 123 | ...(impliedSourceType ? {impliedSourceType} : {}), 124 | ...(impliedPaymentMethodType ? {impliedPaymentMethodType} : {}), 125 | }, 126 | ], 127 | })); 128 | }; 129 | 130 | handleUnregisterElement = (el: Object) => { 131 | this.setState((prevState) => ({ 132 | registeredElements: prevState.registeredElements.filter( 133 | ({element}) => element !== el 134 | ), 135 | })); 136 | }; 137 | 138 | render() { 139 | return React.Children.only(this.props.children); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/components/Elements.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount} from 'enzyme'; 4 | 5 | import Elements from './Elements'; 6 | 7 | describe('Elements', () => { 8 | let stripeMock; 9 | const elementsMock = {}; 10 | 11 | beforeEach(() => { 12 | stripeMock = { 13 | elements: jest 14 | .fn(() => { 15 | throw new Error( 16 | 'elements() should not be called twice in this test.' 17 | ); 18 | }) 19 | .mockReturnValueOnce(elementsMock), 20 | createToken: jest.fn(), 21 | createSource: jest.fn(), 22 | createPaymentMethod: jest.fn(), 23 | }; 24 | }); 25 | 26 | it('creates the context', () => { 27 | const syncContext = { 28 | tag: 'sync', 29 | stripe: stripeMock, 30 | }; 31 | const wrapper = mount( 32 | 33 |
34 | , 35 | {context: syncContext} 36 | ); 37 | const childContext = wrapper.instance().getChildContext(); 38 | expect(Object.keys(childContext)).toEqual([ 39 | 'addElementsLoadListener', 40 | 'registerElement', 41 | 'unregisterElement', 42 | 'getRegisteredElements', 43 | 'elements', 44 | ]); 45 | }); 46 | 47 | it('with sync context: addElementsLoadListener returns the same elements instance ', () => { 48 | const syncContext = { 49 | tag: 'sync', 50 | stripe: stripeMock, 51 | }; 52 | const wrapper = mount( 53 | 54 |
55 | , 56 | {context: syncContext} 57 | ); 58 | const childContext = wrapper.instance().getChildContext(); 59 | 60 | const mockCallback = jest.fn(); 61 | childContext.addElementsLoadListener(mockCallback); 62 | expect(mockCallback).toHaveBeenCalledTimes(1); 63 | expect(mockCallback).toHaveBeenLastCalledWith(elementsMock); 64 | childContext.addElementsLoadListener(mockCallback); 65 | expect(mockCallback).toHaveBeenCalledTimes(2); 66 | expect(mockCallback).toHaveBeenCalledWith(elementsMock); 67 | }); 68 | 69 | it('with sync context, elements is instantiated right away', () => { 70 | const syncContext = { 71 | tag: 'sync', 72 | stripe: stripeMock, 73 | }; 74 | const wrapper = mount( 75 | 76 |
77 | , 78 | {context: syncContext} 79 | ); 80 | const childContext = wrapper.instance().getChildContext(); 81 | 82 | expect(childContext.elements).not.toBe(null); 83 | }); 84 | 85 | it('with async context: addElementsLoadListener returns the same elements instance ', () => { 86 | const asyncContext = { 87 | tag: 'async', 88 | addStripeLoadListener: jest.fn((callback) => 89 | setTimeout(() => callback(stripeMock), 0) 90 | ), 91 | }; 92 | const wrapper = mount( 93 | 94 |
95 | , 96 | {context: asyncContext} 97 | ); 98 | const childContext = wrapper.instance().getChildContext(); 99 | 100 | expect(childContext.elements).toBe(null); 101 | 102 | const a = new Promise((resolve) => 103 | childContext.addElementsLoadListener((first) => { 104 | expect(first).toEqual(elementsMock); 105 | resolve(); 106 | }) 107 | ); 108 | const b = new Promise((resolve) => 109 | childContext.addElementsLoadListener((second) => { 110 | expect(second).toEqual(elementsMock); 111 | resolve(); 112 | }) 113 | ); 114 | return Promise.all([a, b]); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/components/PaymentRequestButtonElement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import shallowEqual from '../utils/shallowEqual'; 5 | import {type ElementContext, elementContextTypes} from './Elements'; 6 | 7 | type Props = { 8 | id?: string, 9 | className?: string, 10 | onBlur: Function, 11 | onClick: Function, 12 | onFocus: Function, 13 | onReady: Function, 14 | paymentRequest: { 15 | canMakePayment: Function, 16 | on: Function, 17 | show: Function, 18 | }, 19 | }; 20 | 21 | const noop = () => {}; 22 | 23 | const _extractOptions = (props: Props): Object => { 24 | const { 25 | id, 26 | className, 27 | onBlur, 28 | onClick, 29 | onFocus, 30 | onReady, 31 | paymentRequest, 32 | ...options 33 | } = props; 34 | return options; 35 | }; 36 | 37 | class PaymentRequestButtonElement extends React.Component { 38 | static propTypes = { 39 | id: PropTypes.string, 40 | className: PropTypes.string, 41 | onBlur: PropTypes.func, 42 | onClick: PropTypes.func, 43 | onFocus: PropTypes.func, 44 | onReady: PropTypes.func, 45 | paymentRequest: PropTypes.shape({ 46 | canMakePayment: PropTypes.func.isRequired, 47 | on: PropTypes.func.isRequired, 48 | show: PropTypes.func.isRequired, 49 | }).isRequired, 50 | }; 51 | static defaultProps = { 52 | id: undefined, 53 | className: undefined, 54 | onBlur: noop, 55 | onClick: noop, 56 | onFocus: noop, 57 | onReady: noop, 58 | }; 59 | 60 | static contextTypes = elementContextTypes; 61 | 62 | constructor(props: Props, context: ElementContext) { 63 | super(props, context); 64 | 65 | const options = _extractOptions(props); 66 | // We keep track of the extracted options on this._options to avoid re-rendering. 67 | // (We would unnecessarily re-render if we were tracking them with state.) 68 | this._options = options; 69 | } 70 | 71 | componentDidMount() { 72 | this.context.addElementsLoadListener((elements: ElementsShape) => { 73 | this._element = elements.create('paymentRequestButton', { 74 | paymentRequest: this.props.paymentRequest, 75 | ...this._options, 76 | }); 77 | this._element.on('ready', () => { 78 | this.props.onReady(this._element); 79 | }); 80 | this._element.on('focus', (...args) => this.props.onFocus(...args)); 81 | this._element.on('click', (...args) => this.props.onClick(...args)); 82 | this._element.on('blur', (...args) => this.props.onBlur(...args)); 83 | this._element.mount(this._ref); 84 | }); 85 | } 86 | componentDidUpdate(prevProps: Props) { 87 | if (this.props.paymentRequest !== prevProps.paymentRequest) { 88 | console.warn( 89 | 'Unsupported prop change: paymentRequest is not a customizable property.' 90 | ); 91 | } 92 | const options = _extractOptions(this.props); 93 | if ( 94 | Object.keys(options).length !== 0 && 95 | !shallowEqual(options, this._options) 96 | ) { 97 | this._options = options; 98 | this._element.update(options); 99 | } 100 | } 101 | componentWillUnmount() { 102 | this._element.destroy(); 103 | } 104 | 105 | context: ElementContext; 106 | _element: ElementShape; 107 | _ref: ?HTMLElement; 108 | _options: Object; 109 | 110 | handleRef = (ref: ?HTMLElement) => { 111 | this._ref = ref; 112 | }; 113 | 114 | render() { 115 | return ( 116 |
121 | ); 122 | } 123 | } 124 | 125 | export default PaymentRequestButtonElement; 126 | -------------------------------------------------------------------------------- /src/components/PaymentRequestButtonElement.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount, shallow} from 'enzyme'; 4 | 5 | import PaymentRequestButtonElement from './PaymentRequestButtonElement'; 6 | 7 | describe('PaymentRequestButtonElement', () => { 8 | let elementMock; 9 | let elementsMock; 10 | let context; 11 | let paymentRequestMock; 12 | beforeEach(() => { 13 | elementMock = { 14 | mount: jest.fn(), 15 | destroy: jest.fn(), 16 | on: jest.fn((event, cb) => { 17 | if (event === 'ready') { 18 | cb(); 19 | } 20 | }), 21 | update: jest.fn(), 22 | }; 23 | elementsMock = { 24 | create: jest.fn().mockReturnValue(elementMock), 25 | }; 26 | context = { 27 | addElementsLoadListener: (fn) => fn(elementsMock), 28 | registerElement: jest.fn(), 29 | unregisterElement: jest.fn(), 30 | }; 31 | paymentRequestMock = { 32 | canMakePayment: jest.fn(), 33 | on: jest.fn(), 34 | show: jest.fn(), 35 | }; 36 | }); 37 | 38 | it('should pass the id to the DOM element', () => { 39 | const id = 'my-id'; 40 | const element = shallow( 41 | , 45 | {context} 46 | ); 47 | expect(element.find(`#${id}`)).toBeTruthy(); 48 | }); 49 | 50 | it('should pass the className to the DOM element', () => { 51 | const className = 'my-class'; 52 | const element = shallow( 53 | , 57 | {context} 58 | ); 59 | expect(element.first().hasClass(className)).toBeTruthy(); 60 | }); 61 | 62 | it('should call onReady', () => { 63 | const onReadyMock = jest.fn(); 64 | 65 | mount( 66 | , 70 | {context} 71 | ); 72 | 73 | expect(elementMock.on.mock.calls[0][0]).toBe('ready'); 74 | expect(onReadyMock).toHaveBeenCalledWith(elementMock); 75 | }); 76 | 77 | it('should not register the Element', () => { 78 | const element = mount( 79 | , 80 | {context} 81 | ); 82 | 83 | expect(context.registerElement).toHaveBeenCalledTimes(0); 84 | 85 | element.unmount(); 86 | expect(elementMock.destroy).toHaveBeenCalledTimes(1); 87 | expect(context.unregisterElement).toHaveBeenCalledTimes(0); 88 | }); 89 | 90 | it('should update the Element when props change', () => { 91 | const style = { 92 | paymentRequestButton: { 93 | theme: 'dark', 94 | height: '64px', 95 | type: 'donate', 96 | }, 97 | }; 98 | const element = mount( 99 | , 103 | {context} 104 | ); 105 | 106 | expect(elementMock.update).toHaveBeenCalledTimes(0); 107 | element.setProps({style}); 108 | expect(elementMock.update).toHaveBeenCalledTimes(0); 109 | 110 | element.setProps({ 111 | style: {paymentRequestButton: {height: '64px'}}, 112 | }); 113 | expect(elementMock.update).toHaveBeenCalledTimes(1); 114 | expect(elementMock.update).toHaveBeenCalledWith({ 115 | style: {paymentRequestButton: {height: '64px'}}, 116 | }); 117 | }); 118 | 119 | it('should warn that the paymentRequest prop can not be changed', () => { 120 | const style = { 121 | paymentRequestButton: { 122 | theme: 'dark', 123 | height: '64px', 124 | type: 'donate', 125 | }, 126 | }; 127 | const element = mount( 128 | , 132 | {context} 133 | ); 134 | 135 | expect(elementMock.update).toHaveBeenCalledTimes(0); 136 | 137 | const originalConsoleWarn = global.console.warn; 138 | const mockConsoleWarn = jest.fn(); 139 | global.console.warn = mockConsoleWarn; 140 | 141 | element.setProps({ 142 | paymentRequest: { 143 | canMakePayment: jest.fn(), 144 | on: jest.fn(), 145 | show: jest.fn(), 146 | }, 147 | }); 148 | expect(elementMock.update).toHaveBeenCalledTimes(0); 149 | expect(mockConsoleWarn).toHaveBeenCalledWith( 150 | 'Unsupported prop change: paymentRequest is not a customizable property.' 151 | ); 152 | 153 | global.console.warn = originalConsoleWarn; 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/components/Provider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | type Props = { 6 | apiKey?: string, 7 | stripe?: mixed, 8 | children?: any, 9 | }; 10 | 11 | type Meta = 12 | | {tag: 'sync', stripe: StripeShape} 13 | | {tag: 'async', stripe: StripeShape | null}; 14 | 15 | type StripeLoadListener = (StripeShape) => void; 16 | 17 | // TODO(jez) 'sync' and 'async' are bad tag names. 18 | // TODO(jez) What if redux also uses this.context.tag? 19 | export type SyncStripeContext = { 20 | tag: 'sync', 21 | stripe: StripeShape, 22 | }; 23 | export type AsyncStripeContext = { 24 | tag: 'async', 25 | addStripeLoadListener: (StripeLoadListener) => void, 26 | }; 27 | 28 | export type ProviderContext = SyncStripeContext | AsyncStripeContext; 29 | 30 | export const providerContextTypes = { 31 | tag: PropTypes.string.isRequired, 32 | stripe: PropTypes.object, 33 | addStripeLoadListener: PropTypes.func, 34 | }; 35 | 36 | const getOrCreateStripe = (apiKey: string, options: mixed): StripeShape => { 37 | /** 38 | * Note that this is not meant to be a generic memoization solution. 39 | * This is specifically a solution for `StripeProvider`s being initialized 40 | * and destroyed regularly (with the same set of props) when users only 41 | * use `StripeProvider` for the subtree that contains their checkout form. 42 | */ 43 | window.Stripe.__cachedInstances = window.Stripe.__cachedInstances || {}; 44 | const cacheKey = `key=${apiKey} options=${JSON.stringify(options)}`; 45 | 46 | const stripe = 47 | window.Stripe.__cachedInstances[cacheKey] || window.Stripe(apiKey, options); 48 | window.Stripe.__cachedInstances[cacheKey] = stripe; 49 | 50 | return stripe; 51 | }; 52 | 53 | const ensureStripeShape = (stripe: mixed): StripeShape => { 54 | if ( 55 | stripe && 56 | stripe.elements && 57 | stripe.createSource && 58 | stripe.createToken && 59 | stripe.createPaymentMethod && 60 | stripe.handleCardPayment 61 | ) { 62 | return ((stripe: any): StripeShape); 63 | } else { 64 | throw new Error( 65 | "Please pass a valid Stripe object to StripeProvider. You can obtain a Stripe object by calling 'Stripe(...)' with your publishable key." 66 | ); 67 | } 68 | }; 69 | 70 | export default class Provider extends React.Component { 71 | // Even though we're using flow, also use PropTypes so we can take advantage of developer warnings. 72 | static propTypes = { 73 | apiKey: PropTypes.string, 74 | // PropTypes.object is the only way we can accept a Stripe instance 75 | // eslint-disable-next-line react/forbid-prop-types 76 | stripe: PropTypes.object, 77 | children: PropTypes.node, 78 | }; 79 | // on the other hand: childContextTypes is *required* to use context. 80 | static childContextTypes = providerContextTypes; 81 | static defaultProps = { 82 | apiKey: undefined, 83 | stripe: undefined, 84 | children: null, 85 | }; 86 | 87 | constructor(props: Props) { 88 | super(props); 89 | 90 | if (this.props.apiKey && this.props.stripe) { 91 | throw new Error( 92 | "Please pass either 'apiKey' or 'stripe' to StripeProvider, not both." 93 | ); 94 | } else if (this.props.apiKey) { 95 | if (!window.Stripe) { 96 | throw new Error( 97 | "Please load Stripe.js (https://js.stripe.com/v3/) on this page to use react-stripe-elements. If Stripe.js isn't available yet (it's loading asynchronously, or you're using server-side rendering), see https://github.com/stripe/react-stripe-elements#advanced-integrations" 98 | ); 99 | } else { 100 | const {apiKey, children, ...options} = this.props; 101 | const stripe = getOrCreateStripe(apiKey, options); 102 | this._meta = {tag: 'sync', stripe}; 103 | this._register(); 104 | } 105 | } else if (this.props.stripe) { 106 | // If we already have a stripe instance (in the constructor), we can behave synchronously. 107 | const stripe = ensureStripeShape(this.props.stripe); 108 | this._meta = {tag: 'sync', stripe}; 109 | this._register(); 110 | } else if (this.props.stripe === null) { 111 | this._meta = { 112 | tag: 'async', 113 | stripe: null, 114 | }; 115 | } else { 116 | throw new Error( 117 | "Please pass either 'apiKey' or 'stripe' to StripeProvider. If you're using 'stripe' but don't have a Stripe instance yet, pass 'null' explicitly." 118 | ); 119 | } 120 | 121 | this._didWarn = false; 122 | this._didWakeUpListeners = false; 123 | this._listeners = []; 124 | } 125 | 126 | getChildContext(): ProviderContext { 127 | // getChildContext is run after the constructor, so we WILL have access to 128 | // the initial state. 129 | // 130 | // However, context doesn't update in respnse to state changes like you 131 | // might expect: context is pulled by the child, not pushed by the parent. 132 | if (this._meta.tag === 'sync') { 133 | return { 134 | tag: 'sync', 135 | stripe: this._meta.stripe, 136 | }; 137 | } else { 138 | return { 139 | tag: 'async', 140 | addStripeLoadListener: (fn: StripeLoadListener) => { 141 | if (this._meta.stripe) { 142 | fn(this._meta.stripe); 143 | } else { 144 | this._listeners.push(fn); 145 | } 146 | }, 147 | }; 148 | } 149 | } 150 | 151 | componentDidUpdate(prevProps: Props) { 152 | const apiKeyChanged = 153 | this.props.apiKey && 154 | prevProps.apiKey && 155 | this.props.apiKey !== prevProps.apiKey; 156 | 157 | const stripeInstanceChanged = 158 | this.props.stripe && 159 | prevProps.stripe && 160 | this.props.stripe !== prevProps.stripe; 161 | if ( 162 | !this._didWarn && 163 | (apiKeyChanged || stripeInstanceChanged) && 164 | window.console && 165 | window.console.error 166 | ) { 167 | this._didWarn = true; 168 | // eslint-disable-next-line no-console 169 | console.error( 170 | 'StripeProvider does not support changing the apiKey parameter.' 171 | ); 172 | return; 173 | } 174 | 175 | if (!this._didWakeUpListeners && this.props.stripe) { 176 | // Wake up the listeners if we've finally been given a StripeShape 177 | this._didWakeUpListeners = true; 178 | const stripe = ensureStripeShape(this.props.stripe); 179 | this._meta.stripe = stripe; 180 | this._register(); 181 | this._listeners.forEach((fn) => { 182 | fn(stripe); 183 | }); 184 | } 185 | } 186 | 187 | _register() { 188 | const {stripe} = this._meta; 189 | 190 | if (!stripe || !stripe._registerWrapper) { 191 | return; 192 | } 193 | 194 | stripe._registerWrapper({ 195 | name: 'react-stripe-elements', 196 | version: process.env.npm_package_version || null, 197 | }); 198 | } 199 | 200 | props: Props; 201 | _didWarn: boolean; 202 | _didWakeUpListeners: boolean; 203 | _listeners: Array; 204 | _meta: Meta; 205 | 206 | render() { 207 | return React.Children.only(this.props.children); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/components/Provider.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount, shallow} from 'enzyme'; 4 | 5 | import StripeProvider from './Provider'; 6 | 7 | describe('StripeProvider', () => { 8 | let stripeMockFn; 9 | let stripeMockResult; 10 | 11 | beforeEach(() => { 12 | stripeMockResult = { 13 | elements: jest.fn(), 14 | createToken: jest.fn(), 15 | createSource: jest.fn(), 16 | createPaymentMethod: jest.fn(), 17 | handleCardPayment: jest.fn(), 18 | }; 19 | stripeMockFn = jest.fn().mockReturnValue(stripeMockResult); 20 | window.Stripe = stripeMockFn; 21 | }); 22 | 23 | it('requires apiKey or stripe prop', () => { 24 | expect(() => { 25 | shallow( 26 | 27 |
28 | 29 | ); 30 | }).toThrow(/Please pass either 'apiKey' or 'stripe' to StripeProvider./); 31 | }); 32 | 33 | it('throws without stripe.js loaded if using apiKey', () => { 34 | window.Stripe = null; 35 | expect(() => shallow()).toThrow( 36 | 'Please load Stripe.js (https://js.stripe.com/v3/) on this page to use react-stripe-elements.' 37 | ); 38 | }); 39 | 40 | it('requires not both apiKey and stripe prop', () => { 41 | expect(() => { 42 | shallow( 43 | 44 |
45 | 46 | ); 47 | }).toThrow( 48 | /Please pass either 'apiKey' or 'stripe' to StripeProvider, not both./ 49 | ); 50 | }); 51 | 52 | it('throws without children', () => { 53 | expect(() => shallow()).toThrow( 54 | 'React.Children.only expected to receive a single React element child' 55 | ); 56 | }); 57 | 58 | it('throws with more than one children', () => { 59 | expect(() => 60 | shallow( 61 | 62 |
63 |
64 | 65 | ) 66 | ).toThrow( 67 | 'React.Children.only expected to receive a single React element child' 68 | ); 69 | }); 70 | 71 | it('renders its single child', () => { 72 | const wrapper = shallow( 73 | 74 |
75 | 76 |
77 |
78 | ); 79 | 80 | expect(wrapper.html()).toBe('
'); 81 | }); 82 | 83 | it('initializes Stripe with apiKey and empty options', () => { 84 | shallow( 85 | 86 |
87 | 88 | ); 89 | expect(stripeMockFn).toHaveBeenCalledWith('made_up_key', {}); 90 | }); 91 | 92 | it('initializes Stripe with apiKey and arbitrary props as options', () => { 93 | shallow( 94 | 95 | 96 | 97 | ); 98 | expect(stripeMockFn).toHaveBeenCalledWith('made_up_key', {foo: 'bar'}); 99 | }); 100 | 101 | it('provides sync context.stripe if using apiKey', () => { 102 | const wrapper = mount( 103 | 104 | 105 | 106 | ); 107 | const childContext = wrapper.instance().getChildContext(); 108 | expect(childContext).toEqual({stripe: stripeMockResult, tag: 'sync'}); 109 | }); 110 | 111 | it('if stripe prop non-null *at mount*, provides sync context', () => { 112 | const wrapper = mount( 113 | 114 | 115 | 116 | ); 117 | const childContext = wrapper.instance().getChildContext(); 118 | expect(childContext).toEqual({stripe: stripeMockResult, tag: 'sync'}); 119 | }); 120 | 121 | it('if stripe prop null *at mount*, provides async context', () => { 122 | const wrapper = mount( 123 | 124 | 125 | 126 | ); 127 | const childContext = wrapper.instance().getChildContext(); 128 | expect(childContext).toHaveProperty('addStripeLoadListener'); 129 | expect(childContext).toHaveProperty('tag', 'async'); 130 | }); 131 | 132 | it('addStripeLoadListener is called when stripe goes from null -> non-null', (done) => { 133 | const wrapper = mount( 134 | 135 | 136 | 137 | ); 138 | 139 | const childContext = wrapper.instance().getChildContext(); 140 | childContext.addStripeLoadListener((stripe) => { 141 | expect(stripe).toEqual(stripeMockResult); 142 | done(); 143 | }); 144 | 145 | wrapper.setProps({stripe: stripeMockResult}); 146 | }); 147 | 148 | it('does not create a new Stripe instance if one exists for the same key', () => { 149 | window.Stripe = jest.fn(() => ({})); 150 | 151 | // First, create the first instance. 152 | let wrapper = mount( 153 | 154 | 155 | 156 | ); 157 | let childContext = wrapper.instance().getChildContext(); 158 | const keyOneInstance = childContext.stripe; 159 | expect(keyOneInstance).toBeTruthy(); 160 | 161 | // Create another! 162 | wrapper = mount( 163 | 164 | 165 | 166 | ); 167 | childContext = wrapper.instance().getChildContext(); 168 | expect(childContext.stripe).toBe(keyOneInstance); 169 | 170 | // Create another, but with a different key! 171 | wrapper = mount( 172 | 173 | 174 | 175 | ); 176 | childContext = wrapper.instance().getChildContext(); 177 | expect(childContext.stripe).not.toBe(keyOneInstance); 178 | }); 179 | 180 | it('warns when trying to change the API key', () => { 181 | const originalConsoleError = global.console.error; 182 | const mockConsoleError = jest.fn(); 183 | global.console.error = mockConsoleError; 184 | const wrapper = shallow( 185 | 186 | 187 | 188 | ); 189 | wrapper.setProps({apiKey: 'a_new_key'}); 190 | expect(mockConsoleError).toHaveBeenCalledWith( 191 | 'StripeProvider does not support changing the apiKey parameter.' 192 | ); 193 | global.console.error = originalConsoleError; 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /src/components/inject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, {type ComponentType} from 'react'; 3 | 4 | import {type InjectContext, injectContextTypes} from './Elements'; 5 | import { 6 | type SyncStripeContext, 7 | type AsyncStripeContext, 8 | providerContextTypes, 9 | } from './Provider'; 10 | 11 | type Context = 12 | | (InjectContext & SyncStripeContext) 13 | | (InjectContext & AsyncStripeContext); 14 | 15 | type Options = { 16 | withRef?: boolean, 17 | }; 18 | 19 | type WrappedStripeShape = { 20 | createToken: Function, 21 | createSource: Function, 22 | createPaymentMethod: Function, 23 | handleCardPayment: Function, 24 | handleCardSetup: Function, 25 | confirmCardPayment: Function, 26 | confirmCardSetup: Function, 27 | }; 28 | 29 | type State = {stripe: WrappedStripeShape | null}; 30 | 31 | export type InjectedProps = { 32 | stripe: WrappedStripeShape | null, 33 | elements: ElementsShape | null, 34 | }; 35 | 36 | // react-redux does a bunch of stuff with pure components / checking if it needs to re-render. 37 | // not sure if we need to do the same. 38 | const inject = ( 39 | WrappedComponent: ComponentType, 40 | componentOptions: Options = {} 41 | ): ComponentType => { 42 | const {withRef = false} = componentOptions; 43 | 44 | return class extends React.Component { 45 | static contextTypes = { 46 | ...providerContextTypes, 47 | ...injectContextTypes, 48 | }; 49 | static displayName = `InjectStripe(${WrappedComponent.displayName || 50 | WrappedComponent.name || 51 | 'Component'})`; 52 | 53 | constructor(props: Props, context: Context) { 54 | if (!context || !context.getRegisteredElements) { 55 | throw new Error( 56 | `It looks like you are trying to inject Stripe context outside of an Elements context. 57 | Please be sure the component that calls createSource or createToken is within an component.` 58 | ); 59 | } 60 | 61 | super(props, context); 62 | 63 | if (this.context.tag === 'sync') { 64 | this.state = { 65 | stripe: this.stripeProps(this.context.stripe), 66 | }; 67 | } else { 68 | this.state = { 69 | stripe: null, 70 | }; 71 | } 72 | } 73 | 74 | componentDidMount() { 75 | if (this.context.tag === 'async') { 76 | this.context.addStripeLoadListener((stripe: StripeShape) => { 77 | this.setState({ 78 | stripe: this.stripeProps(stripe), 79 | }); 80 | }); 81 | } else { 82 | // when 'sync', it's already set in the constructor. 83 | } 84 | } 85 | 86 | getWrappedInstance() { 87 | if (!withRef) { 88 | throw new Error( 89 | 'To access the wrapped instance, the `{withRef: true}` option must be set when calling `injectStripe()`' 90 | ); 91 | } 92 | return this.wrappedInstance; 93 | } 94 | 95 | context: Context; 96 | wrappedInstance: ?React.Component; 97 | 98 | stripeProps(stripe: StripeShape): WrappedStripeShape { 99 | return { 100 | ...stripe, 101 | // These are the only functions that take elements. 102 | createToken: this.wrappedCreateToken(stripe), 103 | createSource: this.wrappedCreateSource(stripe), 104 | createPaymentMethod: this.wrappedCreatePaymentMethod(stripe), 105 | handleCardPayment: this.wrappedHandleCardX(stripe, 'handleCardPayment'), 106 | handleCardSetup: this.wrappedHandleCardX(stripe, 'handleCardSetup'), 107 | }; 108 | } 109 | 110 | parseElementOrData = (elementOrOptions: any) => 111 | elementOrOptions && 112 | typeof elementOrOptions === 'object' && 113 | elementOrOptions._frame && 114 | typeof elementOrOptions._frame === 'object' && 115 | elementOrOptions._frame.id && 116 | typeof elementOrOptions._frame.id === 'string' && 117 | typeof elementOrOptions._componentName === 'string' 118 | ? {type: 'element', element: (elementOrOptions: ElementShape)} 119 | : {type: 'data', data: (elementOrOptions: mixed)}; 120 | 121 | // Finds an Element by the specified type, if one exists. 122 | // Throws if multiple Elements match. 123 | findElement = ( 124 | filterBy: 125 | | 'impliedTokenType' 126 | | 'impliedSourceType' 127 | | 'impliedPaymentMethodType', 128 | specifiedType: string 129 | ): ?ElementShape => { 130 | const allElements = this.context.getRegisteredElements(); 131 | const filteredElements = allElements.filter((e) => e[filterBy]); 132 | const matchingElements = 133 | specifiedType === 'auto' 134 | ? filteredElements 135 | : filteredElements.filter((e) => e[filterBy] === specifiedType); 136 | 137 | if (matchingElements.length === 1) { 138 | return matchingElements[0].element; 139 | } else if (matchingElements.length > 1) { 140 | throw new Error( 141 | `You did not specify the type of Source, Token, or PaymentMethod to create. 142 | We could not infer which Element you want to use for this operation.` 143 | ); 144 | } else { 145 | return null; 146 | } 147 | }; 148 | 149 | // Require that exactly one Element is found for the specified type. 150 | // Throws if no Element is found. 151 | requireElement = ( 152 | filterBy: 153 | | 'impliedTokenType' 154 | | 'impliedSourceType' 155 | | 'impliedPaymentMethodType', 156 | specifiedType: string 157 | ): ElementShape => { 158 | const element = this.findElement(filterBy, specifiedType); 159 | if (element) { 160 | return element; 161 | } else { 162 | throw new Error( 163 | `You did not specify the type of Source, Token, or PaymentMethod to create. 164 | We could not infer which Element you want to use for this operation.` 165 | ); 166 | } 167 | }; 168 | 169 | // Wraps createToken in order to infer the Element that is being tokenized. 170 | wrappedCreateToken = (stripe: StripeShape) => ( 171 | tokenTypeOrOptions: mixed = {}, 172 | options: mixed = {} 173 | ) => { 174 | if (tokenTypeOrOptions && typeof tokenTypeOrOptions === 'object') { 175 | // First argument is options; infer the Element and tokenize 176 | const opts = tokenTypeOrOptions; 177 | const {type: tokenType, ...rest} = opts; 178 | const specifiedType = 179 | typeof tokenType === 'string' ? tokenType : 'auto'; 180 | // Since only options were passed in, a corresponding Element must exist 181 | // for the tokenization to succeed -- thus we call requireElement. 182 | const element = this.requireElement('impliedTokenType', specifiedType); 183 | return stripe.createToken(element, rest); 184 | } else if (typeof tokenTypeOrOptions === 'string') { 185 | // First argument is token type; tokenize with token type and options 186 | const tokenType = tokenTypeOrOptions; 187 | return stripe.createToken(tokenType, options); 188 | } else { 189 | // If a bad value was passed in for options, throw an error. 190 | throw new Error( 191 | `Invalid options passed to createToken. Expected an object, got ${typeof tokenTypeOrOptions}.` 192 | ); 193 | } 194 | }; 195 | 196 | // Wraps createSource in order to infer the Element that is being used for 197 | // source creation. 198 | wrappedCreateSource = (stripe: StripeShape) => (options: mixed = {}) => { 199 | if (options && typeof options === 'object') { 200 | if (typeof options.type !== 'string') { 201 | throw new Error( 202 | `Invalid Source type passed to createSource. Expected string, got ${typeof options.type}.` 203 | ); 204 | } 205 | 206 | const element = this.findElement('impliedSourceType', options.type); 207 | if (element) { 208 | // If an Element exists for the source type, use that to create the 209 | // corresponding source. 210 | // 211 | // NOTE: this prevents users from independently creating sources of 212 | // type `foo` if an Element that can create `foo` sources exists in 213 | // the current context. 214 | return stripe.createSource(element, options); 215 | } else { 216 | // If no Element exists for the source type, directly create a source. 217 | return stripe.createSource(options); 218 | } 219 | } else { 220 | // If a bad value was passed in for options, throw an error. 221 | throw new Error( 222 | `Invalid options passed to createSource. Expected an object, got ${typeof options}.` 223 | ); 224 | } 225 | }; 226 | 227 | // Wraps createPaymentMethod in order to infer the Element that is being 228 | // used for PaymentMethod creation. 229 | wrappedCreatePaymentMethod = (stripe: StripeShape) => ( 230 | paymentMethodType: string, 231 | elementOrData?: mixed, 232 | maybeData?: mixed 233 | ) => { 234 | if (paymentMethodType && typeof paymentMethodType === 'object') { 235 | return stripe.createPaymentMethod(paymentMethodType); 236 | } 237 | 238 | if (!paymentMethodType || typeof paymentMethodType !== 'string') { 239 | throw new Error( 240 | `Invalid PaymentMethod type passed to createPaymentMethod. Expected a string, got ${typeof paymentMethodType}.` 241 | ); 242 | } 243 | 244 | const elementOrDataResult = this.parseElementOrData(elementOrData); 245 | 246 | // Second argument is Element; use passed in Element 247 | if (elementOrDataResult.type === 'element') { 248 | const {element} = elementOrDataResult; 249 | if (maybeData) { 250 | return stripe.createPaymentMethod( 251 | paymentMethodType, 252 | element, 253 | maybeData 254 | ); 255 | } else { 256 | return stripe.createPaymentMethod(paymentMethodType, element); 257 | } 258 | } 259 | 260 | // Second argument is data or undefined; infer the Element 261 | const {data} = elementOrDataResult; 262 | const element = this.findElement( 263 | 'impliedPaymentMethodType', 264 | paymentMethodType 265 | ); 266 | 267 | if (element) { 268 | return data 269 | ? stripe.createPaymentMethod(paymentMethodType, element, data) 270 | : stripe.createPaymentMethod(paymentMethodType, element); 271 | } 272 | 273 | if (data && typeof data === 'object') { 274 | return stripe.createPaymentMethod(paymentMethodType, data); 275 | } else if (!data) { 276 | throw new Error( 277 | `Could not find an Element that can be used to create a PaymentMethod of type: ${paymentMethodType}.` 278 | ); 279 | } else { 280 | // If a bad value was passed in for data, throw an error. 281 | throw new Error( 282 | `Invalid data passed to createPaymentMethod. Expected an object, got ${typeof data}.` 283 | ); 284 | } 285 | }; 286 | 287 | wrappedHandleCardX = ( 288 | stripe: StripeShape, 289 | method: 'handleCardPayment' | 'handleCardSetup' 290 | ) => (clientSecret: mixed, elementOrData: mixed, maybeData: mixed) => { 291 | if (!clientSecret || typeof clientSecret !== 'string') { 292 | // If a bad value was passed in for clientSecret, throw an error. 293 | throw new Error( 294 | `Invalid PaymentIntent client secret passed to handleCardPayment. Expected string, got ${typeof clientSecret}.` 295 | ); 296 | } 297 | 298 | const elementOrDataResult = this.parseElementOrData(elementOrData); 299 | 300 | // Second argument is Element; handle with element 301 | if (elementOrDataResult.type === 'element') { 302 | const {element} = elementOrDataResult; 303 | if (maybeData) { 304 | return stripe[method](clientSecret, element, maybeData); 305 | } else { 306 | return stripe[method](clientSecret, element); 307 | } 308 | } 309 | 310 | // Second argument is data or undefined; see if we can find a mounted Element 311 | // that can create card PaymentMethods 312 | const {data} = elementOrDataResult; 313 | const element = this.findElement('impliedPaymentMethodType', 'card'); 314 | 315 | if (element) { 316 | // If an Element exists that can create card PaymentMethods use it. Otherwise 317 | // assume that we must be calling with data only. 318 | // 319 | // NOTE: this prevents users from using handleCard* with an existing 320 | // Source or PaymentMethod if an Element that can create card PaymentMethods 321 | // exists in the current context. 322 | if (data) { 323 | return stripe[method](clientSecret, element, data); 324 | } else { 325 | return stripe[method](clientSecret, element); 326 | } 327 | } else if (data) { 328 | // if no element exists call handleCard* directly (with data) 329 | return stripe[method](clientSecret, data); 330 | } else { 331 | // if no element exists call handleCard* directly (with only the clientSecret) 332 | return stripe[method](clientSecret); 333 | } 334 | }; 335 | 336 | render() { 337 | return ( 338 | { 345 | this.wrappedInstance = c; 346 | } 347 | : null 348 | } 349 | /> 350 | ); 351 | } 352 | }; 353 | }; 354 | 355 | export default inject; 356 | -------------------------------------------------------------------------------- /src/components/inject.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount, shallow} from 'enzyme'; 4 | 5 | import injectStripe from './inject'; 6 | 7 | describe('injectStripe()', () => { 8 | let WrappedComponent; 9 | let context; 10 | let createSource; 11 | let createToken; 12 | let createPaymentMethod; 13 | let handleCardPayment; 14 | let handleCardSetup; 15 | let elementMock; 16 | let rawElementMock; 17 | let elementsMock; 18 | 19 | // Before ALL tests (sync or async) 20 | beforeEach(() => { 21 | createSource = jest.fn(); 22 | createToken = jest.fn(); 23 | createPaymentMethod = jest.fn(); 24 | handleCardPayment = jest.fn(); 25 | handleCardSetup = jest.fn(); 26 | elementsMock = {}; 27 | rawElementMock = { 28 | _frame: { 29 | id: 'id', 30 | }, 31 | _componentName: 'name', 32 | }; 33 | elementMock = { 34 | element: { 35 | on: jest.fn(), 36 | }, 37 | impliedTokenType: 'card', 38 | impliedSourceType: 'card', 39 | impliedPaymentMethodType: 'card', 40 | }; 41 | WrappedComponent = () =>
; 42 | WrappedComponent.displayName = 'WrappedComponent'; 43 | }); 44 | 45 | describe('[sync]', () => { 46 | // Before ONLY sync tests 47 | beforeEach(() => { 48 | context = { 49 | tag: 'sync', 50 | stripe: { 51 | elements: jest.fn(), 52 | createSource, 53 | createToken, 54 | createPaymentMethod, 55 | handleCardPayment, 56 | handleCardSetup, 57 | }, 58 | elements: elementsMock, 59 | getRegisteredElements: () => [elementMock], 60 | }; 61 | }); 62 | 63 | it('sets the correct displayName', () => { 64 | expect(injectStripe(WrappedComponent).displayName).toBe( 65 | 'InjectStripe(WrappedComponent)' 66 | ); 67 | }); 68 | 69 | it("includes the original component's displayName", () => { 70 | WrappedComponent.displayName = 'foo'; 71 | expect(injectStripe(WrappedComponent).displayName).toBe( 72 | 'InjectStripe(foo)' 73 | ); 74 | }); 75 | 76 | it("falls back to the original component's name if no displayName is set", () => { 77 | WrappedComponent.displayName = undefined; 78 | expect(injectStripe(WrappedComponent).displayName).toBe( 79 | `InjectStripe(${WrappedComponent.name})` 80 | ); 81 | }); 82 | 83 | it('throws when StripeProvider is missing from ancestry', () => { 84 | // Prevent the expected console.error from react to keep the test output clean 85 | const originalConsoleError = global.console.error; 86 | global.console.error = (msg) => { 87 | if ( 88 | !msg.startsWith( 89 | 'Warning: Failed context type: The context `tag` is marked as required' 90 | ) && 91 | !msg.startsWith( 92 | 'Warning: Failed context type: The context `getRegisteredElements` is marked as required' 93 | ) 94 | ) { 95 | originalConsoleError(msg); 96 | } 97 | }; 98 | 99 | const Injected = injectStripe(WrappedComponent()); 100 | 101 | expect(() => shallow()).toThrow( 102 | /It looks like you are trying to inject Stripe context outside of an Elements context/ 103 | ); 104 | global.console.error = originalConsoleError; 105 | }); 106 | 107 | it('renders with `stripe` prop', () => { 108 | const Injected = injectStripe(WrappedComponent); 109 | 110 | const wrapper = shallow(, { 111 | context, 112 | }); 113 | 114 | const props = wrapper.props(); 115 | expect(props).toHaveProperty('stripe'); 116 | expect(props).toHaveProperty('stripe.createSource'); 117 | expect(props).toHaveProperty('stripe.createToken'); 118 | expect(props).toHaveProperty('stripe.createPaymentMethod'); 119 | expect(props).toHaveProperty('stripe.handleCardPayment'); 120 | }); 121 | 122 | it('renders with `elements` prop', () => { 123 | const Injected = injectStripe(WrappedComponent); 124 | 125 | const wrapper = shallow(, { 126 | context, 127 | }); 128 | 129 | const props = wrapper.props(); 130 | expect(props.elements).toBe(elementsMock); 131 | }); 132 | 133 | it('props.stripe.createToken calls createToken with element and empty options when called with no arguments', () => { 134 | const Injected = injectStripe(WrappedComponent); 135 | 136 | const wrapper = shallow(, { 137 | context, 138 | }); 139 | 140 | const props = wrapper.props(); 141 | props.stripe.createToken(); 142 | expect(createToken).toHaveBeenCalledWith(elementMock.element, {}); 143 | }); 144 | 145 | it('props.stripe.createToken calls createToken with element and options when called with options object', () => { 146 | const Injected = injectStripe(WrappedComponent); 147 | 148 | const wrapper = shallow(, { 149 | context, 150 | }); 151 | 152 | const props = wrapper.props(); 153 | props.stripe.createToken({foo: 'bar'}); 154 | expect(createToken).toHaveBeenCalledWith(elementMock.element, { 155 | foo: 'bar', 156 | }); 157 | }); 158 | 159 | it('props.stripe.createToken calls createToken with string as first argument and options object as second', () => { 160 | const Injected = injectStripe(WrappedComponent); 161 | 162 | const wrapper = shallow(, { 163 | context, 164 | }); 165 | 166 | const props = wrapper.props(); 167 | props.stripe.createToken('test', {foo: 'bar'}); 168 | expect(createToken).toHaveBeenCalledWith('test', {foo: 'bar'}); 169 | }); 170 | 171 | it('props.stripe.createToken throws when called with invalid options type', () => { 172 | const Injected = injectStripe(WrappedComponent); 173 | 174 | const wrapper = shallow(, { 175 | context, 176 | }); 177 | 178 | const props = wrapper.props(); 179 | expect(() => props.stripe.createToken(1)).toThrow( 180 | 'Invalid options passed to createToken. Expected an object, got number.' 181 | ); 182 | }); 183 | 184 | it('props.stripe.createToken throws when no element is in the tree', () => { 185 | const Injected = injectStripe(WrappedComponent); 186 | 187 | const wrapper = shallow(, { 188 | context: { 189 | ...context, 190 | getRegisteredElements: () => [], 191 | }, 192 | }); 193 | 194 | const props = wrapper.props(); 195 | expect(props.stripe.createToken).toThrow( 196 | /We could not infer which Element you want to use for this operation./ 197 | ); 198 | }); 199 | 200 | it('props.stripe.createSource errors when called without a type', () => { 201 | const Injected = injectStripe(WrappedComponent); 202 | 203 | const wrapper = shallow(, { 204 | context, 205 | }); 206 | 207 | const props = wrapper.props(); 208 | expect(props.stripe.createSource).toThrow(/Invalid Source type/); 209 | }); 210 | 211 | it('props.stripe.createSource calls createSource with element and type when only type is passed in', () => { 212 | const Injected = injectStripe(WrappedComponent); 213 | 214 | const wrapper = shallow(, { 215 | context, 216 | }); 217 | 218 | const props = wrapper.props(); 219 | props.stripe.createSource({type: 'card'}); 220 | expect(createSource).toHaveBeenCalledWith(elementMock.element, { 221 | type: 'card', 222 | }); 223 | }); 224 | 225 | it('props.stripe.createSource calls createSource with options', () => { 226 | const Injected = injectStripe(WrappedComponent); 227 | 228 | const wrapper = shallow(, { 229 | context, 230 | }); 231 | 232 | const props = wrapper.props(); 233 | props.stripe.createSource({type: 'card', foo: 'bar'}); 234 | expect(createSource).toHaveBeenCalledWith(elementMock.element, { 235 | type: 'card', 236 | foo: 'bar', 237 | }); 238 | }); 239 | 240 | it('props.stripe.createSource calls createSource with options when called with unknown type', () => { 241 | const Injected = injectStripe(WrappedComponent); 242 | 243 | const wrapper = shallow(, { 244 | context, 245 | }); 246 | 247 | const props = wrapper.props(); 248 | props.stripe.createSource({type: 'baz', foo: 'bar'}); 249 | expect(createSource).toHaveBeenCalledWith({type: 'baz', foo: 'bar'}); 250 | }); 251 | 252 | it('props.stripe.createSource throws when called with invalid options argument', () => { 253 | const Injected = injectStripe(WrappedComponent); 254 | 255 | const wrapper = shallow(, { 256 | context, 257 | }); 258 | 259 | const props = wrapper.props(); 260 | expect(() => props.stripe.createSource(1)).toThrow( 261 | 'Invalid options passed to createSource. Expected an object, got number.' 262 | ); 263 | }); 264 | 265 | it('props.stripe.createSource throws when called with source type that matches multiple elements', () => { 266 | const Injected = injectStripe(WrappedComponent); 267 | 268 | const wrapper = shallow(, { 269 | context: { 270 | ...context, 271 | getRegisteredElements: () => [ 272 | { 273 | element: { 274 | on: jest.fn(), 275 | }, 276 | impliedTokenType: 'card', 277 | impliedSourceType: 'card', 278 | }, 279 | { 280 | element: { 281 | on: jest.fn(), 282 | }, 283 | impliedTokenType: 'card', 284 | impliedSourceType: 'card', 285 | }, 286 | ], 287 | }, 288 | }); 289 | 290 | const props = wrapper.props(); 291 | expect(() => props.stripe.createSource({type: 'card'})).toThrow( 292 | /We could not infer which Element you want to use for this operation/ 293 | ); 294 | }); 295 | 296 | it('props.stripe.createPaymentMethod calls createPaymentMethod with element and type when only type is passed in', () => { 297 | const Injected = injectStripe(WrappedComponent); 298 | 299 | const wrapper = shallow(, { 300 | context, 301 | }); 302 | 303 | const props = wrapper.props(); 304 | props.stripe.createPaymentMethod('card'); 305 | expect(createPaymentMethod).toHaveBeenCalledWith( 306 | 'card', 307 | elementMock.element 308 | ); 309 | }); 310 | 311 | it('props.stripe.createPaymentMethod calls createPaymentMethod with data options', () => { 312 | const Injected = injectStripe(WrappedComponent); 313 | 314 | const wrapper = shallow(, { 315 | context, 316 | }); 317 | 318 | const props = wrapper.props(); 319 | props.stripe.createPaymentMethod('card', { 320 | billing_details: { 321 | name: 'Jenny Rosen', 322 | }, 323 | }); 324 | expect(createPaymentMethod).toHaveBeenCalledWith( 325 | 'card', 326 | elementMock.element, 327 | { 328 | billing_details: { 329 | name: 'Jenny Rosen', 330 | }, 331 | } 332 | ); 333 | }); 334 | 335 | it('props.stripe.createPaymentMethod calls createPaymentMethod with element from arguments when passed in', () => { 336 | const Injected = injectStripe(WrappedComponent); 337 | 338 | const wrapper = shallow(, { 339 | context, 340 | }); 341 | 342 | const props = wrapper.props(); 343 | props.stripe.createPaymentMethod('card', rawElementMock); 344 | expect(createPaymentMethod).toHaveBeenCalledWith('card', rawElementMock); 345 | }); 346 | 347 | it('props.stripe.createPaymentMethod calls createPaymentMethod with element and options from arguments when passed in', () => { 348 | const Injected = injectStripe(WrappedComponent); 349 | 350 | const wrapper = shallow(, { 351 | context, 352 | }); 353 | 354 | const props = wrapper.props(); 355 | props.stripe.createPaymentMethod('card', rawElementMock, { 356 | billing_details: { 357 | name: 'Jenny Rosen', 358 | }, 359 | }); 360 | expect(createPaymentMethod).toHaveBeenCalledWith('card', rawElementMock, { 361 | billing_details: { 362 | name: 'Jenny Rosen', 363 | }, 364 | }); 365 | }); 366 | 367 | it('props.stripe.handleCardPayment calls handleCardPayment with element and clientSecret when only clientSecret is passed in', () => { 368 | const Injected = injectStripe(WrappedComponent); 369 | 370 | const wrapper = shallow(, { 371 | context, 372 | }); 373 | 374 | const props = wrapper.props(); 375 | props.stripe.handleCardPayment('clientSecret'); 376 | expect(handleCardPayment).toHaveBeenCalledWith( 377 | 'clientSecret', 378 | elementMock.element 379 | ); 380 | }); 381 | 382 | it('props.stripe.handleCardPayment calls handleCardPayment with data options', () => { 383 | const Injected = injectStripe(WrappedComponent); 384 | 385 | const wrapper = shallow(, { 386 | context, 387 | }); 388 | 389 | const props = wrapper.props(); 390 | props.stripe.handleCardPayment('clientSecret', { 391 | billing_details: { 392 | name: 'Jenny Rosen', 393 | }, 394 | }); 395 | expect(handleCardPayment).toHaveBeenCalledWith( 396 | 'clientSecret', 397 | elementMock.element, 398 | { 399 | billing_details: { 400 | name: 'Jenny Rosen', 401 | }, 402 | } 403 | ); 404 | }); 405 | 406 | it('props.stripe.handleCardPayment calls handleCardPayment with element from arguments when passed in', () => { 407 | const Injected = injectStripe(WrappedComponent); 408 | 409 | const wrapper = shallow(, { 410 | context, 411 | }); 412 | 413 | const props = wrapper.props(); 414 | props.stripe.handleCardPayment('clientSecret', rawElementMock); 415 | expect(handleCardPayment).toHaveBeenCalledWith( 416 | 'clientSecret', 417 | rawElementMock 418 | ); 419 | }); 420 | 421 | it('props.stripe.handleCardPayment calls handleCardPayment with element and data from arguments when passed in', () => { 422 | const Injected = injectStripe(WrappedComponent); 423 | 424 | const wrapper = shallow(, { 425 | context, 426 | }); 427 | 428 | const props = wrapper.props(); 429 | props.stripe.handleCardPayment('clientSecret', rawElementMock, { 430 | billing_details: { 431 | name: 'Jenny Rosen', 432 | }, 433 | }); 434 | expect(handleCardPayment).toHaveBeenCalledWith( 435 | 'clientSecret', 436 | rawElementMock, 437 | { 438 | billing_details: { 439 | name: 'Jenny Rosen', 440 | }, 441 | } 442 | ); 443 | }); 444 | 445 | it('props.stripe.handleCardPayment calls handleCardPayment with only the clientSecret when no element is present', () => { 446 | context.getRegisteredElements = () => []; 447 | 448 | const Injected = injectStripe(WrappedComponent); 449 | 450 | const wrapper = shallow(, { 451 | context, 452 | }); 453 | 454 | const props = wrapper.props(); 455 | props.stripe.handleCardPayment('clientSecret'); 456 | expect(handleCardPayment).toHaveBeenCalledWith('clientSecret'); 457 | }); 458 | 459 | it('props.stripe.handleCardSetup calls handleCardSetup with element and clientSecret when only clientSecret is passed in', () => { 460 | const Injected = injectStripe(WrappedComponent); 461 | 462 | const wrapper = shallow(, { 463 | context, 464 | }); 465 | 466 | const props = wrapper.props(); 467 | props.stripe.handleCardSetup('clientSecret'); 468 | expect(handleCardSetup).toHaveBeenCalledWith( 469 | 'clientSecret', 470 | elementMock.element 471 | ); 472 | }); 473 | 474 | it('props.stripe.handleCardSetup calls handleCardSetup with data options', () => { 475 | const Injected = injectStripe(WrappedComponent); 476 | 477 | const wrapper = shallow(, { 478 | context, 479 | }); 480 | 481 | const props = wrapper.props(); 482 | props.stripe.handleCardSetup('clientSecret', { 483 | billing_details: { 484 | name: 'Jenny Rosen', 485 | }, 486 | }); 487 | expect(handleCardSetup).toHaveBeenCalledWith( 488 | 'clientSecret', 489 | elementMock.element, 490 | { 491 | billing_details: { 492 | name: 'Jenny Rosen', 493 | }, 494 | } 495 | ); 496 | }); 497 | 498 | it('props.stripe.handleCardSetup calls handleCardSetup with element from arguments when passed in', () => { 499 | const Injected = injectStripe(WrappedComponent); 500 | 501 | const wrapper = shallow(, { 502 | context, 503 | }); 504 | 505 | const props = wrapper.props(); 506 | props.stripe.handleCardSetup('clientSecret', rawElementMock); 507 | expect(handleCardSetup).toHaveBeenCalledWith( 508 | 'clientSecret', 509 | rawElementMock 510 | ); 511 | }); 512 | 513 | it('props.stripe.handleCardSetup calls handleCardSetup with element and data from arguments when passed in', () => { 514 | const Injected = injectStripe(WrappedComponent); 515 | 516 | const wrapper = shallow(, { 517 | context, 518 | }); 519 | 520 | const props = wrapper.props(); 521 | props.stripe.handleCardSetup('clientSecret', rawElementMock, { 522 | billing_details: { 523 | name: 'Jenny Rosen', 524 | }, 525 | }); 526 | expect(handleCardSetup).toHaveBeenCalledWith( 527 | 'clientSecret', 528 | rawElementMock, 529 | { 530 | billing_details: { 531 | name: 'Jenny Rosen', 532 | }, 533 | } 534 | ); 535 | }); 536 | 537 | it('props.stripe.handleCardSetup calls handleCardSetup with only the clientSecret when no element is present', () => { 538 | context.getRegisteredElements = () => []; 539 | 540 | const Injected = injectStripe(WrappedComponent); 541 | 542 | const wrapper = shallow(, { 543 | context, 544 | }); 545 | 546 | const props = wrapper.props(); 547 | props.stripe.handleCardSetup('clientSecret'); 548 | expect(handleCardSetup).toHaveBeenCalledWith('clientSecret'); 549 | }); 550 | 551 | it('throws when `getWrappedInstance` is called without `{withRef: true}` option.', () => { 552 | const Injected = injectStripe(WrappedComponent); 553 | 554 | const wrapper = mount(, { 555 | context, 556 | }); 557 | 558 | expect(() => wrapper.instance().getWrappedInstance()).toThrow( 559 | 'To access the wrapped instance, the `{withRef: true}` option must be set when calling `injectStripe()`' 560 | ); 561 | }); 562 | 563 | it('`getWrappedInstance` works whith `{withRef: true}` option.', () => { 564 | // refs won't work with stateless functional components 565 | class WrappedClassComponent extends React.Component { 566 | static displayName = 'WrappedClassComponent'; 567 | foo: 'bar'; 568 | render() { 569 | return
{this.foo}
; 570 | } 571 | } 572 | 573 | const Injected = injectStripe(WrappedClassComponent, {withRef: true}); 574 | 575 | const wrapper = mount(, { 576 | context, 577 | }); 578 | 579 | expect( 580 | wrapper.instance().getWrappedInstance() instanceof WrappedClassComponent 581 | ).toBe(true); 582 | }); 583 | }); 584 | 585 | describe('[async]', () => { 586 | it('props.stripe is null if addStripeLoadListener never fires', () => { 587 | const Injected = injectStripe(WrappedComponent); 588 | const wrapper = mount(, { 589 | context: { 590 | tag: 'async', 591 | // simulate StripeProvider never giving us a StripeShape 592 | addStripeLoadListener: () => {}, 593 | getRegisteredElements: () => [elementMock], 594 | }, 595 | }); 596 | 597 | const props = wrapper.find(WrappedComponent).props(); 598 | expect(props).toHaveProperty('stripe', null); 599 | }); 600 | 601 | it('props.stripe is set when addStripeLoadListener fires', () => { 602 | const Injected = injectStripe(WrappedComponent); 603 | const wrapper = mount(, { 604 | context: { 605 | tag: 'async', 606 | // simulate StripeProvider eventually giving us a StripeShape 607 | addStripeLoadListener: (fn) => { 608 | fn({ 609 | elements: jest.fn(), 610 | createSource, 611 | createToken, 612 | createPaymentMethod, 613 | handleCardPayment, 614 | handleCardSetup, 615 | }); 616 | }, 617 | getRegisteredElements: () => [elementMock], 618 | }, 619 | }); 620 | 621 | const props = wrapper.find(WrappedComponent).props(); 622 | expect(props).toHaveProperty('stripe'); 623 | expect(props).toHaveProperty('stripe.createToken'); 624 | expect(props).toHaveProperty('stripe.createSource'); 625 | expect(props).toHaveProperty('stripe.createPaymentMethod'); 626 | expect(props).toHaveProperty('stripe.handleCardPayment'); 627 | expect(props).toHaveProperty('stripe.handleCardSetup'); 628 | }); 629 | }); 630 | }); 631 | -------------------------------------------------------------------------------- /src/decls/Stripe.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* global StripeShape:false, ElementsShape:false, ElementShape:false */ 3 | // ^For https://github.com/gajus/eslint-plugin-flowtype/issues/84 4 | 5 | type MixedObject = {[string]: mixed}; 6 | 7 | declare type ElementShape = { 8 | mount: Function, 9 | destroy: () => ElementShape, 10 | on: (event: string, handler: Function) => ElementShape, 11 | update: (options: MixedObject) => ElementShape, 12 | }; 13 | 14 | declare type ElementsShape = { 15 | create: (type: string, options: MixedObject) => ElementShape, 16 | getElement: (type: string) => null | ElementShape, 17 | }; 18 | 19 | type ConfirmSetupFn = ( 20 | clientSecret: string, 21 | options?: mixed 22 | ) => Promise<{setupIntent?: MixedObject, error?: MixedObject}>; 23 | 24 | type ConfirmPaymentFn = ( 25 | clientSecret: string, 26 | options?: mixed 27 | ) => Promise<{paymentIntent?: MixedObject, error?: MixedObject}>; 28 | 29 | declare type StripeShape = { 30 | elements: (options: MixedObject) => ElementsShape, 31 | createSource: ( 32 | element: ElementShape | MixedObject, 33 | options: ?{} 34 | ) => Promise<{source?: MixedObject, error?: MixedObject}>, 35 | createToken: ( 36 | type: string | ElementShape, 37 | options: mixed 38 | ) => Promise<{token?: MixedObject, error?: MixedObject}>, 39 | createPaymentMethod: ( 40 | type: mixed, 41 | element?: ElementShape | MixedObject, 42 | data?: mixed 43 | ) => Promise<{paymentMethod?: MixedObject, error?: MixedObject}>, 44 | handleCardPayment: ( 45 | clientSecret: string, 46 | element: mixed, 47 | options: mixed 48 | ) => Promise<{paymentIntent?: MixedObject, error?: MixedObject}>, 49 | handleCardSetup: ( 50 | clientSecret: string, 51 | element: mixed, 52 | options: mixed 53 | ) => Promise<{setupIntent?: MixedObject, error?: MixedObject}>, 54 | confirmCardPayment: ConfirmPaymentFn, 55 | confirmCardSetup: ConfirmSetupFn, 56 | confirmIdealPayment: ConfirmPaymentFn, 57 | confirmSepaDebitPayment: ConfirmPaymentFn, 58 | confirmSepaDebitSetup: ConfirmSetupFn, 59 | _registerWrapper: (wrapper: {| 60 | name: string, 61 | version: string | null, 62 | |}) => void, 63 | }; 64 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import StripeProvider from './components/Provider'; 3 | import injectStripe from './components/inject'; 4 | import Elements from './components/Elements'; 5 | import Element from './components/Element'; 6 | import PaymentRequestButtonElement from './components/PaymentRequestButtonElement'; 7 | 8 | // Define Elements, and register their implied token / source types for 9 | // automatic token / source creation. 10 | 11 | // Card 12 | const CardElement = Element('card', { 13 | impliedTokenType: 'card', 14 | impliedSourceType: 'card', 15 | impliedPaymentMethodType: 'card', 16 | }); 17 | 18 | // Split Fields 19 | // Note: we only register the CardNumberElement for split fields so that we have 20 | // a unique Element to infer when calling `wrappedCreateToken` or `wrappedCreateSource`. 21 | const CardNumberElement = Element('cardNumber', { 22 | impliedTokenType: 'card', 23 | impliedSourceType: 'card', 24 | impliedPaymentMethodType: 'card', 25 | }); 26 | const CardExpiryElement = Element('cardExpiry'); 27 | const CardCvcElement = Element('cardCvc'); 28 | const CardCVCElement = CardCvcElement; // deprecated in favor of CardCvcElement which better matches Elements API 29 | 30 | // IBAN 31 | const IbanElement = Element('iban', { 32 | impliedTokenType: 'bank_account', 33 | impliedSourceType: 'sepa_debit', 34 | }); 35 | 36 | // iDEAL Bank 37 | const IdealBankElement = Element('idealBank', {impliedSourceType: 'ideal'}); 38 | 39 | // fpx Bank 40 | const FpxBankElement = Element('fpxBank'); 41 | 42 | // auBankAccount 43 | // Requires beta access. Contact Stripe support for more information: 44 | // https://support.stripe.com 45 | const AuBankAccountElement = Element('auBankAccount'); 46 | 47 | export { 48 | StripeProvider, 49 | injectStripe, 50 | Elements, 51 | CardElement, 52 | CardNumberElement, 53 | CardExpiryElement, 54 | CardCvcElement, 55 | CardCVCElement, 56 | PaymentRequestButtonElement, 57 | IbanElement, 58 | IdealBankElement, 59 | FpxBankElement, 60 | AuBankAccountElement, 61 | }; 62 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import React from 'react'; 3 | import {mount} from 'enzyme'; 4 | 5 | import { 6 | StripeProvider, 7 | Elements, 8 | injectStripe, 9 | CardElement, 10 | CardNumberElement, 11 | CardExpiryElement, 12 | CardCVCElement, 13 | IbanElement, 14 | } from './index'; 15 | 16 | class PureWrapper extends React.PureComponent { 17 | render() { 18 | return
{this.props.children}
; 19 | } 20 | } 21 | 22 | describe('index', () => { 23 | let elementMock; 24 | let elementsMock; 25 | let stripeMock; 26 | let rawElementMock; 27 | 28 | beforeEach(() => { 29 | elementMock = { 30 | mount: jest.fn(), 31 | destroy: jest.fn(), 32 | on: jest.fn(), 33 | update: jest.fn(), 34 | }; 35 | elementsMock = { 36 | create: jest.fn().mockReturnValue(elementMock), 37 | }; 38 | rawElementMock = { 39 | _frame: { 40 | id: 'id', 41 | }, 42 | _componentName: 'name', 43 | }; 44 | stripeMock = { 45 | elements: jest.fn().mockReturnValue(elementsMock), 46 | createToken: jest.fn(), 47 | createSource: jest.fn(), 48 | createPaymentMethod: jest.fn(), 49 | handleCardPayment: jest.fn(), 50 | handleCardSetup: jest.fn(), 51 | }; 52 | 53 | // jest.spyOn(console, 'error'); 54 | // console.error.mockImplementation(() => {}); 55 | 56 | window.Stripe = jest.fn().mockReturnValue(stripeMock); 57 | }); 58 | 59 | afterEach(() => { 60 | // console.error.mockRestore(); 61 | }); 62 | 63 | const WrappedCheckout = (onSubmit) => { 64 | const MyCheckout = (props) => { 65 | return ( 66 | { 68 | ev.preventDefault(); 69 | onSubmit(props); 70 | }} 71 | > 72 | {props.children} 73 | 74 | 75 | ); 76 | }; 77 | return injectStripe(MyCheckout); 78 | }; 79 | 80 | it('smoke test', () => { 81 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 82 | const app = mount( 83 | 84 | 85 | 86 | Hello world 87 | 88 | 89 | 90 | 91 | ); 92 | expect(app.text()).toMatch(/Hello world/); 93 | }); 94 | 95 | it("shouldn't choke on pure components", () => { 96 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 97 | const app = mount( 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | 109 | // Prevent the console.errors to keep the test output clean 110 | jest.spyOn(console, 'error'); 111 | console.error.mockImplementation(() => {}); 112 | 113 | expect(() => app.find('form').simulate('submit')).not.toThrow(); 114 | 115 | console.error.mockRestore(); 116 | }); 117 | 118 | describe('createToken', () => { 119 | it('should be called when set up properly', () => { 120 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 121 | const app = mount( 122 | 123 | 124 | 125 | Hello world 126 | 127 | 128 | 129 | 130 | ); 131 | app.find('form').simulate('submit'); 132 | expect(stripeMock.createToken).toHaveBeenCalledTimes(1); 133 | expect(stripeMock.createToken).toHaveBeenCalledWith(elementMock, {}); 134 | }); 135 | 136 | it('should be called when set up properly (split)', () => { 137 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 138 | const app = mount( 139 | 140 | 141 | 142 | Hello world 143 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | app.find('form').simulate('submit'); 151 | expect(stripeMock.createToken).toHaveBeenCalledTimes(1); 152 | expect(stripeMock.createToken).toHaveBeenCalledWith(elementMock, {}); 153 | }); 154 | 155 | it('should be callable for other token types', () => { 156 | const Checkout = WrappedCheckout((props) => { 157 | props.stripe.createToken('bank_account', {some: 'data'}); 158 | }); 159 | const app = mount( 160 | 161 | 162 | Hello world 163 | 164 | 165 | ); 166 | app.find('form').simulate('submit'); 167 | expect(stripeMock.createToken).toHaveBeenCalledTimes(1); 168 | expect(stripeMock.createToken).toHaveBeenCalledWith('bank_account', { 169 | some: 'data', 170 | }); 171 | }); 172 | }); 173 | 174 | describe('createSource', () => { 175 | it('should be called when set up properly', () => { 176 | const Checkout = WrappedCheckout((props) => 177 | props.stripe.createSource({type: 'card'}) 178 | ); 179 | const app = mount( 180 | 181 | 182 | 183 | Hello world 184 | 185 | 186 | 187 | 188 | ); 189 | app.find('form').simulate('submit'); 190 | expect(stripeMock.createSource).toHaveBeenCalledTimes(1); 191 | expect(stripeMock.createSource).toHaveBeenCalledWith(elementMock, { 192 | type: 'card', 193 | }); 194 | }); 195 | 196 | it('should take additional parameters', () => { 197 | const Checkout = WrappedCheckout((props) => 198 | props.stripe.createSource({ 199 | type: 'card', 200 | owner: {name: 'Michelle'}, 201 | }) 202 | ); 203 | const app = mount( 204 | 205 | 206 | 207 | Hello world 208 | 209 | 210 | 211 | 212 | ); 213 | app.find('form').simulate('submit'); 214 | expect(stripeMock.createSource).toHaveBeenCalledTimes(1); 215 | expect(stripeMock.createSource).toHaveBeenCalledWith(elementMock, { 216 | type: 'card', 217 | owner: {name: 'Michelle'}, 218 | }); 219 | }); 220 | 221 | it('should be callable when no Element is found', () => { 222 | const Checkout = WrappedCheckout((props) => 223 | props.stripe.createSource({ 224 | type: 'card', 225 | token: 'tok_xxx', 226 | }) 227 | ); 228 | const app = mount( 229 | 230 | 231 | Hello world 232 | 233 | 234 | ); 235 | app.find('form').simulate('submit'); 236 | expect(stripeMock.createSource).toHaveBeenCalledTimes(1); 237 | expect(stripeMock.createSource).toHaveBeenCalledWith({ 238 | type: 'card', 239 | token: 'tok_xxx', 240 | }); 241 | }); 242 | 243 | it('should be callable for other source types', () => { 244 | const Checkout = WrappedCheckout((props) => 245 | props.stripe.createSource({ 246 | type: 'three_d_secure', 247 | three_d_secure: {foo: 'bar'}, 248 | }) 249 | ); 250 | 251 | const app = mount( 252 | 253 | 254 | Hello world 255 | 256 | 257 | ); 258 | app.find('form').simulate('submit'); 259 | expect(stripeMock.createSource).toHaveBeenCalledTimes(1); 260 | expect(stripeMock.createSource).toHaveBeenCalledWith({ 261 | type: 'three_d_secure', 262 | three_d_secure: {foo: 'bar'}, 263 | }); 264 | }); 265 | }); 266 | 267 | describe('createPaymentMethod', () => { 268 | it('should be called when set up properly', () => { 269 | const Checkout = WrappedCheckout((props) => 270 | props.stripe.createPaymentMethod('card') 271 | ); 272 | const app = mount( 273 | 274 | 275 | 276 | Hello world 277 | 278 | 279 | 280 | 281 | ); 282 | app.find('form').simulate('submit'); 283 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledTimes(1); 284 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledWith( 285 | 'card', 286 | elementMock 287 | ); 288 | }); 289 | 290 | it('should take additional parameters', () => { 291 | const Checkout = WrappedCheckout((props) => 292 | props.stripe.createPaymentMethod('card', { 293 | billing_details: {name: 'Michelle'}, 294 | }) 295 | ); 296 | const app = mount( 297 | 298 | 299 | 300 | Hello world 301 | 302 | 303 | 304 | 305 | ); 306 | app.find('form').simulate('submit'); 307 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledTimes(1); 308 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledWith( 309 | 'card', 310 | elementMock, 311 | { 312 | billing_details: {name: 'Michelle'}, 313 | } 314 | ); 315 | }); 316 | 317 | it('should be callable when no Element is found', () => { 318 | const Checkout = WrappedCheckout((props) => 319 | props.stripe.createPaymentMethod('card', { 320 | token: 'tok_xxx', 321 | }) 322 | ); 323 | const app = mount( 324 | 325 | 326 | Hello world 327 | 328 | 329 | ); 330 | app.find('form').simulate('submit'); 331 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledTimes(1); 332 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledWith('card', { 333 | token: 'tok_xxx', 334 | }); 335 | }); 336 | 337 | it('should be callable when an Element is passed in', () => { 338 | const Checkout = WrappedCheckout((props) => 339 | props.stripe.createPaymentMethod('card', rawElementMock, { 340 | billing_details: {name: 'David'}, 341 | }) 342 | ); 343 | const app = mount( 344 | 345 | 346 | 347 | Hello world 348 | 349 | 350 | 351 | 352 | ); 353 | app.find('form').simulate('submit'); 354 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledTimes(1); 355 | expect(stripeMock.createPaymentMethod).toHaveBeenCalledWith( 356 | 'card', 357 | rawElementMock, 358 | { 359 | billing_details: {name: 'David'}, 360 | } 361 | ); 362 | }); 363 | }); 364 | 365 | describe('handleCardPayment', () => { 366 | it('should be called when set up properly', () => { 367 | const Checkout = WrappedCheckout((props) => 368 | props.stripe.handleCardPayment('client_secret') 369 | ); 370 | const app = mount( 371 | 372 | 373 | 374 | Hello world 375 | 376 | 377 | 378 | 379 | ); 380 | app.find('form').simulate('submit'); 381 | expect(stripeMock.handleCardPayment).toHaveBeenCalledTimes(1); 382 | expect(stripeMock.handleCardPayment).toHaveBeenCalledWith( 383 | 'client_secret', 384 | elementMock 385 | ); 386 | }); 387 | 388 | it('should take additional parameters', () => { 389 | const Checkout = WrappedCheckout((props) => 390 | props.stripe.handleCardPayment('client_secret', { 391 | billing_details: {name: 'Michelle'}, 392 | }) 393 | ); 394 | const app = mount( 395 | 396 | 397 | 398 | Hello world 399 | 400 | 401 | 402 | 403 | ); 404 | app.find('form').simulate('submit'); 405 | expect(stripeMock.handleCardPayment).toHaveBeenCalledTimes(1); 406 | expect(stripeMock.handleCardPayment).toHaveBeenCalledWith( 407 | 'client_secret', 408 | elementMock, 409 | { 410 | billing_details: {name: 'Michelle'}, 411 | } 412 | ); 413 | }); 414 | 415 | it('should be callable when no Element is found', () => { 416 | const Checkout = WrappedCheckout((props) => 417 | props.stripe.handleCardPayment('client_secret', { 418 | payment_method: 'pm_xxx', 419 | }) 420 | ); 421 | const app = mount( 422 | 423 | 424 | Hello world 425 | 426 | 427 | ); 428 | app.find('form').simulate('submit'); 429 | expect(stripeMock.handleCardPayment).toHaveBeenCalledTimes(1); 430 | expect(stripeMock.handleCardPayment).toHaveBeenCalledWith( 431 | 'client_secret', 432 | { 433 | payment_method: 'pm_xxx', 434 | } 435 | ); 436 | }); 437 | 438 | it('should be callable when an Element is passed in', () => { 439 | const Checkout = WrappedCheckout((props) => 440 | props.stripe.handleCardPayment('client_secret', rawElementMock, { 441 | billing_details: {name: 'David'}, 442 | }) 443 | ); 444 | const app = mount( 445 | 446 | 447 | 448 | Hello world 449 | 450 | 451 | 452 | 453 | ); 454 | app.find('form').simulate('submit'); 455 | expect(stripeMock.handleCardPayment).toHaveBeenCalledTimes(1); 456 | expect(stripeMock.handleCardPayment).toHaveBeenCalledWith( 457 | 'client_secret', 458 | rawElementMock, 459 | { 460 | billing_details: {name: 'David'}, 461 | } 462 | ); 463 | }); 464 | }); 465 | 466 | describe('handleCardSetup', () => { 467 | it('should be called when set up properly', () => { 468 | const Checkout = WrappedCheckout((props) => 469 | props.stripe.handleCardSetup('client_secret') 470 | ); 471 | const app = mount( 472 | 473 | 474 | 475 | Hello world 476 | 477 | 478 | 479 | 480 | ); 481 | app.find('form').simulate('submit'); 482 | expect(stripeMock.handleCardSetup).toHaveBeenCalledTimes(1); 483 | expect(stripeMock.handleCardSetup).toHaveBeenCalledWith( 484 | 'client_secret', 485 | elementMock 486 | ); 487 | }); 488 | 489 | it('should take additional parameters', () => { 490 | const Checkout = WrappedCheckout((props) => 491 | props.stripe.handleCardSetup('client_secret', { 492 | billing_details: {name: 'Michelle'}, 493 | }) 494 | ); 495 | const app = mount( 496 | 497 | 498 | 499 | Hello world 500 | 501 | 502 | 503 | 504 | ); 505 | app.find('form').simulate('submit'); 506 | expect(stripeMock.handleCardSetup).toHaveBeenCalledTimes(1); 507 | expect(stripeMock.handleCardSetup).toHaveBeenCalledWith( 508 | 'client_secret', 509 | elementMock, 510 | { 511 | billing_details: {name: 'Michelle'}, 512 | } 513 | ); 514 | }); 515 | 516 | it('should be callable when no Element is found', () => { 517 | const Checkout = WrappedCheckout((props) => 518 | props.stripe.handleCardSetup('client_secret', { 519 | payment_method: 'pm_xxx', 520 | }) 521 | ); 522 | const app = mount( 523 | 524 | 525 | Hello world 526 | 527 | 528 | ); 529 | app.find('form').simulate('submit'); 530 | expect(stripeMock.handleCardSetup).toHaveBeenCalledTimes(1); 531 | expect(stripeMock.handleCardSetup).toHaveBeenCalledWith('client_secret', { 532 | payment_method: 'pm_xxx', 533 | }); 534 | }); 535 | 536 | it('should be callable when an Element is passed in', () => { 537 | const Checkout = WrappedCheckout((props) => 538 | props.stripe.handleCardSetup('client_secret', rawElementMock, { 539 | billing_details: {name: 'David'}, 540 | }) 541 | ); 542 | const app = mount( 543 | 544 | 545 | 546 | Hello world 547 | 548 | 549 | 550 | 551 | ); 552 | app.find('form').simulate('submit'); 553 | expect(stripeMock.handleCardSetup).toHaveBeenCalledTimes(1); 554 | expect(stripeMock.handleCardSetup).toHaveBeenCalledWith( 555 | 'client_secret', 556 | rawElementMock, 557 | { 558 | billing_details: {name: 'David'}, 559 | } 560 | ); 561 | }); 562 | }); 563 | 564 | describe('errors', () => { 565 | beforeEach(() => { 566 | // Prevent the console.error to keep the test output clean 567 | jest.spyOn(console, 'error'); 568 | console.error.mockImplementation(() => {}); 569 | }); 570 | 571 | afterEach(() => { 572 | console.error.mockRestore(); 573 | }); 574 | 575 | describe('createSource', () => { 576 | it('should throw if no source type is specified', () => { 577 | const Checkout = WrappedCheckout((props) => 578 | props.stripe.createSource({}) 579 | ); 580 | const app = mount( 581 | 582 | 583 | 584 | Hello world 585 | 586 | 587 | 588 | 589 | ); 590 | expect(() => app.find('form').simulate('submit')).toThrowError( 591 | /Invalid Source type/ 592 | ); 593 | }); 594 | }); 595 | 596 | describe('createPaymentMethod', () => { 597 | it('should throw if no PaymentMethod type is specified', () => { 598 | const Checkout = WrappedCheckout((props) => 599 | props.stripe.createPaymentMethod() 600 | ); 601 | const app = mount( 602 | 603 | 604 | 605 | Hello world 606 | 607 | 608 | 609 | 610 | ); 611 | expect(() => app.find('form').simulate('submit')).toThrowError( 612 | /Invalid PaymentMethod type/ 613 | ); 614 | }); 615 | 616 | it('should throw if no element corresponding to Payment Method type is found', () => { 617 | const Checkout = WrappedCheckout((props) => 618 | props.stripe.createPaymentMethod('card') 619 | ); 620 | const app = mount( 621 | 622 | 623 | 624 | Hello world 625 | 626 | 627 | 628 | 629 | ); 630 | expect(() => app.find('form').simulate('submit')).toThrowError( 631 | /Could not find an Element/ 632 | ); 633 | }); 634 | }); 635 | 636 | describe('handleCardPayment', () => { 637 | it('should throw if no client secret is specified', () => { 638 | const Checkout = WrappedCheckout((props) => 639 | props.stripe.handleCardPayment() 640 | ); 641 | const app = mount( 642 | 643 | 644 | 645 | Hello world 646 | 647 | 648 | 649 | 650 | ); 651 | expect(() => app.find('form').simulate('submit')).toThrowError( 652 | /Invalid PaymentIntent client secret/ 653 | ); 654 | }); 655 | }); 656 | 657 | describe('createToken', () => { 658 | it('should throw when not in Elements', () => { 659 | // Prevent the expected console.error from react to keep the test output clean 660 | const originalConsoleError = global.console.error; 661 | global.console.error = (msg) => { 662 | if ( 663 | !msg.startsWith( 664 | 'Warning: Failed context type: The context `getRegisteredElements` is marked as required' 665 | ) 666 | ) { 667 | originalConsoleError(msg); 668 | } 669 | }; 670 | 671 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 672 | expect(() => 673 | mount( 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | ) 682 | ).toThrowError('Elements context'); 683 | 684 | global.console.error = originalConsoleError; 685 | }); 686 | 687 | it('should throw when no Element found', () => { 688 | const Checkout = WrappedCheckout((props) => props.stripe.createToken()); 689 | const app = mount( 690 | 691 | 692 | Hello world 693 | 694 | 695 | ); 696 | expect(() => app.find('form').simulate('submit')).toThrowError( 697 | /did not specify/ 698 | ); 699 | }); 700 | }); 701 | }); 702 | }); 703 | -------------------------------------------------------------------------------- /src/utils/isEqual.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const PLAIN_OBJECT_STR = '[object Object]'; 3 | 4 | const isEqual = (left: mixed, right: mixed): boolean => { 5 | if (typeof left !== 'object' || typeof right !== 'object') { 6 | return left === right; 7 | } 8 | 9 | if (left === null || right === null) return left === right; 10 | 11 | const leftArray = Array.isArray(left); 12 | const rightArray = Array.isArray(right); 13 | 14 | if (leftArray !== rightArray) return false; 15 | 16 | const leftPlainObject = 17 | Object.prototype.toString.call(left) === PLAIN_OBJECT_STR; 18 | const rightPlainObject = 19 | Object.prototype.toString.call(right) === PLAIN_OBJECT_STR; 20 | 21 | if (leftPlainObject !== rightPlainObject) return false; 22 | 23 | if (!leftPlainObject && !leftArray) return false; 24 | 25 | const leftKeys = Object.keys(left); 26 | const rightKeys = Object.keys(right); 27 | 28 | if (leftKeys.length !== rightKeys.length) return false; 29 | 30 | const keySet = {}; 31 | for (let i = 0; i < leftKeys.length; i += 1) { 32 | keySet[leftKeys[i]] = true; 33 | } 34 | for (let i = 0; i < rightKeys.length; i += 1) { 35 | keySet[rightKeys[i]] = true; 36 | } 37 | const allKeys = Object.keys(keySet); 38 | if (allKeys.length !== leftKeys.length) { 39 | return false; 40 | } 41 | 42 | const l = left; 43 | const r = right; 44 | const pred = (key) => { 45 | return isEqual(l[key], r[key]); 46 | }; 47 | 48 | return allKeys.every(pred); 49 | }; 50 | 51 | export default isEqual; 52 | -------------------------------------------------------------------------------- /src/utils/isEqual.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import isEqual from './isEqual'; 3 | 4 | describe('isEqual', () => { 5 | [ 6 | ['a', 'a'], 7 | [100, 100], 8 | [false, false], 9 | [undefined, undefined], 10 | [null, null], 11 | [{}, {}], 12 | [{a: 10}, {a: 10}], 13 | [{a: null}, {a: null}], 14 | [{a: undefined}, {a: undefined}], 15 | [[], []], 16 | [['a', 'b', 'c'], ['a', 'b', 'c']], 17 | [['a', {inner: [12]}, 'c'], ['a', {inner: [12]}, 'c']], 18 | [{a: {nested: {more: [1, 2, 3]}}}, {a: {nested: {more: [1, 2, 3]}}}], 19 | ].forEach(([left, right]) => { 20 | it(`should should return true for isEqual(${JSON.stringify( 21 | left 22 | )}, ${JSON.stringify(right)})`, () => { 23 | expect(isEqual(left, right)).toBe(true); 24 | expect(isEqual(right, left)).toBe(true); 25 | }); 26 | }); 27 | 28 | [ 29 | ['a', 'b'], 30 | ['0', 0], 31 | [new Date(1), {}], 32 | [false, ''], 33 | [false, true], 34 | [null, undefined], 35 | [{}, []], 36 | [/foo/, /foo/], 37 | [new Date(1), new Date(1)], 38 | [{a: 10}, {a: 11}], 39 | [['a', 'b', 'c'], ['a', 'b', 'c', 'd']], 40 | [['a', 'b', 'c', 'd'], ['a', 'b', 'c']], 41 | [['a', {inner: [12]}, 'c'], ['a', {inner: [null]}, 'c']], 42 | [{a: {nested: {more: [1, 2, 3]}}}, {b: {nested: {more: [1, 2, 3]}}}], 43 | ].forEach(([left, right]) => { 44 | it(`should should return false for isEqual(${JSON.stringify( 45 | left 46 | )}, ${JSON.stringify(right)})`, () => { 47 | expect(isEqual(left, right)).toBe(false); 48 | expect(isEqual(right, left)).toBe(false); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/shallowEqual.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const shallowEqual = (a: Object, b: Object): boolean => { 4 | const keysA = Object.keys(a); 5 | const keysB = Object.keys(b); 6 | 7 | return ( 8 | keysA.length === keysB.length && 9 | keysA.every((key) => b.hasOwnProperty(key) && b[key] === a[key]) 10 | ); 11 | }; 12 | 13 | export default shallowEqual; 14 | -------------------------------------------------------------------------------- /src/utils/shallowEqual.test.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | import shallowEqual from './shallowEqual'; 3 | 4 | describe('shallowEqual', () => { 5 | it('should work', () => { 6 | expect(shallowEqual({}, {})).toBe(true); 7 | expect(shallowEqual({a: 1}, {a: 1})).toBe(true); 8 | 9 | expect(shallowEqual({a: 1, b: 2}, {a: 1})).toBe(false); 10 | expect(shallowEqual({a: {}}, {a: {}})).toBe(false); 11 | expect(shallowEqual({a: undefined}, {b: undefined})).toBe(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/setupJest.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | 3 | import {configure} from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | configure({adapter: new Adapter()}); 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | const config = { 7 | mode: 'development', 8 | module: { 9 | rules: [{test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/}], 10 | }, 11 | entry: { 12 | demo: ['./demo/demo/index.js'], 13 | async: ['./demo/async/main.js'], 14 | paymentIntents: ['./demo/intents/index.js'], 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | filename: '[name].js', 19 | }, 20 | plugins: [ 21 | new HtmlWebpackPlugin({ 22 | inject: false, 23 | filename: 'index.html', 24 | template: './demo/demo/index.html', 25 | }), 26 | new HtmlWebpackPlugin({ 27 | inject: false, 28 | filename: 'async/index.html', 29 | template: './demo/async/index.html', 30 | }), 31 | new HtmlWebpackPlugin({ 32 | inject: false, 33 | filename: 'intents/index.html', 34 | template: './demo/intents/index.html', 35 | }), 36 | ], 37 | }; 38 | 39 | module.exports = config; 40 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | // @noflow 2 | const webpack = require('webpack'); 3 | 4 | const reactExternal = { 5 | root: 'React', 6 | commonjs2: 'react', 7 | commonjs: 'react', 8 | amd: 'react', 9 | }; 10 | 11 | const config = { 12 | mode: 'production', 13 | externals: { 14 | react: reactExternal, 15 | }, 16 | module: { 17 | rules: [{test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/}], 18 | }, 19 | output: { 20 | library: 'ReactStripeElements', 21 | libraryTarget: 'umd', 22 | }, 23 | plugins: [new webpack.optimize.OccurrenceOrderPlugin()], 24 | }; 25 | 26 | module.exports = (env) => { 27 | if (env && env.noMinimize) { 28 | config.optimization = { 29 | minimize: false, 30 | }; 31 | } 32 | 33 | return config; 34 | }; 35 | --------------------------------------------------------------------------------