├── .babelrc.js ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── assets ├── basic.gif ├── bootstrap.gif ├── fannypack.gif ├── react-payment-inputs.png ├── react-payment-inputs.sketch └── wrapper.gif ├── docs ├── 3.3e7c09a6a2f7bbc36612.bundle.js ├── 3.3e7c09a6a2f7bbc36612.bundle.js.map ├── favicon.ico ├── iframe.html ├── index.html ├── main.3e7c09a6a2f7bbc36612.bundle.js ├── main.3e7c09a6a2f7bbc36612.bundle.js.map ├── main.77cad4a5274c0b554b48.bundle.js ├── runtime~main.3e7c09a6a2f7bbc36612.bundle.js ├── runtime~main.3e7c09a6a2f7bbc36612.bundle.js.map ├── runtime~main.4ac396420dd14c580a84.bundle.js ├── sb_dll │ ├── storybook_ui-manifest.json │ ├── storybook_ui_dll.LICENCE │ └── storybook_ui_dll.js ├── vendors~main.3e7c09a6a2f7bbc36612.bundle.js ├── vendors~main.3e7c09a6a2f7bbc36612.bundle.js.map └── vendors~main.e08e57400c6b35755772.bundle.js ├── nwb.config.js ├── package.json ├── rollup.config.js ├── scripts ├── create-proxies.js ├── get-modules.js └── remove-proxies.js ├── src ├── PaymentInputsContainer.js ├── PaymentInputsWrapper.js ├── images │ ├── amex.js │ ├── dinersclub.js │ ├── discover.js │ ├── hipercard.js │ ├── index.js │ ├── jcb.js │ ├── mastercard.js │ ├── placeholder.js │ ├── troy.js │ ├── unionpay.js │ └── visa.js ├── index.js ├── usePaymentInputs.js └── utils │ ├── cardTypes.js │ ├── formatter.js │ ├── index.js │ └── validator.js ├── stories └── index.stories.js ├── tests ├── .eslintrc └── index-test.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-react" 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-proposal-class-properties", 10 | "@babel/plugin-proposal-object-rest-spread", 11 | "@babel/plugin-syntax-dynamic-import", 12 | "@babel/plugin-proposal-export-namespace-from" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@medipass/react-medipass", 3 | "rules": { 4 | "react/prop-types": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | /utils/ 9 | /use-payment-inputs/ 10 | /images/ 11 | /PaymentInputsWrapper/ 12 | /PaymentInputsContainer/ 13 | /usePaymentInputs/ 14 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, ThemeProvider, css, palette } from 'fannypack'; 3 | import { configure, addDecorator } from '@storybook/react'; 4 | 5 | // automatically import all files ending in *.stories.js 6 | const req = require.context('../stories', true, /\.stories\.js$/); 7 | function loadStories() { 8 | req.keys().forEach(filename => req(filename)); 9 | } 10 | 11 | const theme = { 12 | global: { 13 | base: css` 14 | & input { 15 | font-size: 16px; 16 | } 17 | 18 | *:focus { 19 | outline: 2px solid ${palette('primary')}; 20 | outline-offset: 0px; 21 | } 22 | ` 23 | } 24 | } 25 | const Decorator = storyFn => {storyFn()}; 26 | addDecorator(Decorator); 27 | 28 | configure(loadStories, module); 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Payment Inputs 2 | 3 | > A React Hook & Container to help with payment card input fields. 4 | 5 | 6 |

7 | 8 |

9 | 10 | - [React Payment Inputs](#react-payment-inputs) 11 | - [Demos](#demos) 12 | - [Requirements](#requirements) 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [With hooks](#with-hooks) 16 | - [With render props](#with-render-props) 17 | - [Using the built-in styled wrapper](#using-the-built-in-styled-wrapper) 18 | - [More examples](#more-examples) 19 | - [`data = usePaymentInputs(options)`](#data--usepaymentinputsoptions) 20 | - [options](#options) 21 | - [options.cardNumberValidator](#optionscardnumbervalidator) 22 | - [Example](#example) 23 | - [options.cvcValidator](#optionscvcvalidator) 24 | - [options.errorMessages](#optionserrormessages) 25 | - [Example](#example-1) 26 | - [options.expiryDateValidator](#optionsexpirydatevalidator) 27 | - [options.onBlur](#optionsonblur) 28 | - [options.onChange](#optionsonchange) 29 | - [options.onError](#optionsonerror) 30 | - [options.onTouch](#optionsontouch) 31 | - [`data`](#data) 32 | - [getCardNumberProps](#getcardnumberprops) 33 | - [Example snippet](#example-snippet) 34 | - [getExpiryDateProps](#getexpirydateprops) 35 | - [Example snippet](#example-snippet-1) 36 | - [getCVCProps](#getcvcprops) 37 | - [Example snippet](#example-snippet-2) 38 | - [getZIPProps](#getzipprops) 39 | - [Example snippet](#example-snippet-3) 40 | - [getCardImageProps](#getcardimageprops) 41 | - [Example snippet](#example-snippet-4) 42 | - [meta.cardType](#metacardtype) 43 | - [Example snippet](#example-snippet-5) 44 | - [meta.error](#metaerror) 45 | - [Example snippet](#example-snippet-6) 46 | - [meta.isTouched](#metaistouched) 47 | - [meta.erroredInputs](#metaerroredinputs) 48 | - [Example snippet](#example-snippet-7) 49 | - [meta.touchedInputs](#metatouchedinputs) 50 | - [Example snippet](#example-snippet-8) 51 | - [meta.focused](#metafocused) 52 | - [wrapperProps](#wrapperprops) 53 | - [`` props](#paymentinputswrapper-props) 54 | - [styles](#styles) 55 | - [Schema](#schema) 56 | - [errorTextProps](#errortextprops) 57 | - [inputWrapperProps](#inputwrapperprops) 58 | - [Using a third-party UI library](#using-a-third-party-ui-library) 59 | - [Fannypack](#fannypack) 60 | - [Bootstrap](#bootstrap) 61 | - [Form library examples](#form-library-examples) 62 | - [Formik](#formik) 63 | - [React Final Form](#react-final-form) 64 | - [Customising the in-built style wrapper](#customising-the-in-built-style-wrapper) 65 | - [Custom card images](#custom-card-images) 66 | - [License](#license) 67 | 68 | ## [Demos](https://medipass.github.io/react-payment-inputs) 69 | 70 | ## Requirements 71 | 72 | Ensure you are running on a hooks-compatible version of React (v16.8 & above). 73 | 74 | ## Installation 75 | 76 | ``` 77 | npm install react-payment-inputs --save 78 | ``` 79 | 80 | or install with [Yarn](https://yarnpkg.com) if you prefer: 81 | 82 | ``` 83 | yarn add react-payment-inputs 84 | ``` 85 | 86 | ## Usage 87 | 88 |

89 | 90 | By default (as seen above), React Payment Inputs does not come with built-in styling meaning that you can easily adapt React Payment Inputs to your own design system. 91 | 92 | However, if you would like to use the built-in styles as seen in the animation above, [read "Using the built-in styled wrapper"](#using-the-built-in-styled-wrapper). 93 | 94 | ### With hooks 95 | 96 | If you'd like to use the hooks version of React Payment Inputs, you can import `usePaymentInputs` into your component. 97 | 98 | ```jsx 99 | import React from 'react'; 100 | import { usePaymentInputs } from 'react-payment-inputs'; 101 | 102 | export default function PaymentInputs() { 103 | const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps } = usePaymentInputs(); 104 | 105 | return ( 106 |
107 | 108 | 109 | 110 | {meta.isTouched && meta.error && Error: {meta.error}} 111 |
112 | ); 113 | } 114 | ``` 115 | 116 | > By spreading the prop getter functions (e.g. `{...getCardNumberProps()}`) on the inputs as shown above, React Payment Inputs will automatically handle the formatting, focus & validation logic for you. 117 | 118 | > **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the prop getter function (e.g. `getCardNumberProps()`) so the default event handlers in React Payment Inputs don't get overridden. 119 | 120 | ### With render props 121 | 122 | If you'd like to use the render props version of React Payment Inputs, you can import `PaymentInputsContainer` into your component. 123 | 124 | The **props** of `` are the same as the hook [options](#options) and the **render props** are the same as the hook [data](#data). 125 | 126 | ```jsx 127 | import React from 'react'; 128 | import { PaymentInputsContainer } from 'react-payment-inputs'; 129 | 130 | export default function PaymentInputs() { 131 | return ( 132 | 133 | {({ meta, getCardNumberProps, getExpiryDateProps, getCVCProps }) => ( 134 |
135 | 136 | 137 | 138 | {meta.isTouched && meta.error && Error: {meta.error}} 139 |
140 | )} 141 |
142 | ); 143 | } 144 | ``` 145 | 146 | > **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the prop getter function (e.g. `getCardNumberProps()`) so the default event handlers in React Payment Inputs don't get overridden. 147 | 148 | ### Using the built-in styled wrapper 149 | 150 | > Note: `` requires [styled-components](https://styled-components.com) to be installed as a dependency. 151 | 152 | By default, React Payment Inputs does not have built-in styling for it's inputs. However, React Payment Inputs comes with a styled wrapper which combines the card number, expiry & CVC fields seen below: 153 | 154 |

155 | 156 | ```jsx 157 | import React from 'react'; 158 | import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs'; 159 | import images from 'react-payment-inputs/images'; 160 | 161 | export default function PaymentInputs() { 162 | const { 163 | wrapperProps, 164 | getCardImageProps, 165 | getCardNumberProps, 166 | getExpiryDateProps, 167 | getCVCProps 168 | } = usePaymentInputs(); 169 | 170 | return ( 171 | 172 | 173 | 174 | 175 | 176 | 177 | ); 178 | } 179 | ``` 180 | 181 | ### More examples 182 | 183 | - [Storybook](https://medipass.github.io/react-payment-inputs) 184 | - [Source](./stories/index.stories.js) 185 | 186 | ## `data = usePaymentInputs(options)` 187 | 188 | > returns [an object (`data`)](#data) 189 | 190 | ### options 191 | 192 | > `Object({ cardNumberValidator, cvcValidator, errorMessages, expiryValidator, onBlur, onChange, onError, onTouch })` 193 | 194 | #### options.cardNumberValidator 195 | > `function({cardNumber, cardType, errorMessages})` 196 | 197 | Set custom card number validator function 198 | 199 | ##### Example 200 | 201 | ```js 202 | const cardNumberValidator = ({ cardNumber, cardType, errorMessages }) => { 203 | if (cardType.displayName === 'Visa' || cardType.displayName === 'Mastercard') { 204 | return; 205 | } 206 | return 'Card must be Visa or Mastercard'; 207 | } 208 | 209 | export default function MyComponent() { 210 | const { ... } = usePaymentInputs({ 211 | cardNumberValidator 212 | }); 213 | } 214 | ``` 215 | 216 | #### options.cvcValidator 217 | > `function({cvc, cardType, errorMessages})` 218 | 219 | Set custom cvc validator function 220 | 221 | 222 | #### options.errorMessages 223 | 224 | > `Object` 225 | 226 | Set custom error messages for the inputs. 227 | 228 | ##### Example 229 | 230 | ```js 231 | const ERROR_MESSAGES = { 232 | emptyCardNumber: 'El número de la tarjeta es inválido', 233 | invalidCardNumber: 'El número de la tarjeta es inválido', 234 | emptyExpiryDate: 'La fecha de expiración es inválida', 235 | monthOutOfRange: 'El mes de expiración debe estar entre 01 y 12', 236 | yearOutOfRange: 'El año de expiración no puede estar en el pasado', 237 | dateOutOfRange: 'La fecha de expiración no puede estar en el pasado', 238 | invalidExpiryDate: 'La fecha de expiración es inválida', 239 | emptyCVC: 'El código de seguridad es inválido', 240 | invalidCVC: 'El código de seguridad es inválido' 241 | } 242 | 243 | export default function MyComponent() { 244 | const { ... } = usePaymentInputs({ 245 | errorMessages: ERROR_MESSAGES 246 | }); 247 | } 248 | ``` 249 | 250 | #### options.expiryDateValidator 251 | > `function({expiryDate, errorMessages})` 252 | 253 | Set custom expiry date validator function 254 | 255 | 256 | #### options.onBlur 257 | 258 | > `function(event)` 259 | 260 | Function to handle the blur event on the inputs. It is invoked when any of the inputs blur. 261 | 262 | #### options.onChange 263 | 264 | > `function(event)` 265 | 266 | Function to handle the change event on the inputs. It is invoked when any of the inputs change. 267 | 268 | #### options.onError 269 | 270 | > `function(error, erroredInputs)` 271 | 272 | Function to invoke when any of the inputs error. 273 | 274 | #### options.onTouch 275 | 276 | > `function(touchedInput, touchedInputs)` 277 | 278 | Function to invoke when any of the inputs are touched. 279 | 280 | ### `data` 281 | 282 | #### getCardNumberProps 283 | 284 | > `function(overrideProps)` | returns `Object` 285 | 286 | Returns the props to apply to the **card number** input. 287 | 288 | **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the `getCardNumberProps()` so the default event handlers in React Payment Inputs don't get overridden. 289 | 290 | ##### Example snippet 291 | 292 | ```jsx 293 | 294 | ``` 295 | 296 | #### getExpiryDateProps 297 | 298 | > `function(overrideProps)` | returns `Object` 299 | 300 | Returns the props to apply to the **expiry date** input. 301 | 302 | **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the `getExpiryDateProps()` so the default event handlers in React Payment Inputs don't get overridden. 303 | 304 | ##### Example snippet 305 | 306 | ```jsx 307 | 308 | ``` 309 | 310 | #### getCVCProps 311 | 312 | > `function(overrideProps)` | returns `Object` 313 | 314 | Returns the props to apply to the **CVC** input. 315 | 316 | **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the `getCVCProps()` so the default event handlers in React Payment Inputs don't get overridden. 317 | 318 | ##### Example snippet 319 | 320 | ```jsx 321 | 322 | ``` 323 | 324 | #### getZIPProps 325 | 326 | > `function(overrideProps)` | returns `Object` 327 | 328 | Returns the props to apply to the **ZIP** input. 329 | 330 | **IMPORTANT:** You must place your event handlers (e.g. `onChange`, `onBlur`, etc) inside the `getZIPProps()` so the default event handlers in React Payment Inputs don't get overridden. 331 | 332 | ##### Example snippet 333 | 334 | ```jsx 335 | 336 | ``` 337 | 338 | #### getCardImageProps 339 | 340 | > `function({ images })` | returns `Object` 341 | 342 | Returns the props to apply to the **card image** SVG. 343 | 344 | This function only supports SVG elements currently. If you have a need for another format, please raise an issue. 345 | 346 | You can also supply [custom card images](#custom-card-images) using the `images` attribute. The example below uses the default card images from React Payment Inputs. 347 | 348 | ##### Example snippet 349 | 350 | ```jsx 351 | import images from 'react-payment-inputs/images'; 352 | 353 | 354 | ``` 355 | 356 | #### meta.cardType 357 | 358 | > Object 359 | 360 | Returns information about the current card type, including: name, lengths and formats. 361 | 362 | ##### Example snippet 363 | 364 | ```jsx 365 | const { meta } = usePaymentInputs(); 366 | 367 | Current card: {meta.cardType.displayName} 368 | ``` 369 | 370 | #### meta.error 371 | 372 | > string 373 | 374 | Returns the current global error between all rendered inputs. 375 | 376 | ##### Example snippet 377 | 378 | ```jsx 379 | const { meta } = usePaymentInputs(); 380 | 381 | console.log(meta.error); // "Card number is invalid" 382 | ``` 383 | 384 | #### meta.isTouched 385 | 386 | > boolean 387 | 388 | Returns the current global touched state between all rendered inputs. 389 | 390 | #### meta.erroredInputs 391 | 392 | > Object 393 | 394 | Returns the error message of each rendered input. 395 | 396 | ##### Example snippet 397 | 398 | ```jsx 399 | const { meta } = usePaymentInputs(); 400 | 401 | console.log(meta.erroredInputs); 402 | /* 403 | { 404 | cardNumber: undefined, 405 | expiryDate: 'Enter an expiry date', 406 | cvc: 'Enter a CVC' 407 | } 408 | */ 409 | ``` 410 | 411 | #### meta.touchedInputs 412 | 413 | > Object 414 | 415 | Returns the touch state of each rendered input. 416 | 417 | ##### Example snippet 418 | 419 | ```jsx 420 | const { meta } = usePaymentInputs(); 421 | 422 | console.log(meta.touchedInputs); 423 | /* 424 | { 425 | cardNumber: true, 426 | expiryDate: true, 427 | cvc: false 428 | } 429 | */ 430 | ``` 431 | 432 | #### meta.focused 433 | 434 | > string 435 | 436 | Returns the current focused input. 437 | 438 | ```jsx 439 | const { meta } = usePaymentInputs(); 440 | 441 | console.log(meta.focused); // "cardNumber" 442 | ``` 443 | 444 | #### wrapperProps 445 | 446 | > Object 447 | 448 | Returns the props to apply to ``. 449 | 450 | ## `` props 451 | 452 | ### styles 453 | 454 | > Object 455 | 456 | Custom styling to pass through to the wrapper. Either a styled-component's `css` or an Object can be passed. 457 | 458 | #### Schema 459 | 460 | ``` 461 | { 462 | fieldWrapper: { 463 | base: css | Object, 464 | errored: css | Object 465 | }, 466 | inputWrapper: { 467 | base: css | Object, 468 | errored: css | Object, 469 | focused: css | Object 470 | }, 471 | input: { 472 | base: css | Object, 473 | errored: css | Object, 474 | cardNumber: css | Object, 475 | expiryDate: css | Object, 476 | cvc: css | Object 477 | }, 478 | errorText: { 479 | base: css | Object 480 | } 481 | } 482 | ``` 483 | 484 | ### errorTextProps 485 | 486 | > Object 487 | 488 | Custom props to pass to the error text component. 489 | 490 | ### inputWrapperProps 491 | 492 | > Object 493 | 494 | Custom props to pass to the input wrapper component. 495 | 496 | ## Using a third-party UI library 497 | 498 | React Payment Inputs allows you to integrate into pretty much any React UI library. Below are a couple of examples of how you can fit React Payment Inputs into a UI library using `usePaymentInputs`. You can also do the same with ``. 499 | 500 | ### Fannypack 501 | 502 |

503 | 504 | ```jsx 505 | import React from 'react'; 506 | import { FieldSet, InputField } from 'fannypack'; 507 | import { usePaymentInputs } from 'react-payment-inputs'; 508 | import images from 'react-payment-inputs/images'; 509 | 510 | export default function PaymentInputs() { 511 | const { 512 | meta, 513 | getCardNumberProps, 514 | getExpiryDateProps, 515 | getCVCProps 516 | } = usePaymentInputs(); 517 | const { erroredInputs, touchedInputs } = meta; 518 | 519 | return ( 520 |
521 | 532 | 540 | 549 |
550 | ); 551 | } 552 | ``` 553 | 554 | ### Bootstrap 555 | 556 |

557 | 558 | ```jsx 559 | import React from 'react'; 560 | import { FieldSet, InputField } from 'fannypack'; 561 | import { usePaymentInputs } from 'react-payment-inputs'; 562 | import images from 'react-payment-inputs/images'; 563 | 564 | export default function PaymentInputs() { 565 | const { 566 | meta, 567 | getCardNumberProps, 568 | getExpiryDateProps, 569 | getCVCProps 570 | } = usePaymentInputs(); 571 | const { erroredInputs, touchedInputs } = meta; 572 | 573 | return ( 574 |
575 | 576 | 577 | Card number 578 | 585 | {erroredInputs.cardNumber} 586 | 587 | 588 | Expiry date 589 | 593 | {erroredInputs.expiryDate} 594 | 595 | 596 | CVC 597 | 602 | {erroredInputs.cvc} 603 | 604 | 605 |
606 | ); 607 | } 608 | ``` 609 | 610 | ## Form library examples 611 | 612 | React Payment Inputs has support for any type of React form library. Below are examples using [Formik](https://jaredpalmer.com/formik/) & [React Final Form](https://github.com/final-form/react-final-form). 613 | 614 | ### Formik 615 | 616 | ```jsx 617 | import { Formik, Field } from 'formik'; 618 | import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs'; 619 | 620 | function PaymentForm() { 621 | const { 622 | meta, 623 | getCardImageProps, 624 | getCardNumberProps, 625 | getExpiryDateProps, 626 | getCVCProps, 627 | wrapperProps 628 | } = usePaymentInputs(); 629 | 630 | return ( 631 | console.log(data)} 638 | validate={() => { 639 | let errors = {}; 640 | if (meta.erroredInputs.cardNumber) { 641 | errors.cardNumber = meta.erroredInputs.cardNumber; 642 | } 643 | if (meta.erroredInputs.expiryDate) { 644 | errors.expiryDate = meta.erroredInputs.expiryDate; 645 | } 646 | if (meta.erroredInputs.cvc) { 647 | errors.cvc = meta.erroredInputs.cvc; 648 | } 649 | return errors; 650 | }} 651 | > 652 | {({ handleSubmit }) => ( 653 |
654 |
655 | 656 | 657 | 658 | {({ field }) => ( 659 | 660 | )} 661 | 662 | 663 | {({ field }) => ( 664 | 665 | )} 666 | 667 | 668 | {({ field }) => } 669 | 670 | 671 |
672 | 675 |
676 | )} 677 |
678 | ); 679 | } 680 | ``` 681 | 682 | [See this example in Storybook](https://medipass.github.io/react-payment-inputs/?path=/story/usepaymentinputs--using-a-form-library-formik) 683 | 684 | ### React Final Form 685 | 686 | ```jsx 687 | import { Form, Field } from 'react-final-form'; 688 | import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs'; 689 | 690 | function PaymentForm() { 691 | const { 692 | meta, 693 | getCardImageProps, 694 | getCardNumberProps, 695 | getExpiryDateProps, 696 | getCVCProps, 697 | wrapperProps 698 | } = usePaymentInputs(); 699 | 700 | return ( 701 |
console.log(data)} 703 | validate={() => { 704 | let errors = {}; 705 | if (meta.erroredInputs.cardNumber) { 706 | errors.cardNumber = meta.erroredInputs.cardNumber; 707 | } 708 | if (meta.erroredInputs.expiryDate) { 709 | errors.expiryDate = meta.erroredInputs.expiryDate; 710 | } 711 | if (meta.erroredInputs.cvc) { 712 | errors.cvc = meta.erroredInputs.cvc; 713 | } 714 | return errors; 715 | }} 716 | > 717 | {({ handleSubmit }) => ( 718 | 719 |
720 | 721 | 722 | 723 | {({ input }) => ( 724 | 725 | )} 726 | 727 | 728 | {({ input }) => ( 729 | 730 | )} 731 | 732 | 733 | {({ input }) => } 734 | 735 | 736 |
737 | 740 |
741 | )} 742 | 743 | ); 744 | } 745 | ``` 746 | 747 | [See this example in Storybook](https://medipass.github.io/react-payment-inputs/?path=/story/usepaymentinputs--using-a-form-library-react-final-form) 748 | 749 | ## Customising the in-built style wrapper 750 | 751 | React Payment Input's default style wrapper can be customized by supplying a `styles` prop. 752 | 753 | ```jsx 754 | import { css } from 'styled-components'; 755 | import { usePaymentInputs, PaymentInputsWrapper } from 'react-payment-inputs'; 756 | 757 | function PaymentForm() { 758 | const { 759 | getCardNumberProps, 760 | getExpiryDateProps, 761 | getCVCProps, 762 | wrapperProps 763 | } = usePaymentInputs(); 764 | 765 | return ( 766 | 812 | 813 | 814 | 815 | 816 | ); 817 | } 818 | ``` 819 | 820 | [See the example on Storybook](https://medipass.github.io/react-payment-inputs/?path=/story/usepaymentinputs--styled-wrapper-with-custom-styling) 821 | 822 | ## Custom card images 823 | 824 | The card images can be customized by passing the `images` attribute to `getCardImageProps({ images })`. The `images` object must consist of SVG paths. 825 | 826 | ```jsx 827 | import { css } from 'styled-components'; 828 | import { usePaymentInputs, PaymentInputsWrapper } from 'react-payment-inputs'; 829 | 830 | const images = { 831 | mastercard: ( 832 | 833 | 834 | 835 | 836 | 840 | 841 | ) 842 | } 843 | 844 | function PaymentForm() { 845 | const { 846 | getCardNumberProps, 847 | getExpiryDateProps, 848 | getCVCProps, 849 | getCardImageProps, 850 | wrapperProps 851 | } = usePaymentInputs(); 852 | 853 | return ( 854 | 855 | 856 | 857 | 858 | 859 | 860 | ); 861 | } 862 | ``` 863 | 864 | ## License 865 | 866 | MIT © [Medipass Solutions Pty. Ltd.](https://github.com/medipass) 867 | -------------------------------------------------------------------------------- /assets/basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/basic.gif -------------------------------------------------------------------------------- /assets/bootstrap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/bootstrap.gif -------------------------------------------------------------------------------- /assets/fannypack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/fannypack.gif -------------------------------------------------------------------------------- /assets/react-payment-inputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/react-payment-inputs.png -------------------------------------------------------------------------------- /assets/react-payment-inputs.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/react-payment-inputs.sketch -------------------------------------------------------------------------------- /assets/wrapper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medipass/react-payment-inputs/4a548f21e739084aed3b3fc0a43f69e4d1a14361/assets/wrapper.gif -------------------------------------------------------------------------------- /docs/3.3e7c09a6a2f7bbc36612.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[3],{731:function(module,exports,__webpack_require__){var __WEBPACK_AMD_DEFINE_RESULT__;!function(){function aa(a,b,c){return a.call.apply(a.bind,arguments)}function ba(a,b,c){if(!a)throw Error();if(2=b.f?e():a.fonts.load(function fa(a){return H(a)+" "+a.f+"00 300px "+I(a.c)}(b.a),b.h).then(function(a){1<=a.length?d():setTimeout(f,25)},function(){e()})}()}),e=null,f=new Promise(function(a,d){e=setTimeout(d,b.f)});Promise.race([f,d]).then(function(){e&&(clearTimeout(e),e=null),b.g(b.a)},function(){b.j(b.a)})};var R={D:"serif",C:"sans-serif"},S=null;function T(){if(null===S){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent);S=!!a&&(536>parseInt(a[1],10)||536===parseInt(a[1],10)&&11>=parseInt(a[2],10))}return S}function la(a,b,c){for(var d in R)if(R.hasOwnProperty(d)&&b===a.f[R[d]]&&c===a.f[R[d]])return!0;return!1}function U(a){var d,b=a.g.a.offsetWidth,c=a.h.a.offsetWidth;(d=b===a.f.serif&&c===a.f["sans-serif"])||(d=T()&&la(a,b,c)),d?q()-a.A>=a.w?T()&&la(a,b,c)&&(null===a.u||a.u.hasOwnProperty(a.a.c))?V(a,a.v):V(a,a.B):function ma(a){setTimeout(p(function(){U(this)},a),50)}(a):V(a,a.v)}function V(a,b){setTimeout(p(function(){v(this.g.a),v(this.h.a),v(this.j.a),v(this.m.a),b(this.a)},a),0)}function W(a,b,c){this.c=a,this.a=b,this.f=0,this.m=this.j=!1,this.s=c}Q.prototype.start=function(){this.f.serif=this.j.a.offsetWidth,this.f["sans-serif"]=this.m.a.offsetWidth,this.A=q(),U(this)};var X=null;function na(a){0==--a.f&&a.j&&(a.m?((a=a.a).g&&w(a.f,[a.a.c("wf","active")],[a.a.c("wf","loading"),a.a.c("wf","inactive")]),K(a,"active")):L(a.a))}function oa(a){this.j=a,this.a=new ja,this.h=0,this.f=this.g=!0}function qa(a,b,c,d,e){var f=0==--a.h;(a.f||a.g)&&setTimeout(function(){var a=e||null,m=d||{};if(0===c.length&&f)L(b.a);else{b.f+=c.length,f&&(b.j=f);var h,l=[];for(h=0;h=b.f?e():a.fonts.load(fa(b.a),b.h).then(function(a){1<=a.length?d():setTimeout(f,25)},function(){e()})}f()}),e=null,f=new Promise(function(a,d){e=setTimeout(d,b.f)});Promise.race([f,d]).then(function(){e&&(clearTimeout(e),e=null);b.g(b.a)},function(){b.j(b.a)})};function Q(a,b,c,d,e,f,g){this.v=a;this.B=b;this.c=c;this.a=d;this.s=g||\"BESbswy\";this.f={};this.w=e||3E3;this.u=f||null;this.m=this.j=this.h=this.g=null;this.g=new M(this.c,this.s);this.h=new M(this.c,this.s);this.j=new M(this.c,this.s);this.m=new M(this.c,this.s);a=new G(this.a.c+\",serif\",J(this.a));a=O(a);this.g.a.style.cssText=a;a=new G(this.a.c+\",sans-serif\",J(this.a));a=O(a);this.h.a.style.cssText=a;a=new G(\"serif\",J(this.a));a=O(a);this.j.a.style.cssText=a;a=new G(\"sans-serif\",J(this.a));a=\nO(a);this.m.a.style.cssText=a;N(this.g);N(this.h);N(this.j);N(this.m)}var R={D:\"serif\",C:\"sans-serif\"},S=null;function T(){if(null===S){var a=/AppleWebKit\\/([0-9]+)(?:\\.([0-9]+))/.exec(window.navigator.userAgent);S=!!a&&(536>parseInt(a[1],10)||536===parseInt(a[1],10)&&11>=parseInt(a[2],10))}return S}Q.prototype.start=function(){this.f.serif=this.j.a.offsetWidth;this.f[\"sans-serif\"]=this.m.a.offsetWidth;this.A=q();U(this)};\nfunction la(a,b,c){for(var d in R)if(R.hasOwnProperty(d)&&b===a.f[R[d]]&&c===a.f[R[d]])return!0;return!1}function U(a){var b=a.g.a.offsetWidth,c=a.h.a.offsetWidth,d;(d=b===a.f.serif&&c===a.f[\"sans-serif\"])||(d=T()&&la(a,b,c));d?q()-a.A>=a.w?T()&&la(a,b,c)&&(null===a.u||a.u.hasOwnProperty(a.a.c))?V(a,a.v):V(a,a.B):ma(a):V(a,a.v)}function ma(a){setTimeout(p(function(){U(this)},a),50)}function V(a,b){setTimeout(p(function(){v(this.g.a);v(this.h.a);v(this.j.a);v(this.m.a);b(this.a)},a),0)};function W(a,b,c){this.c=a;this.a=b;this.f=0;this.m=this.j=!1;this.s=c}var X=null;W.prototype.g=function(a){var b=this.a;b.g&&w(b.f,[b.a.c(\"wf\",a.c,J(a).toString(),\"active\")],[b.a.c(\"wf\",a.c,J(a).toString(),\"loading\"),b.a.c(\"wf\",a.c,J(a).toString(),\"inactive\")]);K(b,\"fontactive\",a);this.m=!0;na(this)};\nW.prototype.h=function(a){var b=this.a;if(b.g){var c=y(b.f,b.a.c(\"wf\",a.c,J(a).toString(),\"active\")),d=[],e=[b.a.c(\"wf\",a.c,J(a).toString(),\"loading\")];c||d.push(b.a.c(\"wf\",a.c,J(a).toString(),\"inactive\"));w(b.f,d,e)}K(b,\"fontinactive\",a);na(this)};function na(a){0==--a.f&&a.j&&(a.m?(a=a.a,a.g&&w(a.f,[a.a.c(\"wf\",\"active\")],[a.a.c(\"wf\",\"loading\"),a.a.c(\"wf\",\"inactive\")]),K(a,\"active\")):L(a.a))};function oa(a){this.j=a;this.a=new ja;this.h=0;this.f=this.g=!0}oa.prototype.load=function(a){this.c=new ca(this.j,a.context||this.j);this.g=!1!==a.events;this.f=!1!==a.classes;pa(this,new ha(this.c,a),a)};\nfunction qa(a,b,c,d,e){var f=0==--a.h;(a.f||a.g)&&setTimeout(function(){var a=e||null,m=d||null||{};if(0===c.length&&f)L(b.a);else{b.f+=c.length;f&&(b.j=f);var h,l=[];for(h=0;hStorybook

No Preview

Sorry, but you either have no stories or none are selected somehow.

  • Please check the Storybook config.
  • Try reloading the page.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Storybook
-------------------------------------------------------------------------------- /docs/main.3e7c09a6a2f7bbc36612.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.3e7c09a6a2f7bbc36612.bundle.js","sources":["webpack:///./src/utils/cardTypes.js"],"sourcesContent":["export const DEFAULT_CVC_LENGTH = 3;\nexport const DEFAULT_ZIP_LENGTH = 5;\nexport const DEFAULT_CARD_FORMAT = /(\\d{1,4})/g;\nexport const CARD_TYPES = [\n {\n displayName: 'Visa',\n type: 'visa',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^4/,\n gaps: [4, 8, 12],\n lengths: [16, 18, 19],\n code: {\n name: 'CVV',\n length: 3\n }\n },\n {\n displayName: 'Mastercard',\n type: 'mastercard',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(5[1-5]|677189)|^(222[1-9]|2[3-6]\\d{2}|27[0-1]\\d|2720)/,\n gaps: [4, 8, 12],\n lengths: [16],\n code: {\n name: 'CVC',\n length: 3\n }\n },\n {\n displayName: 'American Express',\n type: 'amex',\n format: /(\\d{1,4})(\\d{1,6})?(\\d{1,5})?/,\n startPattern: /^3[47]/,\n gaps: [4, 10],\n lengths: [15],\n code: {\n name: 'CID',\n length: 4\n }\n },\n {\n displayName: 'Diners Club',\n type: 'dinersclub',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(36|38|30[0-5])/,\n gaps: [4, 10],\n lengths: [14, 16, 19],\n code: {\n name: 'CVV',\n length: 3\n }\n },\n {\n displayName: 'Discover',\n type: 'discover',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(6011|65|64[4-9]|622)/,\n gaps: [4, 8, 12],\n lengths: [16, 19],\n code: {\n name: 'CID',\n length: 3\n }\n },\n {\n displayName: 'JCB',\n type: 'jcb',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^35/,\n gaps: [4, 8, 12],\n lengths: [16, 17, 18, 19],\n code: {\n name: 'CVV',\n length: 3\n }\n },\n {\n displayName: 'UnionPay',\n type: 'unionpay',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^62/,\n gaps: [4, 8, 12],\n lengths: [14, 15, 16, 17, 18, 19],\n code: {\n name: 'CVN',\n length: 3\n }\n },\n {\n displayName: 'Maestro',\n type: 'maestro',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(5018|5020|5038|6304|6703|6708|6759|676[1-3])/,\n gaps: [4, 8, 12],\n lengths: [12, 13, 14, 15, 16, 17, 18, 19],\n code: {\n name: 'CVC',\n length: 3\n }\n },\n {\n displayName: 'Elo',\n type: 'elo',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/,\n gaps: [4, 8, 12],\n lengths: [16],\n code: {\n name: 'CVE',\n length: 3\n }\n },\n {\n displayName: 'Hipercard',\n type: 'hipercard',\n format: DEFAULT_CARD_FORMAT,\n startPattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/,\n gaps: [4, 8, 12],\n lengths: [16],\n code: {\n name: 'CVC',\n length: 3\n }\n }\n];\n\nexport const getCardTypeByValue = value => CARD_TYPES.filter(cardType => cardType.startPattern.test(value))[0];\nexport const getCardTypeByType = type => CARD_TYPES.filter(cardType => cardType.type === type)[0];\n"],"mappings":"AAAA","sourceRoot":""} -------------------------------------------------------------------------------- /docs/main.77cad4a5274c0b554b48.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{0:function(n,o,p){p(299),p(383),n.exports=p(384)},383:function(n,o){}},[[0,1,2]]]); -------------------------------------------------------------------------------- /docs/runtime~main.3e7c09a6a2f7bbc36612.bundle.js: -------------------------------------------------------------------------------- 1 | !function(modules){function webpackJsonpCallback(data){for(var moduleId,chunkId,chunkIds=data[0],moreModules=data[1],executeModules=data[2],i=0,resolves=[];i 34 | * 35 | * Copyright (c) 2014-2017, Jon Schlinkert. 36 | * Released under the MIT License. 37 | */ 38 | 39 | /*! 40 | * https://github.com/paulmillr/es6-shim 41 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 42 | * and contributors, MIT License 43 | * es6-shim: v0.35.4 44 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 45 | * Details and documentation: 46 | * https://github.com/paulmillr/es6-shim/ 47 | */ 48 | 49 | /** @license React v16.8.1 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | 58 | /** @license React v0.13.1 59 | * scheduler.production.min.js 60 | * 61 | * Copyright (c) Facebook, Inc. and its affiliates. 62 | * 63 | * This source code is licensed under the MIT license found in the 64 | * LICENSE file in the root directory of this source tree. 65 | */ 66 | 67 | /* 68 | object-assign 69 | (c) Sindre Sorhus 70 | @license MIT 71 | */ 72 | 73 | /** @license React v16.8.1 74 | * react-dom.production.min.js 75 | * 76 | * Copyright (c) Facebook, Inc. and its affiliates. 77 | * 78 | * This source code is licensed under the MIT license found in the 79 | * LICENSE file in the root directory of this source tree. 80 | */ 81 | 82 | /*! 83 | Copyright (c) 2016 Jed Watson. 84 | Licensed under the MIT License (MIT), see 85 | http://jedwatson.github.io/classnames 86 | */ 87 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'PaymentInputs', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | }, 12 | webpack: { 13 | extractCSS: false 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-payment-inputs", 3 | "version": "1.2.0", 4 | "description": "A zero-dependency React Hook & Container to help with payment card input fields.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "unpkg": "umd/react-payment-inputs.min.js", 8 | "files": [ 9 | "css", 10 | "es", 11 | "lib", 12 | "umd", 13 | "images" 14 | ], 15 | "scripts": { 16 | "build": "rollup -c", 17 | "create-proxies": "node scripts/create-proxies.js", 18 | "remove-proxies": "node scripts/remove-proxies.js", 19 | "clean": "rimraf es/ lib/ umd/ && yarn remove-proxies", 20 | "prepublishOnly": "yarn create-proxies && yarn build", 21 | "postpublish": "yarn clean", 22 | "lint": "eslint ./src", 23 | "storybook": "start-storybook -p 6006", 24 | "build-storybook": "build-storybook -o docs/" 25 | }, 26 | "dependencies": {}, 27 | "peerDependencies": { 28 | "react": ">=16.8.0", 29 | "styled-components": ">=4.0.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "7.4.4", 33 | "@babel/plugin-proposal-class-properties": "7.4.4", 34 | "@babel/plugin-proposal-export-namespace-from": "7.2.0", 35 | "@babel/plugin-proposal-object-rest-spread": "7.4.4", 36 | "@babel/plugin-syntax-dynamic-import": "7.2.0", 37 | "@babel/preset-env": "7.4.4", 38 | "@babel/preset-react": "7.0.0", 39 | "@medipass/eslint-config-react-medipass": "8.4.1", 40 | "@storybook/addon-actions": "^5.0.11", 41 | "@storybook/addon-links": "^5.0.11", 42 | "@storybook/addons": "^5.0.11", 43 | "@storybook/react": "^5.0.11", 44 | "babel-eslint": "10.0.1", 45 | "babel-loader": "^8.0.6", 46 | "bootstrap": "4.3.1", 47 | "eslint": "5.16.0", 48 | "eslint-plugin-import": "2.17.2", 49 | "eslint-plugin-prettier": "3.0.1", 50 | "fannypack": "4.19.22", 51 | "final-form": "4.13.0", 52 | "formik": "1.5.7", 53 | "react": "16.8.6", 54 | "react-bootstrap": "1.0.0-beta.8", 55 | "react-dom": "16.8.6", 56 | "react-final-form": "5.1.2", 57 | "rimraf": "2.6.3", 58 | "rollup": "1.12.3", 59 | "rollup-plugin-babel": "4.3.2", 60 | "rollup-plugin-commonjs": "10.0.0", 61 | "rollup-plugin-node-resolve": "5.0.0", 62 | "rollup-plugin-proxy-directories": "1.2.0", 63 | "rollup-plugin-terser": "4.0.4" 64 | }, 65 | "author": "", 66 | "homepage": "", 67 | "license": "MIT", 68 | "repository": "", 69 | "keywords": [ 70 | "react-component" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel'); 2 | const resolve = require('rollup-plugin-node-resolve'); 3 | const commonjs = require('rollup-plugin-commonjs'); 4 | const proxyDirectories = require('rollup-plugin-proxy-directories'); 5 | const { terser } = require('rollup-plugin-terser'); 6 | 7 | const pkg = require('./package.json'); 8 | 9 | const extensions = ['.js', '.jsx', '.json']; 10 | 11 | const makeExternalPredicate = externalArr => { 12 | if (!externalArr.length) { 13 | return () => false; 14 | } 15 | const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`); 16 | return id => pattern.test(id); 17 | }; 18 | 19 | const getExternal = (umd, pkg) => { 20 | const external = [...Object.keys(pkg.peerDependencies), 'prop-types']; 21 | const allExternal = [...external, ...Object.keys(pkg.dependencies)]; 22 | return makeExternalPredicate(umd ? external : allExternal); 23 | }; 24 | 25 | const commonPlugins = [ 26 | babel({ 27 | extensions, 28 | exclude: ['node_modules/**'] 29 | }), 30 | resolve({ extensions, preferBuiltins: false }) 31 | ]; 32 | 33 | const getPlugins = umd => 34 | umd 35 | ? [ 36 | ...commonPlugins, 37 | commonjs({ 38 | include: /node_modules/ 39 | }), 40 | terser() 41 | ] 42 | : commonPlugins; 43 | 44 | const getOutput = (umd, pkg) => 45 | umd 46 | ? { 47 | name: 'ReactPaymentInputs', 48 | file: pkg.unpkg, 49 | format: 'umd', 50 | exports: 'named', 51 | globals: { 52 | react: 'React', 53 | 'react-dom': 'ReactDOM', 54 | 'prop-types': 'PropTypes', 55 | 'styled-components': 'StyledComponents' 56 | } 57 | } 58 | : [ 59 | { 60 | file: pkg.main, 61 | format: 'cjs', 62 | exports: 'named' 63 | }, 64 | { 65 | file: pkg.module, 66 | format: 'es' 67 | } 68 | ]; 69 | 70 | const createConfig = ({ umd, pkg, plugins = [], ...config }) => ({ 71 | external: getExternal(umd, pkg), 72 | plugins: [...getPlugins(umd), ...plugins], 73 | output: getOutput(umd, pkg), 74 | ...config 75 | }); 76 | 77 | export default [ 78 | createConfig({ 79 | pkg, 80 | input: [], 81 | output: [ 82 | { 83 | format: 'es', 84 | dir: 'es' 85 | }, 86 | { 87 | format: 'cjs', 88 | dir: 'lib', 89 | exports: 'named' 90 | } 91 | ], 92 | plugins: [proxyDirectories()] 93 | }), 94 | createConfig({ pkg, input: 'src/index.js', umd: true }) 95 | ]; 96 | -------------------------------------------------------------------------------- /scripts/create-proxies.js: -------------------------------------------------------------------------------- 1 | const { lstatSync, mkdirSync, writeFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | const getModules = require('./get-modules'); 4 | const { name } = require('../package.json'); 5 | 6 | const createProxyPackage = (module, { isFile }) => { 7 | return `{ 8 | "name": "${name}/${module}", 9 | "private": true, 10 | "main": "../lib/${module}.js", 11 | "module": "../es/${module}.js" 12 | }`; 13 | }; 14 | 15 | const createProxies = () => { 16 | const modules = getModules(); 17 | modules.forEach(module => { 18 | let proxyPath = join('./', module); 19 | const isFile = lstatSync(join(__dirname, '../src', module)).isFile(); 20 | if (isFile) { 21 | proxyPath = proxyPath.replace(/\.js$/, ''); 22 | } 23 | mkdirSync(proxyPath); 24 | writeFileSync(join(proxyPath, 'package.json'), createProxyPackage(proxyPath, { isFile })); 25 | }); 26 | }; 27 | 28 | module.exports = createProxies(); 29 | -------------------------------------------------------------------------------- /scripts/get-modules.js: -------------------------------------------------------------------------------- 1 | const { lstatSync, readdirSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | const isDirectory = source => lstatSync(join(__dirname, '../src', source)).isDirectory(); 5 | const isFile = source => lstatSync(join(__dirname, '../src', source)).isFile(); 6 | const isModule = source => isDirectory(source) || (isFile(source) && /.(js|ts|tsx)$/.test(source)); 7 | const isNotPrivate = source => !/^_/.test(source); 8 | const isNotIndex = source => !/^index\.js/.test(source); 9 | 10 | const getModules = () => { 11 | const srcDirectory = join(__dirname, '../src'); 12 | const sources = readdirSync(srcDirectory); 13 | const modules = sources 14 | .filter(isNotPrivate) 15 | .filter(isNotIndex) 16 | .filter(isModule); 17 | return modules; 18 | }; 19 | 20 | module.exports = getModules; 21 | -------------------------------------------------------------------------------- /scripts/remove-proxies.js: -------------------------------------------------------------------------------- 1 | const rimraf = require('rimraf'); 2 | const { lstatSync } = require('fs'); 3 | const { join } = require('path'); 4 | const getModules = require('./get-modules'); 5 | 6 | const createProxies = () => { 7 | const modules = getModules(); 8 | modules.forEach(module => { 9 | let proxyPath = join('./', module); 10 | const isFile = lstatSync(join(__dirname, '../src', module)).isFile(); 11 | if (isFile) { 12 | proxyPath = proxyPath.replace(/\.js$/, ''); 13 | } 14 | rimraf.sync(proxyPath); 15 | }); 16 | }; 17 | 18 | module.exports = createProxies(); 19 | -------------------------------------------------------------------------------- /src/PaymentInputsContainer.js: -------------------------------------------------------------------------------- 1 | import usePaymentInputs from './usePaymentInputs'; 2 | 3 | export default function PaymentInputsContainer(props) { 4 | const paymentInputs = usePaymentInputs(props); 5 | return props.children(paymentInputs); 6 | } 7 | -------------------------------------------------------------------------------- /src/PaymentInputsWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css } from 'styled-components'; 3 | 4 | const FieldWrapper = styled.div` 5 | display: inline-flex; 6 | flex-direction: column; 7 | 8 | & { 9 | ${props => (props.hasErrored && props.styles.fieldWrapper ? props.styles.fieldWrapper.errored : undefined)}; 10 | } 11 | 12 | ${props => (props.styles.fieldWrapper ? props.styles.fieldWrapper.base : undefined)}; 13 | `; 14 | const InputWrapper = styled.div` 15 | align-items: center; 16 | background-color: white; 17 | border: 1px solid #bdbdbd; 18 | box-shadow: inset 0px 1px 2px #e5e5e5; 19 | border-radius: 0.2em; 20 | display: flex; 21 | height: 2.5em; 22 | padding: 0.4em 0.6em; 23 | 24 | & { 25 | ${props => 26 | props.hasErrored && 27 | css` 28 | border-color: #c9444d; 29 | box-shadow: #c9444d 0px 0px 0px 1px; 30 | ${props => props.styles.inputWrapper && props.styles.inputWrapper.errored}; 31 | `}; 32 | } 33 | 34 | & { 35 | ${props => 36 | props.focused && 37 | css` 38 | border-color: #444bc9; 39 | box-shadow: #444bc9 0px 0px 0px 1px; 40 | ${props => props.styles.inputWrapper && props.styles.inputWrapper.focused}; 41 | `}; 42 | } 43 | 44 | & input { 45 | border: unset; 46 | margin: unset; 47 | padding: unset; 48 | outline: unset; 49 | font-size: inherit; 50 | 51 | & { 52 | ${props => (props.hasErrored && props.styles.input ? props.styles.input.errored : undefined)}; 53 | } 54 | 55 | ${props => props.styles.input && props.styles.input.base}; 56 | } 57 | 58 | & svg { 59 | margin-right: 0.6em; 60 | & { 61 | ${props => props.styles.cardImage}; 62 | } 63 | } 64 | 65 | & input#cardNumber { 66 | width: 11em; 67 | & { 68 | ${props => props.styles.input && props.styles.input.cardNumber}; 69 | } 70 | } 71 | 72 | & input#expiryDate { 73 | width: 4em; 74 | & { 75 | ${props => props.styles.input && props.styles.input.expiryDate}; 76 | } 77 | } 78 | 79 | & input#cvc { 80 | width: 2.5em; 81 | & { 82 | ${props => props.styles.input && props.styles.input.cvc}; 83 | } 84 | } 85 | 86 | & input#zip { 87 | width: 4em; 88 | & { 89 | ${props => props.styles.input && props.styles.input.zip}; 90 | } 91 | } 92 | 93 | ${props => (props.styles.inputWrapper ? props.styles.inputWrapper.base : undefined)}; 94 | `; 95 | const ErrorText = styled.div` 96 | color: #c9444d; 97 | font-size: 0.75rem; 98 | margin-top: 0.25rem; 99 | 100 | & { 101 | ${props => (props.styles.errorText ? props.styles.errorText.base : undefined)}; 102 | } 103 | `; 104 | 105 | function PaymentInputsWrapper(props) { 106 | const { children, error, errorTextProps, focused, inputWrapperProps, isTouched, styles, ...restProps } = props; 107 | const hasErrored = error && isTouched; 108 | return ( 109 | 110 | 111 | {children} 112 | 113 | {hasErrored && ( 114 | 115 | {error} 116 | 117 | )} 118 | 119 | ); 120 | } 121 | 122 | PaymentInputsWrapper.defaultProps = { 123 | styles: {} 124 | }; 125 | 126 | export default PaymentInputsWrapper; 127 | -------------------------------------------------------------------------------- /src/images/amex.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 10 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/images/dinersclub.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 8 | 9 | 17 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /src/images/discover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 8 | 9 | 17 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /src/images/hipercard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/images/index.js: -------------------------------------------------------------------------------- 1 | import amex from './amex.js'; 2 | import dinersclub from './dinersclub.js'; 3 | import discover from './discover.js'; 4 | import hipercard from './hipercard.js'; 5 | import jcb from './jcb.js'; 6 | import unionpay from './unionpay.js'; 7 | import mastercard from './mastercard.js'; 8 | import placeholder from './placeholder.js'; 9 | import visa from './visa.js'; 10 | import troy from './troy.js'; 11 | 12 | export default { 13 | amex, 14 | dinersclub, 15 | discover, 16 | hipercard, 17 | jcb, 18 | unionpay, 19 | mastercard, 20 | placeholder, 21 | visa, 22 | troy 23 | }; 24 | -------------------------------------------------------------------------------- /src/images/jcb.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 9 | 13 | 17 | 21 | 25 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/images/mastercard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 8 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/images/placeholder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 16 | 17 | 26 | 35 | 44 | 45 | 46 | ); 47 | -------------------------------------------------------------------------------- /src/images/troy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/images/unionpay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 9 | 13 | 17 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/images/visa.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ( 4 | 5 | 6 | 7 | 8 | 9 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as usePaymentInputs } from './usePaymentInputs'; 2 | export { default as PaymentInputsContainer } from './PaymentInputsContainer'; 3 | export { default as PaymentInputsWrapper } from './PaymentInputsWrapper'; 4 | export { getCardNumberError, getExpiryDateError, getCVCError, getZIPError } from './utils/validator'; 5 | -------------------------------------------------------------------------------- /src/usePaymentInputs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import utils from './utils'; 4 | 5 | export default function usePaymentCard({ 6 | autoFocus = true, 7 | errorMessages, 8 | onBlur, 9 | onChange, 10 | onError, 11 | onTouch, 12 | cardNumberValidator, 13 | cvcValidator, 14 | expiryValidator 15 | } = {}) { 16 | const cardNumberField = React.useRef(); 17 | const expiryDateField = React.useRef(); 18 | const cvcField = React.useRef(); 19 | const zipField = React.useRef(); 20 | 21 | /** ====== START: META STUFF ====== */ 22 | const [touchedInputs, setTouchedInputs] = React.useState({ 23 | cardNumber: false, 24 | expiryDate: false, 25 | cvc: false, 26 | zip: false 27 | }); 28 | const [isTouched, setIsTouched] = React.useState(false); 29 | const [erroredInputs, setErroredInputs] = React.useState({ 30 | cardNumber: undefined, 31 | expiryDate: undefined, 32 | cvc: undefined, 33 | zip: undefined 34 | }); 35 | const [error, setError] = React.useState(); 36 | const [cardType, setCardType] = React.useState(); 37 | const [focused, setFocused] = React.useState(); 38 | 39 | const setInputError = React.useCallback((input, error) => { 40 | setErroredInputs(erroredInputs => { 41 | if (erroredInputs[input] === error) return erroredInputs; 42 | 43 | let newError = error; 44 | const newErroredInputs = { ...erroredInputs, [input]: error }; 45 | if (error) { 46 | setError(error); 47 | } else { 48 | newError = Object.values(newErroredInputs).find(Boolean); 49 | setError(newError); 50 | } 51 | onError && onError(newError, newErroredInputs); 52 | return newErroredInputs; 53 | }); 54 | }, []); // eslint-disable-line 55 | 56 | const setInputTouched = React.useCallback((input, value) => { 57 | requestAnimationFrame(() => { 58 | if (document.activeElement.tagName !== 'INPUT') { 59 | setIsTouched(true); 60 | } else if (value === false) { 61 | setIsTouched(false); 62 | } 63 | }); 64 | 65 | setTouchedInputs(touchedInputs => { 66 | if (touchedInputs[input] === value) return touchedInputs; 67 | 68 | const newTouchedInputs = { ...touchedInputs, [input]: value }; 69 | onTouch && onTouch({ [input]: value }, newTouchedInputs); 70 | return newTouchedInputs; 71 | }); 72 | }, []); // eslint-disable-line 73 | /** ====== END: META STUFF ====== */ 74 | 75 | /** ====== START: CARD NUMBER STUFF ====== */ 76 | const handleBlurCardNumber = React.useCallback( 77 | (props = {}) => { 78 | return e => { 79 | props.onBlur && props.onBlur(e); 80 | onBlur && onBlur(e); 81 | setFocused(undefined); 82 | setInputTouched('cardNumber', true); 83 | }; 84 | }, 85 | [onBlur, setInputTouched] 86 | ); 87 | 88 | const handleChangeCardNumber = React.useCallback( 89 | (props = {}) => { 90 | return e => { 91 | const formattedCardNumber = e.target.value || ''; 92 | const cardNumber = formattedCardNumber.replace(/\s/g, ''); 93 | let cursorPosition = cardNumberField.current.selectionStart; 94 | 95 | const cardType = utils.cardTypes.getCardTypeByValue(cardNumber); 96 | setCardType(cardType); 97 | 98 | setInputTouched('cardNumber', false); 99 | 100 | // @ts-ignore 101 | cardNumberField.current.value = utils.formatter.formatCardNumber(cardNumber); 102 | 103 | props.onChange && props.onChange(e); 104 | onChange && onChange(e); 105 | 106 | // Due to the card number formatting, the selection cursor will fall to the end of 107 | // the input field. Here, we want to reposition the cursor to the correct place. 108 | requestAnimationFrame(() => { 109 | if (document.activeElement !== cardNumberField.current) return; 110 | if (cardNumberField.current.value[cursorPosition - 1] === ' ') { 111 | cursorPosition = cursorPosition + 1; 112 | } 113 | cardNumberField.current.setSelectionRange(cursorPosition, cursorPosition); 114 | }); 115 | 116 | const cardNumberError = utils.validator.getCardNumberError(cardNumber, cardNumberValidator, { errorMessages }); 117 | if (!cardNumberError && autoFocus) { 118 | expiryDateField.current && expiryDateField.current.focus(); 119 | } 120 | setInputError('cardNumber', cardNumberError); 121 | props.onError && props.onError(cardNumberError); 122 | }; 123 | }, 124 | [autoFocus, cardNumberValidator, errorMessages, onChange, setInputError, setInputTouched] 125 | ); 126 | 127 | const handleFocusCardNumber = React.useCallback((props = {}) => { 128 | return e => { 129 | props.onFocus && props.onFocus(e); 130 | setFocused('cardNumber'); 131 | }; 132 | }, []); 133 | 134 | const handleKeyPressCardNumber = React.useCallback((props = {}) => { 135 | return e => { 136 | const formattedCardNumber = e.target.value || ''; 137 | const cardNumber = formattedCardNumber.replace(/\s/g, ''); 138 | 139 | props.onKeyPress && props.onKeyPress(e); 140 | 141 | if (e.key !== utils.ENTER_KEY_CODE) { 142 | if (!utils.validator.isNumeric(e)) { 143 | e.preventDefault(); 144 | } 145 | if (utils.validator.hasCardNumberReachedMaxLength(cardNumber)) { 146 | e.preventDefault(); 147 | } 148 | } 149 | }; 150 | }, []); 151 | 152 | const getCardNumberProps = React.useCallback( 153 | ({ refKey, ...props } = {}) => ({ 154 | 'aria-label': 'Card number', 155 | autoComplete: 'cc-number', 156 | id: 'cardNumber', 157 | name: 'cardNumber', 158 | placeholder: 'Card number', 159 | type: 'tel', 160 | [refKey || 'ref']: cardNumberField, 161 | ...props, 162 | onBlur: handleBlurCardNumber(props), 163 | onChange: handleChangeCardNumber(props), 164 | onFocus: handleFocusCardNumber(props), 165 | onKeyPress: handleKeyPressCardNumber(props) 166 | }), 167 | [handleBlurCardNumber, handleChangeCardNumber, handleFocusCardNumber, handleKeyPressCardNumber] 168 | ); 169 | /** ====== END: CARD NUMBER STUFF ====== */ 170 | 171 | /** ====== START: EXPIRY DATE STUFF ====== */ 172 | const handleBlurExpiryDate = React.useCallback( 173 | (props = {}) => { 174 | return e => { 175 | props.onBlur && props.onBlur(e); 176 | onBlur && onBlur(e); 177 | setFocused(undefined); 178 | setInputTouched('expiryDate', true); 179 | }; 180 | }, 181 | [onBlur, setInputTouched] 182 | ); 183 | 184 | const handleChangeExpiryDate = React.useCallback( 185 | (props = {}) => { 186 | return e => { 187 | setInputTouched('expiryDate', false); 188 | 189 | expiryDateField.current.value = utils.formatter.formatExpiry(e); 190 | 191 | props.onChange && props.onChange(e); 192 | onChange && onChange(e); 193 | const expiryDateError = utils.validator.getExpiryDateError(expiryDateField.current.value, expiryValidator, { 194 | errorMessages 195 | }); 196 | if (!expiryDateError && autoFocus) { 197 | cvcField.current && cvcField.current.focus(); 198 | } 199 | setInputError('expiryDate', expiryDateError); 200 | props.onError && props.onError(expiryDateError); 201 | }; 202 | }, 203 | [autoFocus, errorMessages, expiryValidator, onChange, setInputError, setInputTouched] 204 | ); 205 | 206 | const handleFocusExpiryDate = React.useCallback((props = {}) => { 207 | return e => { 208 | props.onFocus && props.onFocus(e); 209 | setFocused('expiryDate'); 210 | }; 211 | }, []); 212 | 213 | const handleKeyDownExpiryDate = React.useCallback( 214 | (props = {}) => { 215 | return e => { 216 | props.onKeyDown && props.onKeyDown(e); 217 | 218 | if (e.key === utils.BACKSPACE_KEY_CODE && !e.target.value && autoFocus) { 219 | cardNumberField.current && cardNumberField.current.focus(); 220 | } 221 | }; 222 | }, 223 | [autoFocus] 224 | ); 225 | 226 | const handleKeyPressExpiryDate = React.useCallback((props = {}) => { 227 | return e => { 228 | const formattedExpiryDate = e.target.value || ''; 229 | const expiryDate = formattedExpiryDate.replace(' / ', ''); 230 | 231 | props.onKeyPress && props.onKeyPress(e); 232 | 233 | if (e.key !== utils.ENTER_KEY_CODE) { 234 | if (!utils.validator.isNumeric(e)) { 235 | e.preventDefault(); 236 | } 237 | if (expiryDate.length >= 4) { 238 | e.preventDefault(); 239 | } 240 | } 241 | }; 242 | }, []); 243 | 244 | const getExpiryDateProps = React.useCallback( 245 | ({ refKey, ...props } = {}) => ({ 246 | 'aria-label': 'Expiry date in format MM YY', 247 | autoComplete: 'cc-exp', 248 | id: 'expiryDate', 249 | name: 'expiryDate', 250 | placeholder: 'MM/YY', 251 | type: 'tel', 252 | [refKey || 'ref']: expiryDateField, 253 | ...props, 254 | onBlur: handleBlurExpiryDate(props), 255 | onChange: handleChangeExpiryDate(props), 256 | onFocus: handleFocusExpiryDate(props), 257 | onKeyDown: handleKeyDownExpiryDate(props), 258 | onKeyPress: handleKeyPressExpiryDate(props) 259 | }), 260 | [ 261 | handleBlurExpiryDate, 262 | handleChangeExpiryDate, 263 | handleFocusExpiryDate, 264 | handleKeyDownExpiryDate, 265 | handleKeyPressExpiryDate 266 | ] 267 | ); 268 | /** ====== END: EXPIRY DATE STUFF ====== */ 269 | 270 | /** ====== START: CVC STUFF ====== */ 271 | const handleBlurCVC = React.useCallback( 272 | (props = {}) => { 273 | return e => { 274 | props.onBlur && props.onBlur(e); 275 | onBlur && onBlur(e); 276 | setFocused(undefined); 277 | setInputTouched('cvc', true); 278 | }; 279 | }, 280 | [onBlur, setInputTouched] 281 | ); 282 | 283 | const handleChangeCVC = React.useCallback( 284 | (props = {}, { cardType } = {}) => { 285 | return e => { 286 | const cvc = e.target.value; 287 | 288 | setInputTouched('cvc', false); 289 | 290 | props.onChange && props.onChange(e); 291 | onChange && onChange(e); 292 | 293 | const cvcError = utils.validator.getCVCError(cvc, cvcValidator, { cardType, errorMessages }); 294 | if (!cvcError && autoFocus) { 295 | zipField.current && zipField.current.focus(); 296 | } 297 | setInputError('cvc', cvcError); 298 | props.onError && props.onError(cvcError); 299 | }; 300 | }, 301 | [autoFocus, cvcValidator, errorMessages, onChange, setInputError, setInputTouched] 302 | ); 303 | 304 | const handleFocusCVC = React.useCallback((props = {}) => { 305 | return e => { 306 | props.onFocus && props.onFocus(e); 307 | setFocused('cvc'); 308 | }; 309 | }, []); 310 | 311 | const handleKeyDownCVC = React.useCallback( 312 | (props = {}) => { 313 | return e => { 314 | props.onKeyDown && props.onKeyDown(e); 315 | 316 | if (e.key === utils.BACKSPACE_KEY_CODE && !e.target.value && autoFocus) { 317 | expiryDateField.current && expiryDateField.current.focus(); 318 | } 319 | }; 320 | }, 321 | [autoFocus] 322 | ); 323 | 324 | const handleKeyPressCVC = React.useCallback((props = {}, { cardType }) => { 325 | return e => { 326 | const formattedCVC = e.target.value || ''; 327 | const cvc = formattedCVC.replace(' / ', ''); 328 | 329 | props.onKeyPress && props.onKeyPress(e); 330 | 331 | if (e.key !== utils.ENTER_KEY_CODE) { 332 | if (!utils.validator.isNumeric(e)) { 333 | e.preventDefault(); 334 | } 335 | if (cardType && cvc.length >= cardType.code.length) { 336 | e.preventDefault(); 337 | } 338 | if (cvc.length >= 4) { 339 | e.preventDefault(); 340 | } 341 | } 342 | }; 343 | }, []); 344 | 345 | const getCVCProps = React.useCallback( 346 | ({ refKey, ...props } = {}) => ({ 347 | 'aria-label': 'CVC', 348 | autoComplete: 'cc-csc', 349 | id: 'cvc', 350 | name: 'cvc', 351 | placeholder: cardType ? cardType.code.name : 'CVC', 352 | type: 'tel', 353 | [refKey || 'ref']: cvcField, 354 | ...props, 355 | onBlur: handleBlurCVC(props), 356 | onChange: handleChangeCVC(props, { cardType }), 357 | onFocus: handleFocusCVC(props), 358 | onKeyDown: handleKeyDownCVC(props), 359 | onKeyPress: handleKeyPressCVC(props, { cardType }) 360 | }), 361 | [cardType, handleBlurCVC, handleChangeCVC, handleFocusCVC, handleKeyDownCVC, handleKeyPressCVC] 362 | ); 363 | /** ====== END: CVC STUFF ====== */ 364 | 365 | /** ====== START: ZIP STUFF ====== */ 366 | const handleBlurZIP = React.useCallback( 367 | (props = {}) => { 368 | return e => { 369 | props.onBlur && props.onBlur(e); 370 | onBlur && onBlur(e); 371 | setFocused(undefined); 372 | setInputTouched('zip', true); 373 | }; 374 | }, 375 | [onBlur, setInputTouched] 376 | ); 377 | 378 | const handleChangeZIP = React.useCallback( 379 | (props = {}) => { 380 | return e => { 381 | const zip = e.target.value; 382 | 383 | setInputTouched('zip', false); 384 | 385 | props.onChange && props.onChange(e); 386 | onChange && onChange(e); 387 | 388 | const zipError = utils.validator.getZIPError(zip, { errorMessages }); 389 | setInputError('zip', zipError); 390 | props.onError && props.onError(zipError); 391 | }; 392 | }, 393 | [errorMessages, onChange, setInputError, setInputTouched] 394 | ); 395 | 396 | const handleFocusZIP = React.useCallback((props = {}) => { 397 | return e => { 398 | props.onFocus && props.onFocus(e); 399 | setFocused('zip'); 400 | }; 401 | }, []); 402 | 403 | const handleKeyDownZIP = React.useCallback( 404 | (props = {}) => { 405 | return e => { 406 | props.onKeyDown && props.onKeyDown(e); 407 | 408 | if (e.key === utils.BACKSPACE_KEY_CODE && !e.target.value && autoFocus) { 409 | cvcField.current && cvcField.current.focus(); 410 | } 411 | }; 412 | }, 413 | [autoFocus] 414 | ); 415 | 416 | const handleKeyPressZIP = React.useCallback((props = {}) => { 417 | return e => { 418 | props.onKeyPress && props.onKeyPress(e); 419 | 420 | if (e.key !== utils.ENTER_KEY_CODE) { 421 | if (!utils.validator.isNumeric(e)) { 422 | e.preventDefault(); 423 | } 424 | } 425 | }; 426 | }, []); 427 | 428 | const getZIPProps = React.useCallback( 429 | ({ refKey, ...props } = {}) => ({ 430 | autoComplete: 'off', 431 | id: 'zip', 432 | maxLength: '6', 433 | name: 'zip', 434 | placeholder: 'ZIP', 435 | type: 'tel', 436 | [refKey || 'ref']: zipField, 437 | ...props, 438 | onBlur: handleBlurZIP(props), 439 | onChange: handleChangeZIP(props), 440 | onFocus: handleFocusZIP(props), 441 | onKeyDown: handleKeyDownZIP(props), 442 | onKeyPress: handleKeyPressZIP(props) 443 | }), 444 | [handleBlurZIP, handleChangeZIP, handleFocusZIP, handleKeyDownZIP, handleKeyPressZIP] 445 | ); 446 | /** ====== END: ZIP STUFF ====== */ 447 | 448 | /** ====== START: CARD IMAGE STUFF ====== */ 449 | const getCardImageProps = React.useCallback( 450 | (props = {}) => { 451 | const images = props.images || {}; 452 | return { 453 | 'aria-label': cardType ? cardType.displayName : 'Placeholder card', 454 | children: images[cardType ? cardType.type : 'placeholder'] || images.placeholder, 455 | width: '1.5em', 456 | height: '1em', 457 | viewBox: '0 0 24 16', 458 | ...props 459 | }; 460 | }, 461 | [cardType] 462 | ); 463 | /** ====== END: CARD IMAGE STUFF ====== */ 464 | 465 | // Set default field errors 466 | React.useLayoutEffect( 467 | () => { 468 | if (zipField.current) { 469 | const zipError = utils.validator.getZIPError(zipField.current.value, { errorMessages }); 470 | setInputError('zip', zipError); 471 | } 472 | if (cvcField.current) { 473 | const cvcError = utils.validator.getCVCError(cvcField.current.value, cvcValidator, { errorMessages }); 474 | setInputError('cvc', cvcError); 475 | } 476 | if (expiryDateField.current) { 477 | const expiryDateError = utils.validator.getExpiryDateError(expiryDateField.current.value, expiryValidator, { 478 | errorMessages 479 | }); 480 | setInputError('expiryDate', expiryDateError); 481 | } 482 | if (cardNumberField.current) { 483 | const cardNumberError = utils.validator.getCardNumberError(cardNumberField.current.value, cardNumberValidator, { 484 | errorMessages 485 | }); 486 | setInputError('cardNumber', cardNumberError); 487 | } 488 | }, 489 | [cardNumberValidator, cvcValidator, errorMessages, expiryValidator, setInputError] 490 | ); 491 | 492 | // Format default values 493 | React.useLayoutEffect(() => { 494 | if (cardNumberField.current) { 495 | cardNumberField.current.value = utils.formatter.formatCardNumber(cardNumberField.current.value); 496 | } 497 | if (expiryDateField.current) { 498 | expiryDateField.current.value = utils.formatter.formatExpiry({ target: expiryDateField.current }); 499 | } 500 | }, []); 501 | 502 | // Set default card type 503 | React.useLayoutEffect(() => { 504 | if (cardNumberField.current) { 505 | const cardType = utils.cardTypes.getCardTypeByValue(cardNumberField.current.value); 506 | setCardType(cardType); 507 | } 508 | }, []); 509 | 510 | return { 511 | getCardImageProps, 512 | getCardNumberProps, 513 | getExpiryDateProps, 514 | getCVCProps, 515 | getZIPProps, 516 | wrapperProps: { 517 | error, 518 | focused, 519 | isTouched 520 | }, 521 | 522 | meta: { 523 | cardType, 524 | erroredInputs, 525 | error, 526 | focused, 527 | isTouched, 528 | touchedInputs 529 | } 530 | }; 531 | } 532 | -------------------------------------------------------------------------------- /src/utils/cardTypes.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_CVC_LENGTH = 3; 2 | export const DEFAULT_ZIP_LENGTH = 5; 3 | export const DEFAULT_CARD_FORMAT = /(\d{1,4})/g; 4 | export const CARD_TYPES = [ 5 | { 6 | displayName: 'Visa', 7 | type: 'visa', 8 | format: DEFAULT_CARD_FORMAT, 9 | startPattern: /^4/, 10 | gaps: [4, 8, 12], 11 | lengths: [16, 18, 19], 12 | code: { 13 | name: 'CVV', 14 | length: 3 15 | } 16 | }, 17 | { 18 | displayName: 'Mastercard', 19 | type: 'mastercard', 20 | format: DEFAULT_CARD_FORMAT, 21 | startPattern: /^(5[1-5]|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)/, 22 | gaps: [4, 8, 12], 23 | lengths: [16], 24 | code: { 25 | name: 'CVC', 26 | length: 3 27 | } 28 | }, 29 | { 30 | displayName: 'American Express', 31 | type: 'amex', 32 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 33 | startPattern: /^3[47]/, 34 | gaps: [4, 10], 35 | lengths: [15], 36 | code: { 37 | name: 'CID', 38 | length: 4 39 | } 40 | }, 41 | { 42 | displayName: 'Diners Club', 43 | type: 'dinersclub', 44 | format: DEFAULT_CARD_FORMAT, 45 | startPattern: /^(36|38|30[0-5])/, 46 | gaps: [4, 10], 47 | lengths: [14, 16, 19], 48 | code: { 49 | name: 'CVV', 50 | length: 3 51 | } 52 | }, 53 | { 54 | displayName: 'Discover', 55 | type: 'discover', 56 | format: DEFAULT_CARD_FORMAT, 57 | startPattern: /^(6011|65|64[4-9]|622)/, 58 | gaps: [4, 8, 12], 59 | lengths: [16, 19], 60 | code: { 61 | name: 'CID', 62 | length: 3 63 | } 64 | }, 65 | { 66 | displayName: 'JCB', 67 | type: 'jcb', 68 | format: DEFAULT_CARD_FORMAT, 69 | startPattern: /^35/, 70 | gaps: [4, 8, 12], 71 | lengths: [16, 17, 18, 19], 72 | code: { 73 | name: 'CVV', 74 | length: 3 75 | } 76 | }, 77 | { 78 | displayName: 'UnionPay', 79 | type: 'unionpay', 80 | format: DEFAULT_CARD_FORMAT, 81 | startPattern: /^62/, 82 | gaps: [4, 8, 12], 83 | lengths: [14, 15, 16, 17, 18, 19], 84 | code: { 85 | name: 'CVN', 86 | length: 3 87 | } 88 | }, 89 | { 90 | displayName: 'Maestro', 91 | type: 'maestro', 92 | format: DEFAULT_CARD_FORMAT, 93 | startPattern: /^(5018|5020|5038|6304|6703|6708|6759|676[1-3])/, 94 | gaps: [4, 8, 12], 95 | lengths: [12, 13, 14, 15, 16, 17, 18, 19], 96 | code: { 97 | name: 'CVC', 98 | length: 3 99 | } 100 | }, 101 | { 102 | displayName: 'Elo', 103 | type: 'elo', 104 | format: DEFAULT_CARD_FORMAT, 105 | startPattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/, 106 | gaps: [4, 8, 12], 107 | lengths: [16], 108 | code: { 109 | name: 'CVE', 110 | length: 3 111 | } 112 | }, 113 | { 114 | displayName: 'Hipercard', 115 | type: 'hipercard', 116 | format: DEFAULT_CARD_FORMAT, 117 | startPattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/, 118 | gaps: [4, 8, 12], 119 | lengths: [16], 120 | code: { 121 | name: 'CVC', 122 | length: 3 123 | } 124 | }, 125 | { 126 | displayName: 'Troy', 127 | type: 'troy', 128 | format: DEFAULT_CARD_FORMAT, 129 | startPattern: /^9792/, 130 | gaps: [4, 8, 12], 131 | lengths: [16], 132 | code: { 133 | name: 'CVV', 134 | length: 3 135 | } 136 | } 137 | ]; 138 | 139 | export const getCardTypeByValue = value => CARD_TYPES.filter(cardType => cardType.startPattern.test(value))[0]; 140 | export const getCardTypeByType = type => CARD_TYPES.filter(cardType => cardType.type === type)[0]; 141 | -------------------------------------------------------------------------------- /src/utils/formatter.js: -------------------------------------------------------------------------------- 1 | import * as cardTypes from './cardTypes'; 2 | 3 | export const formatCardNumber = cardNumber => { 4 | const cardType = cardTypes.getCardTypeByValue(cardNumber); 5 | 6 | if (!cardType) return (cardNumber.match(/\d+/g) || []).join(''); 7 | 8 | const format = cardType.format; 9 | if (format && format.global) { 10 | return (cardNumber.match(format) || []).join(' '); 11 | } 12 | 13 | if (format) { 14 | const execResult = format.exec(cardNumber.split(' ').join('')); 15 | if (execResult) { 16 | return execResult 17 | .splice(1, 3) 18 | .filter(x => x) 19 | .join(' '); 20 | } 21 | } 22 | 23 | return cardNumber; 24 | }; 25 | 26 | export const formatExpiry = event => { 27 | const eventData = event.nativeEvent && event.nativeEvent.data; 28 | const prevExpiry = event.target.value.split(' / ').join('/'); 29 | 30 | if (!prevExpiry) return null; 31 | let expiry = prevExpiry; 32 | if (/^[2-9]$/.test(expiry)) { 33 | expiry = `0${expiry}`; 34 | } 35 | 36 | if (prevExpiry.length === 2 && +prevExpiry > 12) { 37 | const [head, ...tail] = prevExpiry.split(''); 38 | expiry = `0${head}/${tail.join('')}`; 39 | } 40 | 41 | if (/^1[/-]$/.test(expiry)) { 42 | return `01 / `; 43 | } 44 | 45 | expiry = expiry.match(/(\d{1,2})/g) || []; 46 | if (expiry.length === 1) { 47 | if (!eventData && prevExpiry.includes('/')) { 48 | return expiry[0]; 49 | } 50 | if (/\d{2}/.test(expiry)) { 51 | return `${expiry[0]} / `; 52 | } 53 | } 54 | if (expiry.length > 2) { 55 | const [, month = null, year = null] = expiry.join('').match(/^(\d{2}).*(\d{2})$/) || []; 56 | return [month, year].join(' / '); 57 | } 58 | return expiry.join(' / '); 59 | }; 60 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import * as cardTypes from './cardTypes'; 2 | import * as formatter from './formatter'; 3 | import * as validator from './validator'; 4 | 5 | export const BACKSPACE_KEY_CODE = 'Backspace'; 6 | export const ENTER_KEY_CODE = 'Enter'; 7 | 8 | export const isHighlighted = () => (window.getSelection() || { type: undefined }).type === 'Range'; 9 | 10 | export default { 11 | cardTypes, 12 | formatter, 13 | validator, 14 | BACKSPACE_KEY_CODE, 15 | ENTER_KEY_CODE, 16 | isHighlighted 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/validator.js: -------------------------------------------------------------------------------- 1 | import * as cardTypes from './cardTypes'; 2 | 3 | const MONTH_REGEX = /(0[1-9]|1[0-2])/; 4 | 5 | export const EMPTY_CARD_NUMBER = 'Enter a card number'; 6 | export const EMPTY_EXPIRY_DATE = 'Enter an expiry date'; 7 | export const EMPTY_CVC = 'Enter a CVC'; 8 | export const EMPTY_ZIP = 'Enter a ZIP code'; 9 | 10 | export const INVALID_CARD_NUMBER = 'Card number is invalid'; 11 | export const INVALID_EXPIRY_DATE = 'Expiry date is invalid'; 12 | export const INVALID_CVC = 'CVC is invalid'; 13 | 14 | export const MONTH_OUT_OF_RANGE = 'Expiry month must be between 01 and 12'; 15 | export const YEAR_OUT_OF_RANGE = 'Expiry year cannot be in the past'; 16 | export const DATE_OUT_OF_RANGE = 'Expiry date cannot be in the past'; 17 | 18 | export const hasCardNumberReachedMaxLength = currentValue => { 19 | const cardType = cardTypes.getCardTypeByValue(currentValue); 20 | return cardType && currentValue.length >= cardType.lengths[cardType.lengths.length - 1]; 21 | }; 22 | 23 | export const isNumeric = e => { 24 | return /^\d*$/.test(e.key); 25 | }; 26 | 27 | export const validateLuhn = cardNumber => { 28 | return ( 29 | cardNumber 30 | .split('') 31 | .reverse() 32 | .map(digit => parseInt(digit, 10)) 33 | .map((digit, idx) => (idx % 2 ? digit * 2 : digit)) 34 | .map(digit => (digit > 9 ? (digit % 10) + 1 : digit)) 35 | .reduce((accum, digit) => (accum += digit)) % 36 | 10 === 37 | 0 38 | ); 39 | }; 40 | export const getCardNumberError = (cardNumber, cardNumberValidator, { errorMessages = {} } = {}) => { 41 | if (!cardNumber) { 42 | return errorMessages.emptyCardNumber || EMPTY_CARD_NUMBER; 43 | } 44 | 45 | const rawCardNumber = cardNumber.replace(/\s/g, ''); 46 | const cardType = cardTypes.getCardTypeByValue(rawCardNumber); 47 | if (cardType && cardType.lengths) { 48 | const doesCardNumberMatchLength = cardType.lengths.includes(rawCardNumber.length); 49 | if (doesCardNumberMatchLength) { 50 | const isLuhnValid = validateLuhn(rawCardNumber); 51 | if (isLuhnValid) { 52 | if (cardNumberValidator) { 53 | return cardNumberValidator({ cardNumber: rawCardNumber, cardType, errorMessages }); 54 | } 55 | return; 56 | } 57 | } 58 | } 59 | return errorMessages.invalidCardNumber || INVALID_CARD_NUMBER; 60 | }; 61 | export const getExpiryDateError = (expiryDate, expiryValidator, { errorMessages = {} } = {}) => { 62 | if (!expiryDate) { 63 | return errorMessages.emptyExpiryDate || EMPTY_EXPIRY_DATE; 64 | } 65 | const rawExpiryDate = expiryDate.replace(' / ', '').replace('/', ''); 66 | if (rawExpiryDate.length === 4) { 67 | const month = rawExpiryDate.slice(0, 2); 68 | const year = `20${rawExpiryDate.slice(2, 4)}`; 69 | if (!MONTH_REGEX.test(month)) { 70 | return errorMessages.monthOutOfRange || MONTH_OUT_OF_RANGE; 71 | } 72 | if (parseInt(year) < new Date().getFullYear()) { 73 | return errorMessages.yearOutOfRange || YEAR_OUT_OF_RANGE; 74 | } 75 | if (parseInt(year) === new Date().getFullYear() && parseInt(month) < new Date().getMonth() + 1) { 76 | return errorMessages.dateOutOfRange || DATE_OUT_OF_RANGE; 77 | } 78 | if (expiryValidator) { 79 | return expiryValidator({ expiryDate: { month, year }, errorMessages }); 80 | } 81 | return; 82 | } 83 | return errorMessages.invalidExpiryDate || INVALID_EXPIRY_DATE; 84 | }; 85 | export const getCVCError = (cvc, cvcValidator, { cardType, errorMessages = {} } = {}) => { 86 | if (!cvc) { 87 | return errorMessages.emptyCVC || EMPTY_CVC; 88 | } 89 | if (cvc.length < 3) { 90 | return errorMessages.invalidCVC || INVALID_CVC; 91 | } 92 | if (cardType && cvc.length !== cardType.code.length) { 93 | return errorMessages.invalidCVC || INVALID_CVC; 94 | } 95 | if (cvcValidator) { 96 | return cvcValidator({ cvc, cardType, errorMessages }); 97 | } 98 | return; 99 | }; 100 | export const getZIPError = (zip, { errorMessages = {} } = {}) => { 101 | if (!zip) { 102 | return errorMessages.emptyZIP || EMPTY_ZIP; 103 | } 104 | return; 105 | }; 106 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { Formik, Field as FormikField } from 'formik'; 4 | import { Form, Field as FinalFormField } from 'react-final-form'; 5 | import { css, Button, FieldSet, InputField } from 'fannypack'; 6 | import { Col, Form as BSForm } from 'react-bootstrap'; 7 | import 'bootstrap/dist/css/bootstrap.css'; 8 | 9 | import { PaymentInputsContainer, PaymentInputsWrapper, usePaymentInputs } from '../src'; 10 | import images from '../src/images'; 11 | 12 | storiesOf('usePaymentInputs', module) 13 | .add('basic (no styles)', () => { 14 | function Component() { 15 | const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps } = usePaymentInputs(); 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 | {meta.error && meta.isTouched &&
{meta.error}
} 28 |
29 | ); 30 | } 31 | 32 | return ; 33 | }) 34 | .add('styled wrapper', () => { 35 | function Component() { 36 | const { 37 | getCardNumberProps, 38 | getExpiryDateProps, 39 | getCVCProps, 40 | getCardImageProps, 41 | wrapperProps 42 | } = usePaymentInputs(); 43 | return ( 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | 55 | return ; 56 | }) 57 | .add('styled wrapper (with ZIP)', () => { 58 | function Component() { 59 | const { 60 | getCardNumberProps, 61 | getExpiryDateProps, 62 | getCVCProps, 63 | getZIPProps, 64 | getCardImageProps, 65 | wrapperProps 66 | } = usePaymentInputs(); 67 | return ( 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | ); 78 | } 79 | 80 | return ; 81 | }) 82 | .add('styled wrapper (no CVC)', () => { 83 | function Component() { 84 | const { getCardNumberProps, getExpiryDateProps, getCardImageProps, wrapperProps } = usePaymentInputs(); 85 | return ( 86 |
87 | 88 | 89 | 90 | 91 | 92 |
93 | ); 94 | } 95 | 96 | return ; 97 | }) 98 | .add('styled wrapper (with custom styling)', () => { 99 | function Component() { 100 | const { 101 | getCardNumberProps, 102 | getExpiryDateProps, 103 | getCVCProps, 104 | getCardImageProps, 105 | wrapperProps 106 | } = usePaymentInputs(); 107 | return ( 108 |
109 | 155 | 156 | 157 | 158 | 159 | 160 |
161 | ); 162 | } 163 | 164 | return ; 165 | }) 166 | .add('custom error messages', () => { 167 | function Component() { 168 | const { getCardNumberProps, getExpiryDateProps, getCVCProps, getCardImageProps, wrapperProps } = usePaymentInputs( 169 | { 170 | errorMessages: { 171 | emptyCardNumber: 'El número de la tarjeta es inválido', 172 | invalidCardNumber: 'El número de la tarjeta es inválido', 173 | emptyExpiryDate: 'La fecha de expiración es inválida', 174 | monthOutOfRange: 'El mes de expiración debe estar entre 01 y 12', 175 | yearOutOfRange: 'El año de expiración no puede estar en el pasado', 176 | dateOutOfRange: 'La fecha de expiración no puede estar en el pasado', 177 | invalidExpiryDate: 'La fecha de expiración es inválida', 178 | emptyCVC: 'El código de seguridad es inválido', 179 | invalidCVC: 'El código de seguridad es inválido' 180 | } 181 | } 182 | ); 183 | return ( 184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 |
192 | ); 193 | } 194 | 195 | return ; 196 | }) 197 | .add('using a UI library (Fannypack)', () => { 198 | function Component() { 199 | const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps } = usePaymentInputs(); 200 | return ( 201 |
202 | 210 | 217 | 225 |
226 | ); 227 | } 228 | 229 | return ; 230 | }) 231 | .add('using a UI library (Bootstrap)', () => { 232 | function Component() { 233 | const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps } = usePaymentInputs(); 234 | return ( 235 | 236 | 237 | 238 | Card number 239 | 244 | {meta.erroredInputs.cardNumber} 245 | 246 | 247 | Expiry date 248 | 252 | {meta.erroredInputs.expiryDate} 253 | 254 | 255 | CVC 256 | 261 | {meta.erroredInputs.cvc} 262 | 263 | 264 | 265 | ); 266 | } 267 | 268 | return ; 269 | }) 270 | .add('using a form library (Formik)', () => { 271 | function Component() { 272 | const { 273 | meta, 274 | getCardImageProps, 275 | getCardNumberProps, 276 | getExpiryDateProps, 277 | getCVCProps, 278 | wrapperProps 279 | } = usePaymentInputs(); 280 | 281 | return ( 282 | console.log(data)} 289 | validate={() => { 290 | let errors = {}; 291 | if (meta.erroredInputs.cardNumber) { 292 | errors.cardNumber = meta.erroredInputs.cardNumber; 293 | } 294 | if (meta.erroredInputs.expiryDate) { 295 | errors.expiryDate = meta.erroredInputs.expiryDate; 296 | } 297 | if (meta.erroredInputs.cvc) { 298 | errors.cvc = meta.erroredInputs.cvc; 299 | } 300 | return errors; 301 | }} 302 | > 303 | {({ handleSubmit }) => ( 304 |
305 |
306 | 307 | 308 | 309 | {({ field }) => ( 310 | 311 | )} 312 | 313 | 314 | {({ field }) => ( 315 | 316 | )} 317 | 318 | 319 | {({ field }) => } 320 | 321 | 322 |
323 | 326 |
327 | )} 328 |
329 | ); 330 | } 331 | 332 | return ; 333 | }) 334 | .add('using a form library (React Final Form)', () => { 335 | function Component() { 336 | const { 337 | meta, 338 | getCardImageProps, 339 | getCardNumberProps, 340 | getExpiryDateProps, 341 | getCVCProps, 342 | wrapperProps 343 | } = usePaymentInputs(); 344 | 345 | return ( 346 |
console.log(data)} 348 | validate={() => { 349 | let errors = {}; 350 | if (meta.erroredInputs.cardNumber) { 351 | errors.cardNumber = meta.erroredInputs.cardNumber; 352 | } 353 | if (meta.erroredInputs.expiryDate) { 354 | errors.expiryDate = meta.erroredInputs.expiryDate; 355 | } 356 | if (meta.erroredInputs.cvc) { 357 | errors.cvc = meta.erroredInputs.cvc; 358 | } 359 | return errors; 360 | }} 361 | > 362 | {({ handleSubmit }) => ( 363 | 364 |
365 | 366 | 367 | 368 | {({ input }) => ( 369 | 370 | )} 371 | 372 | 373 | {({ input }) => ( 374 | 375 | )} 376 | 377 | 378 | {({ input }) => } 379 | 380 | 381 |
382 | 385 |
386 | )} 387 | 388 | ); 389 | } 390 | 391 | return ; 392 | }); 393 | 394 | storiesOf('PaymentInputsContainer', module) 395 | .add('basic (no styles)', () => { 396 | function Component() { 397 | return ( 398 | 399 | {({ getCardNumberProps, getExpiryDateProps, getCVCProps }) => ( 400 |
401 |
402 | 403 |
404 |
405 | 406 |
407 |
408 | 409 |
410 |
411 | )} 412 |
413 | ); 414 | } 415 | 416 | return ; 417 | }) 418 | .add('styled wrapper', () => { 419 | function Component() { 420 | return ( 421 | 422 | {({ getCardNumberProps, getExpiryDateProps, getCVCProps, getCardImageProps, wrapperProps }) => ( 423 | 424 | 425 | 426 | 427 | 428 | 429 | )} 430 | 431 | ); 432 | } 433 | 434 | return ; 435 | }) 436 | .add('styled wrapper (with ZIP)', () => { 437 | function Component() { 438 | return ( 439 | 440 | {({ getCardNumberProps, getExpiryDateProps, getCVCProps, getZIPProps, getCardImageProps, wrapperProps }) => ( 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | )} 449 | 450 | ); 451 | } 452 | 453 | return ; 454 | }) 455 | .add('styled wrapper (no CVC)', () => { 456 | function Component() { 457 | return ( 458 | 459 | {({ getCardNumberProps, getExpiryDateProps, getCardImageProps, wrapperProps }) => ( 460 | 461 | 462 | 463 | 464 | 465 | )} 466 | 467 | ); 468 | } 469 | 470 | return ; 471 | }) 472 | .add('custom error messages', () => { 473 | function Component() { 474 | return ( 475 | 488 | {({ getCardNumberProps, getExpiryDateProps, getCardImageProps, getCVCProps, wrapperProps }) => ( 489 | 490 | 491 | 492 | 493 | 494 | 495 | )} 496 | 497 | ); 498 | } 499 | 500 | return ; 501 | }) 502 | .add('styled wrapper (with custom styling)', () => { 503 | function Component() { 504 | return ( 505 | 506 | {({ getCardNumberProps, getExpiryDateProps, getCVCProps, getCardImageProps, wrapperProps }) => ( 507 | 547 | 548 | 549 | 550 | 551 | 552 | )} 553 | 554 | ); 555 | } 556 | 557 | return ; 558 | }); 559 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import {render, unmountComponentAtNode} from 'react-dom' 4 | 5 | import Component from 'src/' 6 | 7 | describe('Component', () => { 8 | let node 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(() => { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('displays a welcome message', () => { 19 | render(, node, () => { 20 | expect(node.innerHTML).toContain('Welcome to React components') 21 | }) 22 | }) 23 | }) 24 | --------------------------------------------------------------------------------