377 lines
14 KiB
TypeScript
377 lines
14 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
View,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
Text,
|
|
StyleSheet,
|
|
ActivityIndicator,
|
|
} from 'react-native';
|
|
import { WalletType } from 'rnwalletman';
|
|
import { useOTPBind } from '../hooks/useOTPBind';
|
|
|
|
interface OTPBindUIProps {
|
|
walletType: WalletType;
|
|
title: string;
|
|
subtitle?: string;
|
|
otpLength?: number;
|
|
mobileLength?: number;
|
|
passwordBeforeOtp?: boolean;
|
|
passwordLabel?: string;
|
|
passwordPlaceholder?: string;
|
|
passwordRequiredMsg?: string;
|
|
resendCooldown?: number;
|
|
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
|
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
|
onSuccess: (result: any) => void;
|
|
onError: (error: string) => void;
|
|
isDebug: boolean;
|
|
additionalParams?: any;
|
|
initialMobile?: string;
|
|
}
|
|
|
|
export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
|
walletType,
|
|
title,
|
|
subtitle,
|
|
otpLength = 6,
|
|
mobileLength = 10,
|
|
passwordBeforeOtp = false,
|
|
passwordLabel = 'Password',
|
|
passwordPlaceholder = 'Enter password',
|
|
passwordRequiredMsg = 'Please enter your password',
|
|
resendCooldown = 20,
|
|
onRequestOTP,
|
|
onVerifyOTP,
|
|
onSuccess,
|
|
onError,
|
|
isDebug,
|
|
additionalParams = {},
|
|
initialMobile = '',
|
|
}) => {
|
|
const [state, actions] = useOTPBind(
|
|
walletType,
|
|
{ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug },
|
|
{ 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 (
|
|
<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>
|
|
{!!subtitle && <Text style={styles.subtitle}>{subtitle}</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.consentHint}>收到短信后点「允许」可填入验证码</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';
|
|
|
|
return (
|
|
<View style={styles.overlay}>
|
|
<View style={styles.card}>
|
|
<Text style={styles.title}>{title}</Text>
|
|
{!!subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
|
|
|
|
{!showOtp && (
|
|
<>
|
|
{!!state.errorMessage && (
|
|
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
|
)}
|
|
<Text style={styles.label}>Mobile</Text>
|
|
<TextInput
|
|
style={[styles.input, !!state.errorMessage && styles.inputError]}
|
|
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={!isLoading}
|
|
/>
|
|
<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>
|
|
</>
|
|
)}
|
|
|
|
{showOtp && (
|
|
<>
|
|
<Text style={styles.hint}>OTP sent to {state.mobile}</Text>
|
|
<Text style={styles.consentHint}>收到短信后点「允许」可填入验证码</Text>
|
|
{!!state.errorMessage && (
|
|
<Text style={styles.errorText}>{state.errorMessage}</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()}
|
|
<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 styles = StyleSheet.create({
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
card: {
|
|
width: '88%',
|
|
backgroundColor: '#fff',
|
|
borderRadius: 16,
|
|
padding: 22,
|
|
alignSelf: 'center',
|
|
},
|
|
title: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
textAlign: 'center',
|
|
color: '#111',
|
|
marginBottom: 18,
|
|
},
|
|
subtitle: {
|
|
fontSize: 12,
|
|
color: '#888',
|
|
textAlign: 'center',
|
|
marginTop: -10,
|
|
marginBottom: 14,
|
|
lineHeight: 17,
|
|
},
|
|
label: {
|
|
fontSize: 13,
|
|
color: '#666',
|
|
marginBottom: 5,
|
|
fontWeight: '500',
|
|
},
|
|
hint: {
|
|
fontSize: 13,
|
|
color: '#555',
|
|
textAlign: 'center',
|
|
marginBottom: 14,
|
|
},
|
|
consentHint: {
|
|
fontSize: 11,
|
|
color: '#888',
|
|
textAlign: 'center',
|
|
marginTop: -8,
|
|
marginBottom: 12,
|
|
},
|
|
input: {
|
|
borderWidth: 1.5,
|
|
borderColor: '#e0e0e0',
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 11,
|
|
fontSize: 16,
|
|
color: '#111',
|
|
marginBottom: 14,
|
|
backgroundColor: '#fafafa',
|
|
},
|
|
inputLocked: {
|
|
backgroundColor: '#f0f0f0',
|
|
color: '#666',
|
|
},
|
|
inputError: {
|
|
borderColor: '#ff3b30',
|
|
backgroundColor: '#fff8f7',
|
|
},
|
|
errorText: {
|
|
color: '#ff3b30',
|
|
fontSize: 12,
|
|
marginBottom: 10,
|
|
textAlign: 'center',
|
|
lineHeight: 17,
|
|
},
|
|
btn: {
|
|
backgroundColor: '#007AFF',
|
|
borderRadius: 10,
|
|
paddingVertical: 13,
|
|
alignItems: 'center',
|
|
marginTop: 2,
|
|
},
|
|
btnDisabled: {
|
|
backgroundColor: '#a0c4ff',
|
|
},
|
|
btnText: {
|
|
color: '#fff',
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
linkBtn: {
|
|
marginTop: 12,
|
|
alignItems: 'center',
|
|
paddingVertical: 4,
|
|
},
|
|
linkText: {
|
|
color: '#007AFF',
|
|
fontSize: 13,
|
|
},
|
|
linkTextDisabled: {
|
|
color: '#aaa',
|
|
},
|
|
});
|