This commit is contained in:
2026-05-23 12:23:47 +08:00
parent 7879cef91e
commit 73fa41007b
6 changed files with 164 additions and 30 deletions

View File

@@ -21,6 +21,7 @@ interface OTPBindUIProps {
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
additionalParams?: any; additionalParams?: any;
initialMobile?: string;
} }
export const OTPBindUI: React.FC<OTPBindUIProps> = ({ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
@@ -34,11 +35,12 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
onError, onError,
isDebug, isDebug,
additionalParams = {}, additionalParams = {},
initialMobile = '',
}) => { }) => {
const [state, actions] = useOTPBind( const [state, actions] = useOTPBind(
walletType, walletType,
{ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug }, { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug },
{ otpLength, mobileLength, additionalParams } { otpLength, mobileLength, additionalParams, initialMobile }
); );
const isLoading = state.loading || state.step === 'processing'; const isLoading = state.loading || state.step === 'processing';

View File

@@ -1,4 +1,4 @@
import React, { Component, useState } from 'react'; import React, { Component, useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'; import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, BharatPeBusinessBindResult, PaytmBusinessBindResult } from 'rnwalletman'; import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, BharatPeBusinessBindResult, PaytmBusinessBindResult } from 'rnwalletman';
import { OTPBindUI } from './OTPBindUI'; import { OTPBindUI } from './OTPBindUI';
@@ -9,6 +9,7 @@ export class FreeChargeBind extends Component<{
onSuccess: (result: FreechargePersonalBindResult) => void; onSuccess: (result: FreechargePersonalBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -21,6 +22,7 @@ export class FreeChargeBind extends Component<{
onSuccess={this.props.onSuccess} onSuccess={this.props.onSuccess}
onError={this.props.onError} onError={this.props.onError}
isDebug={this.props.isDebug} isDebug={this.props.isDebug}
initialMobile={this.props.initialMobile}
/> />
); );
} }
@@ -34,6 +36,7 @@ export class MobikwikOTPBind extends Component<{
isDebug: boolean; isDebug: boolean;
deviceId: string; deviceId: string;
tuneUserId: string; tuneUserId: string;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -50,6 +53,7 @@ export class MobikwikOTPBind extends Component<{
deviceId: this.props.deviceId, deviceId: this.props.deviceId,
tuneUserId: this.props.tuneUserId, tuneUserId: this.props.tuneUserId,
}} }}
initialMobile={this.props.initialMobile}
/> />
); );
} }
@@ -61,6 +65,7 @@ export class PayTmPersonalOTPBind extends Component<{
onSuccess: (result: PaytmPersonalBindResult) => void; onSuccess: (result: PaytmPersonalBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -73,6 +78,7 @@ export class PayTmPersonalOTPBind extends Component<{
onSuccess={this.props.onSuccess} onSuccess={this.props.onSuccess}
onError={this.props.onError} onError={this.props.onError}
isDebug={this.props.isDebug} isDebug={this.props.isDebug}
initialMobile={this.props.initialMobile}
/> />
); );
} }
@@ -84,6 +90,7 @@ export class BharatPeBusinessOTPBind extends Component<{
onSuccess: (result: BharatPeBusinessBindResult) => void; onSuccess: (result: BharatPeBusinessBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -96,6 +103,7 @@ export class BharatPeBusinessOTPBind extends Component<{
onSuccess={this.props.onSuccess} onSuccess={this.props.onSuccess}
onError={this.props.onError} onError={this.props.onError}
isDebug={this.props.isDebug} isDebug={this.props.isDebug}
initialMobile={this.props.initialMobile}
/> />
); );
} }
@@ -107,6 +115,7 @@ export class PaytmBusinessOTPBind extends Component<{
onSuccess: (result: PaytmBusinessBindResult) => void; onSuccess: (result: PaytmBusinessBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -116,25 +125,31 @@ export class PaytmBusinessOTPBind extends Component<{
onSuccess={this.props.onSuccess} onSuccess={this.props.onSuccess}
onError={this.props.onError} onError={this.props.onError}
isDebug={this.props.isDebug} isDebug={this.props.isDebug}
initialMobile={this.props.initialMobile}
/> />
); );
} }
} }
function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug }: { function PaytmBusinessForm({ onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug, initialMobile = '' }: {
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: PaytmBusinessBindResult) => void; onSuccess: (result: PaytmBusinessBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}) { }) {
const [step, setStep] = useState<'credentials' | 'otp'>('credentials'); const [step, setStep] = useState<'credentials' | 'otp'>('credentials');
const [mobile, setMobile] = useState(''); const [mobile, setMobile] = useState(initialMobile);
const [otp, setOtp] = useState(''); const [otp, setOtp] = useState('');
const [sessionToken, setSessionToken] = useState(''); const [sessionToken, setSessionToken] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState(''); const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
if (initialMobile) setMobile(initialMobile);
}, [initialMobile]);
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 () => {
@@ -220,6 +235,7 @@ export class PhonePePersonalOTPBind extends Component<{
onSuccess: (result: PhonePePersonalBindResult) => void; onSuccess: (result: PhonePePersonalBindResult) => void;
onError: (error: string) => void; onError: (error: string) => void;
isDebug: boolean; isDebug: boolean;
initialMobile?: string;
}> { }> {
render() { render() {
return ( return (
@@ -232,6 +248,7 @@ export class PhonePePersonalOTPBind extends Component<{
onSuccess={this.props.onSuccess} onSuccess={this.props.onSuccess}
onError={this.props.onError} onError={this.props.onError}
isDebug={this.props.isDebug} isDebug={this.props.isDebug}
initialMobile={this.props.initialMobile}
/> />
); );
} }

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { WalletType } from 'rnwalletman'; import { WalletType } from 'rnwalletman';
export interface OTPBindCallbacks { export interface OTPBindCallbacks {
@@ -34,6 +34,7 @@ export function useOTPBind(
otpLength?: number; otpLength?: number;
mobileLength?: number; mobileLength?: number;
additionalParams?: any; additionalParams?: any;
initialMobile?: string;
} }
): [OTPBindState, OTPBindActions] { ): [OTPBindState, OTPBindActions] {
const [mobile, setMobile] = useState(''); const [mobile, setMobile] = useState('');
@@ -44,7 +45,11 @@ export function useOTPBind(
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks; const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
const { otpLength = 6, mobileLength = 10, additionalParams = {} } = config || {}; const { otpLength = 6, mobileLength = 10, additionalParams = {}, initialMobile = '' } = config || {};
useEffect(() => {
if (initialMobile) setMobile(initialMobile);
}, [initialMobile]);
const clearError = () => setErrorMessage(''); const clearError = () => setErrorMessage('');

View File

@@ -8,6 +8,7 @@ import {
Modal, Modal,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Switch,
Text, Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
@@ -147,6 +148,30 @@ const WALLET_TYPE_OPTIONS = [
}, },
]; ];
function getBindKeyForWallet(item: WalletItem): string | null {
const otp = item.otpMode === true;
switch (item.walletType) {
case 'paytm':
return otp ? 'paytm_personal_otp' : 'paytm_personal_token';
case 'phonepe':
return otp ? 'phonepe_personal_otp' : 'phonepe_personal_token';
case 'paytm business':
return 'paytm_business';
case 'phonepe business':
return 'phonepe_business';
case 'googlepay business':
return 'googlepay_business';
case 'bharatpe business':
return 'bharatpe_business';
case 'mobikwik':
return 'mobikwik_personal';
case 'freecharge':
return otp ? 'freecharge_personal' : 'freecharge_personal_token';
default:
return null;
}
}
interface HomeScreenState { interface HomeScreenState {
// bind modals // bind modals
showPaytmPersonalBind: boolean; showPaytmPersonalBind: boolean;
@@ -177,6 +202,7 @@ interface HomeScreenState {
vpaModalSelected: string; vpaModalSelected: string;
// add wallet // add wallet
showAddWallet: boolean; showAddWallet: boolean;
bindPrefillMobile: string;
} }
export default class HomeScreen extends Component<any, HomeScreenState> { export default class HomeScreen extends Component<any, HomeScreenState> {
@@ -213,6 +239,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
vpaModalLoading: false, vpaModalLoading: false,
vpaModalSelected: '', vpaModalSelected: '',
showAddWallet: false, showAddWallet: false,
bindPrefillMobile: '',
}; };
this.deviceId = DeviceInfo.getUniqueIdSync(); this.deviceId = DeviceInfo.getUniqueIdSync();
this.tuneUserId = Math.random().toString(36).substring(2, 15); this.tuneUserId = Math.random().toString(36).substring(2, 15);
@@ -330,6 +357,27 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
} }
}; };
toggleWalletActive = async (item: WalletItem, active: boolean) => {
try {
await Api.instance.setWalletStatus(item.id, active);
this.setState((s) => ({
wallets: s.wallets.map((w) =>
w.id === item.id ? { ...w, status: active ? 'ACTIVE' : 'INACTIVE' } : w),
}));
} catch (e) {
Alert.alert('Failed', (e as Error).message);
}
};
handleRebind = (item: WalletItem) => {
const key = getBindKeyForWallet(item);
if (!key) {
Alert.alert('Rebind', 'Unsupported wallet type');
return;
}
this.openWalletBind(key, item.phone);
};
// ---- bind handlers ---- // ---- bind handlers ----
/** Token 等需客户端 register 的流程(与 OTP 同 modal key 时勿用推断 map */ /** Token 等需客户端 register 的流程(与 OTP 同 modal key 时勿用推断 map */
@@ -360,10 +408,11 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
showPaytmPersonalBind, paytmPersonalBindType, showPhonePePersonalBind, phonePePersonalBindType, showPaytmPersonalBind, paytmPersonalBindType, showPhonePePersonalBind, phonePePersonalBindType,
showPaytmBusinessBind, showPhonePeBusinessBind, showGooglePayBusinessBind, showBharatPeBusinessBind, showPaytmBusinessBind, showPhonePeBusinessBind, showGooglePayBusinessBind, showBharatPeBusinessBind,
showMobikwikPersonalBind, showFreechargePersonalBind, freechargePersonalBindType, showMobikwikPersonalBind, showFreechargePersonalBind, freechargePersonalBindType,
bindPrefillMobile,
} = this.state; } = this.state;
const close = (key: keyof HomeScreenState) => () => const close = (key: keyof HomeScreenState) => () =>
this.setState({ [key]: false } as any); this.setState({ [key]: false, bindPrefillMobile: '' } as any);
if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') { if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') {
return ( return (
@@ -390,6 +439,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<PayTmPersonalOTPBind <PayTmPersonalOTPBind
isDebug isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -409,6 +459,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<PhonePePersonalBind <PhonePePersonalBind
processString="Processing..." processString="Processing..."
mode={1}
userToken={Api.instance.getUserToken()}
isDebug isDebug
onSuccess={this.handleBindSuccess('showPhonePePersonalBind', WalletType.PHONEPE_PERSONAL, 'PhonePe Personal bound successfully') as any} onSuccess={this.handleBindSuccess('showPhonePePersonalBind', WalletType.PHONEPE_PERSONAL, 'PhonePe Personal bound successfully') as any}
onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPhonePePersonalBind')(); }} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPhonePePersonalBind')(); }}
@@ -425,6 +477,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<PhonePePersonalOTPBind <PhonePePersonalOTPBind
isDebug isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -444,6 +497,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<PaytmBusinessOTPBind <PaytmBusinessOTPBind
isDebug isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -550,6 +604,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<BharatPeBusinessOTPBind <BharatPeBusinessOTPBind
isDebug isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -571,6 +626,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
isDebug isDebug
deviceId={this.deviceId} deviceId={this.deviceId}
tuneUserId={this.tuneUserId} tuneUserId={this.tuneUserId}
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, { deviceId: p.deviceId, tuneUserId: p.tuneUserId }))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, { deviceId: p.deviceId, tuneUserId: p.tuneUserId }))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -606,6 +662,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
> >
<FreeChargeBind <FreeChargeBind
isDebug isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))} this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))}
onVerifyOTP={async (wt, p) => onVerifyOTP={async (wt, p) =>
@@ -619,8 +676,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
return null; return null;
}; };
openWalletBind = (key: string) => { openWalletBind = (key: string, prefillMobile?: string) => {
this.setState({ showAddWallet: false }); this.setState({ showAddWallet: false, bindPrefillMobile: prefillMobile ?? '' });
setTimeout(() => { setTimeout(() => {
switch (key) { switch (key) {
case 'paytm_personal_otp': case 'paytm_personal_otp':
@@ -779,8 +836,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888'; const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888';
const isActive = item.status === 'ACTIVE'; const isActive = item.status === 'ACTIVE';
return ( return (
<View style={s.walletCard}>
<TouchableOpacity <TouchableOpacity
style={s.walletCard}
onPress={() => this.openVpaModal(item)} onPress={() => this.openVpaModal(item)}
activeOpacity={0.8} activeOpacity={0.8}
> >
@@ -801,6 +858,20 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
<View style={[s.statusDot, { backgroundColor: isActive ? '#2ecc71' : '#bbb' }]} /> <View style={[s.statusDot, { backgroundColor: isActive ? '#2ecc71' : '#bbb' }]} />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<View style={s.walletActions}>
{!isActive && (
<TouchableOpacity style={s.rebindBtn} onPress={() => this.handleRebind(item)}>
<Text style={s.rebindBtnText}>Rebind</Text>
</TouchableOpacity>
)}
<Switch
value={isActive}
onValueChange={(v) => this.toggleWalletActive(item, v)}
trackColor={{ false: '#ddd', true: '#2ecc7180' }}
thumbColor={isActive ? '#2ecc71' : '#999'}
/>
</View>
</View>
); );
}; };
@@ -990,6 +1061,27 @@ const s = StyleSheet.create({
shadowRadius: 4, shadowRadius: 4,
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
}, },
walletActions: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
marginTop: 10,
paddingTop: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#eee',
},
rebindBtn: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
backgroundColor: '#3498db',
marginRight: 12,
},
rebindBtnText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
},
walletBadge: { walletBadge: {
width: 52, width: 52,
height: 52, height: 52,

View File

@@ -7,6 +7,7 @@ export interface WalletItem {
upi?: string; upi?: string;
phone?: string; phone?: string;
status?: string; status?: string;
otpMode?: boolean;
} }
const DEFAULT_DOMAIN = 'aa.pfgame.org'; const DEFAULT_DOMAIN = 'aa.pfgame.org';
@@ -54,6 +55,7 @@ class Api {
private static _instance: Api | null = null; private static _instance: Api | null = null;
private userId: number = 0; private userId: number = 0;
private userToken: string = '';
private constructor() {} private constructor() {}
@@ -65,6 +67,10 @@ class Api {
return this.userId; return this.userId;
} }
public getUserToken(): string {
return this.userToken;
}
public static get instance() { public static get instance() {
if (Api._instance === null) { if (Api._instance === null) {
Api._instance = new Api(); Api._instance = new Api();
@@ -90,6 +96,7 @@ class Api {
const data = await res.json(); const data = await res.json();
if (!data.success) throw new Error(data.message); if (!data.success) throw new Error(data.message);
this.userId = data.data.userId; this.userId = data.data.userId;
this.userToken = data.data.userToken ?? String(data.data.userId);
return this.userId; return this.userId;
} }
@@ -153,6 +160,17 @@ class Api {
return data.data?.vpa ?? ''; return data.data?.vpa ?? '';
} }
public async setWalletStatus(walletId: string, active: boolean): Promise<string> {
const res = await fetch(`${Api.BASE_URL}/wallet/set-status`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ walletId, active }),
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
return data.data?.status ?? '';
}
public async generateLink(walletId: string, amount: string): Promise<{ link: string; orderId: string }> { public async generateLink(walletId: string, amount: string): Promise<{ link: string; orderId: string }> {
const res = await fetch(`${Api.BASE_URL}/generate-link`, { const res = await fetch(`${Api.BASE_URL}/generate-link`, {
method: 'POST', method: 'POST',