279 lines
8.8 KiB
TypeScript
279 lines
8.8 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
|
import { Platform } from 'react-native';
|
|
import { WalletType } from 'rnwalletman';
|
|
import { startOtpSmsListener } from '../utils/smsRetriever';
|
|
|
|
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;
|
|
(async () => {
|
|
smsStopRef.current?.();
|
|
const stop = await startOtpSmsListener(otpLength, (code) => {
|
|
if (cancelled) return;
|
|
setOtp(code);
|
|
log('OTP from SMS Retriever:', code);
|
|
});
|
|
if (cancelled) {
|
|
stop();
|
|
return;
|
|
}
|
|
smsStopRef.current = stop;
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
smsStopRef.current?.();
|
|
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 },
|
|
];
|
|
}
|