Compare commits
2 Commits
2a6fd5149f
...
bb215fd492
| Author | SHA1 | Date | |
|---|---|---|---|
| bb215fd492 | |||
| 01e597ac93 |
206
components/OTPBindUI.tsx
Normal file
206
components/OTPBindUI.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
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;
|
||||
otpLength?: number;
|
||||
mobileLength?: 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;
|
||||
}
|
||||
|
||||
export const OTPBindUI: React.FC<OTPBindUIProps> = ({
|
||||
walletType,
|
||||
title,
|
||||
otpLength = 6,
|
||||
mobileLength = 10,
|
||||
onRequestOTP,
|
||||
onVerifyOTP,
|
||||
onSuccess,
|
||||
onError,
|
||||
isDebug,
|
||||
additionalParams = {},
|
||||
}) => {
|
||||
const [state, actions] = useOTPBind(
|
||||
walletType,
|
||||
{
|
||||
onRequestOTP,
|
||||
onVerifyOTP,
|
||||
onSuccess,
|
||||
onError,
|
||||
isDebug,
|
||||
},
|
||||
{
|
||||
otpLength,
|
||||
mobileLength,
|
||||
additionalParams,
|
||||
}
|
||||
);
|
||||
|
||||
if (state.step === 'processing') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
<Text style={styles.processingText}>处理中...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.form}>
|
||||
<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' && (
|
||||
<>
|
||||
<Text style={styles.hint}>验证码已发送至 {state.mobile}</Text>
|
||||
{state.errorMessage ? (
|
||||
<Text style={styles.errorText}>{state.errorMessage}</Text>
|
||||
) : null}
|
||||
<TextInput
|
||||
style={[styles.input, state.errorMessage ? styles.inputError : {}]}
|
||||
placeholder={`请输入 ${otpLength} 位验证码`}
|
||||
placeholderTextColor="#999"
|
||||
keyboardType="number-pad"
|
||||
maxLength={otpLength}
|
||||
value={state.otp}
|
||||
onChangeText={(text) => {
|
||||
actions.setOtp(text);
|
||||
if (state.errorMessage) actions.clearError();
|
||||
}}
|
||||
editable={!state.loading}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, state.loading && styles.buttonDisabled]}
|
||||
onPress={actions.verifyOTP}
|
||||
disabled={state.loading}
|
||||
>
|
||||
{state.loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>验证并绑定</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.linkButton}
|
||||
onPress={actions.resetToMobile}
|
||||
disabled={state.loading}
|
||||
>
|
||||
<Text style={styles.linkText}>重新输入手机号</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
form: {
|
||||
width: '80%',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 10,
|
||||
padding: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
marginBottom: 15,
|
||||
},
|
||||
inputError: {
|
||||
borderColor: '#ff3b30',
|
||||
},
|
||||
errorText: {
|
||||
color: '#ff3b30',
|
||||
fontSize: 14,
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderRadius: 5,
|
||||
padding: 15,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#ccc',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
linkButton: {
|
||||
marginTop: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
linkText: {
|
||||
color: '#007AFF',
|
||||
fontSize: 14,
|
||||
},
|
||||
hint: {
|
||||
fontSize: 14,
|
||||
color: '#666',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
},
|
||||
processingText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
marginTop: 10,
|
||||
},
|
||||
});
|
||||
101
components/WalletBindComponents.tsx
Normal file
101
components/WalletBindComponents.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult } from 'rnwalletman';
|
||||
import { OTPBindUI } from './OTPBindUI';
|
||||
|
||||
export class FreeChargeBind extends Component<{
|
||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onSuccess: (result: FreechargePersonalBindResult) => void;
|
||||
onError: (error: string) => void;
|
||||
isDebug: boolean;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<OTPBindUI
|
||||
walletType={WalletType.FREECHARGE_PERSONAL}
|
||||
title="Freecharge 绑定"
|
||||
otpLength={4}
|
||||
onRequestOTP={this.props.onRequestOTP}
|
||||
onVerifyOTP={this.props.onVerifyOTP}
|
||||
onSuccess={this.props.onSuccess}
|
||||
onError={this.props.onError}
|
||||
isDebug={this.props.isDebug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MobikwikOTPBind extends Component<{
|
||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onSuccess: (result: MobikwikPersonalBindResult) => void;
|
||||
onError: (error: string) => void;
|
||||
isDebug: boolean;
|
||||
deviceId: string;
|
||||
tuneUserId: string;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<OTPBindUI
|
||||
walletType={WalletType.MOBIKWIK_PERSONAL}
|
||||
title="Mobikwik 绑定"
|
||||
otpLength={6}
|
||||
onRequestOTP={this.props.onRequestOTP}
|
||||
onVerifyOTP={this.props.onVerifyOTP}
|
||||
onSuccess={this.props.onSuccess}
|
||||
onError={this.props.onError}
|
||||
isDebug={this.props.isDebug}
|
||||
additionalParams={{
|
||||
deviceId: this.props.deviceId,
|
||||
tuneUserId: this.props.tuneUserId,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PayTmPersonalOTPBind extends Component<{
|
||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onSuccess: (result: PaytmPersonalBindResult) => void;
|
||||
onError: (error: string) => void;
|
||||
isDebug: boolean;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<OTPBindUI
|
||||
walletType={WalletType.PAYTM_PERSONAL}
|
||||
title="Paytm 绑定"
|
||||
otpLength={6}
|
||||
onRequestOTP={this.props.onRequestOTP}
|
||||
onVerifyOTP={this.props.onVerifyOTP}
|
||||
onSuccess={this.props.onSuccess}
|
||||
onError={this.props.onError}
|
||||
isDebug={this.props.isDebug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class PhonePePersonalOTPBind extends Component<{
|
||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onSuccess: (result: PhonePePersonalBindResult) => void;
|
||||
onError: (error: string) => void;
|
||||
isDebug: boolean;
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<OTPBindUI
|
||||
walletType={WalletType.PHONEPE_PERSONAL}
|
||||
title="PhonePe 绑定"
|
||||
otpLength={8}
|
||||
onRequestOTP={this.props.onRequestOTP}
|
||||
onVerifyOTP={this.props.onVerifyOTP}
|
||||
onSuccess={this.props.onSuccess}
|
||||
onError={this.props.onError}
|
||||
isDebug={this.props.isDebug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
151
hooks/useOTPBind.ts
Normal file
151
hooks/useOTPBind.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useState } from 'react';
|
||||
import { WalletType } from 'rnwalletman';
|
||||
|
||||
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;
|
||||
step: 'mobile' | 'otp' | 'processing';
|
||||
loading: boolean;
|
||||
otpData: any;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export interface OTPBindActions {
|
||||
setMobile: (mobile: string) => void;
|
||||
setOtp: (otp: 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;
|
||||
}
|
||||
): [OTPBindState, OTPBindActions] {
|
||||
const [mobile, setMobile] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [step, setStep] = useState<'mobile' | 'otp' | 'processing'>('mobile');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [otpData, setOtpData] = useState<any>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
|
||||
const { otpLength = 6, mobileLength = 10, additionalParams = {} } = config || {};
|
||||
|
||||
const clearError = () => setErrorMessage('');
|
||||
|
||||
const log = (...args: any[]) => {
|
||||
if (isDebug) console.log(`[${walletType}]`, ...args);
|
||||
};
|
||||
|
||||
const error = (...args: any[]) => {
|
||||
if (isDebug) console.error(`[${walletType}]`, ...args);
|
||||
};
|
||||
|
||||
const requestOTP = async () => {
|
||||
if (!mobile || mobile.length !== mobileLength) {
|
||||
const msg = 'Invalid mobile number';
|
||||
setErrorMessage(msg);
|
||||
onError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setErrorMessage('');
|
||||
log('Requesting OTP for:', mobile);
|
||||
|
||||
try {
|
||||
const response = await onRequestOTP(walletType, {
|
||||
mobile,
|
||||
...additionalParams,
|
||||
});
|
||||
log('OTP response:', response);
|
||||
|
||||
if (response.success) {
|
||||
setOtpData(response.data);
|
||||
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 = `请输入 ${otpLength} 位验证码`;
|
||||
setErrorMessage(msg);
|
||||
onError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setErrorMessage('');
|
||||
setStep('processing');
|
||||
log('Verifying OTP:', otp);
|
||||
|
||||
try {
|
||||
const response = await onVerifyOTP(walletType, {
|
||||
mobile,
|
||||
otp,
|
||||
...additionalParams,
|
||||
...(otpData || {}),
|
||||
});
|
||||
log('Verify response:', response);
|
||||
|
||||
if (response.success) {
|
||||
setErrorMessage('');
|
||||
onSuccess(response.data);
|
||||
} else {
|
||||
error('Verify failed:', response.message);
|
||||
const msg = response.message || 'Failed to verify OTP';
|
||||
setStep('otp');
|
||||
setErrorMessage(msg);
|
||||
onError(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
error('Verify OTP error:', e);
|
||||
const msg = e instanceof Error ? e.message : 'Failed to verify OTP';
|
||||
setStep('otp');
|
||||
setErrorMessage(msg);
|
||||
onError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetToMobile = () => {
|
||||
setStep('mobile');
|
||||
setOtp('');
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
return [
|
||||
{ mobile, otp, step, loading, otpData, errorMessage },
|
||||
{ setMobile, setOtp, requestOTP, verifyOTP, resetToMobile, clearError },
|
||||
];
|
||||
}
|
||||
Submodule libs/rnwalletman updated: ad66622161...0179dfbc68
Submodule servers/walletman updated: aa50a6588d...ef6bc8907c
79
services/api.ts
Normal file
79
services/api.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { WalletType } from 'rnwalletman';
|
||||
|
||||
class Api {
|
||||
public static readonly BASE_URL = 'http://192.168.1.117:16000';
|
||||
private static _instance: Api | null = null;
|
||||
private userId: number = 0;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public setUserId(userId: number) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public getUserId(): number {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public static get instance() {
|
||||
if (Api._instance === null) {
|
||||
Api._instance = new Api();
|
||||
}
|
||||
return Api._instance;
|
||||
}
|
||||
|
||||
private headers(): Record<string, string> {
|
||||
const h: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (this.userId > 0) {
|
||||
h['X-User-ID'] = String(this.userId);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
public async login(username: string, password: string): Promise<number> {
|
||||
const res = await fetch(`${Api.BASE_URL}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
this.userId = data.data.userId;
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public async register(walletType: WalletType, params: any) {
|
||||
const res = await fetch(`${Api.BASE_URL}/register`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ walletType, params }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async requestOTP(walletType: WalletType, mobile: string, params: any = {}) {
|
||||
const res = await fetch(`${Api.BASE_URL}/request-otp`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ walletType, mobile, ...params }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
public async verifyOTP(walletType: WalletType, mobile: string, otp: string, params: any = {}) {
|
||||
const res = await fetch(`${Api.BASE_URL}/verify-otp`, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ walletType, mobile, otp, params }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default Api;
|
||||
45
styles/index.ts
Normal file
45
styles/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
button: {
|
||||
padding: 10,
|
||||
backgroundColor: "lightblue",
|
||||
borderRadius: 5,
|
||||
width: 200,
|
||||
height: 55,
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
modal: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
modalContent: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
bindButton: {
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
backgroundColor: "#007AFF",
|
||||
borderRadius: 5,
|
||||
width: "90%",
|
||||
height: 45,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
bindButtonText: {
|
||||
fontSize: 14,
|
||||
// fontWeight: "bold",
|
||||
color: "#fff",
|
||||
},
|
||||
});
|
||||
25
types/index.ts
Normal file
25
types/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface AppProps {}
|
||||
|
||||
export interface WalletmanAppState {
|
||||
/* Paytm Personal */
|
||||
showPaytmPersonalBind: boolean;
|
||||
paytmPersonalBindType: 'otpMode' | 'tokenMode';
|
||||
showPaytmBusinessBind: boolean;
|
||||
|
||||
/* PhonePe Personal */
|
||||
showPhonePePersonalBind: boolean;
|
||||
phonePePersonalBindType: 'otpMode' | 'tokenMode';
|
||||
showPhonePeBusinessBind: boolean;
|
||||
|
||||
/* GooglePay Business */
|
||||
showGooglePayBusinessBind: boolean;
|
||||
|
||||
/* BharatPe Business */
|
||||
showBharatPeBusinessBind: boolean;
|
||||
|
||||
/* Mobikwik Personal */
|
||||
showMobikwikPersonalBind: boolean;
|
||||
|
||||
/* Freecharge Personal */
|
||||
showFreechargePersonalBind: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user