diff --git a/components/OTPBindUI.tsx b/components/OTPBindUI.tsx index 76012ae..1531f47 100644 --- a/components/OTPBindUI.tsx +++ b/components/OTPBindUI.tsx @@ -21,6 +21,7 @@ interface OTPBindUIProps { onError: (error: string) => void; isDebug: boolean; additionalParams?: any; + initialMobile?: string; } export const OTPBindUI: React.FC = ({ @@ -34,11 +35,12 @@ export const OTPBindUI: React.FC = ({ onError, isDebug, additionalParams = {}, + initialMobile = '', }) => { const [state, actions] = useOTPBind( walletType, { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug }, - { otpLength, mobileLength, additionalParams } + { otpLength, mobileLength, additionalParams, initialMobile } ); const isLoading = state.loading || state.step === 'processing'; diff --git a/components/WalletBindComponents.tsx b/components/WalletBindComponents.tsx index be24836..12f4cee 100644 --- a/components/WalletBindComponents.tsx +++ b/components/WalletBindComponents.tsx @@ -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 { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, BharatPeBusinessBindResult, PaytmBusinessBindResult } from 'rnwalletman'; import { OTPBindUI } from './OTPBindUI'; @@ -9,6 +9,7 @@ export class FreeChargeBind extends Component<{ onSuccess: (result: FreechargePersonalBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }> { render() { return ( @@ -21,6 +22,7 @@ export class FreeChargeBind extends Component<{ onSuccess={this.props.onSuccess} onError={this.props.onError} isDebug={this.props.isDebug} + initialMobile={this.props.initialMobile} /> ); } @@ -34,6 +36,7 @@ export class MobikwikOTPBind extends Component<{ isDebug: boolean; deviceId: string; tuneUserId: string; + initialMobile?: string; }> { render() { return ( @@ -50,6 +53,7 @@ export class MobikwikOTPBind extends Component<{ deviceId: this.props.deviceId, tuneUserId: this.props.tuneUserId, }} + initialMobile={this.props.initialMobile} /> ); } @@ -61,6 +65,7 @@ export class PayTmPersonalOTPBind extends Component<{ onSuccess: (result: PaytmPersonalBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }> { render() { return ( @@ -73,6 +78,7 @@ export class PayTmPersonalOTPBind extends Component<{ onSuccess={this.props.onSuccess} onError={this.props.onError} isDebug={this.props.isDebug} + initialMobile={this.props.initialMobile} /> ); } @@ -84,6 +90,7 @@ export class BharatPeBusinessOTPBind extends Component<{ onSuccess: (result: BharatPeBusinessBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }> { render() { return ( @@ -96,6 +103,7 @@ export class BharatPeBusinessOTPBind extends Component<{ onSuccess={this.props.onSuccess} onError={this.props.onError} isDebug={this.props.isDebug} + initialMobile={this.props.initialMobile} /> ); } @@ -107,6 +115,7 @@ export class PaytmBusinessOTPBind extends Component<{ onSuccess: (result: PaytmBusinessBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }> { render() { return ( @@ -116,25 +125,31 @@ export class PaytmBusinessOTPBind extends Component<{ onSuccess={this.props.onSuccess} onError={this.props.onError} 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; onVerifyOTP: (walletType: WalletType, params: any) => Promise; onSuccess: (result: PaytmBusinessBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }) { const [step, setStep] = useState<'credentials' | 'otp'>('credentials'); - const [mobile, setMobile] = useState(''); + const [mobile, setMobile] = useState(initialMobile); const [otp, setOtp] = useState(''); const [sessionToken, setSessionToken] = useState(''); const [loading, setLoading] = useState(false); const [errorMsg, setErrorMsg] = useState(''); + useEffect(() => { + if (initialMobile) setMobile(initialMobile); + }, [initialMobile]); + const log = (...args: any[]) => { if (isDebug) console.log('[PaytmBusiness]', ...args); }; const handleRequestOTP = async () => { @@ -220,6 +235,7 @@ export class PhonePePersonalOTPBind extends Component<{ onSuccess: (result: PhonePePersonalBindResult) => void; onError: (error: string) => void; isDebug: boolean; + initialMobile?: string; }> { render() { return ( @@ -232,6 +248,7 @@ export class PhonePePersonalOTPBind extends Component<{ onSuccess={this.props.onSuccess} onError={this.props.onError} isDebug={this.props.isDebug} + initialMobile={this.props.initialMobile} /> ); } diff --git a/hooks/useOTPBind.ts b/hooks/useOTPBind.ts index 2c4c1d2..04bb842 100644 --- a/hooks/useOTPBind.ts +++ b/hooks/useOTPBind.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { WalletType } from 'rnwalletman'; export interface OTPBindCallbacks { @@ -34,6 +34,7 @@ export function useOTPBind( otpLength?: number; mobileLength?: number; additionalParams?: any; + initialMobile?: string; } ): [OTPBindState, OTPBindActions] { const [mobile, setMobile] = useState(''); @@ -44,7 +45,11 @@ export function useOTPBind( const [errorMessage, setErrorMessage] = useState(''); 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(''); diff --git a/libs/rnwalletman b/libs/rnwalletman index 4ef1f1f..9e2e07c 160000 --- a/libs/rnwalletman +++ b/libs/rnwalletman @@ -1 +1 @@ -Subproject commit 4ef1f1fe27134708c4acb53e05f9553753df2a55 +Subproject commit 9e2e07c0edd35ebbfdb18de45c7246b4b23c1a56 diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx index 4498a3f..6cdb3b6 100644 --- a/screens/HomeScreen.tsx +++ b/screens/HomeScreen.tsx @@ -8,6 +8,7 @@ import { Modal, ScrollView, StyleSheet, + Switch, Text, TextInput, 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 { // bind modals showPaytmPersonalBind: boolean; @@ -177,6 +202,7 @@ interface HomeScreenState { vpaModalSelected: string; // add wallet showAddWallet: boolean; + bindPrefillMobile: string; } export default class HomeScreen extends Component { @@ -213,6 +239,7 @@ export default class HomeScreen extends Component { vpaModalLoading: false, vpaModalSelected: '', showAddWallet: false, + bindPrefillMobile: '', }; this.deviceId = DeviceInfo.getUniqueIdSync(); this.tuneUserId = Math.random().toString(36).substring(2, 15); @@ -330,6 +357,27 @@ export default class HomeScreen extends Component { } }; + 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 ---- /** Token 等需客户端 register 的流程(与 OTP 同 modal key 时勿用推断 map) */ @@ -360,10 +408,11 @@ export default class HomeScreen extends Component { showPaytmPersonalBind, paytmPersonalBindType, showPhonePePersonalBind, phonePePersonalBindType, showPaytmBusinessBind, showPhonePeBusinessBind, showGooglePayBusinessBind, showBharatPeBusinessBind, showMobikwikPersonalBind, showFreechargePersonalBind, freechargePersonalBindType, + bindPrefillMobile, } = this.state; const close = (key: keyof HomeScreenState) => () => - this.setState({ [key]: false } as any); + this.setState({ [key]: false, bindPrefillMobile: '' } as any); if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') { return ( @@ -390,6 +439,7 @@ export default class HomeScreen extends Component { > this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} onVerifyOTP={async (wt, p) => @@ -409,6 +459,8 @@ export default class HomeScreen extends Component { > { Alert.alert('Bind Failed', e); close('showPhonePePersonalBind')(); }} @@ -425,6 +477,7 @@ export default class HomeScreen extends Component { > this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} onVerifyOTP={async (wt, p) => @@ -444,6 +497,7 @@ export default class HomeScreen extends Component { > this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {}))} onVerifyOTP={async (wt, p) => @@ -550,6 +604,7 @@ export default class HomeScreen extends Component { > this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))} onVerifyOTP={async (wt, p) => @@ -571,6 +626,7 @@ export default class HomeScreen extends Component { isDebug deviceId={this.deviceId} tuneUserId={this.tuneUserId} + initialMobile={bindPrefillMobile} onRequestOTP={async (wt, p) => this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, { deviceId: p.deviceId, tuneUserId: p.tuneUserId }))} onVerifyOTP={async (wt, p) => @@ -606,6 +662,7 @@ export default class HomeScreen extends Component { > this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))} onVerifyOTP={async (wt, p) => @@ -619,8 +676,8 @@ export default class HomeScreen extends Component { return null; }; - openWalletBind = (key: string) => { - this.setState({ showAddWallet: false }); + openWalletBind = (key: string, prefillMobile?: string) => { + this.setState({ showAddWallet: false, bindPrefillMobile: prefillMobile ?? '' }); setTimeout(() => { switch (key) { case 'paytm_personal_otp': @@ -779,28 +836,42 @@ export default class HomeScreen extends Component { const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888'; const isActive = item.status === 'ACTIVE'; return ( - this.openVpaModal(item)} - activeOpacity={0.8} - > - - - {WALLET_ICONS[item.walletType] ? ( - - ) : ( - - {item.walletType.split('_')[0]} - - )} + + this.openVpaModal(item)} + activeOpacity={0.8} + > + + + {WALLET_ICONS[item.walletType] ? ( + + ) : ( + + {item.walletType.split('_')[0]} + + )} + + + {item.phone || '—'} + {item.upi || 'No UPI'} + + - - {item.phone || '—'} - {item.upi || 'No UPI'} - - + + + {!isActive && ( + this.handleRebind(item)}> + Rebind + + )} + this.toggleWalletActive(item, v)} + trackColor={{ false: '#ddd', true: '#2ecc7180' }} + thumbColor={isActive ? '#2ecc71' : '#999'} + /> - + ); }; @@ -990,6 +1061,27 @@ const s = StyleSheet.create({ shadowRadius: 4, 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: { width: 52, height: 52, diff --git a/services/api.ts b/services/api.ts index 97d8dc8..4117c74 100644 --- a/services/api.ts +++ b/services/api.ts @@ -7,6 +7,7 @@ export interface WalletItem { upi?: string; phone?: string; status?: string; + otpMode?: boolean; } const DEFAULT_DOMAIN = 'aa.pfgame.org'; @@ -54,6 +55,7 @@ class Api { private static _instance: Api | null = null; private userId: number = 0; + private userToken: string = ''; private constructor() {} @@ -65,6 +67,10 @@ class Api { return this.userId; } + public getUserToken(): string { + return this.userToken; + } + public static get instance() { if (Api._instance === null) { Api._instance = new Api(); @@ -90,6 +96,7 @@ class Api { const data = await res.json(); if (!data.success) throw new Error(data.message); this.userId = data.data.userId; + this.userToken = data.data.userToken ?? String(data.data.userId); return this.userId; } @@ -153,6 +160,17 @@ class Api { return data.data?.vpa ?? ''; } + public async setWalletStatus(walletId: string, active: boolean): Promise { + 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 }> { const res = await fetch(`${Api.BASE_URL}/generate-link`, { method: 'POST',