import React, { useState, useRef, useCallback, useEffect } from 'react'; import { verifyEmailOtp, sendEmailOtp } from '../core/checkout-api'; import type { EmailOtpStepProps } from '../core/types'; const OTP_LENGTH = 6; const RESEND_COOLDOWN = 60; // seconds — email resend is slower than SMS /** Mask email for display: y***@example.com */ function maskEmail(email: string): string { const [local, domain] = email.split('@'); if (!domain || local.length <= 1) return email; return `${local[0]}***@${domain}`; } const EmailOtpStep: React.FC = ({ email, codeReused = false, onVerified, onBack, functionBaseUrl, merchantId, sessionContext, }) => { const [digits, setDigits] = useState(Array(OTP_LENGTH).fill('')); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [resendCountdown, setResendCountdown] = useState(RESEND_COOLDOWN); const [resendLoading, setResendLoading] = useState(false); const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const timerRef = useRef | null>(null); useEffect(() => { timerRef.current = setInterval(() => { setResendCountdown((c) => { if (c <= 1) { clearInterval(timerRef.current!); return 0; } return c - 1; }); }, 1000); return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, []); const focusBox = (index: number) => inputRefs.current[index]?.focus(); const handleDigitChange = useCallback((index: number, value: string) => { const char = value.replace(/\D/g, '').slice(-1); setError(null); setDigits((prev) => { const next = [...prev]; next[index] = char; return next; }); if (char && index < OTP_LENGTH - 1) focusBox(index + 1); }, []); const handleKeyDown = useCallback((index: number, e: React.KeyboardEvent) => { if (e.key === 'Backspace') { if (digits[index]) { setDigits((prev) => { const n = [...prev]; n[index] = ''; return n; }); } else if (index > 0) { setDigits((prev) => { const n = [...prev]; n[index - 1] = ''; return n; }); focusBox(index - 1); } } else if (e.key === 'ArrowLeft' && index > 0) { focusBox(index - 1); } else if (e.key === 'ArrowRight' && index < OTP_LENGTH - 1) { focusBox(index + 1); } }, [digits]); const handlePaste = useCallback((e: React.ClipboardEvent) => { e.preventDefault(); const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, OTP_LENGTH); if (!pasted) return; const next = Array(OTP_LENGTH).fill(''); pasted.split('').forEach((ch, i) => { next[i] = ch; }); setDigits(next); setError(null); focusBox(Math.min(pasted.length, OTP_LENGTH - 1)); }, []); const code = digits.join(''); const isComplete = code.length === OTP_LENGTH && !digits.includes(''); const handleVerify = useCallback(async () => { if (!isComplete) return; setLoading(true); setError(null); try { const result = await verifyEmailOtp({ functionBaseUrl, merchantId }, email, code, sessionContext); onVerified(result.session_token, result.buyer_id, result.session_id); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Verification failed. Please try again.'); setDigits(Array(OTP_LENGTH).fill('')); focusBox(0); } finally { setLoading(false); } }, [isComplete, code, email, functionBaseUrl, merchantId, onVerified, sessionContext]); useEffect(() => { if (isComplete && !loading) handleVerify(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isComplete]); const handleResend = useCallback(async () => { if (resendCountdown > 0 || resendLoading) return; setResendLoading(true); setError(null); setDigits(Array(OTP_LENGTH).fill('')); try { await sendEmailOtp({ functionBaseUrl, merchantId }, email); setResendCountdown(RESEND_COOLDOWN); timerRef.current = setInterval(() => { setResendCountdown((c) => { if (c <= 1) { clearInterval(timerRef.current!); return 0; } return c - 1; }); }, 1000); focusBox(0); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Failed to resend code.'); } finally { setResendLoading(false); } }, [resendCountdown, resendLoading, functionBaseUrl, merchantId, email]); return (
{/* Icon */}

Check your email

{codeReused ? <>Use the code we sent earlier — it's still valid for your next checkout. : <>6-digit code sent to {maskEmail(email)}}

{digits.map((d, i) => ( { inputRefs.current[i] = el; }} type="text" inputMode="numeric" maxLength={1} className={`qr-otp-box${d ? ' qr-otp-box--filled' : ''}${error ? ' qr-otp-box--error' : ''}`} value={d} onChange={(e) => handleDigitChange(i, e.target.value)} onKeyDown={(e) => handleKeyDown(i, e)} onPaste={handlePaste} onFocus={(e) => e.target.select()} aria-label={`Code digit ${i + 1}`} disabled={loading} autoComplete={i === 0 ? 'one-time-code' : 'off'} /> ))}
{error && (

{error}

)}

{resendCountdown > 0 ? ( Resend code in {resendCountdown}s ) : ( )}

); }; export default EmailOtpStep;