import React, { Component } from 'react'; import { Alert, AppState, AppStateStatus, Image, Modal, NativeModules, Platform, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, ActivityIndicator, } from 'react-native'; import * as Animatable from 'react-native-animatable'; import DeviceInfo from 'react-native-device-info'; import { GooglePayBusinessBind, PhonePeBusinessBind, WalletType, PaytmBusinessBindResult, PaytmPersonalBind, FreechargePersonalBindResult, GooglePayBusinessBindResult, BharatPeBusinessBindResult, PhonePePersonalBind, FreechargePersonalBind, proxyBackgroundService, type RebindConfig, type PatchConfig, type FcmNotificationTapPayload, PhonePePersonalBindResult, PaytmPersonalBindResult, MobikwikPersonalBind, MobikwikPersonalBindResult, BindErrorCode, } from 'rnwalletman'; import { FreeChargeBind, MobikwikPersonalOTPBind, PayTmPersonalOTPBind, PhonePePersonalOTPBind, BharatPeBusinessOTPBind, PaytmBusinessOTPBind, PhonePeBusinessOTPBind, AmazonPayOTPBind, } from '../components/WalletBindComponents'; import { WalletSelectModal, WALLET_ICONS, WALLET_TYPE_COLORS } from '../components/WalletSelectModal'; import { ServerSettingsModal } from '../components/ServerSettingsModal'; import Api, { WalletItem, loadServerDomain, getServerDomain, } from '../services/api'; /** ipay patch 期望,bind 与 ProxyService FCM 重绑共用 */ const PATCH_EXPECT: PatchConfig = { chType: 'ipay', paytmChVersion: '3', phonepeChVersion: '1', mobikwikChVersion: '1', }; function phonePePatchFailReason(chType?: string, chVersion?: string): string | null { if (!chType || chType !== PATCH_EXPECT.chType) { return 'Patched PhonePe app not installed. Reinstall required'; } if (chVersion !== PATCH_EXPECT.phonepeChVersion) { return 'App version outdated. Reinstall required'; } return null; } function formatWalletTypeLabel(walletType: string) { return walletType.replace(/\b\w/g, (c) => c.toUpperCase()); } function groupBoundWallets(wallets: WalletItem[]) { const order: string[] = []; const map = new Map(); for (const w of wallets) { if (!map.has(w.walletType)) { map.set(w.walletType, []); order.push(w.walletType); } map.get(w.walletType)!.push(w); } return order.map((walletType) => ({ walletType, items: map.get(walletType)!, })); } function getBindKeyForWalletType(walletType: string): string | null { switch (walletType) { case 'paytm': return 'paytm_personal_token'; case 'phonepe': return 'phonepe_personal_token'; case 'mobikwik': return 'mobikwik_personal_token'; case 'freecharge': return 'freecharge_personal_token'; default: return null; } } 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 otp ? 'phonepe_business_otp' : 'phonepe_business_web'; case 'googlepay business': return 'googlepay_business'; case 'bharatpe business': return 'bharatpe_business'; case 'mobikwik': return otp ? 'mobikwik_personal' : 'mobikwik_personal_token'; case 'freecharge': return otp ? 'freecharge_personal' : 'freecharge_personal_token'; case 'amazonpay': return 'amazonpay_personal'; default: return null; } } interface HomeScreenState { // bind modals showPaytmPersonalBind: boolean; paytmPersonalBindType: 'otpMode' | 'tokenMode'; showPaytmBusinessBind: boolean; showPhonePePersonalBind: boolean; phonePePersonalBindType: 'otpMode' | 'tokenMode'; showPhonePeBusinessBind: boolean; phonePeBusinessBindType: 'otpMode' | 'webviewMode'; freechargePersonalBindType: 'otpMode' | 'tokenMode'; showGooglePayBusinessBind: boolean; showBharatPeBusinessBind: boolean; showMobikwikPersonalBind: boolean; mobikwikPersonalBindType: 'otpMode' | 'tokenMode'; showFreechargePersonalBind: boolean; showAmazonPayPersonalBind: boolean; // proxy proxyStatus: 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'; proxyError?: string; // server settings showServerSettings: boolean; settingsHost: string; settingsPort: string; // wallet list wallets: WalletItem[]; loadingWallets: boolean; // vpa modal vpaModalWallet: WalletItem | null; vpaModalVpas: string[]; vpaModalLoading: boolean; vpaModalSelected: string; // add wallet showAddWallet: boolean; bindPrefillMobile: string; expandedBoundWalletGroups: Record; } export default class HomeScreen extends Component { private deviceId: string; private androidId: string; private adid: string = ''; private clientId: string = ''; private appStateSubscription?: any; constructor(props: any) { super(props); this.state = { paytmPersonalBindType: 'otpMode', showPaytmPersonalBind: false, showPaytmBusinessBind: false, showPhonePePersonalBind: false, phonePePersonalBindType: 'otpMode', showPhonePeBusinessBind: false, phonePeBusinessBindType: 'otpMode', showGooglePayBusinessBind: false, showBharatPeBusinessBind: false, showMobikwikPersonalBind: false, mobikwikPersonalBindType: 'otpMode', showFreechargePersonalBind: false, freechargePersonalBindType: 'otpMode', showAmazonPayPersonalBind: false, proxyStatus: 'idle', showServerSettings: false, settingsHost: '', settingsPort: '', wallets: [], loadingWallets: false, vpaModalWallet: null, vpaModalVpas: [], vpaModalLoading: false, vpaModalSelected: '', showAddWallet: false, bindPrefillMobile: '', expandedBoundWalletGroups: {}, }; this.deviceId = DeviceInfo.getUniqueIdSync(); this.androidId = DeviceInfo.getAndroidIdSync(); } private loadAdid = async (): Promise => { if (Platform.OS !== 'android' || !NativeModules.AdId?.getAdvertisingId) { this.adid = ''; return ''; } try { const id = await NativeModules.AdId.getAdvertisingId(); this.adid = (id || '').trim(); } catch (e) { console.log('[ADID] getAdvertisingId failed:', e); this.adid = ''; } return this.adid; }; private ensureMobikwikOtpParams = async () => { const adid = this.adid || await this.loadAdid(); if (!adid) { throw new Error('无法获取 GAID (广告 ID),请确认已安装 Google Play 服务且未关闭广告 ID'); } return { androidId: this.androidId, adid, tuneUserId: adid, }; }; async componentDidMount() { proxyBackgroundService.setNotificationTapHandler(this.handleNotificationTap); await loadServerDomain(); await this.loadAdid(); await proxyBackgroundService.syncPatchConfig(PATCH_EXPECT); const doLogin = () => { Api.instance.login('test123', '123456').then(async () => { await this.startProxyClient(); this.fetchWallets(); }).catch((error) => { console.log('[Login] retry in 3s:', error); setTimeout(doLogin, 3000); }); }; doLogin(); this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange); } componentWillUnmount() { proxyBackgroundService.setNotificationTapHandler(null); this.stopProxyClient(); this.appStateSubscription?.remove(); } handleNotificationTap = (payload: FcmNotificationTapPayload) => { if (payload.cmd !== 'rebind_wallet') return; const { walletId, walletType, phone } = payload.params; const item = walletId ? this.state.wallets.find(w => w.id === walletId) : undefined; if (item) { this.handleRebind(item); return; } if (walletType) { const key = getBindKeyForWalletType(walletType); if (key) this.openWalletBind(key, phone); } }; handleAppStateChange = (nextAppState: AppStateStatus) => { if (nextAppState === 'active') { this.fetchWallets(); void proxyBackgroundService.syncPendingNotificationTap(); } }; buildRebindConfig = (): RebindConfig => ({ baseUrl: Api.BASE_URL, userId: Api.instance.getUserId(), userToken: Api.instance.getUserToken(), onRebound: () => this.fetchWallets(), }); async startProxyClient() { try { this.clientId = DeviceInfo.getUniqueIdSync(); const userId = Api.instance.getUserId(); this.setState({ proxyStatus: 'connecting' }); await proxyBackgroundService.syncPatchConfig(PATCH_EXPECT); await proxyBackgroundService.syncRebindConfig(this.buildRebindConfig()); await proxyBackgroundService.start({ wsUrl: Api.WS_URL, clientId: this.clientId || '', userId, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity, registerFcmToken: (clientId, fcmToken) => Api.instance.registerFcmToken(clientId, fcmToken), onConnected: () => this.setState({ proxyStatus: 'connected' }), onDisconnected: () => this.setState({ proxyStatus: 'disconnected' }), onError: (error: string) => this.setState({ proxyStatus: 'error', proxyError: error }), }); } catch (error) { console.error('[Proxy] init failed:', error); } } stopProxyClient() { try { void proxyBackgroundService.syncRebindConfig(null); proxyBackgroundService.stop(); } catch { /* ignore */ } } /** OTP / bind:API catch → { success:false, message } */ private wrapOtpCall = async (fn: () => Promise): Promise => { try { return await fn(); } catch (e) { return { success: false, message: (e as Error).message }; } }; fetchWallets = async () => { this.setState({ loadingWallets: true }); try { const wallets = await Api.instance.listWallets(); this.setState({ wallets }); } catch (e) { console.log('[fetchWallets]', e); } finally { this.setState({ loadingWallets: false }); } }; openVpaModal = async (item: WalletItem) => { this.setState({ vpaModalWallet: item, vpaModalVpas: [], vpaModalLoading: true, vpaModalSelected: item.upi ?? '' }); try { const vpas = await Api.instance.getWalletVpas(item.id); this.setState({ vpaModalVpas: vpas, vpaModalLoading: false }); } catch { this.setState({ vpaModalLoading: false }); } }; closeVpaModal = () => this.setState({ vpaModalWallet: null }); confirmVpa = async () => { const { vpaModalWallet, vpaModalVpas, vpaModalSelected } = this.state; if (!vpaModalWallet || !vpaModalSelected) return; const idx = vpaModalVpas.indexOf(vpaModalSelected); if (idx < 0) return; try { await Api.instance.setCurrentVpa(vpaModalWallet.id, idx); const walletId = vpaModalWallet.id; const vpa = vpaModalSelected; this.setState((s) => ({ vpaModalWallet: null, wallets: s.wallets.map((w) => (w.id === walletId ? { ...w, upi: vpa } : w)), })); } catch (e) { Alert.alert('Set Failed', (e as Error).message); } }; 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 bind: client calls register (do not infer modal key from wallet type alone) */ handleBindSuccess = (key: keyof HomeScreenState, walletType: WalletType, msg: string) => async (result: any) => { const finishSuccess = () => { this.setState({ [key]: false } as any); Alert.alert('Bind Success', msg); this.fetchWallets(); }; const finishSilent = () => { this.setState({ [key]: false } as any); }; try { await Api.instance.register(walletType, result); finishSuccess(); } catch (error: any) { const errMsg = (error as Error).message || ''; const phonePeMod = NativeModules.PhonePePersonalModule; if ( walletType === WalletType.PHONEPE_PERSONAL && /please login/i.test(errMsg) && phonePeMod?.getTokenAfterGtk ) { try { const raw = await phonePeMod.getTokenAfterGtk(Api.instance.getUserToken()); const data = JSON.parse(raw); const patchErr = phonePePatchFailReason(data.chType, data.chVersion); if (patchErr) { finishSilent(); Alert.alert('Bind Failed', patchErr); return; } await Api.instance.register(walletType, { ...result, mobile: data.mobile ?? data.phoneNumber ?? result.mobile, token: data.token, refreshToken: data.refreshToken, userId: data.userId, deviceId: data.deviceId, deviceUnifierId: data.deviceUnifierId, chType: data.chType, chVersion: data.chVersion, appVersion: data.appVersion ?? data.version ?? result.appVersion, tokenExpiresAt: data.tokenExpiresAt ?? data.expiresAt, }); finishSuccess(); return; } catch (retryErr) { finishSilent(); Alert.alert('Bind Failed', (retryErr as Error).message || 'Bind failed'); return; } } finishSilent(); Alert.alert('Bind Failed', errMsg || 'Bind failed'); } }; /** OTP bind: server registers on verify; only show success and close modal */ onOtpBindSuccess = (key: keyof HomeScreenState, msg: string) => () => { Alert.alert('Bind Success', msg); this.setState({ [key]: false } as any); this.fetchWallets(); }; // ---- modals ---- renderBindModal = () => { const { showPaytmPersonalBind, paytmPersonalBindType, showPhonePePersonalBind, phonePePersonalBindType, showPaytmBusinessBind, showPhonePeBusinessBind, phonePeBusinessBindType, showGooglePayBusinessBind, showBharatPeBusinessBind, showMobikwikPersonalBind, mobikwikPersonalBindType, showFreechargePersonalBind, freechargePersonalBindType, showAmazonPayPersonalBind, bindPrefillMobile, } = this.state; const close = (key: keyof HomeScreenState) => () => this.setState({ [key]: false, bindPrefillMobile: '' } as any); if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') { const { chType, paytmChVersion: remoteVersion } = PATCH_EXPECT; return ( { if (result.chType != chType) { Alert.alert('Bind Failed', 'Patched Paytm app not installed. Reinstall required'); return; } if (result.chVersion != remoteVersion) { Alert.alert('Bind Failed', 'App version outdated. Reinstall required'); return; } if (!result.token) { Alert.alert('Bind Failed', 'Please login in paytm app first'); return; } this.handleBindSuccess('showPaytmPersonalBind', WalletType.PAYTM_PERSONAL, 'Paytm Personal bound successfully')(result); }} onError={(code: string, message: string) => { switch (code) { case BindErrorCode.NATIVE_MODULE_UNAVAILABLE: Alert.alert('Bind Failed', 'Native module not available'); break; case BindErrorCode.NOT_LOGGED_IN: Alert.alert('Bind Failed', 'Please login in paytm app first'); break; case BindErrorCode.ERROR: Alert.alert('Bind Failed', message); break; case BindErrorCode.NO_DATA: Alert.alert('Bind Failed', 'No data received from Paytm'); break; case BindErrorCode.SERVICE_DISCONNECTED: Alert.alert('Bind Failed', 'Paytm service disconnected'); break; case BindErrorCode.NOT_INSTALLED: Alert.alert('Bind Failed', 'Patched Paytm app not installed. Reinstall required'); break; case BindErrorCode.BIND_ERROR: Alert.alert('Bind Failed', 'Paytm bind error'); break; default: Alert.alert('Bind Failed', `[${code}] ${message}`); break; } close('showPaytmPersonalBind')(); }} /> ); } if (showPaytmPersonalBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {})); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionId: p.sessionId })); }} onSuccess={this.onOtpBindSuccess('showPaytmPersonalBind', 'Paytm Personal OTP')} onError={() => {}} /> ); } if (showPhonePePersonalBind && phonePePersonalBindType === 'tokenMode') { return ( { const patchErr = phonePePatchFailReason(result.chType, result.chVersion); if (patchErr) { Alert.alert('Bind Failed', patchErr); close('showPhonePePersonalBind')(); return; } this.handleBindSuccess('showPhonePePersonalBind', WalletType.PHONEPE_PERSONAL, 'PhonePe Personal bound successfully')(result); }} onError={(code: string, message: string) => { switch (code) { case BindErrorCode.NOT_INSTALLED: Alert.alert('Bind Failed', 'Patched PhonePe app not installed. Reinstall required'); break; case BindErrorCode.SERVICE_DISCONNECTED: Alert.alert('Bind Failed', 'Service unavailable'); break; case BindErrorCode.NO_DATA: Alert.alert('Bind Failed', 'Invalid response from PhonePe'); break; case BindErrorCode.BIND_ERROR: Alert.alert('Bind Failed', 'Bind failed. Open PhonePe manually and try again'); break; case BindErrorCode.ERROR: Alert.alert('Bind Failed', message); break; default: Alert.alert('Bind Failed', 'Unknown error'); break; } close('showPhonePePersonalBind')(); }} /> ); } if (showPhonePePersonalBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {})); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionId: p.sessionId })); }} onSuccess={this.onOtpBindSuccess('showPhonePePersonalBind', 'PhonePe Personal OTP')} onError={() => {}} /> ); } if (showPaytmBusinessBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {})); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionToken: p.sessionToken })); }} onSuccess={this.onOtpBindSuccess('showPaytmBusinessBind', 'Paytm Business bound successfully')} onError={() => {}} /> ); } if (showPhonePeBusinessBind && phonePeBusinessBindType === 'otpMode') { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, {})); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionToken: p.sessionToken })); }} onSuccess={this.onOtpBindSuccess('showPhonePeBusinessBind', 'PhonePe Business bound successfully')} onError={() => {}} /> ); } if (showPhonePeBusinessBind && phonePeBusinessBindType === 'webviewMode') { return ( { Alert.alert('Bind Failed', e); close('showPhonePeBusinessBind')(); }} /> ); } if (showGooglePayBusinessBind) { return ( { Alert.alert('Bind Failed', e); close('showGooglePayBusinessBind')(); }} /> ); } if (showBharatPeBusinessBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile)); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionToken: p.sessionToken })); }} onSuccess={this.onOtpBindSuccess('showBharatPeBusinessBind', 'BharatPe Business bound successfully')} onError={() => {}} /> ); } if (showMobikwikPersonalBind && mobikwikPersonalBindType === 'tokenMode') { return ( { if (!result.hashId) { Alert.alert('Bind Failed', 'Please login in Mobikwik app first'); return; } if (!result.token) { Alert.alert('Bind Failed', 'Invalid hashId from Mobikwik'); return; } this.handleBindSuccess( 'showMobikwikPersonalBind', WalletType.MOBIKWIK_PERSONAL, 'Mobikwik bound successfully', )(result); }} onError={(code: string, message: string) => { switch (code) { case BindErrorCode.NATIVE_MODULE_UNAVAILABLE: Alert.alert('Bind Failed', 'Native module not available'); break; case BindErrorCode.NOT_INSTALLED: Alert.alert('Bind Failed', 'Patched Mobikwik app not installed. Install mobikwik_ipay_2365.apk'); break; case BindErrorCode.NOT_LOGGED_IN: Alert.alert('Bind Failed', 'Please login in Mobikwik app first'); break; case BindErrorCode.SERVICE_DISCONNECTED: Alert.alert('Bind Failed', 'Mobikwik service unavailable. Open the app and try again'); break; case BindErrorCode.NO_DATA: Alert.alert('Bind Failed', 'No login data received'); break; case BindErrorCode.BIND_ERROR: Alert.alert('Bind Failed', 'Bind failed. Open Mobikwik manually and try again'); break; case BindErrorCode.ERROR: Alert.alert('Bind Failed', message); break; default: Alert.alert('Bind Failed', `[${code}] ${message}`); break; } close('showMobikwikPersonalBind')(); }} /> ); } if (showMobikwikPersonalBind) { return ( { try { const otpParams = await this.ensureMobikwikOtpParams(); return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, otpParams)); } catch (e) { return { success: false, message: (e as Error).message }; } }} onVerifyOTP={async (wt, p) => this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, p)) } onSuccess={this.onOtpBindSuccess('showMobikwikPersonalBind', 'Mobikwik bound successfully')} onError={() => {}} /> ); } if (showFreechargePersonalBind && freechargePersonalBindType === 'tokenMode') { return ( { Alert.alert('Bind Failed', e); close('showFreechargePersonalBind')(); }} /> ); } if (showFreechargePersonalBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile)); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { otpId: p.otpId, deviceId: p.deviceId, csrfId: p.csrfId, appFc: p.appFc })); }} onSuccess={this.onOtpBindSuccess('showFreechargePersonalBind', 'Freecharge bound successfully')} onError={() => {}} /> ); } if (showAmazonPayPersonalBind) { return ( { return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile, { ...(p.sessionId ? { sessionId: p.sessionId } : {}), ...(p.password ? { password: p.password } : {}), })); }} onVerifyOTP={async (wt, p) => { return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, { sessionId: p.sessionId, ...(p.password ? { password: p.password } : {}), })); }} onSuccess={this.onOtpBindSuccess('showAmazonPayPersonalBind', 'Amazon Pay bound successfully')} onError={() => {}} /> ); } return null; }; openWalletBind = (key: string, prefillMobile?: string) => { this.setState({ showAddWallet: false, bindPrefillMobile: prefillMobile ?? '' }); setTimeout(() => { switch (key) { case 'paytm_personal_otp': this.setState({ showPaytmPersonalBind: true, paytmPersonalBindType: 'otpMode' }); break; case 'paytm_personal_token': this.setState({ showPaytmPersonalBind: true, paytmPersonalBindType: 'tokenMode' }); break; case 'paytm_business': this.setState({ showPaytmBusinessBind: true }); break; case 'phonepe_personal_otp': this.setState({ showPhonePePersonalBind: true, phonePePersonalBindType: 'otpMode' }); break; case 'phonepe_personal_token': this.setState({ showPhonePePersonalBind: true, phonePePersonalBindType: 'tokenMode' }); break; case 'phonepe_business_otp': this.setState({ showPhonePeBusinessBind: true, phonePeBusinessBindType: 'otpMode' }); break; case 'phonepe_business_web': this.setState({ showPhonePeBusinessBind: true, phonePeBusinessBindType: 'webviewMode' }); break; case 'googlepay_business': this.setState({ showGooglePayBusinessBind: true }); break; case 'bharatpe_business': this.setState({ showBharatPeBusinessBind: true }); break; case 'mobikwik_personal': this.setState({ showMobikwikPersonalBind: true, mobikwikPersonalBindType: 'otpMode' }); break; case 'mobikwik_personal_token': this.setState({ showMobikwikPersonalBind: true, mobikwikPersonalBindType: 'tokenMode' }); break; case 'freecharge_personal': this.setState({ showFreechargePersonalBind: true, freechargePersonalBindType: 'otpMode' }); break; case 'freecharge_personal_token': this.setState({ showFreechargePersonalBind: true, freechargePersonalBindType: 'tokenMode' }); break; case 'amazonpay_personal': this.setState({ showAmazonPayPersonalBind: true }); break; } }, 300); }; toggleBoundWalletGroup = (walletType: string) => { this.setState((prev) => ({ expandedBoundWalletGroups: { ...prev.expandedBoundWalletGroups, [walletType]: !prev.expandedBoundWalletGroups[walletType], }, })); }; renderWalletItem = (item: WalletItem) => { 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]} )} {item.phone || '—'} {item.upi || 'No UPI'} {!isActive && ( this.handleRebind(item)}> Rebind )} this.toggleWalletActive(item, v)} trackColor={{ false: '#ddd', true: '#2ecc7180' }} thumbColor={isActive ? '#2ecc71' : '#999'} /> ); }; renderBoundWalletList = () => { const { wallets, loadingWallets, expandedBoundWalletGroups } = this.state; if (loadingWallets && wallets.length === 0) { return ( Loading… ); } if (wallets.length === 0) { return ( No wallets. Tap + Add to get started. ); } const groups = groupBoundWallets(wallets); return ( {groups.map(({ walletType, items }) => { const expanded = !!expandedBoundWalletGroups[walletType]; const color = WALLET_TYPE_COLORS[walletType] ?? '#888'; return ( this.toggleBoundWalletGroup(walletType)} activeOpacity={0.7} > {WALLET_ICONS[walletType] ? ( ) : ( {walletType.split(' ')[0]} )} {formatWalletTypeLabel(walletType)} {items.length} account{items.length > 1 ? 's' : ''} {expanded ? '▼' : '▶'} {expanded && items.map((item) => ( {this.renderWalletItem(item)} ))} ); })} ); }; renderVpaModal() { const { vpaModalWallet, vpaModalVpas, vpaModalLoading, vpaModalSelected } = this.state; const color = WALLET_TYPE_COLORS[vpaModalWallet?.walletType ?? ''] ?? '#3498db'; return ( Select VPA {vpaModalWallet?.phone} {vpaModalLoading ? ( ) : vpaModalVpas.length === 0 ? ( No VPA data ) : ( vpaModalVpas.map((vpa) => ( this.setState({ vpaModalSelected: vpa })} activeOpacity={0.7} > {vpa === vpaModalSelected && } {vpa} )) )} Cancel Confirm ); } render() { const { proxyStatus, proxyError, loadingWallets } = this.state; const proxyCfg: Record = { idle: { label: 'Idle', color: '#95a5a6' }, connecting: { label: 'Connecting…', color: '#f39c12' }, connected: { label: 'Connected', color: '#2ecc71' }, disconnected: { label: 'Disconnected', color: '#e74c3c' }, error: { label: 'Error', color: '#e74c3c' }, }; const { label, color } = proxyCfg[proxyStatus]; return ( {/* top bar */} Proxy {label}{proxyStatus === 'error' && proxyError ? `: ${proxyError}` : ''} { const domain = getServerDomain(); const i = domain.lastIndexOf(':'); const host = i > 0 ? domain.slice(0, i) : domain; const port = i > 0 ? domain.slice(i + 1) : ''; this.setState({ showServerSettings: true, settingsHost: host, settingsPort: port }); }} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} style={{ padding: 6 }} > this.setState({ showAddWallet: true })} style={s.addBtn} > + Add {/* wallet list */} Bound Wallets {loadingWallets ? 'Refreshing…' : 'Refresh'} {this.renderBoundWalletList()} {this.renderBindModal()} this.setState({ settingsHost })} onPortChange={(settingsPort) => this.setState({ settingsPort })} onClose={() => this.setState({ showServerSettings: false })} /> this.setState({ showAddWallet: false })} onSelectBind={this.openWalletBind} /> {this.renderVpaModal()} ); } } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f0f0f0', }, topBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 14, paddingVertical: 8, backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, autoRebindRow: { flexDirection: 'row', alignItems: 'center', marginTop: 6, gap: 8, }, autoRebindLabel: { fontSize: 12, color: '#666', }, addBtn: { backgroundColor: '#3498db', borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, }, addBtnText: { color: '#fff', fontSize: 13, fontWeight: '600', }, listHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 10, }, listHeaderText: { fontSize: 14, fontWeight: '600', color: '#333', }, walletCard: { backgroundColor: '#fff', borderRadius: 10, padding: 14, marginBottom: 10, elevation: 1, shadowColor: '#000', shadowOpacity: 0.06, 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, borderRadius: 10, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f5f5f5', }, walletIcon: { width: 52, height: 52, borderRadius: 10, }, walletIconInactive: { opacity: 0.3 }, walletBadgeInactive: { backgroundColor: '#f0f0f0' }, walletIconFallback: { width: 52, height: 52, borderRadius: 10, alignItems: 'center', justifyContent: 'center', }, walletBadgeText: { color: '#fff', fontSize: 9, fontWeight: '700', textAlign: 'center', }, walletPhone: { fontSize: 15, fontWeight: '600', color: '#222', }, walletUpi: { fontSize: 12, color: '#888', marginTop: 2, }, statusDot: { width: 8, height: 8, borderRadius: 4, }, vpaModalBox: { backgroundColor: '#fff', borderRadius: 20, width: '88%', maxHeight: '72%', overflow: 'hidden', shadowColor: '#000', shadowOpacity: 0.18, shadowRadius: 20, shadowOffset: { width: 0, height: 8 }, elevation: 10, }, vpaModalTitle: { fontSize: 17, fontWeight: '700', color: '#222', paddingHorizontal: 20, paddingTop: 22, }, vpaModalSub: { fontSize: 13, color: '#999', paddingHorizontal: 20, marginTop: 3, marginBottom: 14, }, vpaModalList: { paddingHorizontal: 14, maxHeight: 260, }, vpaOptionRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1.5, borderColor: '#eee', marginBottom: 8, }, radioOuter: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#ccc', alignItems: 'center', justifyContent: 'center', marginRight: 12, }, radioInner: { width: 10, height: 10, borderRadius: 5, }, vpaOptionText: { fontSize: 14, color: '#333', flex: 1, }, vpaModalFooter: { flexDirection: 'row', borderTopWidth: 1, borderTopColor: '#f0f0f0', marginTop: 8, }, vpaModalCancelBtn: { flex: 1, paddingVertical: 16, alignItems: 'center', borderRightWidth: 1, borderRightColor: '#f0f0f0', }, vpaModalConfirmBtn: { flex: 1, paddingVertical: 16, alignItems: 'center', borderRadius: 0, }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', }, boundWalletGroup: { marginBottom: 4, }, boundWalletGroupHeader: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 10, padding: 12, marginBottom: 8, borderWidth: 1, borderColor: '#eee', }, boundWalletGroupTitle: { fontSize: 15, fontWeight: '600', color: '#222', }, boundWalletGroupSub: { fontSize: 12, color: '#888', marginTop: 2, }, boundWalletGroupItem: { marginLeft: 10, }, walletGroupChevron: { fontSize: 12, color: '#999', marginLeft: 8, width: 16, textAlign: 'right', }, otpBar: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: '#fff', padding: 16, borderTopWidth: 1, borderTopColor: '#e0e0e0', gap: 10, }, otpInput: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, color: '#333', }, otpBtn: { backgroundColor: '#5a2d9c', borderRadius: 8, paddingVertical: 12, alignItems: 'center', }, otpBtnText: { color: '#fff', fontSize: 15, fontWeight: '600', }, errText: { color: '#e53935', fontSize: 13, }, });