From c79a088597b1c290d5b7e31b1309978e0beb5181 Mon Sep 17 00:00:00 2001 From: TQCasey <494294315@qq.com> Date: Thu, 28 May 2026 15:21:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=20verify=20=E5=92=8C=20otp?= =?UTF-8?q?=20=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=A2=9E=E5=8A=A0=20password=20?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/OTPBindUI.tsx | 183 +++++++++++++++++++++++----- components/WalletBindComponents.tsx | 36 +++--- hooks/useOTPBind.ts | 96 ++++++++++++--- screens/HomeScreen.tsx | 5 +- 4 files changed, 256 insertions(+), 64 deletions(-) diff --git a/components/OTPBindUI.tsx b/components/OTPBindUI.tsx index 8ae6d16..013c4e5 100644 --- a/components/OTPBindUI.tsx +++ b/components/OTPBindUI.tsx @@ -15,6 +15,11 @@ interface OTPBindUIProps { title: string; otpLength?: number; mobileLength?: number; + passwordBeforeOtp?: boolean; + passwordLabel?: string; + passwordPlaceholder?: string; + passwordRequiredMsg?: string; + resendCooldown?: number; onRequestOTP: (walletType: WalletType, params: any) => Promise; onVerifyOTP: (walletType: WalletType, params: any) => Promise; onSuccess: (result: any) => void; @@ -29,6 +34,11 @@ export const OTPBindUI: React.FC = ({ title, otpLength = 6, mobileLength = 10, + passwordBeforeOtp = false, + passwordLabel = 'Password', + passwordPlaceholder = 'Enter password', + passwordRequiredMsg = 'Please enter your password', + resendCooldown = 20, onRequestOTP, onVerifyOTP, onSuccess, @@ -40,10 +50,132 @@ export const OTPBindUI: React.FC = ({ const [state, actions] = useOTPBind( walletType, { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug }, - { otpLength, mobileLength, additionalParams, initialMobile } + { otpLength, mobileLength, additionalParams, initialMobile, passwordBeforeOtp, passwordRequiredMsg, resendCooldown } ); const isLoading = state.loading || state.step === 'processing'; + + const renderResendOtp = () => { + if (!state.otpSent) return null; + const disabled = isLoading || state.resendCountdown > 0; + return ( + + + {state.resendCountdown > 0 + ? `Resend in ${state.resendCountdown}s` + : 'Resend OTP'} + + + ); + }; + + if (passwordBeforeOtp) { + return ( + + + {title} + {!!state.errorMessage && ( + {state.errorMessage} + )} + Mobile + { + actions.setMobile(t); + if (state.errorMessage) actions.clearError(); + }} + editable={!state.formStarted && !isLoading} + /> + {state.formStarted && state.passwordRequired && ( + <> + {passwordLabel} + { + actions.setPassword(t); + if (state.errorMessage) actions.clearError(); + }} + editable={!isLoading} + /> + + )} + {!state.otpSent && ( + + {isLoading ? ( + + ) : ( + Send OTP + )} + + )} + {state.otpSent && ( + <> + OTP sent to {state.mobile} + Verification code + { + actions.setOtp(t); + if (state.errorMessage) actions.clearError(); + }} + editable={!isLoading} + /> + + {isLoading ? ( + + ) : ( + Verify & Bind + )} + + {renderResendOtp()} + + )} + {state.formStarted && ( + + + Change mobile number + + + )} + + + ); + } + const showOtp = state.step === 'otp' || state.step === 'processing'; return ( @@ -51,15 +183,15 @@ export const OTPBindUI: React.FC = ({ {title} - {state.step === 'mobile' && ( + {!showOtp && ( <> {!!state.errorMessage && ( {state.errorMessage} )} - 手机号 + Mobile = ({ {isLoading ? ( ) : ( - 获取验证码 + Send OTP )} @@ -87,37 +219,14 @@ export const OTPBindUI: React.FC = ({ {showOtp && ( <> - - {state.needPassword - ? `验证码已发送至 ${state.mobile},该账号需输入密码` - : `验证码已发送至 ${state.mobile}`} - + OTP sent to {state.mobile} {!!state.errorMessage && ( {state.errorMessage} )} - {state.needPassword && ( - <> - Amazon 密码 - { - actions.setPassword(t); - if (state.errorMessage) actions.clearError(); - }} - editable={!isLoading} - /> - - )} - 验证码 + Verification code = ({ {isLoading ? ( ) : ( - 验证并绑定 + Verify & Bind )} + {renderResendOtp()} - 重新输入手机号 + Change mobile number @@ -200,6 +310,10 @@ const styles = StyleSheet.create({ marginBottom: 14, backgroundColor: '#fafafa', }, + inputLocked: { + backgroundColor: '#f0f0f0', + color: '#666', + }, inputError: { borderColor: '#ff3b30', backgroundColor: '#fff8f7', @@ -235,4 +349,7 @@ const styles = StyleSheet.create({ color: '#007AFF', fontSize: 13, }, + linkTextDisabled: { + color: '#aaa', + }, }); diff --git a/components/WalletBindComponents.tsx b/components/WalletBindComponents.tsx index 331a6ab..c597043 100644 --- a/components/WalletBindComponents.tsx +++ b/components/WalletBindComponents.tsx @@ -15,7 +15,7 @@ export class FreeChargeBind extends Component<{ return ( { if (isDebug) console.log('[PaytmBusiness]', ...args); }; const handleRequestOTP = async () => { - if (!mobile || mobile.length !== 10) { setErrorMsg('请输入10位手机号'); return; } + if (!mobile || mobile.length !== 10) { setErrorMsg('Please enter a 10-digit mobile number'); return; } setLoading(true); setErrorMsg(''); try { const res = await onRequestOTP(WalletType.PAYTM_BUSINESS, { mobile }); @@ -161,7 +161,7 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe }; const handleVerifyOTP = async () => { - if (!otp || otp.length !== 6) { setErrorMsg('请输入6位验证码'); return; } + if (!otp || otp.length !== 6) { setErrorMsg('Please enter the 6-digit verification code'); return; } setLoading(true); setErrorMsg(''); try { const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionToken }); @@ -182,25 +182,25 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe return ( - Paytm Business 绑定 + Bind Paytm Business {errorMsg ? {errorMsg} : null} {step === 'credentials' && ( <> - { setMobile(t); setErrorMsg(''); }} editable={!loading} /> + { setMobile(t); setErrorMsg(''); }} editable={!loading} /> - {loading ? : 获取验证码} + {loading ? : Send OTP} )} {step === 'otp' && ( <> - 验证码已发送至 {mobile} - { setOtp(t); setErrorMsg(''); }} editable={!loading} /> + OTP sent to {mobile} + { setOtp(t); setErrorMsg(''); }} editable={!loading} /> - {loading ? : 验证并绑定} + {loading ? : Verify & Bind} setStep('credentials')} disabled={loading}> - 重新输入手机号 + Change mobile number )} @@ -235,7 +235,7 @@ export class PhonePeBusinessOTPBind extends Component<{ return ( ('mobile'); const [loading, setLoading] = useState(false); const [otpData, setOtpData] = useState(null); const [errorMessage, setErrorMessage] = useState(''); + const [resendCountdown, setResendCountdown] = useState(0); const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks; - const { otpLength = 6, mobileLength = 10, additionalParams = {}, initialMobile = '' } = config || {}; + const { + otpLength = 6, + mobileLength = 10, + additionalParams = {}, + initialMobile = '', + passwordBeforeOtp = false, + passwordRequiredMsg = 'Please enter your password', + resendCooldown = 20, + } = config || {}; useEffect(() => { if (initialMobile) setMobile(initialMobile); }, [initialMobile]); + useEffect(() => { + if (resendCountdown <= 0) return; + const timer = setInterval(() => { + setResendCountdown(prev => (prev <= 1 ? 0 : prev - 1)); + }, 1000); + return () => clearInterval(timer); + }, [resendCountdown]); + + const startResendCooldown = () => setResendCountdown(resendCooldown); + const clearError = () => setErrorMessage(''); const log = (...args: any[]) => { @@ -67,6 +94,19 @@ export function useOTPBind( }; 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); @@ -76,19 +116,31 @@ export function useOTPBind( setLoading(true); setErrorMessage(''); - log('Requesting OTP for:', mobile); + 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); - setNeedPassword(!!response.data?.needPassword); - setStep('otp'); + setFormStarted(true); + if (passwordBeforeOtp && response.data?.needPassword && !withPassword && !isResend) { + setPasswordRequired(true); + setOtpSent(false); + } else { + setPasswordRequired(!!response.data?.needPassword || withPassword); + setOtpSent(true); + startResendCooldown(); + if (!passwordBeforeOtp) { + setStep('otp'); + } + } setErrorMessage(''); } else { error('OTP request failed:', response.message); @@ -108,13 +160,13 @@ export function useOTPBind( const verifyOTP = async () => { if (!otp || otp.length !== otpLength) { - const msg = `请输入 ${otpLength} 位验证码`; + const msg = `Please enter the ${otpLength}-digit verification code`; setErrorMessage(msg); onError(msg); return; } - if (needPassword && !password.trim()) { - const msg = '请输入 Amazon 账号密码'; + if (passwordRequired && !password.trim()) { + const msg = passwordRequiredMsg; setErrorMessage(msg); onError(msg); return; @@ -129,7 +181,7 @@ export function useOTPBind( const response = await onVerifyOTP(walletType, { mobile, otp, - password: needPassword ? password : undefined, + password: passwordRequired ? password : undefined, ...additionalParams, ...(otpData || {}), }); @@ -141,13 +193,13 @@ export function useOTPBind( } else { log('Verify failed:', response.message); const msg = response.message || 'Failed to verify OTP'; - setStep('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('otp'); + setStep(passwordBeforeOtp ? 'mobile' : 'otp'); setErrorMessage(msg); } finally { setLoading(false); @@ -158,12 +210,28 @@ export function useOTPBind( setStep('mobile'); setOtp(''); setPassword(''); - setNeedPassword(false); + setPasswordRequired(false); + setFormStarted(false); + setOtpSent(false); + setOtpData(null); setErrorMessage(''); + setResendCountdown(0); }; return [ - { mobile, otp, password, needPassword, step, loading, otpData, errorMessage }, + { + mobile, + otp, + password, + passwordRequired, + formStarted, + otpSent, + step, + loading, + otpData, + errorMessage, + resendCountdown, + }, { setMobile, setOtp, setPassword, requestOTP, verifyOTP, resetToMobile, clearError }, ]; } diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx index a626440..453bcda 100644 --- a/screens/HomeScreen.tsx +++ b/screens/HomeScreen.tsx @@ -798,7 +798,10 @@ export default class HomeScreen extends Component { isDebug initialMobile={bindPrefillMobile} onRequestOTP={async (wt, p) => { - return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {})); + return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, { + ...(p.sessionId ? { sessionId: p.sessionId } : {}), + ...(p.password ? { password: p.password } : {}), + })); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, {