1524 lines
58 KiB
TypeScript
1524 lines
58 KiB
TypeScript
import React, { Component } from 'react';
|
||
import {
|
||
Alert,
|
||
AppState,
|
||
AppStateStatus,
|
||
Image,
|
||
Modal,
|
||
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,
|
||
MobikwikPersonalBindResult,
|
||
FreechargePersonalBindResult,
|
||
GooglePayBusinessBindResult,
|
||
BharatPeBusinessBindResult,
|
||
onSmsMessage,
|
||
startSmsListener,
|
||
stopSmsListener,
|
||
checkSmsPermission,
|
||
requestSmsPermission,
|
||
PhonePePersonalBind,
|
||
FreechargePersonalBind,
|
||
SmsMessage,
|
||
proxyBackgroundService,
|
||
PhonePePersonalBindResult,
|
||
PaytmPersonalBindResult,
|
||
BindErrorCode,
|
||
} from 'rnwalletman';
|
||
|
||
import {
|
||
FreeChargeBind,
|
||
MobikwikOTPBind,
|
||
PayTmPersonalOTPBind,
|
||
PhonePePersonalOTPBind,
|
||
BharatPeBusinessOTPBind,
|
||
PaytmBusinessOTPBind,
|
||
PhonePeBusinessOTPBind,
|
||
} from '../components/WalletBindComponents';
|
||
|
||
import Api, {
|
||
WalletItem,
|
||
loadServerDomain,
|
||
saveServerDomain,
|
||
getServerDomain,
|
||
} from '../services/api';
|
||
|
||
// key matches server WalletType string (see types.go)
|
||
const WALLET_ICONS: Record<string, any> = {
|
||
paytm: require('../res/paytm.png'),
|
||
'paytm business': require('../res/paytm-business.png'),
|
||
phonepe: require('../res/phonepe.webp'),
|
||
'phonepe business': require('../res/phonepe-business.webp'),
|
||
'googlepay business': require('../res/googlepay-business.webp'),
|
||
'bharatpe business': require('../res/bharatpe-business.webp'),
|
||
mobikwik: require('../res/mobikwik.png'),
|
||
freecharge: require('../res/freecharge.png'),
|
||
};
|
||
|
||
const WALLET_TYPE_COLORS: Record<string, string> = {
|
||
paytm: '#002970',
|
||
'paytm business': '#002970',
|
||
phonepe: '#5a2d9c',
|
||
'phonepe business': '#5a2d9c',
|
||
'googlepay business': '#4285f4',
|
||
'bharatpe business': '#e91e63',
|
||
mobikwik: '#00bcd4',
|
||
freecharge: '#ff5722',
|
||
};
|
||
|
||
// wallet type display info (walletType matches server)
|
||
const WALLET_TYPE_OPTIONS = [
|
||
{
|
||
key: 'paytm_personal_otp',
|
||
walletType: 'paytm',
|
||
label: 'Paytm Personal (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'paytm_personal_token',
|
||
walletType: 'paytm',
|
||
label: 'Paytm Personal (Token)',
|
||
mode: 'token',
|
||
},
|
||
{
|
||
key: 'paytm_business',
|
||
walletType: 'paytm business',
|
||
label: 'Paytm Business (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'phonepe_personal_otp',
|
||
walletType: 'phonepe',
|
||
label: 'PhonePe Personal (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'phonepe_personal_token',
|
||
walletType: 'phonepe',
|
||
label: 'PhonePe Personal (Token)',
|
||
mode: 'token',
|
||
},
|
||
{
|
||
key: 'phonepe_business_otp',
|
||
walletType: 'phonepe business',
|
||
label: 'PhonePe Business (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'phonepe_business_web',
|
||
walletType: 'phonepe business',
|
||
label: 'PhonePe Business (Web)',
|
||
mode: 'token',
|
||
},
|
||
{
|
||
key: 'googlepay_business',
|
||
walletType: 'googlepay business',
|
||
label: 'GooglePay Business',
|
||
mode: 'token',
|
||
},
|
||
{
|
||
key: 'bharatpe_business',
|
||
walletType: 'bharatpe business',
|
||
label: 'BharatPe Business (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'mobikwik_personal',
|
||
walletType: 'mobikwik',
|
||
label: 'Mobikwik Personal (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'freecharge_personal',
|
||
walletType: 'freecharge',
|
||
label: 'Freecharge Personal (OTP)',
|
||
mode: 'otp',
|
||
},
|
||
{
|
||
key: 'freecharge_personal_token',
|
||
walletType: 'freecharge',
|
||
label: 'Freecharge Personal (Token)',
|
||
mode: 'token',
|
||
},
|
||
];
|
||
|
||
function formatWalletTypeLabel(walletType: string) {
|
||
return walletType.replace(/\b\w/g, (c) => c.toUpperCase());
|
||
}
|
||
|
||
function groupBoundWallets(wallets: WalletItem[]) {
|
||
const order: string[] = [];
|
||
const map = new Map<string, WalletItem[]>();
|
||
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 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 'mobikwik_personal';
|
||
case 'freecharge':
|
||
return otp ? 'freecharge_personal' : 'freecharge_personal_token';
|
||
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;
|
||
showFreechargePersonalBind: 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<string, boolean>;
|
||
}
|
||
|
||
export default class HomeScreen extends Component<any, HomeScreenState> {
|
||
private deviceId: string;
|
||
private androidId: string;
|
||
|
||
private tuneUserId: 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,
|
||
showFreechargePersonalBind: false,
|
||
freechargePersonalBindType: 'otpMode',
|
||
proxyStatus: 'idle',
|
||
showServerSettings: false,
|
||
settingsHost: '',
|
||
settingsPort: '',
|
||
wallets: [],
|
||
loadingWallets: false,
|
||
vpaModalWallet: null,
|
||
vpaModalVpas: [],
|
||
vpaModalLoading: false,
|
||
vpaModalSelected: '',
|
||
showAddWallet: false,
|
||
bindPrefillMobile: '',
|
||
expandedBoundWalletGroups: {},
|
||
};
|
||
this.deviceId = DeviceInfo.getUniqueIdSync();
|
||
this.tuneUserId = "yz8mxybytus";//Math.random().toString(36).substring(2, 15);
|
||
this.androidId = DeviceInfo.getAndroidIdSync();
|
||
}
|
||
|
||
async componentDidMount() {
|
||
await loadServerDomain();
|
||
await this.setupPermissions();
|
||
|
||
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() {
|
||
this.stopProxyClient();
|
||
stopSmsListener();
|
||
this.appStateSubscription?.remove();
|
||
}
|
||
|
||
handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||
if (nextAppState === 'active') this.fetchWallets();
|
||
};
|
||
|
||
async setupPermissions() {
|
||
const hasSms = await checkSmsPermission();
|
||
if (!hasSms) await requestSmsPermission();
|
||
startSmsListener();
|
||
onSmsMessage((msg: SmsMessage) => {
|
||
console.log('[SMS]', msg.address, msg.body);
|
||
});
|
||
}
|
||
|
||
async startProxyClient() {
|
||
try {
|
||
this.clientId = DeviceInfo.getUniqueIdSync();
|
||
const userId = Api.instance.getUserId();
|
||
this.setState({ proxyStatus: 'connecting' });
|
||
await proxyBackgroundService.start({
|
||
wsUrl: Api.WS_URL, clientId: this.clientId || '', userId,
|
||
debug: true, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity,
|
||
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 {
|
||
proxyBackgroundService.stop();
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
/** OTP / bind:API catch → { success:false, message } */
|
||
private wrapOtpCall = async (fn: () => Promise<any>): Promise<any> => {
|
||
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 等需客户端 register 的流程(与 OTP 同 modal key 时勿用推断 map) */
|
||
handleBindSuccess = (key: keyof HomeScreenState, walletType: WalletType, msg: string) =>
|
||
async (result: any) => {
|
||
try {
|
||
await Api.instance.register(walletType, result);
|
||
this.setState({ [key]: false } as any);
|
||
Alert.alert('Bind Success', msg);
|
||
this.fetchWallets();
|
||
} catch (error) {
|
||
this.setState({ [key]: false } as any);
|
||
Alert.alert('Bind Failed', (error as Error).message);
|
||
}
|
||
};
|
||
|
||
/** OTP:服务端 verify 已注册,只提示并关弹窗 */
|
||
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, showFreechargePersonalBind, freechargePersonalBindType,
|
||
bindPrefillMobile,
|
||
} = this.state;
|
||
|
||
const close = (key: keyof HomeScreenState) => () =>
|
||
this.setState({ [key]: false, bindPrefillMobile: '' } as any);
|
||
|
||
if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') {
|
||
const remoteVersion = '1'; // 目前版本是 1
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPaytmPersonalBind')}
|
||
>
|
||
<PaytmPersonalBind
|
||
processString="Processing..."
|
||
isDebug
|
||
onSuccess={(result: PaytmPersonalBindResult) => {
|
||
if (result.chType != 'ipay') {
|
||
// 有 AIDL 通信,能拿回数据,但是不是我们的魔改 type
|
||
// 视作未安装
|
||
Alert.alert('Bind Failed', '未安装Paytm 魔改包,需要重新安装');
|
||
return;
|
||
}
|
||
if (result.chVersion != remoteVersion) {
|
||
// 版本有更新
|
||
Alert.alert('Bind Failed', '版本有更新,需要重新安装');
|
||
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: // catch 错误,提示错误即可
|
||
Alert.alert('Bind Failed', message);
|
||
break;
|
||
case BindErrorCode.NO_DATA: // 拿到了错误的信息,提示错误就可以 (aidl 给了错误的 resp,几率低),提示错误就可以
|
||
Alert.alert('Bind Failed', 'No data received from Paytm');
|
||
break;
|
||
case BindErrorCode.SERVICE_DISCONNECTED: // 服务不可用 (可能aidl 通信失败,几率低)提示错误就可以
|
||
Alert.alert('Bind Failed', 'Paytm service disconnected');
|
||
break;
|
||
case BindErrorCode.NOT_INSTALLED: // 未安装,提示用户安装
|
||
Alert.alert('Bind Failed', '未安装Paytm 魔改包,需要重新安装');
|
||
break;
|
||
case BindErrorCode.BIND_ERROR: // 绑定错误,aidl 通信失败,可能存在 paytm 未打开的情况,出现这个,手动拉起一下
|
||
Alert.alert('Bind Failed', 'Paytm bind error');
|
||
break;
|
||
default:
|
||
Alert.alert('Bind Failed', `[${code}] ${message}`);
|
||
break;
|
||
}
|
||
close('showPaytmPersonalBind')();
|
||
}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPaytmPersonalBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPaytmPersonalBind')}
|
||
>
|
||
<PayTmPersonalOTPBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPhonePePersonalBind && phonePePersonalBindType === 'tokenMode') {
|
||
const remoteVersion = '1'; // 目前版本是 1
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPhonePePersonalBind')}
|
||
>
|
||
<PhonePePersonalBind
|
||
processString="Processing..."
|
||
userToken={Api.instance.getUserToken()}
|
||
isDebug
|
||
onSuccess={(result: PhonePePersonalBindResult) => {
|
||
if (result.chType != 'ipay') {
|
||
// 有 AIDL 通信,能拿回数据,但是不是我们的魔改 type
|
||
// 视作未安装
|
||
Alert.alert('Bind Failed', '未安装Phonepe 魔改包,需要重新安装');
|
||
return;
|
||
}
|
||
if (result.chVersion != remoteVersion) {
|
||
// 版本有更新
|
||
Alert.alert('Bind Failed', '版本有更新,需要重新安装');
|
||
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', '未安装Phonepe 魔改包,需要重新安装');
|
||
break;
|
||
case BindErrorCode.SERVICE_DISCONNECTED:
|
||
// 服务不可用 (可能aidl 通信失败,几率低)提示错误就可以
|
||
Alert.alert('Bind Failed', '服务不可用');
|
||
break;
|
||
case BindErrorCode.NO_DATA:
|
||
// 拿到了错误的信息,提示错误就可以 (aidl 给了错误的 resp,几率低),提示错误就可以
|
||
Alert.alert('Bind Failed', '未知错误信息');
|
||
break;
|
||
case BindErrorCode.BIND_ERROR:
|
||
// 绑定错误,aidl 通信失败,可能存在 paytm / phonepe 未打开的情况,出现这个,手动拉起一下
|
||
Alert.alert('Bind Failed', '绑定失败,请手动打开PhonePe后重试');
|
||
break;
|
||
case BindErrorCode.ERROR:
|
||
// catch 错误,通用错误,提示错误即可
|
||
Alert.alert('Bind Failed', message);
|
||
break;
|
||
default:
|
||
Alert.alert('Bind Failed', '未知错误');
|
||
break;
|
||
}
|
||
|
||
close('showPhonePePersonalBind')();
|
||
}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPhonePePersonalBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPhonePePersonalBind')}
|
||
>
|
||
<PhonePePersonalOTPBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPaytmBusinessBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPaytmBusinessBind')}
|
||
>
|
||
<PaytmBusinessOTPBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPhonePeBusinessBind && phonePeBusinessBindType === 'otpMode') {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPhonePeBusinessBind')}
|
||
>
|
||
<PhonePeBusinessOTPBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showPhonePeBusinessBind && phonePeBusinessBindType === 'webviewMode') {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showPhonePeBusinessBind')}
|
||
>
|
||
<PhonePeBusinessBind
|
||
processString="Processing..."
|
||
isDebug
|
||
clearCookie
|
||
onSuccess={this.handleBindSuccess('showPhonePeBusinessBind', WalletType.PHONEPE_BUSINESS, 'PhonePe Business bound successfully') as any}
|
||
onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPhonePeBusinessBind')(); }}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showGooglePayBusinessBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showGooglePayBusinessBind')}
|
||
>
|
||
<GooglePayBusinessBind
|
||
processString="Processing..."
|
||
isDebug
|
||
onSuccess={this.handleBindSuccess('showGooglePayBusinessBind', WalletType.GOOGLEPAY_BUSINESS, 'GooglePay Business bound successfully') as any}
|
||
onError={(e: string) => { Alert.alert('Bind Failed', e); close('showGooglePayBusinessBind')(); }}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showBharatPeBusinessBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showBharatPeBusinessBind')}
|
||
>
|
||
<BharatPeBusinessOTPBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showMobikwikPersonalBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showMobikwikPersonalBind')}
|
||
>
|
||
<MobikwikOTPBind
|
||
isDebug={true}
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))
|
||
}}
|
||
onVerifyOTP={async (wt, p) => {
|
||
return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, p));
|
||
}}
|
||
onSuccess={this.onOtpBindSuccess('showMobikwikPersonalBind', 'Mobikwik bound successfully')}
|
||
onError={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showFreechargePersonalBind && freechargePersonalBindType === 'tokenMode') {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showFreechargePersonalBind')}
|
||
>
|
||
<FreechargePersonalBind
|
||
processString="Processing..."
|
||
isDebug
|
||
onSuccess={this.handleBindSuccess('showFreechargePersonalBind', WalletType.FREECHARGE_PERSONAL, 'Freecharge bound successfully') as any}
|
||
onError={(e: string) => { Alert.alert('Bind Failed', e); close('showFreechargePersonalBind')(); }}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
if (showFreechargePersonalBind) {
|
||
return (
|
||
<Modal
|
||
visible
|
||
transparent
|
||
onRequestClose={close('showFreechargePersonalBind')}
|
||
>
|
||
<FreeChargeBind
|
||
isDebug
|
||
initialMobile={bindPrefillMobile}
|
||
onRequestOTP={async (wt, p) => {
|
||
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={() => {}}
|
||
/>
|
||
</Modal>
|
||
);
|
||
}
|
||
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 });
|
||
break;
|
||
case 'freecharge_personal':
|
||
this.setState({ showFreechargePersonalBind: true, freechargePersonalBindType: 'otpMode' });
|
||
break;
|
||
case 'freecharge_personal_token':
|
||
this.setState({ showFreechargePersonalBind: true, freechargePersonalBindType: 'tokenMode' });
|
||
break;
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
toggleBoundWalletGroup = (walletType: string) => {
|
||
this.setState((prev) => ({
|
||
expandedBoundWalletGroups: {
|
||
...prev.expandedBoundWalletGroups,
|
||
[walletType]: !prev.expandedBoundWalletGroups[walletType],
|
||
},
|
||
}));
|
||
};
|
||
|
||
renderAddWalletModal() {
|
||
return (
|
||
<Modal
|
||
visible={this.state.showAddWallet}
|
||
transparent
|
||
animationType="none"
|
||
onRequestClose={() => this.setState({ showAddWallet: false })}
|
||
>
|
||
<View style={s.modalOverlay}>
|
||
<Animatable.View
|
||
animation="zoomIn"
|
||
duration={220}
|
||
easing="ease-out-back"
|
||
useNativeDriver
|
||
style={s.addModalBox}
|
||
>
|
||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||
<Text style={s.addModalTitle}>Select Wallet Type</Text>
|
||
<TouchableOpacity onPress={() => this.setState({ showAddWallet: false })}>
|
||
<Text style={{ fontSize: 20, color: '#999' }}>✕</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<ScrollView bounces={false}>
|
||
{WALLET_TYPE_OPTIONS.map((opt) => (
|
||
<TouchableOpacity
|
||
key={opt.key}
|
||
style={s.walletTypeRow}
|
||
onPress={() => this.openWalletBind(opt.key)}
|
||
activeOpacity={0.7}
|
||
>
|
||
{WALLET_ICONS[opt.walletType] ? (
|
||
<Image source={WALLET_ICONS[opt.walletType]} style={s.walletTypeIcon} resizeMode="contain" />
|
||
) : (
|
||
<View style={[s.walletTypeDot, { backgroundColor: WALLET_TYPE_COLORS[opt.walletType] ?? '#888' }]} />
|
||
)}
|
||
<Text style={s.walletTypeLabel}>{opt.label}</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</ScrollView>
|
||
</Animatable.View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
renderServerSettingsModal() {
|
||
const { showServerSettings, settingsHost, settingsPort } = this.state;
|
||
const presets = [
|
||
{ label: 'aa.pfgame.org', host: 'aa.pfgame.org', port: '443' },
|
||
{ label: '192.168.1.117:16000', host: '192.168.1.117', port: '16000' },
|
||
];
|
||
return (
|
||
<Modal
|
||
visible={showServerSettings}
|
||
transparent
|
||
animationType="fade"
|
||
>
|
||
<View style={s.modalOverlay}>
|
||
<View style={s.settingsBox}>
|
||
<Text style={s.settingsTitle}>Server Settings</Text>
|
||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginBottom: 14 }}>
|
||
{presets.map((p) => (
|
||
<TouchableOpacity
|
||
key={p.label}
|
||
onPress={() => this.setState({ settingsHost: p.host, settingsPort: p.port })}
|
||
style={[s.presetBtn, settingsHost === p.host && settingsPort === p.port && s.presetBtnActive]}
|
||
>
|
||
<Text style={{ fontSize: 12, color: settingsHost === p.host && settingsPort === p.port ? '#fff' : '#333' }}>
|
||
{p.label}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
<Text style={s.inputLabel}>Host</Text>
|
||
<TextInput
|
||
style={s.textInput}
|
||
value={settingsHost}
|
||
onChangeText={(t) => this.setState({ settingsHost: t })}
|
||
placeholder="192.168.1.198"
|
||
autoCapitalize="none"
|
||
/>
|
||
<Text style={s.inputLabel}>Port</Text>
|
||
<TextInput
|
||
style={[s.textInput, { marginBottom: 20 }]}
|
||
value={settingsPort}
|
||
onChangeText={(t) => this.setState({ settingsPort: t })}
|
||
placeholder="16000"
|
||
keyboardType="number-pad"
|
||
/>
|
||
<View style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>
|
||
<TouchableOpacity
|
||
onPress={() => this.setState({ showServerSettings: false })}
|
||
style={{ paddingHorizontal: 16, paddingVertical: 8, marginRight: 10 }}
|
||
>
|
||
<Text style={{ color: '#666' }}>Cancel</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
onPress={async () => {
|
||
const { settingsHost, settingsPort } = this.state;
|
||
const domain = settingsPort ? `${settingsHost}:${settingsPort}` : settingsHost;
|
||
await saveServerDomain(domain, settingsPort === '443');
|
||
this.setState({ showServerSettings: false });
|
||
Alert.alert('Saved', 'Restart app to take effect');
|
||
}}
|
||
style={s.saveBtn}
|
||
>
|
||
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Save</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
renderWalletItem = (item: WalletItem) => {
|
||
const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888';
|
||
const isActive = item.status === 'ACTIVE';
|
||
return (
|
||
<View style={s.walletCard}>
|
||
<TouchableOpacity
|
||
onPress={() => this.openVpaModal(item)}
|
||
activeOpacity={0.8}
|
||
>
|
||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||
<View style={[s.walletBadge, !isActive && s.walletBadgeInactive]}>
|
||
{WALLET_ICONS[item.walletType] ? (
|
||
<Image source={WALLET_ICONS[item.walletType]} style={[s.walletIcon, !isActive && s.walletIconInactive]} resizeMode="contain" />
|
||
) : (
|
||
<View style={[s.walletIconFallback, { backgroundColor: isActive ? color : '#ccc' }]}>
|
||
<Text style={s.walletBadgeText}>{item.walletType.split('_')[0]}</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={{ flex: 1, marginLeft: 12 }}>
|
||
<Text style={s.walletPhone}>{item.phone || '—'}</Text>
|
||
<Text style={s.walletUpi} numberOfLines={1}>{item.upi || 'No UPI'}</Text>
|
||
</View>
|
||
<View style={[s.statusDot, { backgroundColor: isActive ? '#2ecc71' : '#bbb' }]} />
|
||
</View>
|
||
</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>
|
||
);
|
||
};
|
||
|
||
renderBoundWalletList = () => {
|
||
const { wallets, loadingWallets, expandedBoundWalletGroups } = this.state;
|
||
|
||
if (loadingWallets && wallets.length === 0) {
|
||
return (
|
||
<View style={{ alignItems: 'center', marginTop: 60 }}>
|
||
<Text style={{ color: '#aaa', fontSize: 14 }}>Loading…</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
if (wallets.length === 0) {
|
||
return (
|
||
<View style={{ alignItems: 'center', marginTop: 60 }}>
|
||
<Text style={{ color: '#aaa', fontSize: 14 }}>No wallets. Tap + Add to get started.</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const groups = groupBoundWallets(wallets);
|
||
|
||
return (
|
||
<ScrollView
|
||
style={{ flex: 1, width: '100%' }}
|
||
contentContainerStyle={{ paddingHorizontal: 14, paddingBottom: 20 }}
|
||
bounces={false}
|
||
>
|
||
{groups.map(({ walletType, items }) => {
|
||
if (items.length === 1) {
|
||
return this.renderWalletItem(items[0]);
|
||
}
|
||
|
||
const expanded = !!expandedBoundWalletGroups[walletType];
|
||
const color = WALLET_TYPE_COLORS[walletType] ?? '#888';
|
||
|
||
return (
|
||
<View key={walletType} style={s.boundWalletGroup}>
|
||
<TouchableOpacity
|
||
style={s.boundWalletGroupHeader}
|
||
onPress={() => this.toggleBoundWalletGroup(walletType)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={s.walletBadge}>
|
||
{WALLET_ICONS[walletType] ? (
|
||
<Image source={WALLET_ICONS[walletType]} style={s.walletIcon} resizeMode="contain" />
|
||
) : (
|
||
<View style={[s.walletIconFallback, { backgroundColor: color }]}>
|
||
<Text style={s.walletBadgeText}>{walletType.split(' ')[0]}</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
<View style={{ flex: 1, marginLeft: 12 }}>
|
||
<Text style={s.boundWalletGroupTitle}>{formatWalletTypeLabel(walletType)}</Text>
|
||
<Text style={s.boundWalletGroupSub}>{items.length} accounts</Text>
|
||
</View>
|
||
<Text style={s.walletGroupChevron}>{expanded ? '▼' : '▶'}</Text>
|
||
</TouchableOpacity>
|
||
{expanded && items.map((item) => (
|
||
<View key={`${item.id}-wrap`} style={s.boundWalletGroupItem}>
|
||
{this.renderWalletItem(item)}
|
||
</View>
|
||
))}
|
||
</View>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
);
|
||
};
|
||
|
||
renderVpaModal() {
|
||
const { vpaModalWallet, vpaModalVpas, vpaModalLoading, vpaModalSelected } = this.state;
|
||
const color = WALLET_TYPE_COLORS[vpaModalWallet?.walletType ?? ''] ?? '#3498db';
|
||
return (
|
||
<Modal
|
||
visible={!!vpaModalWallet}
|
||
transparent
|
||
animationType="none"
|
||
onRequestClose={this.closeVpaModal}
|
||
>
|
||
<View style={s.modalOverlay}>
|
||
<Animatable.View
|
||
animation="zoomIn"
|
||
duration={220}
|
||
easing="ease-out-back"
|
||
useNativeDriver
|
||
style={s.vpaModalBox}
|
||
>
|
||
<Text style={s.vpaModalTitle}>Select VPA</Text>
|
||
<Text style={s.vpaModalSub}>{vpaModalWallet?.phone}</Text>
|
||
|
||
<ScrollView style={s.vpaModalList} bounces={false}>
|
||
{vpaModalLoading ? (
|
||
<ActivityIndicator size="large" color={color} style={{ marginVertical: 28 }} />
|
||
) : vpaModalVpas.length === 0 ? (
|
||
<Text style={{ color: '#aaa', textAlign: 'center', marginVertical: 28 }}>No VPA data</Text>
|
||
) : (
|
||
vpaModalVpas.map((vpa) => (
|
||
<TouchableOpacity
|
||
key={vpa}
|
||
style={[s.vpaOptionRow, vpa === vpaModalSelected && { borderColor: color, backgroundColor: `${color}10` }]}
|
||
onPress={() => this.setState({ vpaModalSelected: vpa })}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={[s.radioOuter, vpa === vpaModalSelected && { borderColor: color }]}>
|
||
{vpa === vpaModalSelected && <View style={[s.radioInner, { backgroundColor: color }]} />}
|
||
</View>
|
||
<Text style={[s.vpaOptionText, vpa === vpaModalSelected && { color, fontWeight: '600' }]}>{vpa}</Text>
|
||
</TouchableOpacity>
|
||
))
|
||
)}
|
||
</ScrollView>
|
||
|
||
<View style={s.vpaModalFooter}>
|
||
<TouchableOpacity style={s.vpaModalCancelBtn} onPress={this.closeVpaModal}>
|
||
<Text style={{ color: '#666', fontSize: 15 }}>Cancel</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
style={[s.vpaModalConfirmBtn, { backgroundColor: vpaModalSelected ? color : '#ccc' }]}
|
||
onPress={this.confirmVpa}
|
||
disabled={!vpaModalSelected}
|
||
>
|
||
<Text style={{ color: '#fff', fontSize: 15, fontWeight: '600' }}>Confirm</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</Animatable.View>
|
||
</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
render() {
|
||
const { proxyStatus, proxyError, loadingWallets } = this.state;
|
||
const proxyCfg: Record<string, { label: string; color: string }> = {
|
||
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 (
|
||
<View style={s.container}>
|
||
{/* top bar */}
|
||
<View style={s.topBar}>
|
||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color, marginRight: 6 }} />
|
||
<Text style={{ color, fontSize: 13 }}>
|
||
Proxy {label}{proxyStatus === 'error' && proxyError ? `: ${proxyError}` : ''}
|
||
</Text>
|
||
</View>
|
||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||
<TouchableOpacity
|
||
onPress={() => {
|
||
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 }}
|
||
>
|
||
<Text style={{ fontSize: 20, color: '#3498db' }}>⚙</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity
|
||
onPress={() => this.setState({ showAddWallet: true })}
|
||
style={s.addBtn}
|
||
>
|
||
<Text style={s.addBtnText}>+ Add</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
|
||
{/* wallet list */}
|
||
<View style={s.listHeader}>
|
||
<Text style={s.listHeaderText}>Bound Wallets</Text>
|
||
<TouchableOpacity onPress={this.fetchWallets} disabled={loadingWallets}>
|
||
<Text style={{ fontSize: 13, color: '#3498db' }}>{loadingWallets ? 'Refreshing…' : 'Refresh'}</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{this.renderBoundWalletList()}
|
||
|
||
{this.renderBindModal()}
|
||
{this.renderServerSettingsModal()}
|
||
{this.renderAddWalletModal()}
|
||
{this.renderVpaModal()}
|
||
</View>
|
||
);
|
||
}
|
||
}
|
||
|
||
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',
|
||
},
|
||
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',
|
||
},
|
||
addModalBox: {
|
||
backgroundColor: '#fff',
|
||
borderRadius: 12,
|
||
padding: 20,
|
||
width: '88%',
|
||
maxHeight: '70%',
|
||
},
|
||
addModalTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#222',
|
||
},
|
||
walletTypeRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingVertical: 12,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#f0f0f0',
|
||
},
|
||
walletTypeLabel: {
|
||
fontSize: 14,
|
||
color: '#333',
|
||
},
|
||
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',
|
||
},
|
||
walletTypeIcon: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 6,
|
||
marginRight: 12,
|
||
},
|
||
walletTypeDot: {
|
||
width: 10,
|
||
height: 10,
|
||
borderRadius: 5,
|
||
marginRight: 12,
|
||
},
|
||
settingsBox: {
|
||
backgroundColor: '#fff',
|
||
borderRadius: 10,
|
||
padding: 20,
|
||
width: '85%',
|
||
},
|
||
settingsTitle: {
|
||
fontSize: 16,
|
||
fontWeight: 'bold',
|
||
marginBottom: 12,
|
||
},
|
||
presetBtn: {
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 5,
|
||
borderRadius: 6,
|
||
backgroundColor: '#f0f0f0',
|
||
},
|
||
presetBtnActive: { backgroundColor: '#3498db' },
|
||
inputLabel: {
|
||
fontSize: 13,
|
||
color: '#666',
|
||
marginBottom: 4,
|
||
},
|
||
textInput: {
|
||
borderWidth: 1,
|
||
borderColor: '#ddd',
|
||
borderRadius: 6,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 8,
|
||
marginBottom: 12,
|
||
fontSize: 14,
|
||
},
|
||
saveBtn: {
|
||
backgroundColor: '#3498db',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 8,
|
||
borderRadius: 6,
|
||
},
|
||
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,
|
||
},
|
||
});
|