import React, { Component } from 'react'; import { Alert, AppState, AppStateStatus, FlatList, Image, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, ActivityIndicator, } from 'react-native'; import * as Animatable from 'react-native-animatable'; import DeviceInfo from 'react-native-device-info'; import { PhonePeBusinessBind, GooglePayBusinessBind, WalletType, PaytmBusinessBindResult, PhonePeBusinessBindResult, PaytmPersonalBind, MobikwikPersonalBindResult, FreechargePersonalBindResult, GooglePayBusinessBindResult, BharatPeBusinessBindResult, onSmsMessage, startSmsListener, stopSmsListener, checkSmsPermission, requestSmsPermission, PhonePePersonalBind, SmsMessage, proxyBackgroundService, } from 'rnwalletman'; import { FreeChargeBind, MobikwikOTPBind, PayTmPersonalOTPBind, PhonePePersonalOTPBind, BharatPeBusinessOTPBind, PaytmBusinessOTPBind, } from '../components/WalletBindComponents'; import Api, { WalletItem, loadServerDomain, saveServerDomain, getServerDomain, } from '../services/api'; // key matches server WalletType string (see types.go) const WALLET_ICONS: Record = { 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 = { 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', walletType: 'phonepe business', label: 'PhonePe Business (OTP-WEB)', mode: 'otp', }, { 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', }, ]; interface HomeScreenState { // bind modals showPaytmPersonalBind: boolean; paytmPersonalBindType: 'otpMode' | 'tokenMode'; showPaytmBusinessBind: boolean; showPhonePePersonalBind: boolean; phonePePersonalBindType: 'otpMode' | 'tokenMode'; showPhonePeBusinessBind: boolean; 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; } export default class HomeScreen extends Component { private deviceId: 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, showGooglePayBusinessBind: false, showBharatPeBusinessBind: false, showMobikwikPersonalBind: false, showFreechargePersonalBind: false, proxyStatus: 'idle', showServerSettings: false, settingsHost: '', settingsPort: '', wallets: [], loadingWallets: false, vpaModalWallet: null, vpaModalVpas: [], vpaModalLoading: false, vpaModalSelected: '', showAddWallet: false, }; this.deviceId = DeviceInfo.getUniqueIdSync(); this.tuneUserId = Math.random().toString(36).substring(2, 15); } 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 */ } } 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); } }; // ---- 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, showGooglePayBusinessBind, showBharatPeBusinessBind, showMobikwikPersonalBind, showFreechargePersonalBind, } = this.state; const close = (key: keyof HomeScreenState) => () => this.setState({ [key]: false } as any); if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') { return ( { Alert.alert('Bind Failed', e); close('showPaytmPersonalBind')(); }} /> ); } if (showPaytmPersonalBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, {}, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { sessionId: p.sessionId }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showPaytmPersonalBind', 'Paytm Personal OTP', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPaytmPersonalBind')(); }} /> ); } if ( showPhonePePersonalBind && phonePePersonalBindType === 'tokenMode' ) { return ( { Alert.alert('Bind Failed', e); close('showPhonePePersonalBind')(); }} /> ); } if (showPhonePePersonalBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, {}, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { sessionId: p.sessionId }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showPhonePePersonalBind', 'PhonePe Personal OTP', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPhonePePersonalBind')(); }} /> ); } if (showPaytmBusinessBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, { password: p.password }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { sessionId: p.sessionId }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showPaytmBusinessBind', 'Paytm Business bound successfully', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showPaytmBusinessBind')(); }} /> ); } if (showPhonePeBusinessBind) { return ( { Alert.alert('Bind Failed', e); close('showPhonePeBusinessBind')(); }} onRenderBottomView={({ showOtpInput, loading, formError, phone, otp, onPhoneChange, onOtpChange, onGetOtp, onSubmitOtp, }) => ( {!showOtpInput ? ( <> {!!formError && ( {formError} )} {loading ? 'Loading...' : 'GET OTP'} ) : ( <> {!!formError && ( {formError} )} {loading ? 'Loading...' : 'Verify OTP'} )} )} /> ); } if (showGooglePayBusinessBind) { return ( { Alert.alert('Bind Failed', e); close('showGooglePayBusinessBind')(); }} /> ); } if (showBharatPeBusinessBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { sessionId: p.sessionId }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showBharatPeBusinessBind', 'BharatPe Business bound successfully', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showBharatPeBusinessBind')(); }} /> ); } if (showMobikwikPersonalBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, { deviceId: p.deviceId, tuneUserId: p.tuneUserId, }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { sessionId: p.sessionId, deviceId: p.deviceId, tuneUserId: p.tuneUserId, nid: p.nid, }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showMobikwikPersonalBind', 'Mobikwik bound successfully', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showMobikwikPersonalBind')(); }} /> ); } if (showFreechargePersonalBind) { return ( { try { return await Api.instance.requestOTP( wt, p.mobile, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onVerifyOTP={async (wt, p) => { try { return await Api.instance.verifyOTP( wt, p.mobile, p.otp, { otpId: p.otpId, deviceId: p.deviceId, csrfId: p.csrfId, appFc: p.appFc, }, ); } catch (e) { return { success: false, message: (e as Error).message, }; } }} onSuccess={this.onOtpBindSuccess( 'showFreechargePersonalBind', 'Freecharge bound successfully', )} onError={(e: string) => { Alert.alert('Bind Failed', e); close('showFreechargePersonalBind')(); }} /> ); } return null; }; openWalletBind = (key: string) => { this.setState({ showAddWallet: false }); 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': this.setState({ showPhonePeBusinessBind: true }); 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 }); break; } }, 300); }; renderAddWalletModal() { return ( this.setState({ showAddWallet: false })} > Select Wallet Type this.setState({ showAddWallet: false }) } > {WALLET_TYPE_OPTIONS.map((opt) => ( this.openWalletBind(opt.key)} activeOpacity={0.7} > {WALLET_ICONS[opt.walletType] ? ( ) : ( )} {opt.label} ))} ); } 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 ( Server Settings {presets.map((p) => ( this.setState({ settingsHost: p.host, settingsPort: p.port, }) } style={[ s.presetBtn, settingsHost === p.host && settingsPort === p.port && s.presetBtnActive, ]} > {p.label} ))} Host this.setState({ settingsHost: t }) } placeholder="192.168.1.198" autoCapitalize="none" /> Port this.setState({ settingsPort: t }) } placeholder="16000" keyboardType="number-pad" /> this.setState({ showServerSettings: false }) } style={{ paddingHorizontal: 16, paddingVertical: 8, marginRight: 10, }} > Cancel { 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} > Save ); } renderWalletItem = ({ item }: { 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'} ); }; 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) => { const selected = vpa === vpaModalSelected; return ( this.setState({ vpaModalSelected: vpa, }) } activeOpacity={0.7} > {selected && ( )} {vpa} ); }) )} Cancel Confirm ); } render() { const { proxyStatus, proxyError, wallets, 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 colonIdx = domain.lastIndexOf(':'); const host = colonIdx > 0 ? domain.substring(0, colonIdx) : domain; const port = colonIdx > 0 ? domain.substring(colonIdx + 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'} item.id} renderItem={this.renderWalletItem} ListEmptyComponent={ {loadingWallets ? 'Loading…' : 'No wallets. Tap + Add to get started.'} } /> {this.renderBindModal()} {this.renderServerSettingsModal()} {this.renderAddWalletModal()} {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', }, 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 }, }, 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', }, walletTypeIcon: { width: 32, height: 32, borderRadius: 6, marginRight: 12, }, walletTypeDot: { width: 10, height: 10, borderRadius: 5, marginRight: 12, }, walletTypeLabel: { fontSize: 14, color: '#333', }, 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, }, });