diff --git a/package.json b/package.json
index 8d8002b..b2096b2 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"rimraf": "^2.6.2",
"rollup": "^0.54.1",
"rollup-plugin-babel": "^3.0.3",
+ "rollup-plugin-node-resolve": "^4.2.3",
"styled-components": "4.2.0"
},
"peerDependencies": {
diff --git a/rollup.config.js b/rollup.config.js
index 81e7041..f43c4f5 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,33 +1,47 @@
import babel from 'rollup-plugin-babel';
+// https://github.com/rollup/rollup/issues/346
+import resolve from 'rollup-plugin-node-resolve';
-export default {
+const globals = {
+ react: 'React',
+ payment: 'payment',
+ 'credit-card-type': 'creditCardType',
+ 'styled-components': 'styled'
+};
+
+const external = [
+ 'react',
+ 'credit-card-type',
+ 'payment',
+ 'styled-components'
+];
+
+const plugins = [
+ resolve({}),
+ babel({
+ presets: [['env', { modules: false }], 'react', 'flow'],
+ plugins: [
+ 'external-helpers',
+ 'transform-class-properties',
+ 'transform-object-rest-spread'
+ ]
+ })
+]
+
+export default [{
input: `${__dirname}/src/index.js`,
output: {
name: 'CreditCardInput',
file: `${__dirname}/lib/index.js`,
format: 'umd',
- globals: {
- react: 'React',
- payment: 'payment',
- 'credit-card-type': 'creditCardType',
- 'styled-components': 'styled'
- }
- },
- sourcemap: true,
- external: [
- 'react',
- 'credit-card-type',
- 'payment',
- 'styled-components'
- ],
- plugins: [
- babel({
- presets: [['env', { modules: false }], 'react', 'flow'],
- plugins: [
- 'external-helpers',
- 'transform-class-properties',
- 'transform-object-rest-spread'
- ]
- })
- ]
-};
+ globals
+ }
+}, {
+ input: `${__dirname}/src/hooks/use-credit-card-input.js`,
+ output: {
+ name: 'useCreditCardInput',
+ file: `${__dirname}/lib/useCreditCardInput.js`,
+ format: 'umd',
+ globals
+ }
+}].map(output => Object.assign(output, {external, plugins, sourcemap: true}));
diff --git a/src/credit-card-input.js b/src/credit-card-input.js
new file mode 100644
index 0000000..a316032
--- /dev/null
+++ b/src/credit-card-input.js
@@ -0,0 +1,214 @@
+// @flow
+
+import React, { useEffect } from 'react';
+import styled from 'styled-components';
+import useCreditCardInput from './hooks/use-credit-card-input';
+
+const Container = styled.div`
+ display: inline-block;
+ ${({ styled }) => ({ ...styled })};
+`;
+
+const FieldWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ background-color: white;
+ padding: 10px;
+ border-radius: 3px;
+ overflow: hidden;
+ ${({ styled }) => ({ ...styled })};
+
+ &.is-invalid {
+ border: 1px solid #ff3860;
+ ${({ invalidStyled }) => ({ ...invalidStyled })};
+ }
+`;
+
+const CardImage = styled.img`
+ height: 1em;
+ ${({ styled }) => ({ ...styled })};
+`;
+
+const InputWrapper = styled.label`
+ align-items: center;
+ display: ${props => (props.isActive ? 'flex' : 'none')};
+ margin-left: 0.5em;
+ position: relative;
+ transition: transform 0.5s;
+ transform: translateX(${props => (props.translateX ? '4rem' : '0')});
+
+ &::after {
+ content: attr(data-max);
+ visibility: hidden;
+ height: 0;
+ }
+
+ & .credit-card-input {
+ border: 0px;
+ position: absolute;
+ width: 100%;
+ font-size: 1em;
+ ${({ inputStyled }) => ({ ...inputStyled })};
+
+ &:focus {
+ outline: 0px;
+ }
+ }
+
+ & .zip-input {
+ display: ${props => (props.isZipActive ? 'flex' : 'none')};
+ }
+`;
+
+const DangerText = styled.p`
+ font-size: 0.8rem;
+ margin: 5px 0 0 0;
+ color: #ff3860;
+ ${({ styled }) => ({ ...styled })};
+`;
+
+type CreditCardInputProps = {
+ cardTypes: Object,
+ onError?: Function,
+ cardExpiryInputProps: Object,
+ cardNumberInputProps: Object,
+ cardCVCInputProps: Object,
+ cardZipInputProps: Object,
+ cardImageClassName: string,
+ cardImageStyle: Object,
+ containerClassName: string,
+ containerStyle: Object,
+ dangerTextClassName: string,
+ dangerTextStyle: Object,
+ fieldClassName: string,
+ fieldStyle: Object,
+ images: Object,
+ inputClassName: string,
+ inputStyle: Object,
+ invalidClassName: string,
+ invalidStyle: Object,
+ customTextLabels: Object
+};
+
+const defaultProps: CreditCardInputProps = {
+ cardExpiryInputProps: {},
+ cardNumberInputProps: {},
+ cardCVCInputProps: {},
+ cardZipInputProps: {},
+ cardImageClassName: '',
+ cardImageStyle: {},
+ containerClassName: '',
+ containerStyle: {},
+ dangerTextClassName: '',
+ dangerTextStyle: {},
+ fieldClassName: '',
+ fieldStyle: {},
+ inputClassName: '',
+ inputStyle: {},
+ invalidClassName: 'is-invalid',
+ invalidStyle: {},
+ customTextLabels: {}
+};
+
+const CreditCardInput = (props: CreditCardInputProps) => {
+ const {
+ state,
+ cardNumberProps,
+ cardExpiryProps,
+ cardCvcProps,
+ cardZipProps
+ } = useCreditCardInput(props);
+
+ const {
+ cardImageClassName,
+ cardImageStyle,
+ containerClassName,
+ containerStyle,
+ dangerTextClassName,
+ dangerTextStyle,
+ fieldClassName,
+ fieldStyle,
+ inputStyle,
+ invalidClassName,
+ invalidStyle
+ } = props;
+
+ const { cardImage, enableZipInput, errorText, showZip } = state;
+
+ const setFieldInvalid = (errorText: string) => {
+ // $FlowFixMe
+ document.getElementById('field-wrapper').classList.add(invalidClassName);
+ };
+
+ const setFieldValid = () => {
+ // $FlowFixMe
+ document.getElementById('field-wrapper').classList.remove(invalidClassName);
+ };
+
+ useEffect(() => {
+ if (errorText) {
+ setFieldInvalid(errorText);
+ } else {
+ setFieldValid();
+ }
+ }, [errorText]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {errorText && (
+
+ {errorText}
+
+ )}
+
+ );
+};
+
+CreditCardInput.defaultProps = defaultProps;
+
+export default CreditCardInput;
diff --git a/src/events/events.js b/src/events/events.js
new file mode 100644
index 0000000..4272766
--- /dev/null
+++ b/src/events/events.js
@@ -0,0 +1,30 @@
+export default function() {
+ var topics = {};
+ var hOP = topics.hasOwnProperty;
+
+ return {
+ subscribe: function(topic, listener) {
+ // Create the topic's object if not yet created
+ if (!hOP.call(topics, topic)) topics[topic] = [];
+
+ // Add the listener to queue
+ var index = topics[topic].push(listener) - 1;
+
+ // Provide handle back for removal of topic
+ return {
+ remove: function() {
+ delete topics[topic][index];
+ }
+ };
+ },
+ publish: function(topic, info) {
+ // If the topic doesn't exist, or there's no listeners in queue, just leave
+ if (!hOP.call(topics, topic)) return;
+
+ // Cycle through topics queue, fire!
+ topics[topic].forEach(function(item) {
+ item(info !== undefined ? info : {});
+ });
+ }
+ };
+}
diff --git a/src/events/index.js b/src/events/index.js
new file mode 100644
index 0000000..b890785
--- /dev/null
+++ b/src/events/index.js
@@ -0,0 +1,9 @@
+import events from './events';
+import * as types from './types';
+
+export default function() {
+ return {
+ ...events(),
+ types
+ };
+}
diff --git a/src/events/types.js b/src/events/types.js
new file mode 100644
index 0000000..3ee911e
--- /dev/null
+++ b/src/events/types.js
@@ -0,0 +1,4 @@
+export const FOCUS_ON_CARD_NUMBER = Symbol('FOCUS_ON_CARD_NUMBER');
+export const FOCUS_ON_CARD_EXPIRY = Symbol('FOCUS_ON_CARD_EXPIRY');
+export const FOCUS_ON_CARD_CVC = Symbol('FOCUS_ON_CARD_CVC');
+export const FOCUS_ON_CARD_ZIP = Symbol('FOCUS_ON_CARD_ZIP');
diff --git a/src/hooks/use-card-cvc-input.js b/src/hooks/use-card-cvc-input.js
new file mode 100644
index 0000000..3da6cec
--- /dev/null
+++ b/src/hooks/use-card-cvc-input.js
@@ -0,0 +1,126 @@
+// @flow
+
+import { useEffect, useRef } from 'react';
+import payment from 'payment';
+import {
+ formatCvc,
+ hasCVCReachedMaxLength,
+ isHighlighted
+} from '../utils/formatter';
+import checkIsNumeric from '../utils/check-is-numeric';
+import handleKeyDown from '../utils/handle-key-down';
+import { CHANGE_ERROR_TEXT } from '../reducer/actions';
+
+type CardCVCInputProps = {
+ cardCVCInputProps: Object,
+ dispatch: Function,
+ state: Object
+};
+
+const defaultProps: CardCVCInputProps = {
+ cardCVCInputProps: {}
+};
+
+const useCardCVCInput = (props: CardCVCInputProps = defaultProps) => {
+ const cvcField = useRef(null);
+ const { cardCVCInputProps, dispatch, state } = props;
+ const {
+ cardNumber,
+ customTextLabels,
+ enableZipInput,
+ events,
+ inputClassName,
+ showZip
+ } = state;
+
+ useEffect(() => {
+ const subscription = events.subscribe(
+ events.types.FOCUS_ON_CARD_CVC,
+ () => {
+ cvcField.current.focus();
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, []);
+
+ const setFieldInvalid = (errorText: string) => {
+ const { onError } = cardCVCInputProps;
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText });
+ if (onError) {
+ onError({ error: errorText });
+ }
+ };
+
+ const setFieldValid = () => {
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText: null });
+ };
+
+ const handleCardCVCBlur = (
+ { onBlur }: { onBlur?: ?Function } = { onBlur: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ if (!payment.fns.validateCardCVC(e.target.value)) {
+ setFieldInvalid(customTextLabels.invalidCvc || 'CVC is invalid');
+ }
+ cardCVCInputProps.onBlur && cardCVCInputProps.onBlur(e);
+ onBlur && onBlur(e);
+ };
+
+ const handleCardCVCChange = (
+ { onChange }: { onChange?: ?Function } = { onChange: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ const value = formatCvc(e.target.value);
+ cvcField.current.value = value;
+ const CVC = value;
+ const CVCLength = CVC.length;
+ const isZipFieldAvailable = enableZipInput && showZip;
+ const cardType = payment.fns.cardType(cardNumber);
+
+ setFieldValid();
+ if (CVCLength >= 4) {
+ if (!payment.fns.validateCardCVC(CVC, cardType)) {
+ setFieldInvalid(customTextLabels.invalidCvc || 'CVC is invalid');
+ }
+ }
+
+ if (isZipFieldAvailable && hasCVCReachedMaxLength(cardType, CVCLength)) {
+ events.publish(events.types.FOCUS_ON_CARD_ZIP);
+ }
+
+ cardCVCInputProps.onChange && cardCVCInputProps.onChange(e);
+ onChange && onChange(e);
+ };
+
+ const handleCardCVCKeyPress = (e: any) => {
+ const cardType = payment.fns.cardType(cardNumber);
+ const value = e.target.value;
+ checkIsNumeric(e);
+ if (value && !isHighlighted()) {
+ const valueLength = value.split(' / ').join('').length;
+ if (hasCVCReachedMaxLength(cardType, valueLength)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ return {
+ id: 'cvc',
+ ref: cvcField,
+ maxLength: '5',
+ autoComplete: 'off',
+ className: `credit-card-input ${inputClassName}`,
+ placeholder: customTextLabels.cvcPlaceholder || 'CVC',
+ type: 'tel',
+ ...cardCVCInputProps,
+ onBlur: handleCardCVCBlur(),
+ onChange: handleCardCVCChange(),
+ onKeyDown: handleKeyDown(() => {
+ events.publish(events.types.FOCUS_ON_CARD_EXPIRY);
+ }),
+ onKeyPress: handleCardCVCKeyPress
+ };
+};
+
+export default useCardCVCInput;
diff --git a/src/hooks/use-card-expiry-input.js b/src/hooks/use-card-expiry-input.js
new file mode 100644
index 0000000..2b56ce7
--- /dev/null
+++ b/src/hooks/use-card-expiry-input.js
@@ -0,0 +1,120 @@
+// @flow
+
+import { useEffect, useRef } from 'react';
+import { CHANGE_ERROR_TEXT } from '../reducer/actions';
+import { formatExpiry, isHighlighted } from '../utils/formatter';
+import isExpiryInvalid from '../utils/is-expiry-invalid';
+import checkIsNumeric from '../utils/check-is-numeric';
+import handleKeyDown from '../utils/handle-key-down';
+import isMonthDashKey from '../utils/is-month-dash-key';
+
+type CardExpiryInputProps = {
+ cardExpiryInputProps: Object,
+ dispatch: Function,
+ state: Object
+};
+
+const defaultProps: CardExpiryInputProps = {
+ cardExpiryInputProps: {}
+};
+
+const useCardExpiryInput = (props: CardExpiryInputProps = defaultProps) => {
+ const cardExpiryField = useRef(null);
+ const { cardExpiryInputProps, dispatch, state } = props;
+ const { customTextLabels, events, inputClassName } = state;
+
+ useEffect(() => {
+ const subscription = events.subscribe(
+ events.types.FOCUS_ON_CARD_EXPIRY,
+ () => {
+ cardExpiryField.current.focus();
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, []);
+
+ const setFieldInvalid = (errorText: string) => {
+ const { onError } = cardExpiryInputProps;
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText });
+ if (onError) {
+ onError({ error: errorText });
+ }
+ };
+
+ const setFieldValid = () => {
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText: null });
+ };
+
+ const handleCardExpiryBlur = (
+ { onBlur }: { onBlur?: ?Function } = { onBlur: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ const cardExpiry = e.target.value.split(' / ').join('/');
+ const expiryError = isExpiryInvalid(
+ cardExpiry,
+ customTextLabels.expiryError
+ );
+ if (expiryError) {
+ setFieldInvalid(expiryError);
+ }
+
+ cardExpiryInputProps.onBlur && cardExpiryInputProps.onBlur(e);
+ onBlur && onBlur(e);
+ };
+
+ const handleCardExpiryChange = (
+ { onChange }: { onChange?: ?Function } = { onChange: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ cardExpiryField.current.value = formatExpiry(e);
+ const value = cardExpiryField.current.value.split(' / ').join('/');
+
+ setFieldValid();
+
+ const expiryError = isExpiryInvalid(value, customTextLabels.expiryError);
+ if (value.length > 4) {
+ if (expiryError) {
+ setFieldInvalid(expiryError);
+ } else {
+ events.publish(events.types.FOCUS_ON_CARD_CVC);
+ }
+ }
+
+ cardExpiryInputProps.onChange && cardExpiryInputProps.onChange(e);
+ onChange && onChange(e);
+ };
+
+ const handleCardExpiryKeyPress = (e: any) => {
+ const value = e.target.value;
+
+ if (!isMonthDashKey(e)) {
+ checkIsNumeric(e);
+ }
+
+ if (value && !isHighlighted()) {
+ const valueLength = value.split(' / ').join('').length;
+ if (valueLength >= 4) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ return {
+ id: 'card-expiry',
+ ref: cardExpiryField,
+ autoComplete: 'cc-exp',
+ className: `credit-card-input ${inputClassName}`,
+ placeholder: customTextLabels.expiryPlaceholder || 'MM/YY',
+ type: 'tel',
+ ...cardExpiryInputProps,
+ onBlur: handleCardExpiryBlur(),
+ onChange: handleCardExpiryChange(),
+ onKeyDown: handleKeyDown(() => {
+ events.publish(events.types.FOCUS_ON_CARD_NUMBER);
+ }),
+ onKeyPress: handleCardExpiryKeyPress
+ };
+};
+
+export default useCardExpiryInput;
diff --git a/src/hooks/use-card-number-input.js b/src/hooks/use-card-number-input.js
new file mode 100644
index 0000000..6d1737e
--- /dev/null
+++ b/src/hooks/use-card-number-input.js
@@ -0,0 +1,157 @@
+// @flow
+
+import { useEffect, useRef } from 'react';
+import payment from 'payment';
+import creditCardType from 'credit-card-type';
+import checkIsNumeric from '../utils/check-is-numeric';
+import {
+ formatCardNumber,
+ hasCardNumberReachedMaxLength,
+ isHighlighted
+} from '../utils/formatter';
+import {
+ CHANGE_CARD_IMAGE,
+ CHANGE_CARD_NUMBER,
+ CHANGE_ERROR_TEXT,
+ SHOW_ZIP
+} from '../reducer/actions';
+
+type CardNumberInputProps = {
+ cardNumberInputProps: Object,
+ dispatch: Function,
+ state: Object
+};
+
+const defaultProps: CardNumberInputProps = {
+ cardNumberInputProps: {}
+};
+
+const useCardNumberInput = (props: CardNumberInputProps = defaultProps) => {
+ const cardNumberField = useRef(null);
+ const { cardNumberInputProps, dispatch, state } = props;
+ const {
+ cardTypes: CARD_TYPES,
+ customTextLabels,
+ enableZipInput,
+ events,
+ images,
+ inputClassName
+ } = state;
+
+ useEffect(() => {
+ const subscription = events.subscribe(
+ events.types.FOCUS_ON_CARD_NUMBER,
+ () => {
+ cardNumberField.current.focus();
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, []);
+
+ const setFieldInvalid = (errorText: string) => {
+ const { onError } = cardNumberInputProps;
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText });
+ if (onError) {
+ onError({ error: errorText });
+ }
+ };
+
+ const setFieldValid = () => {
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText: null });
+ };
+
+ const handleCardNumberBlur = (
+ { onBlur }: { onBlur?: ?Function } = { onBlur: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ if (!payment.fns.validateCardNumber(e.target.value)) {
+ setFieldInvalid(
+ customTextLabels.invalidCardNumber || 'Card number is invalid'
+ );
+ }
+
+ cardNumberInputProps.onBlur && cardNumberInputProps.onBlur(e);
+ onBlur && onBlur(e);
+ };
+
+ const handleCardNumberChange = (
+ { onChange }: { onChange?: ?Function } = { onChange: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ const cardNumber = e.target.value;
+ const cardNumberLength = cardNumber.split(' ').join('').length;
+ const cardType = payment.fns.cardType(cardNumber);
+ const cardTypeInfo =
+ creditCardType.getTypeInfo(creditCardType.types[CARD_TYPES[cardType]]) ||
+ {};
+ const cardTypeLengths = cardTypeInfo.lengths || [16];
+
+ cardNumberField.current.value = formatCardNumber(cardNumber);
+
+ dispatch({
+ type: CHANGE_CARD_IMAGE,
+ cardImage: images[cardType] || images.placeholder
+ });
+ dispatch({
+ type: CHANGE_CARD_NUMBER,
+ cardNumber
+ });
+
+ if (enableZipInput) {
+ dispatch({
+ type: SHOW_ZIP,
+ showZip: cardNumberLength >= 6
+ });
+ }
+
+ setFieldValid();
+ if (cardTypeLengths) {
+ const lastCardTypeLength = cardTypeLengths[cardTypeLengths.length - 1];
+ for (let length of cardTypeLengths) {
+ if (
+ length === cardNumberLength &&
+ payment.fns.validateCardNumber(cardNumber)
+ ) {
+ events.publish(events.types.FOCUS_ON_CARD_EXPIRY);
+ break;
+ }
+ if (cardNumberLength === lastCardTypeLength) {
+ setFieldInvalid(
+ customTextLabels.invalidCardNumber || 'Card number is invalid'
+ );
+ }
+ }
+ }
+
+ cardNumberInputProps.onChange && cardNumberInputProps.onChange(e);
+ onChange && onChange(e);
+ };
+
+ const handleCardNumberKeyPress = (e: any) => {
+ const value = e.target.value;
+ checkIsNumeric(e);
+ if (value && !isHighlighted()) {
+ const valueLength = value.split(' ').join('').length;
+ if (hasCardNumberReachedMaxLength(value, valueLength)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ return {
+ id: 'card-number',
+ ref: cardNumberField,
+ maxLength: '19',
+ autoComplete: 'cc-number',
+ className: `credit-card-input ${inputClassName}`,
+ placeholder: customTextLabels.cardNumberPlaceholder || 'Card number',
+ type: 'tel',
+ ...cardNumberInputProps,
+ onBlur: handleCardNumberBlur(),
+ onChange: handleCardNumberChange(),
+ onKeyPress: handleCardNumberKeyPress
+ };
+};
+
+export default useCardNumberInput;
diff --git a/src/hooks/use-card-zip-input.js b/src/hooks/use-card-zip-input.js
new file mode 100644
index 0000000..b752517
--- /dev/null
+++ b/src/hooks/use-card-zip-input.js
@@ -0,0 +1,108 @@
+// @flow
+
+import { useEffect, useRef } from 'react';
+import payment from 'payment';
+import { hasZipReachedMaxLength, isHighlighted } from '../utils/formatter';
+import isZipValid from '../utils/is-zip-valid';
+import checkIsNumeric from '../utils/check-is-numeric';
+import handleKeyDown from '../utils/handle-key-down';
+import { CHANGE_ERROR_TEXT } from '../reducer/actions';
+
+type CardZipInputProps = {
+ cardZipInputProps: Object,
+ dispatch: Function,
+ state: Object
+};
+
+const defaultProps: CardZipInputProps = {
+ cardZipInputProps: {}
+};
+
+const useCardZipInput = (props: CardZipInputProps = defaultProps) => {
+ const zipField = useRef(null);
+ const { cardZipInputProps, dispatch, state } = props;
+ const { cardNumber, customTextLabels, events, inputClassName } = state;
+
+ useEffect(() => {
+ const subscription = events.subscribe(
+ events.types.FOCUS_ON_CARD_ZIP,
+ () => {
+ zipField.current.focus();
+ }
+ );
+
+ return () => {
+ subscription.remove();
+ };
+ }, []);
+
+ const setFieldInvalid = (errorText: string) => {
+ const { onError } = cardZipInputProps;
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText });
+ if (onError) {
+ onError({ error: errorText });
+ }
+ };
+
+ const setFieldValid = () => {
+ dispatch({ type: CHANGE_ERROR_TEXT, errorText: null });
+ };
+
+ const handleCardZipBlur = (
+ { onBlur }: { onBlur?: ?Function } = { onBlur: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ if (!isZipValid(e.target.value)) {
+ setFieldInvalid(customTextLabels.invalidZipCode || 'Zip code is invalid');
+ }
+
+ cardZipInputProps.onBlur && cardZipInputProps.onBlur(e);
+ onBlur && onBlur(e);
+ };
+
+ const handleCardZipChange = (
+ { onChange }: { onChange?: ?Function } = { onChange: null }
+ ) => (e: SyntheticInputEvent<*>) => {
+ const zip = e.target.value;
+ const zipLength = zip.length;
+
+ setFieldValid();
+
+ if (zipLength >= 5 && !isZipValid(zip)) {
+ setFieldInvalid(customTextLabels.invalidZipCode || 'Zip code is invalid');
+ }
+
+ cardZipInputProps.onChange && cardZipInputProps.onChange(e);
+ onChange && onChange(e);
+ };
+
+ const handleCardZipKeyPress = (e: any) => {
+ const cardType = payment.fns.cardType(cardNumber);
+ const value = e.target.value;
+ checkIsNumeric(e);
+ if (value && !isHighlighted()) {
+ const valueLength = value.split(' / ').join('').length;
+ if (hasZipReachedMaxLength(cardType, valueLength)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ return {
+ id: 'zip',
+ ref: zipField,
+ maxLength: '6',
+ className: `credit-card-input zip-input ${inputClassName}`,
+ pattern: '[0-9]*',
+ placeholder: customTextLabels.zipPlaceholder || 'Zip',
+ type: 'text',
+ ...cardZipInputProps,
+ onBlur: handleCardZipBlur(),
+ onChange: handleCardZipChange(),
+ onKeyDown: handleKeyDown(() => {
+ events.publish(events.types.FOCUS_ON_CARD_CVC);
+ }),
+ onKeyPress: handleCardZipKeyPress
+ };
+};
+
+export default useCardZipInput;
diff --git a/src/hooks/use-credit-card-input.js b/src/hooks/use-credit-card-input.js
new file mode 100644
index 0000000..4545705
--- /dev/null
+++ b/src/hooks/use-credit-card-input.js
@@ -0,0 +1,99 @@
+// @flow
+
+import { useEffect, useReducer } from 'react';
+import useCardNumberInput from './use-card-number-input';
+import useCardExpiryInput from './use-card-expiry-input';
+import useCardCvcInput from './use-card-cvc-input';
+import useCardZipInput from './use-card-zip-input';
+import { inititalState, reducer } from '../reducer';
+import {
+ CHANGE_CARD_TYPES,
+ CHANGE_IMAGES,
+ CHANGE_CUSTOM_TEXT_LABELS,
+ CHANGE_INPUT_CLASS_NAME,
+ ENABLE_ZIP_INPUT
+} from '../reducer/actions';
+
+type CreditCardInputProps = {
+ cardTypes: Object,
+ onError?: Function,
+ cardExpiryInputProps: Object,
+ cardNumberInputProps: Object,
+ cardCVCInputProps: Object,
+ cardZipInputProps: Object,
+ images: Object,
+ inputClassName: string,
+ customTextLabels: Object
+};
+
+const defaultProps: CreditCardInputProps = {
+ cardExpiryInputProps: {},
+ cardNumberInputProps: {},
+ cardCVCInputProps: {},
+ cardZipInputProps: {},
+ inputClassName: '',
+ customTextLabels: {}
+};
+
+const useCreditCardInput = (props: CreditCardInputProps = defaultProps) => {
+ const [state, dispatch] = useReducer(reducer, inititalState);
+
+ const {
+ cardCVCInputProps,
+ cardZipInputProps,
+ cardExpiryInputProps,
+ cardNumberInputProps,
+ onError
+ } = props;
+
+ const { errorText } = state;
+
+ useEffect(() => {
+ const {
+ images,
+ inputClassName,
+ cardTypes,
+ customTextLabels,
+ enableZipInput
+ } = props;
+ dispatch({ type: CHANGE_CARD_TYPES, cardTypes });
+ dispatch({ type: CHANGE_IMAGES, images });
+ dispatch({ type: ENABLE_ZIP_INPUT, enableZipInput });
+ dispatch({ type: CHANGE_CUSTOM_TEXT_LABELS, customTextLabels });
+ dispatch({ type: CHANGE_INPUT_CLASS_NAME, inputClassName });
+ }, []);
+
+ const setFieldInvalid = (errorText: string) => {
+ if (onError) {
+ onError({ error: errorText });
+ }
+ };
+
+ const setFieldValid = () => {};
+
+ useEffect(() => {
+ if (errorText) {
+ setFieldInvalid(errorText);
+ } else {
+ setFieldValid();
+ }
+ }, [errorText]);
+
+ return {
+ cardNumberProps: useCardNumberInput({
+ cardNumberInputProps,
+ dispatch,
+ state
+ }),
+ cardExpiryProps: useCardExpiryInput({
+ cardExpiryInputProps,
+ dispatch,
+ state
+ }),
+ cardCvcProps: useCardCvcInput({ cardCVCInputProps, dispatch, state }),
+ cardZipProps: useCardZipInput({ cardZipInputProps, dispatch, state }),
+ state
+ };
+};
+
+export default useCreditCardInput;
diff --git a/src/index.js b/src/index.js
index b191cee..ee6d1b4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,633 +1,5 @@
-// @flow
-
-import React, { Component } from 'react';
-import payment from 'payment';
-import creditCardType from 'credit-card-type';
-import styled from 'styled-components';
-
-import {
- formatCardNumber,
- formatExpiry,
- formatCvc,
- hasCardNumberReachedMaxLength,
- hasCVCReachedMaxLength,
- hasZipReachedMaxLength,
- isHighlighted
-} from './utils/formatter';
-import images from './utils/images';
-import isExpiryInvalid from './utils/is-expiry-invalid';
-import isZipValid from './utils/is-zip-valid';
-
-const Container = styled.div`
- display: inline-block;
- ${({ styled }) => ({ ...styled })};
-`;
-
-const FieldWrapper = styled.div`
- display: flex;
- align-items: center;
- position: relative;
- background-color: white;
- padding: 10px;
- border-radius: 3px;
- overflow: hidden;
- ${({ styled }) => ({ ...styled })};
-
- &.is-invalid {
- border: 1px solid #ff3860;
- ${({ invalidStyled }) => ({ ...invalidStyled })};
- }
-`;
-
-const CardImage = styled.img`
- height: 1em;
- ${({ styled }) => ({ ...styled })};
-`;
-
-const InputWrapper = styled.label`
- align-items: center;
- display: ${props => (props.isActive ? 'flex' : 'none')};
- margin-left: 0.5em;
- position: relative;
- transition: transform 0.5s;
- transform: translateX(${props => (props.translateX ? '4rem' : '0')});
-
- &::after {
- content: attr(data-max);
- visibility: hidden;
- height: 0;
- }
-
- & .credit-card-input {
- border: 0px;
- position: absolute;
- width: 100%;
- font-size: 1em;
- ${({ inputStyled }) => ({ ...inputStyled })};
-
- &:focus {
- outline: 0px;
- }
- }
-
- & .zip-input {
- display: ${props => (props.isZipActive ? 'flex' : 'none')};
- }
-`;
-
-const DangerText = styled.p`
- font-size: 0.8rem;
- margin: 5px 0 0 0;
- color: #ff3860;
- ${({ styled }) => ({ ...styled })};
-`;
-
-const BACKSPACE_KEY_CODE = 8;
-const CARD_TYPES = {
- mastercard: 'MASTERCARD',
- visa: 'VISA',
- amex: 'AMERICAN_EXPRESS'
-};
-
-type Props = {
- CARD_TYPES: Object,
- cardCVCInputRenderer: Function,
- cardExpiryInputRenderer: Function,
- cardNumberInputRenderer: Function,
- cardZipInputRenderer: Function,
- onError?: Function,
- cardExpiryInputProps: Object,
- cardNumberInputProps: Object,
- cardCVCInputProps: Object,
- cardZipInputProps: Object,
- cardImageClassName: string,
- cardImageStyle: Object,
- containerClassName: string,
- containerStyle: Object,
- dangerTextClassName: string,
- dangerTextStyle: Object,
- fieldClassName: string,
- fieldStyle: Object,
- enableZipInput: boolean,
- images: Object,
- inputComponent: Function | Object | string,
- inputClassName: string,
- inputStyle: Object,
- invalidClassName: string,
- invalidStyle: Object,
- customTextLabels: Object
-};
-type State = {
- cardImage: string,
- cardNumberLength: number,
- cardNumber: ?string,
- errorText: ?string,
- showZip: boolean
-};
-
-const inputRenderer = ({ props }: Object) => ;
-
-class CreditCardInput extends Component {
- cardExpiryField: any;
- cardNumberField: any;
- cvcField: any;
- zipField: any;
-
- static defaultProps = {
- cardCVCInputRenderer: inputRenderer,
- cardExpiryInputRenderer: inputRenderer,
- cardNumberInputRenderer: inputRenderer,
- cardZipInputRenderer: inputRenderer,
- cardExpiryInputProps: {},
- cardNumberInputProps: {},
- cardCVCInputProps: {},
- cardZipInputProps: {},
- cardImageClassName: '',
- cardImageStyle: {},
- containerClassName: '',
- containerStyle: {},
- dangerTextClassName: '',
- dangerTextStyle: {},
- enableZipInput: false,
- fieldClassName: '',
- fieldStyle: {},
- inputComponent: 'input',
- inputClassName: '',
- inputStyle: {},
- invalidClassName: 'is-invalid',
- invalidStyle: {},
- customTextLabels: {}
- };
-
- constructor(props: Props) {
- super(props);
- this.CARD_TYPES = Object.assign({}, CARD_TYPES, props.CARD_TYPES);
- this.images = Object.assign({}, images, props.images);
- this.state = {
- cardImage: this.images.placeholder,
- cardNumberLength: 0,
- cardNumber: null,
- errorText: null,
- showZip: false
- };
- }
-
- componentDidMount = () => {
- this.setState({ cardNumber: this.cardNumberField.value }, () => {
- const cardType = payment.fns.cardType(this.state.cardNumber);
- const images = this.images;
- this.setState({
- cardImage: images[cardType] || images.placeholder
- });
- });
- };
-
- isMonthDashKey = ({ key, target: { value } } = {}) => {
- return !value.match(/[/-]/) && /^[/-]$/.test(key);
- };
-
- checkIsNumeric = (e: any) => {
- if (!/^\d*$/.test(e.key)) {
- e.preventDefault();
- }
- };
-
- handleCardNumberBlur = (
- { onBlur }: { onBlur?: ?Function } = { onBlur: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- if (!payment.fns.validateCardNumber(e.target.value)) {
- this.setFieldInvalid(
- customTextLabels.invalidCardNumber || 'Card number is invalid',
- 'cardNumber'
- );
- }
-
- const { cardNumberInputProps } = this.props;
- cardNumberInputProps.onBlur && cardNumberInputProps.onBlur(e);
- onBlur && onBlur(e);
- };
-
- handleCardNumberChange = (
- { onChange }: { onChange?: ?Function } = { onChange: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const {
- customTextLabels,
- enableZipInput,
- cardNumberInputProps
- } = this.props;
- const images = this.images;
- const cardNumber = e.target.value;
- const cardNumberLength = cardNumber.split(' ').join('').length;
- const cardType = payment.fns.cardType(cardNumber);
- const cardTypeInfo =
- creditCardType.getTypeInfo(
- creditCardType.types[this.CARD_TYPES[cardType]]
- ) || {};
- const cardTypeLengths = cardTypeInfo.lengths || [16];
-
- this.cardNumberField.value = formatCardNumber(cardNumber);
-
- this.setState({
- cardImage: images[cardType] || images.placeholder,
- cardNumber
- });
-
- if (enableZipInput) {
- this.setState({ showZip: cardNumberLength >= 6 });
- }
-
- this.setFieldValid();
- if (cardTypeLengths) {
- const lastCardTypeLength = cardTypeLengths[cardTypeLengths.length - 1];
- for (let length of cardTypeLengths) {
- if (
- length === cardNumberLength &&
- payment.fns.validateCardNumber(cardNumber)
- ) {
- this.cardExpiryField.focus();
- break;
- }
- if (cardNumberLength === lastCardTypeLength) {
- this.setFieldInvalid(
- customTextLabels.invalidCardNumber || 'Card number is invalid',
- 'cardNumber'
- );
- }
- }
- }
-
- cardNumberInputProps.onChange && cardNumberInputProps.onChange(e);
- onChange && onChange(e);
- };
-
- handleCardNumberKeyPress = (e: any) => {
- const value = e.target.value;
- this.checkIsNumeric(e);
- if (value && !isHighlighted()) {
- const valueLength = value.split(' ').join('').length;
- if (hasCardNumberReachedMaxLength(value, valueLength)) {
- e.preventDefault();
- }
- }
- };
-
- handleCardExpiryBlur = (
- { onBlur }: { onBlur?: ?Function } = { onBlur: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- const cardExpiry = e.target.value.split(' / ').join('/');
- const expiryError = isExpiryInvalid(
- cardExpiry,
- customTextLabels.expiryError
- );
- if (expiryError) {
- this.setFieldInvalid(expiryError, 'cardExpiry');
- }
-
- const { cardExpiryInputProps } = this.props;
- cardExpiryInputProps.onBlur && cardExpiryInputProps.onBlur(e);
- onBlur && onBlur(e);
- };
-
- handleCardExpiryChange = (
- { onChange }: { onChange?: ?Function } = { onChange: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
-
- this.cardExpiryField.value = formatExpiry(e);
- const value = this.cardExpiryField.value.split(' / ').join('/');
-
- this.setFieldValid();
-
- const expiryError = isExpiryInvalid(value, customTextLabels.expiryError);
- if (value.length > 4) {
- if (expiryError) {
- this.setFieldInvalid(expiryError, 'cardExpiry');
- } else {
- this.cvcField.focus();
- }
- }
-
- const { cardExpiryInputProps } = this.props;
- cardExpiryInputProps.onChange && cardExpiryInputProps.onChange(e);
- onChange && onChange(e);
- };
-
- handleCardExpiryKeyPress = (e: any) => {
- const value = e.target.value;
-
- if (!this.isMonthDashKey(e)) {
- this.checkIsNumeric(e);
- }
-
- if (value && !isHighlighted()) {
- const valueLength = value.split(' / ').join('').length;
- if (valueLength >= 4) {
- e.preventDefault();
- }
- }
- };
-
- handleCardCVCBlur = (
- { onBlur }: { onBlur?: ?Function } = { onBlur: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- if (!payment.fns.validateCardCVC(e.target.value)) {
- this.setFieldInvalid(
- customTextLabels.invalidCvc || 'CVC is invalid',
- 'cardCVC'
- );
- }
-
- const { cardCVCInputProps } = this.props;
- cardCVCInputProps.onBlur && cardCVCInputProps.onBlur(e);
- onBlur && onBlur(e);
- };
-
- handleCardCVCChange = (
- { onChange }: { onChange?: ?Function } = { onChange: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- const value = formatCvc(e.target.value);
- this.cvcField.value = value;
- const CVC = value;
- const CVCLength = CVC.length;
- const isZipFieldAvailable = this.props.enableZipInput && this.state.showZip;
- const cardType = payment.fns.cardType(this.state.cardNumber);
-
- this.setFieldValid();
- if (CVCLength >= 4) {
- if (!payment.fns.validateCardCVC(CVC, cardType)) {
- this.setFieldInvalid(
- customTextLabels.invalidCvc || 'CVC is invalid',
- 'cardCVC'
- );
- }
- }
-
- if (isZipFieldAvailable && hasCVCReachedMaxLength(cardType, CVCLength)) {
- this.zipField.focus();
- }
-
- const { cardCVCInputProps } = this.props;
- cardCVCInputProps.onChange && cardCVCInputProps.onChange(e);
- onChange && onChange(e);
- };
-
- handleCardCVCKeyPress = (e: any) => {
- const cardType = payment.fns.cardType(this.state.cardNumber);
- const value = e.target.value;
- this.checkIsNumeric(e);
- if (value && !isHighlighted()) {
- const valueLength = value.split(' / ').join('').length;
- if (hasCVCReachedMaxLength(cardType, valueLength)) {
- e.preventDefault();
- }
- }
- };
-
- handleCardZipBlur = (
- { onBlur }: { onBlur?: ?Function } = { onBlur: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- if (!isZipValid(e.target.value)) {
- this.setFieldInvalid(
- customTextLabels.invalidZipCode || 'Zip code is invalid',
- 'cardZip'
- );
- }
-
- const { cardZipInputProps } = this.props;
- cardZipInputProps.onBlur && cardZipInputProps.onBlur(e);
- onBlur && onBlur(e);
- };
-
- handleCardZipChange = (
- { onChange }: { onChange?: ?Function } = { onChange: null }
- ) => (e: SyntheticInputEvent<*>) => {
- const { customTextLabels } = this.props;
- const zip = e.target.value;
- const zipLength = zip.length;
-
- this.setFieldValid();
-
- if (zipLength >= 5 && !isZipValid(zip)) {
- this.setFieldInvalid(
- customTextLabels.invalidZipCode || 'Zip code is invalid',
- 'cardZip'
- );
- }
-
- const { cardZipInputProps } = this.props;
- cardZipInputProps.onChange && cardZipInputProps.onChange(e);
- onChange && onChange(e);
- };
-
- handleCardZipKeyPress = (e: any) => {
- const cardType = payment.fns.cardType(this.state.cardNumber);
- const value = e.target.value;
- this.checkIsNumeric(e);
- if (value && !isHighlighted()) {
- const valueLength = value.split(' / ').join('').length;
- if (hasZipReachedMaxLength(cardType, valueLength)) {
- e.preventDefault();
- }
- }
- };
-
- handleKeyDown = (ref: any) => {
- return (e: SyntheticInputEvent<*>) => {
- if (e.keyCode === BACKSPACE_KEY_CODE && !e.target.value) {
- ref.focus();
- }
- };
- };
-
- setFieldInvalid = (errorText: string, inputName?: string) => {
- const { invalidClassName, onError } = this.props;
- // $FlowFixMe
- document.getElementById('field-wrapper').classList.add(invalidClassName);
- this.setState({ errorText });
-
- if (inputName) {
- const { onError } = this.props[`${inputName}InputProps`];
- onError && onError(errorText);
- }
-
- if (onError) {
- onError({ inputName, error: errorText });
- }
- };
-
- setFieldValid = () => {
- const { invalidClassName } = this.props;
- // $FlowFixMe
- document.getElementById('field-wrapper').classList.remove(invalidClassName);
- this.setState({ errorText: null });
- };
-
- render = () => {
- const { cardImage, errorText, showZip } = this.state;
- const {
- cardImageClassName,
- cardImageStyle,
- cardCVCInputProps,
- cardZipInputProps,
- cardExpiryInputProps,
- cardNumberInputProps,
- cardCVCInputRenderer,
- cardExpiryInputRenderer,
- cardNumberInputRenderer,
- cardZipInputRenderer,
- containerClassName,
- containerStyle,
- dangerTextClassName,
- dangerTextStyle,
- enableZipInput,
- fieldClassName,
- fieldStyle,
- inputClassName,
- inputStyle,
- invalidStyle,
- customTextLabels
- } = this.props;
-
- return (
-
-
-
-
- {cardNumberInputRenderer({
- handleCardNumberChange: onChange =>
- this.handleCardNumberChange({ onChange }),
- handleCardNumberBlur: onBlur =>
- this.handleCardNumberBlur({ onBlur }),
- props: {
- id: 'card-number',
- ref: cardNumberField => {
- this.cardNumberField = cardNumberField;
- },
- maxLength: '19',
- autoComplete: 'cc-number',
- className: `credit-card-input ${inputClassName}`,
- placeholder:
- customTextLabels.cardNumberPlaceholder || 'Card number',
- type: 'tel',
- ...cardNumberInputProps,
- onBlur: this.handleCardNumberBlur(),
- onChange: this.handleCardNumberChange(),
- onKeyPress: this.handleCardNumberKeyPress
- }
- })}
-
-
- {cardExpiryInputRenderer({
- handleCardExpiryChange: onChange =>
- this.handleCardExpiryChange({ onChange }),
- handleCardExpiryBlur: onBlur =>
- this.handleCardExpiryBlur({ onBlur }),
- props: {
- id: 'card-expiry',
- ref: cardExpiryField => {
- this.cardExpiryField = cardExpiryField;
- },
- autoComplete: 'cc-exp',
- className: `credit-card-input ${inputClassName}`,
- placeholder: customTextLabels.expiryPlaceholder || 'MM/YY',
- type: 'tel',
- ...cardExpiryInputProps,
- onBlur: this.handleCardExpiryBlur(),
- onChange: this.handleCardExpiryChange(),
- onKeyDown: this.handleKeyDown(this.cardNumberField),
- onKeyPress: this.handleCardExpiryKeyPress
- }
- })}
-
-
- {cardCVCInputRenderer({
- handleCardCVCChange: onChange =>
- this.handleCardCVCChange({ onChange }),
- handleCardCVCBlur: onBlur => this.handleCardCVCBlur({ onBlur }),
- props: {
- id: 'cvc',
- ref: cvcField => {
- this.cvcField = cvcField;
- },
- maxLength: '5',
- autoComplete: 'off',
- className: `credit-card-input ${inputClassName}`,
- placeholder: customTextLabels.cvcPlaceholder || 'CVC',
- type: 'tel',
- ...cardCVCInputProps,
- onBlur: this.handleCardCVCBlur(),
- onChange: this.handleCardCVCChange(),
- onKeyDown: this.handleKeyDown(this.cardExpiryField),
- onKeyPress: this.handleCardCVCKeyPress
- }
- })}
-
-
- {cardZipInputRenderer({
- handleCardZipChange: onChange =>
- this.handleCardZipChange({ onChange }),
- handleCardZipBlur: onBlur => this.handleCardZipBlur({ onBlur }),
- props: {
- id: 'zip',
- ref: zipField => {
- this.zipField = zipField;
- },
- maxLength: '6',
- className: `credit-card-input zip-input ${inputClassName}`,
- pattern: '[0-9]*',
- placeholder: customTextLabels.zipPlaceholder || 'Zip',
- type: 'text',
- ...cardZipInputProps,
- onBlur: this.handleCardZipBlur(),
- onChange: this.handleCardZipChange(),
- onKeyDown: this.handleKeyDown(this.cvcField),
- onKeyPress: this.handleCardZipKeyPress
- }
- })}
-
-
- {errorText && (
-
- {errorText}
-
- )}
-
- );
- };
-}
+import CreditCardInput from './credit-card-input';
+import useCreditCardInput from './hooks/use-credit-card-input';
+export { useCreditCardInput };
export default CreditCardInput;
diff --git a/src/index.stories.js b/src/index.stories.js
index 73b6668..ee5c81a 100644
--- a/src/index.stories.js
+++ b/src/index.stories.js
@@ -32,12 +32,20 @@ storiesOf('CreditCardInput', module)
onChange: e => console.log('number change', e),
onError: err => console.log(`number error: ${err}`)
}}
+ onError={err => console.log('onError', err)}
/>
))
.add('with zip field enabled', () => (
-
+ console.log('zip blur', e),
+ onChange: e => console.log('zip change', e),
+ onError: err => console.log(`zip error: ${err}`)
+ }}
+ />
))
.add('with pre-filled values', () => (
@@ -59,7 +67,7 @@ storiesOf('CreditCardInput', module)
images={{
dinersclub: `data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDggNDgiIHZlcnNpb249IjEuMSIgd2lkdGg9IjQ4cHgiIGhlaWdodD0iNDhweCI+CjxnIGlkPSJzdXJmYWNlMSI+CjxwYXRoIHN0eWxlPSIgZmlsbDojRTFFN0VBOyIgZD0iTSA0NSAzNSBDIDQ1IDM3LjE5OTIxOSA0My4xOTkyMTkgMzkgNDEgMzkgTCA3IDM5IEMgNC44MDA3ODEgMzkgMyAzNy4xOTkyMTkgMyAzNSBMIDMgMTMgQyAzIDEwLjgwMDc4MSA0LjgwMDc4MSA5IDcgOSBMIDQxIDkgQyA0My4xOTkyMTkgOSA0NSAxMC44MDA3ODEgNDUgMTMgWiAiLz4KPHBhdGggc3R5bGU9IiBmaWxsOiNGRjZEMDA7IiBkPSJNIDQ1IDM1IEMgNDUgMzcuMTk5MjE5IDQzLjE5OTIxOSAzOSA0MSAzOSBMIDE2IDM5IEMgMTYgMzkgMzkuNjAxNTYzIDM1LjE5OTIxOSA0NSAyNCBaIE0gMjIgMjQgQyAyMiAyNS42OTkyMTkgMjMuMzAwNzgxIDI3IDI1IDI3IEMgMjYuNjk5MjE5IDI3IDI4IDI1LjY5OTIxOSAyOCAyNCBDIDI4IDIyLjMwMDc4MSAyNi42OTkyMTkgMjEgMjUgMjEgQyAyMy4zMDA3ODEgMjEgMjIgMjIuMzAwNzgxIDIyIDI0IFogIi8+CjxwYXRoIHN0eWxlPSIgIiBkPSJNIDExLjE5OTIxOSAyMSBMIDEyLjMwMDc4MSAyMSBMIDEyLjMwMDc4MSAyNyBMIDExLjE5OTIxOSAyNyBaIE0gMTcuMTk5MjE5IDI0IEMgMTcuMTk5MjE5IDI1LjY5OTIxOSAxOC41IDI3IDIwLjE5OTIxOSAyNyBDIDIwLjY5OTIxOSAyNyAyMS4xMDE1NjMgMjYuODk4NDM4IDIxLjYwMTU2MyAyNi42OTkyMTkgTCAyMS42MDE1NjMgMjUuMzk4NDM4IEMgMjEuMTk5MjE5IDI1LjgwMDc4MSAyMC44MDA3ODEgMjYgMjAuMTk5MjE5IDI2IEMgMTkuMTAxNTYzIDI2IDE4LjMwMDc4MSAyNS4xOTkyMTkgMTguMzAwNzgxIDI0IEMgMTguMzAwNzgxIDIyLjg5ODQzOCAxOS4xMDE1NjMgMjIgMjAuMTk5MjE5IDIyIEMgMjAuNjk5MjE5IDIyIDIxLjEwMTU2MyAyMi4xOTkyMTkgMjEuNjAxNTYzIDIyLjYwMTU2MyBMIDIxLjYwMTU2MyAyMS4zMDA3ODEgQyAyMS4xMDE1NjMgMjEuMTAxNTYzIDIwLjY5OTIxOSAyMC44OTg0MzggMjAuMTk5MjE5IDIwLjg5ODQzOCBDIDE4LjUgMjEgMTcuMTk5MjE5IDIyLjM5ODQzOCAxNy4xOTkyMTkgMjQgWiBNIDMwLjYwMTU2MyAyNC44OTg0MzggTCAyOSAyMSBMIDI3LjgwMDc4MSAyMSBMIDMwLjMwMDc4MSAyNyBMIDMwLjg5ODQzOCAyNyBMIDMzLjM5ODQzOCAyMSBMIDMyLjE5OTIxOSAyMSBaIE0gMzMuODk4NDM4IDI3IEwgMzcuMTAxNTYzIDI3IEwgMzcuMTAxNTYzIDI2IEwgMzUgMjYgTCAzNSAyNC4zOTg0MzggTCAzNyAyNC4zOTg0MzggTCAzNyAyMy4zOTg0MzggTCAzNSAyMy4zOTg0MzggTCAzNSAyMiBMIDM3LjEwMTU2MyAyMiBMIDM3LjEwMTU2MyAyMSBMIDMzLjg5ODQzOCAyMSBaIE0gNDEuNSAyMi44MDA3ODEgQyA0MS41IDIxLjY5OTIxOSA0MC44MDA3ODEgMjEgMzkuNSAyMSBMIDM3LjgwMDc4MSAyMSBMIDM3LjgwMDc4MSAyNyBMIDM4Ljg5ODQzOCAyNyBMIDM4Ljg5ODQzOCAyNC42MDE1NjMgTCAzOSAyNC42MDE1NjMgTCA0MC42MDE1NjMgMjcgTCA0MiAyNyBMIDQwLjE5OTIxOSAyNC41IEMgNDEgMjQuMzAwNzgxIDQxLjUgMjMuNjk5MjE5IDQxLjUgMjIuODAwNzgxIFogTSAzOS4xOTkyMTkgMjMuODAwNzgxIEwgMzguODk4NDM4IDIzLjgwMDc4MSBMIDM4Ljg5ODQzOCAyMiBMIDM5LjE5OTIxOSAyMiBDIDM5Ljg5ODQzOCAyMiA0MC4zMDA3ODEgMjIuMzAwNzgxIDQwLjMwMDc4MSAyMi44OTg0MzggQyA0MC4zMDA3ODEgMjMuMzk4NDM4IDQwIDIzLjgwMDc4MSAzOS4xOTkyMTkgMjMuODAwNzgxIFogTSA3LjY5OTIxOSAyMSBMIDYgMjEgTCA2IDI3IEwgNy42MDE1NjMgMjcgQyAxMC4xMDE1NjMgMjcgMTAuNjk5MjE5IDI0Ljg5ODQzOCAxMC42OTkyMTkgMjQgQyAxMC44MDA3ODEgMjIuMTk5MjE5IDkuNSAyMSA3LjY5OTIxOSAyMSBaIE0gNy4zOTg0MzggMjYgTCA3LjEwMTU2MyAyNiBMIDcuMTAxNTYzIDIyIEwgNy41IDIyIEMgOSAyMiA5LjYwMTU2MyAyMyA5LjYwMTU2MyAyNCBDIDkuNjAxNTYzIDI0LjM5ODQzOCA5LjUgMjYgNy4zOTg0MzggMjYgWiBNIDE1LjMwMDc4MSAyMy4zMDA3ODEgQyAxNC42MDE1NjMgMjMgMTQuMzk4NDM4IDIyLjg5ODQzOCAxNC4zOTg0MzggMjIuNjAxNTYzIEMgMTQuMzk4NDM4IDIyLjE5OTIxOSAxNC44MDA3ODEgMjIgMTUuMTk5MjE5IDIyIEMgMTUuNSAyMiAxNS44MDA3ODEgMjIuMTAxNTYzIDE2LjEwMTU2MyAyMi41IEwgMTYuNjk5MjE5IDIxLjY5OTIxOSBDIDE2LjE5OTIxOSAyMS4xOTkyMTkgMTUuNjk5MjE5IDIxIDE1IDIxIEMgMTQgMjEgMTMuMTk5MjE5IDIxLjY5OTIxOSAxMy4xOTkyMTkgMjIuNjk5MjE5IEMgMTMuMTk5MjE5IDIzLjUgMTMuNjAxNTYzIDIzLjg5ODQzOCAxNC42MDE1NjMgMjQuMzAwNzgxIEMgMTUuMTk5MjE5IDI0LjUgMTUuNjk5MjE5IDI0LjY5OTIxOSAxNS42OTkyMTkgMjUuMTk5MjE5IEMgMTUuNjk5MjE5IDI1LjY5OTIxOSAxNS4zMDA3ODEgMjYgMTQuODAwNzgxIDI2IEMgMTQuMzAwNzgxIDI2IDEzLjgwMDc4MSAyNS42OTkyMTkgMTMuNjAxNTYzIDI1LjE5OTIxOSBMIDEyLjg5ODQzOCAyNS44OTg0MzggQyAxMy4zOTg0MzggMjYuNjk5MjE5IDE0IDI3IDE0Ljg5ODQzOCAyNyBDIDE2LjEwMTU2MyAyNyAxNi44OTg0MzggMjYuMTk5MjE5IDE2Ljg5ODQzOCAyNS4xMDE1NjMgQyAxNi44OTg0MzggMjQuMTk5MjE5IDE2LjUgMjMuODAwNzgxIDE1LjMwMDc4MSAyMy4zMDA3ODEgWiAiLz4KPC9nPgo8L3N2Zz4K`
}}
- CARD_TYPES={{
+ cardTypes={{
dinersclub: 'DINERS_CLUB'
}}
/>
diff --git a/src/reducer/actions.js b/src/reducer/actions.js
new file mode 100644
index 0000000..3e58859
--- /dev/null
+++ b/src/reducer/actions.js
@@ -0,0 +1,9 @@
+export const CHANGE_CARD_IMAGE = Symbol('CHANGE_CARD_IMAGE');
+export const CHANGE_CARD_NUMBER = Symbol('CHANGE_CARD_NUMBER');
+export const CHANGE_CARD_TYPES = Symbol('CHANGE_CARD_TYPES');
+export const CHANGE_CUSTOM_TEXT_LABELS = Symbol('CHANGE_CUSTOM_TEXT_LABELS');
+export const CHANGE_ERROR_TEXT = Symbol('CHANGE_ERROR_TEXT');
+export const CHANGE_IMAGES = Symbol('CHANGE_IMAGES');
+export const CHANGE_INPUT_CLASS_NAME = Symbol('CHANGE_INPUT_CLASS_NAME');
+export const ENABLE_ZIP_INPUT = Symbol('ENABLE_ZIP_INPUT');
+export const SHOW_ZIP = Symbol('SHOW_ZIP');
diff --git a/src/reducer/index.js b/src/reducer/index.js
new file mode 100644
index 0000000..e4d0eca
--- /dev/null
+++ b/src/reducer/index.js
@@ -0,0 +1,100 @@
+import cardTypes from '../utils/card-types';
+import images from '../utils/images';
+import events from '../events';
+import {
+ CHANGE_CARD_IMAGE,
+ CHANGE_CARD_NUMBER,
+ CHANGE_CARD_TYPES,
+ CHANGE_CUSTOM_TEXT_LABELS,
+ CHANGE_ERROR_TEXT,
+ CHANGE_IMAGES,
+ CHANGE_INPUT_CLASS_NAME,
+ ENABLE_ZIP_INPUT,
+ SHOW_ZIP
+} from './actions';
+
+type State = {
+ cardTypes: Object,
+ cardImage: string,
+ cardNumber: ?string,
+ customTextLabels: Object,
+ enableZipInput: boolean,
+ errorText: ?string,
+ events: Object,
+ images: Object,
+ inputClassName: string,
+ showZip: boolean
+};
+
+const inititalState: State = {
+ cardImage: images.placeholder,
+ cardNumber: null,
+ cardTypes,
+ customTextLabels: {},
+ enableZipInput: false,
+ errorText: null,
+ events: events(),
+ images,
+ inputClassName: '',
+ showZip: false
+};
+
+const reducer = (state: State, action) => {
+ switch (action.type) {
+ case CHANGE_CARD_IMAGE:
+ return {
+ ...state,
+ cardImage: action.cardImage
+ };
+ case CHANGE_CARD_NUMBER:
+ return {
+ ...state,
+ cardNumber: action.cardNumber
+ };
+ case CHANGE_CARD_TYPES:
+ return {
+ ...state,
+ cardTypes: {
+ ...state.cardTypes,
+ ...action.cardTypes
+ }
+ };
+ case CHANGE_CUSTOM_TEXT_LABELS:
+ return {
+ ...state,
+ customTextLabels: action.customTextLabels
+ };
+ case CHANGE_ERROR_TEXT:
+ return {
+ ...state,
+ errorText: action.errorText
+ };
+ case CHANGE_IMAGES:
+ return {
+ ...state,
+ images: {
+ ...state.images,
+ ...action.images
+ }
+ };
+ case CHANGE_INPUT_CLASS_NAME:
+ return {
+ ...state,
+ inputClassName: action.inputClassName
+ };
+ case ENABLE_ZIP_INPUT:
+ return {
+ ...state,
+ enableZipInput: action.enableZipInput
+ };
+ case SHOW_ZIP:
+ return {
+ ...state,
+ showZip: !!action.showZip
+ };
+ default:
+ throw new Error();
+ }
+};
+
+export { inititalState, reducer };
diff --git a/src/utils/card-types.js b/src/utils/card-types.js
new file mode 100644
index 0000000..963c525
--- /dev/null
+++ b/src/utils/card-types.js
@@ -0,0 +1,5 @@
+export default {
+ mastercard: 'MASTERCARD',
+ visa: 'VISA',
+ amex: 'AMERICAN_EXPRESS'
+};
diff --git a/src/utils/check-is-numeric.js b/src/utils/check-is-numeric.js
new file mode 100644
index 0000000..fd81a91
--- /dev/null
+++ b/src/utils/check-is-numeric.js
@@ -0,0 +1,9 @@
+// @flow
+
+const checkIsNumeric = (e: any) => {
+ if (!/^\d*$/.test(e.key)) {
+ e.preventDefault();
+ }
+};
+
+export default checkIsNumeric;
diff --git a/src/utils/handle-key-down.js b/src/utils/handle-key-down.js
new file mode 100644
index 0000000..5b94a8e
--- /dev/null
+++ b/src/utils/handle-key-down.js
@@ -0,0 +1,13 @@
+// @flow
+
+const BACKSPACE_KEY_CODE = 8;
+
+const handleKeyDown = (callback: Function) => {
+ return (e: SyntheticInputEvent<*>) => {
+ if (e.keyCode === BACKSPACE_KEY_CODE && !e.target.value) {
+ callback && callback();
+ }
+ };
+};
+
+export default handleKeyDown;
diff --git a/src/utils/is-month-dash-key.js b/src/utils/is-month-dash-key.js
new file mode 100644
index 0000000..811ee47
--- /dev/null
+++ b/src/utils/is-month-dash-key.js
@@ -0,0 +1,7 @@
+// @flow
+
+const isMonthDashKey = ({ key, target: { value } } = {}) => {
+ return !value.match(/[/-]/) && /^[/-]$/.test(key);
+};
+
+export default isMonthDashKey;