import React, { useLayoutEffect } from 'react'; import type { UseSecureCardFormRefMethods } from './hooks/useSecureCardForm'; import { useUpdateEffect } from './hooks/useUpdateEffect'; import { MoneyHash } from '../nativeModule'; import type { SecureFieldType, VaultData } from './types'; import { throwIf, throwMhError } from '../utils/throwIf'; import type { IntentDetails, IntentStateDetails, LookupData } from '../models'; import { type InstallmentPlanData, installmentPlanDataToJson, } from '../models/InstallmentPlanData'; import { intentDetailsFromJson } from '../models/IntentDetails'; import { lookupDataFromJson } from '../models/LookupData'; import { cardFormConfigurationToJson, type CardFormConfiguration, } from '../models/CardFormConfiguration'; export const fieldNamesMap: Record = { cardNumber: 'card_number', cardHolderName: 'card_holder_name', expiryYear: 'expiry_year', expiryMonth: 'expiry_month', cvv: 'cvv', }; type SecureCardFormContextType = { updateValue: (options: { name: string; value: string }) => void; updateFieldValidity: (options: { name: string; isValid: boolean }) => void; // The detected card brand, shared so the CVV can re-validate when it changes. // CVV length is brand-dependent (e.g. Amex requires a 4-digit CVV), so its // validity can change when the brand changes even though its value didn't. brand?: string; setBrand: (brand?: string) => void; }; const SecureCardFormContext = React.createContext(null); export type SecureCardFormProps = { children: React.ReactNode; onFormValidityChange?: (isValid: boolean) => void; configuration?: CardFormConfiguration; }; export const SecureCardForm = React.forwardRef( function SecureCardForm( { children, onFormValidityChange, configuration }, ref ) { const valuesRef = React.useRef>({}); const [fieldsValidityMap, setFieldsValidityMap] = React.useState< Record >({}); const [isMoneyHashFormValid, setIsMoneyHashFormValid] = React.useState(false); React.useEffect(() => { const checkMoneyHashValidity = async () => { try { const result = await MoneyHash.isValidCardForm(); const parsed = JSON.parse(result); setIsMoneyHashFormValid(parsed.isValid); } catch (error) { setIsMoneyHashFormValid(true); console.error(error); } }; checkMoneyHashValidity(); }, [fieldsValidityMap]); const isValidForm = React.useMemo(() => { const areFieldsValid = Object.values(fieldsValidityMap).every(Boolean); return areFieldsValid && isMoneyHashFormValid; }, [fieldsValidityMap, isMoneyHashFormValid]); useLayoutEffect(() => { if (!configuration) return; MoneyHash.setCardFormConfiguration( cardFormConfigurationToJson(configuration) ); }, []); useUpdateEffect(() => { onFormValidityChange?.(isValidForm); }, [isValidForm]); React.useImperativeHandle( ref, () => ({ async collect(options?: { accessToken: string }) { const missingField = getMissingField( Object.keys(valuesRef.current) as SecureFieldType[] ); throwIf( !!missingField, `${missingField} must be registered as within ` ); return collectCardForm({ accessToken: options?.accessToken, }); }, pay, createCardToken, binLookup, }), [] ); const updateValue: SecureCardFormContextType['updateValue'] = React.useCallback( ({ name, value }) => (valuesRef.current[name] = value), [] ); const updateFieldValidity: SecureCardFormContextType['updateFieldValidity'] = React.useCallback( ({ name, isValid }) => setFieldsValidityMap((prev) => ({ ...prev, [name]: isValid })), [] ); const [brand, setBrand] = React.useState(); const contextValue = React.useMemo( () => ({ updateValue, updateFieldValidity, brand, setBrand, }), [updateValue, updateFieldValidity, brand] ); return ( {children} ); } ); function collectCardForm({ accessToken, }: { accessToken?: string; }): Promise { return new Promise((resolve, reject) => { MoneyHash.collectCardForm(accessToken) .then((result: any) => { try { const data = JSON.parse(result); resolve(data); } catch (error: any) { throwMhError(error, reject); } }) .catch((error: any) => { throwMhError(error, reject); }); }); } function pay({ intentId, saveCard = false, cardData, billingData, shippingData, installmentPlanData, }: { intentId: string; saveCard?: boolean; cardData: VaultData; billingData?: Record; shippingData?: Record; installmentPlanData?: InstallmentPlanData; }): Promise { return new Promise((resolve, reject) => { MoneyHash.pay( intentId, cardData, saveCard ?? false, billingData, shippingData, installmentPlanData ? installmentPlanDataToJson(installmentPlanData) : {} ) .then((result: any) => { try { const data = JSON.parse(result); resolve(intentDetailsFromJson(data)); } catch (error: any) { throwMhError(error, reject); } }) .catch((error: any) => { throwMhError(error, reject); }); }); } function createCardToken({ cardIntentId, cardData, }: { cardIntentId: string; cardData: VaultData; }): Promise { return new Promise((resolve, reject) => { MoneyHash.createCardToken(cardIntentId, cardData) .then((result: any) => { try { const data = JSON.parse(result); resolve(data); } catch (error: any) { throwMhError(error, reject); } }) .catch((error: any) => { throwMhError(error, reject); }); }); } function binLookup(): Promise; function binLookup(cardData: VaultData): Promise; function binLookup(cardData?: VaultData): Promise { return new Promise((resolve, reject) => { const nativeCall = cardData ? MoneyHash.binLookupWithCardData(cardData) : MoneyHash.binLookup(); nativeCall .then((result: any) => { try { const parsedData = JSON.parse(result); resolve(lookupDataFromJson(parsedData)); } catch (error: any) { throwMhError(error, reject); } }) .catch((error: any) => { throwMhError(error, reject); }); }); } export function useSecureCardForm() { const context = React.useContext(SecureCardFormContext); if (!context) { throw new Error( ' must be used within a ' ); } return context; } function getMissingField(registeredFields: SecureFieldType[]) { const requiredFields = new Set([ 'cardNumber', 'expiryMonth', 'expiryYear', 'cvv', ]); for (const element of requiredFields) { if (!registeredFields.includes(element)) { return element; } } return null; }