合并 verify 和 otp 界面,增加 password 界面
This commit is contained in:
@@ -15,6 +15,11 @@ interface OTPBindUIProps {
|
|||||||
title: string;
|
title: string;
|
||||||
otpLength?: number;
|
otpLength?: number;
|
||||||
mobileLength?: number;
|
mobileLength?: number;
|
||||||
|
passwordBeforeOtp?: boolean;
|
||||||
|
passwordLabel?: string;
|
||||||
|
passwordPlaceholder?: string;
|
||||||
|
passwordRequiredMsg?: string;
|
||||||
|
resendCooldown?: number;
|
||||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||||
onSuccess: (result: any) => void;
|
onSuccess: (result: any) => void;
|
||||||
@@ -29,6 +34,11 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
title,
|
title,
|
||||||
otpLength = 6,
|
otpLength = 6,
|
||||||
mobileLength = 10,
|
mobileLength = 10,
|
||||||
|
passwordBeforeOtp = false,
|
||||||
|
passwordLabel = 'Password',
|
||||||
|
passwordPlaceholder = 'Enter password',
|
||||||
|
passwordRequiredMsg = 'Please enter your password',
|
||||||
|
resendCooldown = 20,
|
||||||
onRequestOTP,
|
onRequestOTP,
|
||||||
onVerifyOTP,
|
onVerifyOTP,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -40,10 +50,132 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
const [state, actions] = useOTPBind(
|
const [state, actions] = useOTPBind(
|
||||||
walletType,
|
walletType,
|
||||||
{ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug },
|
{ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug },
|
||||||
{ otpLength, mobileLength, additionalParams, initialMobile }
|
{ otpLength, mobileLength, additionalParams, initialMobile, passwordBeforeOtp, passwordRequiredMsg, resendCooldown }
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = state.loading || state.step === 'processing';
|
const isLoading = state.loading || state.step === 'processing';
|
||||||
|
|
||||||
|
const renderResendOtp = () => {
|
||||||
|
if (!state.otpSent) return null;
|
||||||
|
const disabled = isLoading || state.resendCountdown > 0;
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.linkBtn}
|
||||||
|
onPress={actions.requestOTP}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<Text style={[styles.linkText, disabled && styles.linkTextDisabled]}>
|
||||||
|
{state.resendCountdown > 0
|
||||||
|
? `Resend in ${state.resendCountdown}s`
|
||||||
|
: 'Resend OTP'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (passwordBeforeOtp) {
|
||||||
|
return (
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
{!!state.errorMessage && (
|
||||||
|
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
||||||
|
)}
|
||||||
|
<Text style={styles.label}>Mobile</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, !!state.errorMessage && styles.inputError, state.formStarted && styles.inputLocked]}
|
||||||
|
placeholder={`Enter ${mobileLength}-digit mobile number`}
|
||||||
|
placeholderTextColor="#aaa"
|
||||||
|
keyboardType="phone-pad"
|
||||||
|
maxLength={mobileLength}
|
||||||
|
value={state.mobile}
|
||||||
|
onChangeText={t => {
|
||||||
|
actions.setMobile(t);
|
||||||
|
if (state.errorMessage) actions.clearError();
|
||||||
|
}}
|
||||||
|
editable={!state.formStarted && !isLoading}
|
||||||
|
/>
|
||||||
|
{state.formStarted && state.passwordRequired && (
|
||||||
|
<>
|
||||||
|
<Text style={styles.label}>{passwordLabel}</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
||||||
|
placeholder={passwordPlaceholder}
|
||||||
|
placeholderTextColor="#aaa"
|
||||||
|
secureTextEntry
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
value={state.password}
|
||||||
|
onChangeText={t => {
|
||||||
|
actions.setPassword(t);
|
||||||
|
if (state.errorMessage) actions.clearError();
|
||||||
|
}}
|
||||||
|
editable={!isLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!state.otpSent && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.btn, isLoading && styles.btnDisabled]}
|
||||||
|
onPress={actions.requestOTP}
|
||||||
|
disabled={isLoading}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.btnText}>Send OTP</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
{state.otpSent && (
|
||||||
|
<>
|
||||||
|
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
||||||
|
<Text style={styles.label}>Verification code</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
||||||
|
placeholder={`Enter ${otpLength}-digit code`}
|
||||||
|
placeholderTextColor="#aaa"
|
||||||
|
keyboardType="number-pad"
|
||||||
|
maxLength={otpLength}
|
||||||
|
value={state.otp}
|
||||||
|
onChangeText={t => {
|
||||||
|
actions.setOtp(t);
|
||||||
|
if (state.errorMessage) actions.clearError();
|
||||||
|
}}
|
||||||
|
editable={!isLoading}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.btn, isLoading && styles.btnDisabled]}
|
||||||
|
onPress={actions.verifyOTP}
|
||||||
|
disabled={isLoading}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.btnText}>Verify & Bind</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
{renderResendOtp()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{state.formStarted && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.linkBtn}
|
||||||
|
onPress={actions.resetToMobile}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Text style={[styles.linkText, isLoading && { opacity: 0.4 }]}>
|
||||||
|
Change mobile number
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const showOtp = state.step === 'otp' || state.step === 'processing';
|
const showOtp = state.step === 'otp' || state.step === 'processing';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,15 +183,15 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.title}>{title}</Text>
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
|
||||||
{state.step === 'mobile' && (
|
{!showOtp && (
|
||||||
<>
|
<>
|
||||||
{!!state.errorMessage && (
|
{!!state.errorMessage && (
|
||||||
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
||||||
)}
|
)}
|
||||||
<Text style={styles.label}>手机号</Text>
|
<Text style={styles.label}>Mobile</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
||||||
placeholder={`请输入 ${mobileLength} 位手机号`}
|
placeholder={`Enter ${mobileLength}-digit mobile number`}
|
||||||
placeholderTextColor="#aaa"
|
placeholderTextColor="#aaa"
|
||||||
keyboardType="phone-pad"
|
keyboardType="phone-pad"
|
||||||
maxLength={mobileLength}
|
maxLength={mobileLength}
|
||||||
@@ -79,7 +211,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator color="#fff" />
|
<ActivityIndicator color="#fff" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.btnText}>获取验证码</Text>
|
<Text style={styles.btnText}>Send OTP</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
@@ -87,37 +219,14 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
|
|
||||||
{showOtp && (
|
{showOtp && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.hint}>
|
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
||||||
{state.needPassword
|
|
||||||
? `验证码已发送至 ${state.mobile},该账号需输入密码`
|
|
||||||
: `验证码已发送至 ${state.mobile}`}
|
|
||||||
</Text>
|
|
||||||
{!!state.errorMessage && (
|
{!!state.errorMessage && (
|
||||||
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
||||||
)}
|
)}
|
||||||
{state.needPassword && (
|
<Text style={styles.label}>Verification code</Text>
|
||||||
<>
|
|
||||||
<Text style={styles.label}>Amazon 密码</Text>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
|
||||||
placeholder="请输入账号密码"
|
|
||||||
placeholderTextColor="#aaa"
|
|
||||||
secureTextEntry
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
value={state.password}
|
|
||||||
onChangeText={t => {
|
|
||||||
actions.setPassword(t);
|
|
||||||
if (state.errorMessage) actions.clearError();
|
|
||||||
}}
|
|
||||||
editable={!isLoading}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Text style={styles.label}>验证码</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
||||||
placeholder={`请输入 ${otpLength} 位验证码`}
|
placeholder={`Enter ${otpLength}-digit code`}
|
||||||
placeholderTextColor="#aaa"
|
placeholderTextColor="#aaa"
|
||||||
keyboardType="number-pad"
|
keyboardType="number-pad"
|
||||||
maxLength={otpLength}
|
maxLength={otpLength}
|
||||||
@@ -137,16 +246,17 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator color="#fff" />
|
<ActivityIndicator color="#fff" />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.btnText}>验证并绑定</Text>
|
<Text style={styles.btnText}>Verify & Bind</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{renderResendOtp()}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.linkBtn}
|
style={styles.linkBtn}
|
||||||
onPress={actions.resetToMobile}
|
onPress={actions.resetToMobile}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<Text style={[styles.linkText, isLoading && { opacity: 0.4 }]}>
|
<Text style={[styles.linkText, isLoading && { opacity: 0.4 }]}>
|
||||||
重新输入手机号
|
Change mobile number
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
@@ -200,6 +310,10 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 14,
|
marginBottom: 14,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: '#fafafa',
|
||||||
},
|
},
|
||||||
|
inputLocked: {
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
inputError: {
|
inputError: {
|
||||||
borderColor: '#ff3b30',
|
borderColor: '#ff3b30',
|
||||||
backgroundColor: '#fff8f7',
|
backgroundColor: '#fff8f7',
|
||||||
@@ -235,4 +349,7 @@ const styles = StyleSheet.create({
|
|||||||
color: '#007AFF',
|
color: '#007AFF',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
},
|
},
|
||||||
|
linkTextDisabled: {
|
||||||
|
color: '#aaa',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class FreeChargeBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.FREECHARGE_PERSONAL}
|
walletType={WalletType.FREECHARGE_PERSONAL}
|
||||||
title="Freecharge 绑定"
|
title="Bind Freecharge"
|
||||||
otpLength={4}
|
otpLength={4}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -40,7 +40,7 @@ export class MobikwikOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.MOBIKWIK_PERSONAL}
|
walletType={WalletType.MOBIKWIK_PERSONAL}
|
||||||
title="Mobikwik 绑定"
|
title="Bind Mobikwik"
|
||||||
otpLength={6}
|
otpLength={6}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -65,7 +65,7 @@ export class PayTmPersonalOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.PAYTM_PERSONAL}
|
walletType={WalletType.PAYTM_PERSONAL}
|
||||||
title="Paytm 绑定"
|
title="Bind Paytm"
|
||||||
otpLength={6}
|
otpLength={6}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -90,7 +90,7 @@ export class BharatPeBusinessOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.BHARATPE_BUSINESS}
|
walletType={WalletType.BHARATPE_BUSINESS}
|
||||||
title="BharatPe 绑定"
|
title="Bind BharatPe"
|
||||||
otpLength={4}
|
otpLength={4}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -147,7 +147,7 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe
|
|||||||
const log = (...args: any[]) => { if (isDebug) console.log('[PaytmBusiness]', ...args); };
|
const log = (...args: any[]) => { if (isDebug) console.log('[PaytmBusiness]', ...args); };
|
||||||
|
|
||||||
const handleRequestOTP = async () => {
|
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('');
|
setLoading(true); setErrorMsg('');
|
||||||
try {
|
try {
|
||||||
const res = await onRequestOTP(WalletType.PAYTM_BUSINESS, { mobile });
|
const res = await onRequestOTP(WalletType.PAYTM_BUSINESS, { mobile });
|
||||||
@@ -161,7 +161,7 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifyOTP = async () => {
|
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('');
|
setLoading(true); setErrorMsg('');
|
||||||
try {
|
try {
|
||||||
const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionToken });
|
const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionToken });
|
||||||
@@ -182,25 +182,25 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe
|
|||||||
return (
|
return (
|
||||||
<View style={ptStyles.container}>
|
<View style={ptStyles.container}>
|
||||||
<View style={ptStyles.form}>
|
<View style={ptStyles.form}>
|
||||||
<Text style={ptStyles.title}>Paytm Business 绑定</Text>
|
<Text style={ptStyles.title}>Bind Paytm Business</Text>
|
||||||
{errorMsg ? <Text style={ptStyles.errorText}>{errorMsg}</Text> : null}
|
{errorMsg ? <Text style={ptStyles.errorText}>{errorMsg}</Text> : null}
|
||||||
{step === 'credentials' && (
|
{step === 'credentials' && (
|
||||||
<>
|
<>
|
||||||
<TextInput style={ptStyles.input} placeholder="手机号" placeholderTextColor="#999" keyboardType="phone-pad" maxLength={10} value={mobile} onChangeText={t => { setMobile(t); setErrorMsg(''); }} editable={!loading} />
|
<TextInput style={ptStyles.input} placeholder="Mobile number" placeholderTextColor="#999" keyboardType="phone-pad" maxLength={10} value={mobile} onChangeText={t => { setMobile(t); setErrorMsg(''); }} editable={!loading} />
|
||||||
<TouchableOpacity style={[ptStyles.button, loading && ptStyles.buttonDisabled]} onPress={handleRequestOTP} disabled={loading}>
|
<TouchableOpacity style={[ptStyles.button, loading && ptStyles.buttonDisabled]} onPress={handleRequestOTP} disabled={loading}>
|
||||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={ptStyles.buttonText}>获取验证码</Text>}
|
{loading ? <ActivityIndicator color="#fff" /> : <Text style={ptStyles.buttonText}>Send OTP</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{step === 'otp' && (
|
{step === 'otp' && (
|
||||||
<>
|
<>
|
||||||
<Text style={ptStyles.hint}>验证码已发送至 {mobile}</Text>
|
<Text style={ptStyles.hint}>OTP sent to {mobile}</Text>
|
||||||
<TextInput style={ptStyles.input} placeholder="6位验证码" placeholderTextColor="#999" keyboardType="number-pad" maxLength={6} value={otp} onChangeText={t => { setOtp(t); setErrorMsg(''); }} editable={!loading} />
|
<TextInput style={ptStyles.input} placeholder="6-digit code" placeholderTextColor="#999" keyboardType="number-pad" maxLength={6} value={otp} onChangeText={t => { setOtp(t); setErrorMsg(''); }} editable={!loading} />
|
||||||
<TouchableOpacity style={[ptStyles.button, loading && ptStyles.buttonDisabled]} onPress={handleVerifyOTP} disabled={loading}>
|
<TouchableOpacity style={[ptStyles.button, loading && ptStyles.buttonDisabled]} onPress={handleVerifyOTP} disabled={loading}>
|
||||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={ptStyles.buttonText}>验证并绑定</Text>}
|
{loading ? <ActivityIndicator color="#fff" /> : <Text style={ptStyles.buttonText}>Verify & Bind</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={ptStyles.linkButton} onPress={() => setStep('credentials')} disabled={loading}>
|
<TouchableOpacity style={ptStyles.linkButton} onPress={() => setStep('credentials')} disabled={loading}>
|
||||||
<Text style={ptStyles.linkText}>重新输入手机号</Text>
|
<Text style={ptStyles.linkText}>Change mobile number</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -235,7 +235,7 @@ export class PhonePeBusinessOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.PHONEPE_BUSINESS}
|
walletType={WalletType.PHONEPE_BUSINESS}
|
||||||
title="PhonePe Business 绑定"
|
title="Bind PhonePe Business"
|
||||||
otpLength={5}
|
otpLength={5}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -260,7 +260,7 @@ export class PhonePePersonalOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.PHONEPE_PERSONAL}
|
walletType={WalletType.PHONEPE_PERSONAL}
|
||||||
title="PhonePe 绑定"
|
title="Bind PhonePe"
|
||||||
otpLength={5}
|
otpLength={5}
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
@@ -285,8 +285,12 @@ export class AmazonPayOTPBind extends Component<{
|
|||||||
return (
|
return (
|
||||||
<OTPBindUI
|
<OTPBindUI
|
||||||
walletType={WalletType.AMAZONPAY_PERSONAL}
|
walletType={WalletType.AMAZONPAY_PERSONAL}
|
||||||
title="Amazon Pay 绑定"
|
title="Bind Amazon Pay"
|
||||||
otpLength={6}
|
otpLength={6}
|
||||||
|
passwordBeforeOtp
|
||||||
|
passwordLabel="Amazon password"
|
||||||
|
passwordPlaceholder="Enter Amazon account password"
|
||||||
|
passwordRequiredMsg="Please enter your Amazon account password"
|
||||||
onRequestOTP={this.props.onRequestOTP}
|
onRequestOTP={this.props.onRequestOTP}
|
||||||
onVerifyOTP={this.props.onVerifyOTP}
|
onVerifyOTP={this.props.onVerifyOTP}
|
||||||
onSuccess={this.props.onSuccess}
|
onSuccess={this.props.onSuccess}
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ export interface OTPBindState {
|
|||||||
mobile: string;
|
mobile: string;
|
||||||
otp: string;
|
otp: string;
|
||||||
password: string;
|
password: string;
|
||||||
needPassword: boolean;
|
passwordRequired: boolean;
|
||||||
|
formStarted: boolean;
|
||||||
|
otpSent: boolean;
|
||||||
step: 'mobile' | 'otp' | 'processing';
|
step: 'mobile' | 'otp' | 'processing';
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
otpData: any;
|
otpData: any;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
resendCountdown: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OTPBindActions {
|
export interface OTPBindActions {
|
||||||
@@ -38,24 +41,48 @@ export function useOTPBind(
|
|||||||
mobileLength?: number;
|
mobileLength?: number;
|
||||||
additionalParams?: any;
|
additionalParams?: any;
|
||||||
initialMobile?: string;
|
initialMobile?: string;
|
||||||
|
passwordBeforeOtp?: boolean;
|
||||||
|
passwordRequiredMsg?: string;
|
||||||
|
resendCooldown?: number;
|
||||||
}
|
}
|
||||||
): [OTPBindState, OTPBindActions] {
|
): [OTPBindState, OTPBindActions] {
|
||||||
const [mobile, setMobile] = useState('');
|
const [mobile, setMobile] = useState('');
|
||||||
const [otp, setOtp] = useState('');
|
const [otp, setOtp] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [needPassword, setNeedPassword] = useState(false);
|
const [passwordRequired, setPasswordRequired] = useState(false);
|
||||||
|
const [formStarted, setFormStarted] = useState(false);
|
||||||
|
const [otpSent, setOtpSent] = useState(false);
|
||||||
const [step, setStep] = useState<'mobile' | 'otp' | 'processing'>('mobile');
|
const [step, setStep] = useState<'mobile' | 'otp' | 'processing'>('mobile');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [otpData, setOtpData] = useState<any>(null);
|
const [otpData, setOtpData] = useState<any>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [resendCountdown, setResendCountdown] = useState(0);
|
||||||
|
|
||||||
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
|
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(() => {
|
useEffect(() => {
|
||||||
if (initialMobile) setMobile(initialMobile);
|
if (initialMobile) setMobile(initialMobile);
|
||||||
}, [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 clearError = () => setErrorMessage('');
|
||||||
|
|
||||||
const log = (...args: any[]) => {
|
const log = (...args: any[]) => {
|
||||||
@@ -67,6 +94,19 @@ export function useOTPBind(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requestOTP = async () => {
|
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) {
|
if (!mobile || mobile.length !== mobileLength) {
|
||||||
const msg = 'Invalid mobile number';
|
const msg = 'Invalid mobile number';
|
||||||
setErrorMessage(msg);
|
setErrorMessage(msg);
|
||||||
@@ -76,19 +116,31 @@ export function useOTPBind(
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
log('Requesting OTP for:', mobile);
|
log(isResend ? 'Resending OTP for:' : withPassword ? 'Requesting OTP with password for:' : 'Requesting OTP for:', mobile);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await onRequestOTP(walletType, {
|
const response = await onRequestOTP(walletType, {
|
||||||
mobile,
|
mobile,
|
||||||
|
...(withPassword ? { password, sessionId: otpData?.sessionId } : {}),
|
||||||
|
...(isResend && !withPassword && otpData ? { ...otpData } : {}),
|
||||||
...additionalParams,
|
...additionalParams,
|
||||||
});
|
});
|
||||||
log('OTP response:', response);
|
log('OTP response:', response);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setOtpData(response.data);
|
setOtpData(response.data);
|
||||||
setNeedPassword(!!response.data?.needPassword);
|
setFormStarted(true);
|
||||||
setStep('otp');
|
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('');
|
setErrorMessage('');
|
||||||
} else {
|
} else {
|
||||||
error('OTP request failed:', response.message);
|
error('OTP request failed:', response.message);
|
||||||
@@ -108,13 +160,13 @@ export function useOTPBind(
|
|||||||
|
|
||||||
const verifyOTP = async () => {
|
const verifyOTP = async () => {
|
||||||
if (!otp || otp.length !== otpLength) {
|
if (!otp || otp.length !== otpLength) {
|
||||||
const msg = `请输入 ${otpLength} 位验证码`;
|
const msg = `Please enter the ${otpLength}-digit verification code`;
|
||||||
setErrorMessage(msg);
|
setErrorMessage(msg);
|
||||||
onError(msg);
|
onError(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (needPassword && !password.trim()) {
|
if (passwordRequired && !password.trim()) {
|
||||||
const msg = '请输入 Amazon 账号密码';
|
const msg = passwordRequiredMsg;
|
||||||
setErrorMessage(msg);
|
setErrorMessage(msg);
|
||||||
onError(msg);
|
onError(msg);
|
||||||
return;
|
return;
|
||||||
@@ -129,7 +181,7 @@ export function useOTPBind(
|
|||||||
const response = await onVerifyOTP(walletType, {
|
const response = await onVerifyOTP(walletType, {
|
||||||
mobile,
|
mobile,
|
||||||
otp,
|
otp,
|
||||||
password: needPassword ? password : undefined,
|
password: passwordRequired ? password : undefined,
|
||||||
...additionalParams,
|
...additionalParams,
|
||||||
...(otpData || {}),
|
...(otpData || {}),
|
||||||
});
|
});
|
||||||
@@ -141,13 +193,13 @@ export function useOTPBind(
|
|||||||
} else {
|
} else {
|
||||||
log('Verify failed:', response.message);
|
log('Verify failed:', response.message);
|
||||||
const msg = response.message || 'Failed to verify OTP';
|
const msg = response.message || 'Failed to verify OTP';
|
||||||
setStep('otp');
|
setStep(passwordBeforeOtp ? 'mobile' : 'otp');
|
||||||
setErrorMessage(msg);
|
setErrorMessage(msg);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error('Verify OTP error:', e);
|
error('Verify OTP error:', e);
|
||||||
const msg = e instanceof Error ? e.message : 'Failed to verify OTP';
|
const msg = e instanceof Error ? e.message : 'Failed to verify OTP';
|
||||||
setStep('otp');
|
setStep(passwordBeforeOtp ? 'mobile' : 'otp');
|
||||||
setErrorMessage(msg);
|
setErrorMessage(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -158,12 +210,28 @@ export function useOTPBind(
|
|||||||
setStep('mobile');
|
setStep('mobile');
|
||||||
setOtp('');
|
setOtp('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setNeedPassword(false);
|
setPasswordRequired(false);
|
||||||
|
setFormStarted(false);
|
||||||
|
setOtpSent(false);
|
||||||
|
setOtpData(null);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
setResendCountdown(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return [
|
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 },
|
{ setMobile, setOtp, setPassword, requestOTP, verifyOTP, resetToMobile, clearError },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -798,7 +798,10 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
|||||||
isDebug
|
isDebug
|
||||||
initialMobile={bindPrefillMobile}
|
initialMobile={bindPrefillMobile}
|
||||||
onRequestOTP={async (wt, p) => {
|
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) => {
|
onVerifyOTP={async (wt, p) => {
|
||||||
return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, {
|
return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, {
|
||||||
|
|||||||
Reference in New Issue
Block a user