import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { useDispatch } from 'react-redux'; import { useAppSelector } from '@akinon/next/redux/hooks'; import { Button, Input, Icon } from '@akinon/next/components'; import { LoaderSpinner } from '@akinon/next/components'; import { createCreditCardFormSchema, type CreditCardFormData } from '../utils/validation-schemas'; import { formatCardNumber, formatExpiryDate, formatCVV, detectCardType, getCvcLength, getCardIcon } from '../utils/card-utils'; import { setNewCardFormData } from '../redux/reducer'; import clsx from 'clsx'; import MasterpassSecurityInfo from './masterpass-security-info'; import { CreditCardFormProps } from '../types/custom-render.types'; const CreditCardForm: React.FC = ({ onSaveCard, isLoading = false, showSaveOption = true, initialValues = {}, onBinChange, texts }) => { const dispatch = useDispatch(); const { newCardFormData } = useAppSelector((state) => state.masterpassRest); const [cardType, setCardType] = useState('unknown'); const [isBinChecking, setIsBinChecking] = useState(false); const lastCheckedBinRef = useRef(''); const binCheckTimeoutRef = useRef(null); const validationSchema = React.useMemo( () => createCreditCardFormSchema(texts), [texts] ); const { register, handleSubmit, watch, setValue, formState: { errors, isValid } } = useForm({ resolver: yupResolver(validationSchema) as any, defaultValues: { cardNumber: '', expiryDate: '', cvv: '', cardholderName: '', saveCard: false, cardAlias: '', ...initialValues, ...newCardFormData }, mode: 'all' }); const watchedCardNumber = watch('cardNumber'); const watchedSaveCard = watch('saveCard'); const watchedCardholderName = watch('cardholderName'); const watchedExpiryDate = watch('expiryDate'); const watchedCvv = watch('cvv'); const watchedCardAlias = watch('cardAlias'); useEffect(() => { const formData = { cardNumber: watchedCardNumber, cardholderName: watchedCardholderName, expiryDate: watchedExpiryDate, cvv: watchedCvv, saveCard: watchedSaveCard, cardAlias: watchedCardAlias }; dispatch(setNewCardFormData(formData)); }, [ watchedCardNumber, watchedCardholderName, watchedExpiryDate, watchedCvv, watchedSaveCard, watchedCardAlias, dispatch ]); useEffect(() => { const detectedType = detectCardType(watchedCardNumber); setCardType(detectedType); if (binCheckTimeoutRef.current) { clearTimeout(binCheckTimeoutRef.current); } if (watchedCardNumber && onBinChange) { const cleanCardNumber = watchedCardNumber.replace(/\D/g, ''); if (cleanCardNumber.length >= 6) { const bin = cleanCardNumber.substring(0, 6); if (bin !== lastCheckedBinRef.current) { binCheckTimeoutRef.current = setTimeout(() => { setIsBinChecking(true); lastCheckedBinRef.current = bin; onBinChange(bin).finally(() => { setIsBinChecking(false); }); }, 500); } } } return () => { if (binCheckTimeoutRef.current) { clearTimeout(binCheckTimeoutRef.current); } }; }, [ watchedCardNumber, watchedCardholderName, watchedExpiryDate, watchedCvv, watchedSaveCard, watchedCardAlias, onBinChange ]); const onFormSubmit = (data: CreditCardFormData) => { if (data.saveCard && onSaveCard) { const cleanedData = { ...data, cardNumber: data.cardNumber.replace(/\D/g, '') }; onSaveCard(cleanedData); } }; const expectedCVVLength = getCvcLength(cardType); return (
{texts.creditCardInfoTitle} {isBinChecking && (
{texts.checkingCardText}
)}
) => { const formatted = formatCardNumber(e.target.value); setValue('cardNumber', formatted, { shouldValidate: true }); }} error={errors.cardNumber} maxLength={23} className="pr-20" /> {cardType !== 'unknown' && (
{cardType}
)}
) => { const formatted = formatExpiryDate(e.target.value); setValue('expiryDate', formatted, { shouldValidate: true }); }} error={errors.expiryDate} maxLength={5} />
) => { const formatted = formatCVV(e.target.value); setValue('cvv', formatted, { shouldValidate: true }); }} error={errors.cvv} maxLength={expectedCVVLength} />
{showSaveOption && (
{watchedSaveCard && ( <>
)}
)} {watchedSaveCard && (
)}
); }; export default CreditCardForm;