This commit is contained in:
2026-05-10 01:25:47 +08:00
parent 07d867ca3f
commit 3bee6d9b65
4 changed files with 154 additions and 165 deletions

View File

@@ -1,5 +1,12 @@
import React from 'react'; import React from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; import {
View,
TextInput,
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { WalletType } from 'rnwalletman'; import { WalletType } from 'rnwalletman';
import { useOTPBind } from '../hooks/useOTPBind'; import { useOTPBind } from '../hooks/useOTPBind';
@@ -30,177 +37,170 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
}) => { }) => {
const [state, actions] = useOTPBind( const [state, actions] = useOTPBind(
walletType, walletType,
{ { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug },
onRequestOTP, { otpLength, mobileLength, additionalParams }
onVerifyOTP,
onSuccess,
onError,
isDebug,
},
{
otpLength,
mobileLength,
additionalParams,
}
); );
if (state.step === 'processing') { const isLoading = state.loading || state.step === 'processing';
return ( const showOtp = state.step === 'otp' || state.step === 'processing';
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
<Text style={styles.processingText}>...</Text>
</View>
);
}
return ( return (
<View style={styles.container}> <View style={styles.card}>
<View style={styles.form}> <Text style={styles.title}>{title}</Text>
<Text style={styles.title}>{title}</Text>
{state.step === 'mobile' && (
<>
{state.errorMessage ? (
<Text style={styles.errorText}>{state.errorMessage}</Text>
) : null}
<TextInput
style={[styles.input, state.errorMessage ? styles.inputError : {}]}
placeholder="请输入手机号"
placeholderTextColor="#999"
keyboardType="phone-pad"
maxLength={mobileLength}
value={state.mobile}
onChangeText={(text) => {
actions.setMobile(text);
if (state.errorMessage) actions.clearError();
}}
editable={!state.loading}
/>
<TouchableOpacity
style={[styles.button, state.loading && styles.buttonDisabled]}
onPress={actions.requestOTP}
disabled={state.loading}
>
{state.loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}></Text>
)}
</TouchableOpacity>
</>
)}
{state.step === 'otp' && ( {state.step === 'mobile' && (
<> <>
<Text style={styles.hint}> {state.mobile}</Text> {!!state.errorMessage && (
{state.errorMessage ? ( <Text style={styles.errorText}>{state.errorMessage}</Text>
<Text style={styles.errorText}>{state.errorMessage}</Text> )}
) : null} <Text style={styles.label}></Text>
<TextInput <TextInput
style={[styles.input, state.errorMessage ? styles.inputError : {}]} style={[styles.input, !!state.errorMessage && styles.inputError]}
placeholder={`请输入 ${otpLength}验证码`} placeholder={`请输入 ${mobileLength}手机号`}
placeholderTextColor="#999" placeholderTextColor="#aaa"
keyboardType="number-pad" keyboardType="phone-pad"
maxLength={otpLength} maxLength={mobileLength}
value={state.otp} value={state.mobile}
onChangeText={(text) => { onChangeText={t => {
actions.setOtp(text); actions.setMobile(t);
if (state.errorMessage) actions.clearError(); if (state.errorMessage) actions.clearError();
}} }}
editable={!state.loading} editable={!isLoading}
/> />
<TouchableOpacity <TouchableOpacity
style={[styles.button, state.loading && styles.buttonDisabled]} style={[styles.btn, isLoading && styles.btnDisabled]}
onPress={actions.verifyOTP} onPress={actions.requestOTP}
disabled={state.loading} disabled={isLoading}
> activeOpacity={0.8}
{state.loading ? ( >
<ActivityIndicator color="#fff" /> {isLoading ? (
) : ( <ActivityIndicator color="#fff" />
<Text style={styles.buttonText}></Text> ) : (
)} <Text style={styles.btnText}></Text>
</TouchableOpacity> )}
<TouchableOpacity </TouchableOpacity>
style={styles.linkButton} </>
onPress={actions.resetToMobile} )}
disabled={state.loading}
> {showOtp && (
<Text style={styles.linkText}></Text> <>
</TouchableOpacity> <Text style={styles.hint}> {state.mobile}</Text>
</> {!!state.errorMessage && (
)} <Text style={styles.errorText}>{state.errorMessage}</Text>
</View> )}
<Text style={styles.label}></Text>
<TextInput
style={[styles.input, !!state.errorMessage && styles.inputError]}
placeholder={`请输入 ${otpLength} 位验证码`}
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}></Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.linkBtn}
onPress={actions.resetToMobile}
disabled={isLoading}
>
<Text style={[styles.linkText, isLoading && { opacity: 0.4 }]}>
</Text>
</TouchableOpacity>
</>
)}
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { card: {
flex: 1, width: '92%',
backgroundColor: 'rgba(0,0,0,0.8)',
justifyContent: 'center',
alignItems: 'center',
},
form: {
width: '80%',
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 10, borderRadius: 20,
padding: 20, padding: 28,
alignSelf: 'center',
}, },
title: { title: {
fontSize: 20, fontSize: 22,
fontWeight: 'bold', fontWeight: '700',
textAlign: 'center', textAlign: 'center',
marginBottom: 20, color: '#111',
marginBottom: 24,
},
label: {
fontSize: 13,
color: '#666',
marginBottom: 6,
fontWeight: '500',
},
hint: {
fontSize: 14,
color: '#555',
textAlign: 'center',
marginBottom: 18,
}, },
input: { input: {
borderWidth: 1, borderWidth: 1.5,
borderColor: '#ddd', borderColor: '#e0e0e0',
borderRadius: 5, borderRadius: 10,
padding: 12, paddingHorizontal: 14,
fontSize: 16, paddingVertical: 14,
marginBottom: 15, fontSize: 17,
color: '#111',
marginBottom: 18,
backgroundColor: '#fafafa',
}, },
inputError: { inputError: {
borderColor: '#ff3b30', borderColor: '#ff3b30',
backgroundColor: '#fff8f7',
}, },
errorText: { errorText: {
color: '#ff3b30', color: '#ff3b30',
fontSize: 14, fontSize: 13,
marginBottom: 10, marginBottom: 12,
textAlign: 'center', textAlign: 'center',
lineHeight: 18,
}, },
button: { btn: {
backgroundColor: '#007AFF', backgroundColor: '#007AFF',
borderRadius: 5, borderRadius: 12,
padding: 15, paddingVertical: 16,
alignItems: 'center', alignItems: 'center',
marginTop: 4,
}, },
buttonDisabled: { btnDisabled: {
backgroundColor: '#ccc', backgroundColor: '#a0c4ff',
}, },
buttonText: { btnText: {
color: '#fff', color: '#fff',
fontSize: 16, fontSize: 17,
fontWeight: 'bold', fontWeight: '600',
letterSpacing: 0.3,
}, },
linkButton: { linkBtn: {
marginTop: 10, marginTop: 16,
alignItems: 'center', alignItems: 'center',
paddingVertical: 4,
}, },
linkText: { linkText: {
color: '#007AFF', color: '#007AFF',
fontSize: 14, fontSize: 14,
}, },
hint: {
fontSize: 14,
color: '#666',
marginBottom: 10,
textAlign: 'center',
},
processingText: {
color: '#fff',
fontSize: 16,
marginTop: 10,
},
}); });

View File

@@ -128,7 +128,7 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
}) { }) {
const [step, setStep] = useState<'credentials' | 'otp' | 'processing'>('credentials'); const [step, setStep] = useState<'credentials' | 'otp'>('credentials');
const [mobile, setMobile] = useState(''); const [mobile, setMobile] = useState('');
const [otp, setOtp] = useState(''); const [otp, setOtp] = useState('');
const [sessionId, setSessionId] = useState(''); const [sessionId, setSessionId] = useState('');
@@ -153,30 +153,22 @@ 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('请输入6位验证码'); return; }
setLoading(true); setErrorMsg(''); setStep('processing'); setLoading(true); setErrorMsg('');
try { try {
const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionId }); const res = await onVerifyOTP(WalletType.PAYTM_BUSINESS, { mobile, otp, sessionId });
log('VerifyOTP:', res); log('VerifyOTP:', res);
if (res.success) { if (res.success) {
onSuccess({ type: WalletType.PAYTM_BUSINESS, success: true, cookie: res.data?.cookie || '', xCsrfToken: res.data?.xCsrfToken || '', qrData: res.data?.qrData || [] }); onSuccess({ type: WalletType.PAYTM_BUSINESS, success: true, cookie: res.data?.cookie || '', xCsrfToken: res.data?.xCsrfToken || '', qrData: res.data?.qrData || [] });
} else { } else {
const msg = res.message || 'Failed to verify OTP'; setErrorMsg(res.message || 'Failed to verify OTP');
setErrorMsg(msg); setStep('otp'); onError(msg); setStep('otp');
} }
} catch (e) { } catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to verify OTP'; setErrorMsg(e instanceof Error ? e.message : 'Failed to verify OTP');
setErrorMsg(msg); setStep('otp'); onError(msg); setStep('otp');
} finally { setLoading(false); } } finally { setLoading(false); }
}; };
if (step === 'processing') {
return (
<View style={ptStyles.container}>
<ActivityIndicator size="large" color="#fff" />
<Text style={ptStyles.processingText}>...</Text>
</View>
);
}
return ( return (
<View style={ptStyles.container}> <View style={ptStyles.container}>
@@ -210,17 +202,16 @@ function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDe
const ptStyles = StyleSheet.create({ const ptStyles = StyleSheet.create({
container: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' }, container: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', justifyContent: 'center', alignItems: 'center' },
form: { width: '80%', backgroundColor: '#fff', borderRadius: 10, padding: 20 }, form: { width: '92%', backgroundColor: '#fff', borderRadius: 20, padding: 28 },
title: { fontSize: 20, fontWeight: 'bold', textAlign: 'center', marginBottom: 20 }, title: { fontSize: 22, fontWeight: '700', textAlign: 'center', color: '#111', marginBottom: 24 },
input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 5, padding: 12, fontSize: 16, marginBottom: 15 }, input: { borderWidth: 1.5, borderColor: '#e0e0e0', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 14, fontSize: 17, color: '#111', marginBottom: 18, backgroundColor: '#fafafa' },
errorText: { color: '#ff3b30', fontSize: 14, marginBottom: 10, textAlign: 'center' }, errorText: { color: '#ff3b30', fontSize: 13, marginBottom: 12, textAlign: 'center' },
button: { backgroundColor: '#007AFF', borderRadius: 5, padding: 15, alignItems: 'center' }, button: { backgroundColor: '#007AFF', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 4 },
buttonDisabled: { backgroundColor: '#ccc' }, buttonDisabled: { backgroundColor: '#a0c4ff' },
buttonText: { color: '#fff', fontSize: 16, fontWeight: 'bold' }, buttonText: { color: '#fff', fontSize: 17, fontWeight: '600' },
linkButton: { marginTop: 10, alignItems: 'center' }, linkButton: { marginTop: 16, alignItems: 'center' },
linkText: { color: '#007AFF', fontSize: 14 }, linkText: { color: '#007AFF', fontSize: 14 },
hint: { fontSize: 14, color: '#666', marginBottom: 10, textAlign: 'center' }, hint: { fontSize: 14, color: '#555', marginBottom: 18, textAlign: 'center' },
processingText: { color: '#fff', fontSize: 16, marginTop: 10 },
}); });
export class PhonePePersonalOTPBind extends Component<{ export class PhonePePersonalOTPBind extends Component<{

View File

@@ -125,14 +125,12 @@ export function useOTPBind(
const msg = response.message || 'Failed to verify OTP'; const msg = response.message || 'Failed to verify OTP';
setStep('otp'); setStep('otp');
setErrorMessage(msg); setErrorMessage(msg);
onError(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('otp');
setErrorMessage(msg); setErrorMessage(msg);
onError(msg);
} finally { } finally {
setLoading(false); setLoading(false);
} }