src/components/StripeCardInput/index.js
import React, { useMemo, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import scriptLoader from 'react-async-script-loader';
import withReduxForm from '../../libs/hocs/withReduxForm';
import { useTheme, useUniqueIdentifier, useLocale } from '../../libs/hooks';
import { getError, getCardElementOptions, getElementsOptions } from './helpers';
import RenderIf from '../RenderIf';
import Label from '../Input/label';
import HelpText from '../Input/styled/helpText';
import ErrorText from '../Input/styled/errorText';
import StyledContainer from './styled/container';
import StyledCardInput from './styled/cardInput';
/**
* Stripe Card Input component are used for freeform data entry.
* @category Form
*/
const StripeCardInput = React.forwardRef((props, ref) => {
const {
id,
apiKey,
label,
labelAlignment,
hideLabel,
bottomHelpText,
error,
disabled,
required,
locale,
onChange,
onFocus,
onBlur,
className,
style,
isScriptLoaded,
isScriptLoadSucceed,
size,
borderRadius,
} = props;
const [stripe, setStripe] = useState(null);
const cardRef = useRef();
const stripeCardInputId = useUniqueIdentifier('stripe-card-input');
const errorMessageId = useUniqueIdentifier('error-message');
const theme = useTheme().rainbow;
const cardElementOptions = useMemo(() => getCardElementOptions(theme, disabled, size), [
disabled,
size,
theme,
]);
const localeStripe = useLocale(locale);
const elementsOptions = useMemo(() => getElementsOptions(localeStripe), [localeStripe]);
useEffect(() => {
if (isScriptLoaded && isScriptLoadSucceed && window.Stripe && apiKey) {
setStripe(window.Stripe(apiKey));
}
}, [apiKey, isScriptLoadSucceed, isScriptLoaded]);
// eslint-disable-next-line consistent-return
useEffect(() => {
if (stripe) {
const cardNode = cardRef.current;
const elements = stripe.elements(elementsOptions);
const card = elements.create('card', cardElementOptions);
card.mount(cardNode);
card.on('change', event => {
const stripeCardEvent = {
stripe,
card,
isEmpty: event.empty,
isComplete: event.complete,
cardBrand: event.brand,
error: getError(event.error),
};
onChange(stripeCardEvent);
});
card.on('focus', onFocus);
card.on('blur', onBlur);
return () => {
card.unmount();
card.destroy();
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cardElementOptions, elementsOptions, stripe]);
return (
<StyledContainer
id={id}
ref={ref}
className={className}
style={style}
disabled={disabled}
error={error}
size={size}
>
<Label
label={label}
hideLabel={hideLabel}
labelAlignment={labelAlignment}
inputId={stripeCardInputId}
required={required}
size={size}
/>
<StyledCardInput
ref={cardRef}
id={stripeCardInputId}
disabled={disabled}
size={size}
borderRadius={borderRadius}
/>
<RenderIf isTrue={bottomHelpText}>
<HelpText alignSelf="center">{bottomHelpText}</HelpText>
</RenderIf>
<RenderIf isTrue={error}>
<ErrorText id={errorMessageId} alignSelf="center">
{error}
</ErrorText>
</RenderIf>
</StyledContainer>
);
});
StripeCardInput.propTypes = {
/** The id of the outer element. */
id: PropTypes.string,
/** The application's API key. To use Stripe,
* you must get an API Key. See https://dashboard.stripe.com/account/apikeys
* to get an API Key. */
apiKey: PropTypes.string.isRequired,
/** Text label for the component. */
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
/** Describes the position of the StripeCardInput label. Options include left, center and right.
* This value defaults to center. */
labelAlignment: PropTypes.oneOf(['left', 'center', 'right']),
/** A boolean to hide the StripeCardInput label. */
hideLabel: PropTypes.bool,
/** Shows the help message below the input. */
bottomHelpText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
/** Specifies that an input field must be filled out before submitting the form. */
error: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
/** Specifies that an input element should be disabled. This value defaults to false. */
disabled: PropTypes.bool,
/** Specifies that an input field must be filled out before submitting the form.
* This value defaults to false. */
required: PropTypes.bool,
/** The component locale. If the locale is not passed, it defaults to the context language, and if the context language is not passed, it will default to the browser's language. */
locale: PropTypes.oneOf([
'ar',
'da',
'de',
'en',
'es',
'fi',
'fr',
'he',
'it',
'ja',
'lt',
'lv',
'ms',
'nb',
'nl',
'pl',
'pt',
'pt-BR',
'ru',
'sv',
'zh',
]),
/** The action triggered when some value of the component changes. */
onChange: PropTypes.func,
/** The action triggered when the element receives focus. */
onFocus: PropTypes.func,
/** The action triggered when the element releases focus. */
onBlur: PropTypes.func,
/** A CSS class for the outer element, in addition to the component's base classes. */
className: PropTypes.string,
/** An object with custom style applied to the outer element. */
style: PropTypes.object,
/**
* This prop that should not be visible in the documentation.
* @ignore
*/
isScriptLoaded: PropTypes.bool.isRequired,
/**
* This prop that should not be visible in the documentation.
* @ignore
*/
isScriptLoadSucceed: PropTypes.bool.isRequired,
/** The size of the input. Valid values are small, medium, and large. */
size: PropTypes.oneOf(['small', 'medium', 'large']),
/** The border radius of the input. Valid values are square, semi-square, semi-rounded and rounded. This value defaults to rounded. */
borderRadius: PropTypes.oneOf(['square', 'semi-square', 'semi-rounded', 'rounded']),
};
StripeCardInput.defaultProps = {
id: undefined,
label: undefined,
labelAlignment: 'center',
hideLabel: false,
bottomHelpText: null,
error: null,
disabled: false,
required: false,
locale: undefined,
onChange: () => {},
onFocus: () => {},
onBlur: () => {},
className: undefined,
style: undefined,
size: 'medium',
borderRadius: 'rounded',
};
export default scriptLoader('https://js.stripe.com/v3')(withReduxForm(StripeCardInput));
export { StripeCardInput as Component };