Skip to content

medipass/react-payment-inputs

Repository files navigation

React Payment Inputs

A React Hook & Container to help with payment card input fields.

Requirements

Ensure you are running on a hooks-compatible version of React (v16.8 & above).

Installation

npm install react-payment-inputs --save

or install with Yarn if you prefer:

yarn add react-payment-inputs

Usage

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.

However, if you would like to use the built-in styles as seen in the animation above, read "Using the built-in styled wrapper".

With hooks

If you'd like to use the hooks version of React Payment Inputs, you can import usePaymentInputs into your component.

import React from 'react';
import { usePaymentInputs } from 'react-payment-inputs';

export default function PaymentInputs() {
  const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps } = usePaymentInputs();

  return (
    <div>
      <input {...getCardNumberProps({ onChange: handleChangeCardNumber })} value={cardNumber} />
      <input {...getExpiryDateProps({ onChange: handleChangeExpiryDate })} value={expiryDate} />
      <input {...getCVCProps({ onChange: handleChangeCVC })} value={cvc} />
      {meta.isTouched && meta.error && <span>Error: {meta.error}</span>}
    </div>
  );
}

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.

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.

With render props

If you'd like to use the render props version of React Payment Inputs, you can import PaymentInputsContainer into your component.

The props of <PaymentInputsContainer> are the same as the hook options and the render props are the same as the hook data.

import React from 'react';
import { PaymentInputsContainer } from 'react-payment-inputs';

export default function PaymentInputs() {
  return (
    <PaymentInputsContainer>
      {({ meta, getCardNumberProps, getExpiryDateProps, getCVCProps }) => (
        <div>
          <input {...getCardNumberProps({ onChange: handleChangeCardNumber })} value={cardNumber} />
          <input {...getExpiryDateProps({ onChange: handleChangeExpiryDate })} value={expiryDate} />
          <input {...getCVCProps({ onChange: handleChangeCVC })} value={cvc} />
          {meta.isTouched && meta.error && <span>Error: {meta.error}</span>}
        </div>
      )}
    </PaymentInputsContainer>
  );
}

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.

Using the built-in styled wrapper

Note: <PaymentInputsWrapper> requires styled-components to be installed as a dependency.

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:

import React from 'react';
import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs';
import images from 'react-payment-inputs/images';

export default function PaymentInputs() {
  const {
    wrapperProps,
    getCardImageProps,
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps
  } = usePaymentInputs();

  return (
    <PaymentInputsWrapper {...wrapperProps}>
      <svg {...getCardImageProps({ images })} />
      <input {...getCardNumberProps()} />
      <input {...getExpiryDateProps()} />
      <input {...getCVCProps()} />
    </PaymentInputsWrapper>
  );
}

More examples

data = usePaymentInputs(options)

returns an object (data)

options

Object({ cardNumberValidator, cvcValidator, errorMessages, expiryValidator, onBlur, onChange, onError, onTouch })

options.cardNumberValidator

function({cardNumber, cardType, errorMessages})

Set custom card number validator function

Example
const cardNumberValidator = ({ cardNumber, cardType, errorMessages }) => {
  if (cardType.displayName === 'Visa' || cardType.displayName === 'Mastercard') {
    return;
  }
  return 'Card must be Visa or Mastercard';
}

export default function MyComponent() {
  const { ... } = usePaymentInputs({
    cardNumberValidator
  });
}

options.cvcValidator

function({cvc, cardType, errorMessages})

Set custom cvc validator function

options.errorMessages

Object

Set custom error messages for the inputs.

Example
const ERROR_MESSAGES = {
  emptyCardNumber: 'El número de la tarjeta es inválido',
  invalidCardNumber: 'El número de la tarjeta es inválido',
  emptyExpiryDate: 'La fecha de expiración es inválida',
  monthOutOfRange: 'El mes de expiración debe estar entre 01 y 12',
  yearOutOfRange: 'El año de expiración no puede estar en el pasado',
  dateOutOfRange: 'La fecha de expiración no puede estar en el pasado',
  invalidExpiryDate: 'La fecha de expiración es inválida',
  emptyCVC: 'El código de seguridad es inválido',
  invalidCVC: 'El código de seguridad es inválido'
}

export default function MyComponent() {
  const { ... } = usePaymentInputs({
    errorMessages: ERROR_MESSAGES
  });
}

options.expiryDateValidator

function({expiryDate, errorMessages})

Set custom expiry date validator function

options.onBlur

function(event)

Function to handle the blur event on the inputs. It is invoked when any of the inputs blur.

options.onChange

function(event)

Function to handle the change event on the inputs. It is invoked when any of the inputs change.

options.onError

function(error, erroredInputs)

Function to invoke when any of the inputs error.

options.onTouch

function(touchedInput, touchedInputs)

Function to invoke when any of the inputs are touched.

data

getCardNumberProps

function(overrideProps) | returns Object<props>

Returns the props to apply to the card number input.

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.

Example snippet
<input {...getCardNumberProps({ onBlur: handleBlur, onChange: handleChange })} />

getExpiryDateProps

function(overrideProps) | returns Object<props>

Returns the props to apply to the expiry date input.

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.

Example snippet
<input {...getExpiryDateProps({ onBlur: handleBlur, onChange: handleChange })} />

getCVCProps

function(overrideProps) | returns Object<props>

Returns the props to apply to the CVC input.

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.

Example snippet
<input {...getCVCProps({ onBlur: handleBlur, onChange: handleChange })} />

getZIPProps

function(overrideProps) | returns Object<props>

Returns the props to apply to the ZIP input.

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.

Example snippet
<input {...getZIPProps({ onBlur: handleBlur, onChange: handleChange })} />

getCardImageProps

function({ images }) | returns Object<props>

Returns the props to apply to the card image SVG.

This function only supports SVG elements currently. If you have a need for another format, please raise an issue.

You can also supply custom card images using the images attribute. The example below uses the default card images from React Payment Inputs.

Example snippet
import images from 'react-payment-inputs/images';

<svg {...getCardImageProps({ images })} />

meta.cardType

Object

Returns information about the current card type, including: name, lengths and formats.

Example snippet
const { meta } = usePaymentInputs();

<span>Current card: {meta.cardType.displayName}</span>

meta.error

string

Returns the current global error between all rendered inputs.

Example snippet
const { meta } = usePaymentInputs();

console.log(meta.error); // "Card number is invalid"

meta.isTouched

boolean

Returns the current global touched state between all rendered inputs.

meta.erroredInputs

Object

Returns the error message of each rendered input.

Example snippet
const { meta } = usePaymentInputs();

console.log(meta.erroredInputs);
/*
{
  cardNumber: undefined,
  expiryDate: 'Enter an expiry date',
  cvc: 'Enter a CVC'
}
*/

meta.touchedInputs

Object

Returns the touch state of each rendered input.

Example snippet
const { meta } = usePaymentInputs();

console.log(meta.touchedInputs);
/*
{
  cardNumber: true,
  expiryDate: true,
  cvc: false
}
*/

meta.focused

string

Returns the current focused input.

const { meta } = usePaymentInputs();

console.log(meta.focused); // "cardNumber"

wrapperProps

Object

Returns the props to apply to <PaymentInputsWrapper>.

<PaymentInputsWrapper> props

styles

Object

Custom styling to pass through to the wrapper. Either a styled-component's css or an Object can be passed.

Schema

{
  fieldWrapper: {
    base: css | Object,
    errored: css | Object
  },
  inputWrapper: {
    base: css | Object,
    errored: css | Object,
    focused: css | Object
  },
  input: {
    base: css | Object,
    errored: css | Object,
    cardNumber: css | Object,
    expiryDate: css | Object,
    cvc: css | Object
  },
  errorText: {
    base: css | Object
  }
}

errorTextProps

Object

Custom props to pass to the error text component.

inputWrapperProps

Object

Custom props to pass to the input wrapper component.

Using a third-party UI library

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 <PaymentInputsContainer>.

Fannypack

import React from 'react';
import { FieldSet, InputField } from 'fannypack';
import { usePaymentInputs } from 'react-payment-inputs';
import images from 'react-payment-inputs/images';

export default function PaymentInputs() {
  const {
    meta,
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps
  } = usePaymentInputs();
  const { erroredInputs, touchedInputs } = meta;

  return (
    <FieldSet isHorizontal>
      <InputField
        // Here is where React Payment Inputs injects itself into the input element.
        {...getCardNumberProps()}
        placeholder="0000 0000 0000 0000"
        label="Card number"
        inputRef={getCardNumberProps().ref}
        // You can retrieve error state by making use of the error & touched attributes in `meta`.
        state={erroredInputs.cardNumber && touchedInputs.cardNumber ? 'danger' : undefined}
        validationText={touchedInputs.cardNumber && erroredInputs.cardNumber}
        maxWidth="15rem"
      />
      <InputField
        {...getExpiryDateProps()}
        label="Expiry date"
        inputRef={getExpiryDateProps().ref}
        state={erroredInputs.expiryDate && touchedInputs.expiryDate ? 'danger' : undefined}
        validationText={touchedInputs.expiryDate && erroredInputs.expiryDate}
        maxWidth="8rem"
      />
      <InputField
        {...getCVCProps()}
        placeholder="123"
        label="CVC"
        inputRef={getCVCProps().ref}
        state={erroredInputs.cvc && touchedInputs.cvc ? 'danger' : undefined}
        validationText={touchedInputs.cvc && erroredInputs.cvc}
        maxWidth="5rem"
      />
    </FieldSet>
  );
}

Bootstrap

import React from 'react';
import { FieldSet, InputField } from 'fannypack';
import { usePaymentInputs } from 'react-payment-inputs';
import images from 'react-payment-inputs/images';

export default function PaymentInputs() {
  const {
    meta,
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps
  } = usePaymentInputs();
  const { erroredInputs, touchedInputs } = meta;

  return (
    <Form>
      <Form.Row>
        <Form.Group as={Col} style={{ maxWidth: '15rem' }}>
          <Form.Label>Card number</Form.Label>
          <Form.Control
            // Here is where React Payment Inputs injects itself into the input element.
            {...getCardNumberProps()}
            // You can retrieve error state by making use of the error & touched attributes in `meta`.
            isInvalid={touchedInputs.cardNumber && erroredInputs.cardNumber}
            placeholder="0000 0000 0000 0000"
          />
          <Form.Control.Feedback type="invalid">{erroredInputs.cardNumber}</Form.Control.Feedback>
        </Form.Group>
        <Form.Group as={Col} style={{ maxWidth: '10rem' }}>
          <Form.Label>Expiry date</Form.Label>
          <Form.Control
            {...getExpiryDateProps()}
            isInvalid={touchedInputs.expiryDate && erroredInputs.expiryDate}
          />
          <Form.Control.Feedback type="invalid">{erroredInputs.expiryDate}</Form.Control.Feedback>
        </Form.Group>
        <Form.Group as={Col} style={{ maxWidth: '7rem' }}>
          <Form.Label>CVC</Form.Label>
          <Form.Control
            {...getCVCProps()}
            isInvalid={touchedInputs.cvc && erroredInputs.cvc}
            placeholder="123"
          />
          <Form.Control.Feedback type="invalid">{erroredInputs.cvc}</Form.Control.Feedback>
        </Form.Group>
      </Form.Row>
    </Form>
  );
}

Form library examples

React Payment Inputs has support for any type of React form library. Below are examples using Formik & React Final Form.

Formik

import { Formik, Field } from 'formik';
import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs';

function PaymentForm() {
  const {
    meta,
    getCardImageProps,
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps,
    wrapperProps
  } = usePaymentInputs();

  return (
    <Formik
      initialValues={{
        cardNumber: '',
        expiryDate: '',
        cvc: ''
      }}
      onSubmit={data => console.log(data)}
      validate={() => {
        let errors = {};
        if (meta.erroredInputs.cardNumber) {
          errors.cardNumber = meta.erroredInputs.cardNumber;
        }
        if (meta.erroredInputs.expiryDate) {
          errors.expiryDate = meta.erroredInputs.expiryDate;
        }
        if (meta.erroredInputs.cvc) {
          errors.cvc = meta.erroredInputs.cvc;
        }
        return errors;
      }}
    >
      {({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <PaymentInputsWrapper {...wrapperProps}>
              <svg {...getCardImageProps({ images })} />
              <Field name="cardNumber">
                {({ field }) => (
                  <input {...getCardNumberProps({ onBlur: field.onBlur, onChange: field.onChange })} />
                )}
              </Field>
              <Field name="expiryDate">
                {({ field }) => (
                  <input {...getExpiryDateProps({ onBlur: field.onBlur, onChange: field.onChange })} />
                )}
              </Field>
              <Field name="cvc">
                {({ field }) => <input {...getCVCProps({ onBlur: field.onBlur, onChange: field.onChange })} />}
              </Field>
            </PaymentInputsWrapper>
          </div>
          <Button marginTop="major-2" type="submit">
            Submit
          </Button>
        </form>
      )}
    </Formik>
  );
}

See this example in Storybook

React Final Form

import { Form, Field } from 'react-final-form';
import { PaymentInputsWrapper, usePaymentInputs } from 'react-payment-inputs';

function PaymentForm() {
  const {
    meta,
    getCardImageProps,
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps,
    wrapperProps
  } = usePaymentInputs();

  return (
    <Form
      onSubmit={data => console.log(data)}
      validate={() => {
        let errors = {};
        if (meta.erroredInputs.cardNumber) {
          errors.cardNumber = meta.erroredInputs.cardNumber;
        }
        if (meta.erroredInputs.expiryDate) {
          errors.expiryDate = meta.erroredInputs.expiryDate;
        }
        if (meta.erroredInputs.cvc) {
          errors.cvc = meta.erroredInputs.cvc;
        }
        return errors;
      }}
    >
      {({ handleSubmit }) => (
        <form onSubmit={handleSubmit}>
          <div>
            <PaymentInputsWrapper {...wrapperProps}>
              <svg {...getCardImageProps({ images })} />
              <Field name="cardNumber">
                {({ input }) => (
                  <input {...getCardNumberProps({ onBlur: input.onBlur, onChange: input.onChange })} />
                )}
              </Field>
              <Field name="expiryDate">
                {({ input }) => (
                  <input {...getExpiryDateProps({ onBlur: input.onBlur, onChange: input.onChange })} />
                )}
              </Field>
              <Field name="cvc">
                {({ input }) => <input {...getCVCProps({ onBlur: input.onBlur, onChange: input.onChange })} />}
              </Field>
            </PaymentInputsWrapper>
          </div>
          <Button marginTop="major-2" type="submit">
            Submit
          </Button>
        </form>
      )}
    </Form>
  );
}

See this example in Storybook

Customising the in-built style wrapper

React Payment Input's default style wrapper can be customized by supplying a styles prop.

import { css } from 'styled-components';
import { usePaymentInputs, PaymentInputsWrapper } from 'react-payment-inputs';

function PaymentForm() {
  const {
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps,
    wrapperProps
  } = usePaymentInputs();

  return (
    <PaymentInputsWrapper
      {...wrapperProps}
      styles={{
        fieldWrapper: {
          base: css`
            margin-bottom: 1rem;
          `
        },
        inputWrapper: {
          base: css`
            border-color: green;
          `,
          errored: css`
            border-color: maroon;
          `,
          focused: css`
            border-color: unset;
            box-shadow: unset;
            outline: 2px solid blue;
            outline-offset: 2px;
          `
        },
        input: {
          base: css`
            color: green;
          `,
          errored: css`
            color: maroon;
          `,
          cardNumber: css`
            width: 15rem;
          `,
          expiryDate: css`
            width: 10rem;
          `,
          cvc: css`
            width: 5rem;
          `
        },
        errorText: {
          base: css`
            color: maroon;
          `
        }
      }}
    >
      <input {...getCardNumberProps()} />
      <input {...getExpiryDateProps()} />
      <input {...getCVCProps()} />
    </PaymentInputsWrapper>
  );
}

See the example on Storybook

Custom card images

The card images can be customized by passing the images attribute to getCardImageProps({ images }). The images object must consist of SVG paths.

import { css } from 'styled-components';
import { usePaymentInputs, PaymentInputsWrapper } from 'react-payment-inputs';

const images = {
  mastercard: (
    <g fill="none" fillRule="evenodd">
      <rect fill="#252525" height="16" rx="2" width="24" />
      <circle cx="9" cy="8" fill="#eb001b" r="5" />
      <circle cx="15" cy="8" fill="#f79e1b" r="5" />
      <path
        d="m12 3.99963381c1.2144467.91220633 2 2.36454836 2 4.00036619s-.7855533 3.0881599-2 4.0003662c-1.2144467-.9122063-2-2.36454837-2-4.0003662s.7855533-3.08815986 2-4.00036619z"
        fill="#ff5f00"
      />
    </g>
  )
}

function PaymentForm() {
  const {
    getCardNumberProps,
    getExpiryDateProps,
    getCVCProps,
    getCardImageProps,
    wrapperProps
  } = usePaymentInputs();

  return (
    <PaymentInputsWrapper {...wrapperProps}>
      <svg {...getCardImageProps({ images })} />
      <input {...getCardNumberProps()} />
      <input {...getExpiryDateProps()} />
      <input {...getCVCProps()} />
    </PaymentInputsWrapper>
  );
}

License

MIT © Medipass Solutions Pty. Ltd.