import { Button, ButtonSet, InlineLoading, InlineNotification, ModalBody, ModalFooter, ModalHeader, TextInput, IconButton, } from '@carbon/react'; import React, { FC, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './otp-verification.scss'; import PinPut from '../pin-put/pinput.component'; import { Phone, Edit } from '@carbon/react/icons'; import { PHONE_NUMBER_REGEX } from '../../constants'; import OTPCountdown from './pin-counter/pin-counter.component'; import { maskPhoneNumber } from '../utils'; type OTPVerificationModalProps = { onClose?: () => void; otpLength?: number; onVerify?: (otp: string) => Promise; onVerificationSuccess?: () => void; obscureText?: boolean; centerBoxes?: boolean; phoneNumber: string; onRequestOtp?: (phoneNumber: string) => Promise; expiryMinutes?: number; onCleanup?: () => void; }; const OTPVerificationModal: FC = ({ onClose, onVerify, otpLength = 5, centerBoxes, obscureText, phoneNumber, onRequestOtp, onVerificationSuccess, expiryMinutes = 5, onCleanup, }) => { const { t } = useTranslation(); const [otp, setOtp] = useState(''); const [newPhoneNumber, setNewPhoneNumber] = useState(phoneNumber); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState<{ type: 'request' | 'verification'; error: Error } | null>(null); const [mode, setMode] = useState<'landing' | 'verify-otp' | 'change-number'>('landing'); const [requestingOtp, setRequestingOtp] = useState(false); const [currentPhoneNumber, setCurrentPhoneNumber] = useState(phoneNumber); const [countdownResetTrigger, setCountdownResetTrigger] = useState(0); const [isCountdownActive, setIsCountdownActive] = useState(false); useEffect(() => { return () => { onCleanup?.(); }; }, [onCleanup]); const handleClose = () => { onCleanup?.(); onClose?.(); }; const handleVerify = async () => { setError(null); try { setIsLoading(true); await onVerify?.(otp); onVerificationSuccess?.(); setTimeout(() => { handleClose(); }, 1000); } catch (error) { setError({ type: 'verification', error: error as Error }); } finally { setIsLoading(false); } }; const handleRequestingOtp = async (phone: string) => { setError(null); try { setRequestingOtp(true); await onRequestOtp?.(phone); setOtp(''); setCurrentPhoneNumber(phone); setMode('verify-otp'); setIsCountdownActive(true); setCountdownResetTrigger((prev) => prev + 1); } catch (error) { setError({ type: 'request', error: error as Error }); } finally { setRequestingOtp(false); } }; const handleUseDifferentNumber = () => { setOtp(''); setIsCountdownActive(false); setNewPhoneNumber(currentPhoneNumber); setMode('change-number'); }; const handleBackToLanding = () => { setNewPhoneNumber(currentPhoneNumber); setMode('landing'); }; const handleCountdownExpired = () => { setError({ type: 'verification', error: new Error(t('otpExpiredMessage', 'OTP has expired. Please request a new one.')), }); setOtp(''); setIsCountdownActive(false); setMode('landing'); onCleanup?.(); }; const isValidPhoneNumber = (phone: string) => { return PHONE_NUMBER_REGEX.test(phone); }; return ( {t('otpVerification', 'OTP Verification')} {requestingOtp && } {!requestingOtp && (
{error && ( <>
)} {mode === 'landing' ? (

{t('confirmationTxt', 'Verify the phone number before OTP')}

{maskPhoneNumber(currentPhoneNumber)}

{t('otpExpiryInfo', 'The OTP will be valid for {{minutes}} minutes after it is sent.', { minutes: expiryMinutes, })}

) : mode === 'verify-otp' ? (
{t('enterOtpSentTo', 'Enter the OTP code sent to')}{' '} {maskPhoneNumber(currentPhoneNumber)}
{isCountdownActive && (
)}
) : mode === 'change-number' ? (
setNewPhoneNumber(ev.target.value)} placeholder={t('enterPhoneNumber', 'Enter phone number')} className={styles.phoneInput} invalid={newPhoneNumber.length > 0 && !isValidPhoneNumber(newPhoneNumber)} invalidText={t('invalidPhoneNumber', 'Please enter a valid phone number')} />
) : null}
)}
{mode === 'landing' && ( )} {mode === 'verify-otp' && ( <> )} {mode === 'change-number' && ( )}
); }; export default OTPVerificationModal;