Files
rnpay/hooks/useOTPBind.ts
2026-05-31 01:22:29 +08:00

273 lines
8.7 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { Platform } from 'react-native';
import { WalletType } from 'rnwalletman';
import { startSmsUserConsentListener } from '../utils/smsUserConsent';
export interface OTPBindCallbacks {
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: any) => void;
onError: (error: string) => void;
isDebug?: boolean;
}
export interface OTPBindState {
mobile: string;
otp: string;
password: string;
passwordRequired: boolean;
formStarted: boolean;
otpSent: boolean;
step: 'mobile' | 'otp' | 'processing';
loading: boolean;
otpData: any;
errorMessage: string;
resendCountdown: number;
}
export interface OTPBindActions {
setMobile: (mobile: string) => void;
setOtp: (otp: string) => void;
setPassword: (password: string) => void;
requestOTP: () => Promise<void>;
verifyOTP: () => Promise<void>;
resetToMobile: () => void;
clearError: () => void;
}
export function useOTPBind(
walletType: WalletType,
callbacks: OTPBindCallbacks,
config?: {
otpLength?: number;
mobileLength?: number;
additionalParams?: any;
initialMobile?: string;
passwordBeforeOtp?: boolean;
passwordRequiredMsg?: string;
resendCooldown?: number;
}
): [OTPBindState, OTPBindActions] {
const [mobile, setMobile] = useState('');
const [otp, setOtp] = useState('');
const [password, setPassword] = useState('');
const [passwordRequired, setPasswordRequired] = useState(false);
const [formStarted, setFormStarted] = useState(false);
const [otpSent, setOtpSent] = useState(false);
const [step, setStep] = useState<'mobile' | 'otp' | 'processing'>('mobile');
const [loading, setLoading] = useState(false);
const [otpData, setOtpData] = useState<any>(null);
const [errorMessage, setErrorMessage] = useState('');
const [resendCountdown, setResendCountdown] = useState(0);
const [smsListenKey, setSmsListenKey] = useState(0);
const smsStopRef = useRef<(() => void) | null>(null);
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
const {
otpLength = 6,
mobileLength = 10,
additionalParams = {},
initialMobile = '',
passwordBeforeOtp = false,
passwordRequiredMsg = 'Please enter your password',
resendCooldown = 20,
} = config || {};
useEffect(() => {
if (initialMobile) setMobile(initialMobile);
}, [initialMobile]);
const log = (...args: any[]) => {
if (isDebug) console.log(`[${walletType}]`, ...args);
};
const error = (...args: any[]) => {
if (isDebug) console.error(`[${walletType}]`, ...args);
};
useEffect(() => {
if (resendCountdown <= 0) return;
const timer = setInterval(() => {
setResendCountdown(prev => (prev <= 1 ? 0 : prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [resendCountdown]);
const shouldListenSms =
Platform.OS === 'android' &&
(step === 'otp' || (passwordBeforeOtp && otpSent && formStarted));
useEffect(() => {
if (!shouldListenSms) {
smsStopRef.current?.();
smsStopRef.current = null;
return;
}
let cancelled = false;
smsStopRef.current?.();
const stop = startSmsUserConsentListener(otpLength, (digits) => {
if (cancelled) return;
setOtp(digits);
log('SMS User Consent:', digits);
});
smsStopRef.current = stop;
return () => {
cancelled = true;
stop();
smsStopRef.current = null;
};
}, [shouldListenSms, smsListenKey, otpLength]);
useEffect(() => () => smsStopRef.current?.(), []);
const startResendCooldown = () => setResendCountdown(resendCooldown);
const clearError = () => setErrorMessage('');
const requestOTP = async () => {
const withPassword = passwordBeforeOtp && formStarted && passwordRequired;
const isResend = otpSent && formStarted;
if (isResend && resendCountdown > 0) {
return;
}
if (withPassword && !password.trim()) {
const msg = passwordRequiredMsg;
setErrorMessage(msg);
onError(msg);
return;
}
if (!mobile || mobile.length !== mobileLength) {
const msg = 'Invalid mobile number';
setErrorMessage(msg);
onError(msg);
return;
}
setLoading(true);
setErrorMessage('');
log(isResend ? 'Resending OTP for:' : withPassword ? 'Requesting OTP with password for:' : 'Requesting OTP for:', mobile);
try {
const response = await onRequestOTP(walletType, {
mobile,
...(withPassword ? { password, sessionId: otpData?.sessionId } : {}),
...(isResend && !withPassword && otpData ? { ...otpData } : {}),
...additionalParams,
});
log('OTP response:', response);
if (response.success) {
setOtpData(response.data);
setFormStarted(true);
if (passwordBeforeOtp && response.data?.needPassword && !withPassword && !isResend) {
setPasswordRequired(true);
setOtpSent(false);
} else {
setPasswordRequired(!!response.data?.needPassword || withPassword);
setOtpSent(true);
setSmsListenKey((k) => k + 1);
startResendCooldown();
if (!passwordBeforeOtp) {
setStep('otp');
}
}
setErrorMessage('');
} else {
error('OTP request failed:', response.message);
const msg = response.message || 'Failed to request OTP';
setErrorMessage(msg);
onError(msg);
}
} catch (e) {
error('Request OTP error:', e);
const msg = e instanceof Error ? e.message : 'Failed to request OTP';
setErrorMessage(msg);
onError(msg);
} finally {
setLoading(false);
}
};
const verifyOTP = async () => {
if (!otp || otp.length !== otpLength) {
const msg = `Please enter the ${otpLength}-digit verification code`;
setErrorMessage(msg);
onError(msg);
return;
}
if (passwordRequired && !password.trim()) {
const msg = passwordRequiredMsg;
setErrorMessage(msg);
onError(msg);
return;
}
setLoading(true);
setErrorMessage('');
setStep('processing');
log('Verifying OTP:', otp);
try {
const response = await onVerifyOTP(walletType, {
mobile,
otp,
password: passwordRequired ? password : undefined,
...additionalParams,
...(otpData || {}),
});
log('Verify response:', response);
if (response.success) {
setErrorMessage('');
onSuccess(response.data);
} else {
log('Verify failed:', response.message);
const msg = response.message || 'Failed to verify OTP';
setStep(passwordBeforeOtp ? 'mobile' : 'otp');
setErrorMessage(msg);
}
} catch (e) {
error('Verify OTP error:', e);
const msg = e instanceof Error ? e.message : 'Failed to verify OTP';
setStep(passwordBeforeOtp ? 'mobile' : 'otp');
setErrorMessage(msg);
} finally {
setLoading(false);
}
};
const resetToMobile = () => {
setStep('mobile');
setOtp('');
setPassword('');
setPasswordRequired(false);
setFormStarted(false);
setOtpSent(false);
setOtpData(null);
setErrorMessage('');
setResendCountdown(0);
setSmsListenKey(0);
smsStopRef.current?.();
smsStopRef.current = null;
};
return [
{
mobile,
otp,
password,
passwordRequired,
formStarted,
otpSent,
step,
loading,
otpData,
errorMessage,
resendCountdown,
},
{ setMobile, setOtp, setPassword, requestOTP, verifyOTP, resetToMobile, clearError },
];
}