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, PaytmPersonalBindResult, MobikwikPersonalBindResult, FreechargePersonalBindResult, GooglePayBusinessBindResult, BharatPeBusinessBindResult, onSmsMessage, startSmsListener, stopSmsListener, checkSmsPermission, requestSmsPermission, PhonePePersonalBindResult, 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 与服务端 WalletType 字符串一致(见 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', }; // 钱包类型展示信息(walletType 与服务端一致) 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] 初始化失败:', error); } } stopProxyClient() { try { proxyBackgroundService.stop(); } catch {} } 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('设置失败', (e as Error).message); } }; // ---- bind handlers ---- handleBindSuccess = (key: keyof HomeScreenState, msg: string) => async (result: any) => { try { const typeMap: Record = { showPaytmPersonalBind: WalletType.PAYTM_PERSONAL, showPaytmBusinessBind: WalletType.PAYTM_BUSINESS, showPhonePePersonalBind: WalletType.PHONEPE_PERSONAL, showPhonePeBusinessBind: WalletType.PHONEPE_BUSINESS, showGooglePayBusinessBind: WalletType.GOOGLEPAY_BUSINESS, showBharatPeBusinessBind: WalletType.BHARATPE_BUSINESS, showMobikwikPersonalBind: WalletType.MOBIKWIK_PERSONAL, showFreechargePersonalBind: WalletType.FREECHARGE_PERSONAL, }; const wt = typeMap[key as string]; if (wt) await Api.instance.register(wt as WalletType, result); this.setState({ [key]: false } as any); Alert.alert('绑定成功', msg); this.fetchWallets(); } catch (error) { this.setState({ [key]: false } as any); Alert.alert('绑定失败', (error as Error).message); } }; // ---- 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('绑定失败', 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={(r: PaytmPersonalBindResult) => { Alert.alert('绑定成功', 'Paytm Personal OTP'); this.setState({ showPaytmPersonalBind: false }); this.fetchWallets(); }} onError={(e: string) => { Alert.alert('绑定失败', e); close('showPaytmPersonalBind')(); }} /> ); if (showPhonePePersonalBind && phonePePersonalBindType === 'tokenMode') return ( { Alert.alert('绑定失败', 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={(r: PhonePePersonalBindResult) => { Alert.alert('绑定成功', 'PhonePe Personal OTP'); this.setState({ showPhonePePersonalBind: false }); this.fetchWallets(); }} onError={(e: string) => { Alert.alert('绑定失败', 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.handleBindSuccess('showPaytmBusinessBind', 'Paytm Business 绑定成功') as any} onError={(e: string) => { Alert.alert('绑定失败', e); close('showPaytmBusinessBind')(); }} /> ); if (showPhonePeBusinessBind) return ( { Alert.alert('绑定失败', 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('绑定失败', 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.handleBindSuccess('showBharatPeBusinessBind', 'BharatPe Business 绑定成功') as any} onError={(e: string) => { Alert.alert('绑定失败', 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.handleBindSuccess('showMobikwikPersonalBind', 'Mobikwik 绑定成功') as any} onError={(e: string) => { Alert.alert('绑定失败', 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.handleBindSuccess('showFreechargePersonalBind', 'Freecharge 绑定成功') as any} onError={(e: string) => { Alert.alert('绑定失败', 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 })}> 选择钱包类型 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 ( 服务器设置 {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 }}> 取消 { const { settingsHost, settingsPort } = this.state; const domain = settingsPort ? `${settingsHost}:${settingsPort}` : settingsHost; await saveServerDomain(domain, settingsPort === '443'); this.setState({ showServerSettings: false }); Alert.alert('已保存', '重启 App 后生效'); }} style={s.saveBtn}> 保存 ); } 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 ( 选择 VPA {vpaModalWallet?.phone} {vpaModalLoading ? : vpaModalVpas.length === 0 ? 无 VPA 数据 : vpaModalVpas.map(vpa => { const selected = vpa === vpaModalSelected; return ( this.setState({ vpaModalSelected: vpa })} activeOpacity={0.7} > {selected && } {vpa} ); }) } 取消 确认 ); } render() { const { proxyStatus, proxyError, wallets, loadingWallets } = this.state; const proxyCfg: Record = { idle: { label: '未连接', color: '#95a5a6' }, connecting: { label: '连接中…', color: '#f39c12' }, connected: { label: '已连接', color: '#2ecc71' }, disconnected: { label: '已断开', color: '#e74c3c' }, error: { label: '连接失败', color: '#e74c3c' }, }; const { label, color } = proxyCfg[proxyStatus]; return ( {/* 顶栏 */} 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 {/* 钱包列表 */} 已绑定钱包 {loadingWallets ? '刷新中…' : '刷新'} item.id} renderItem={this.renderWalletItem} ListEmptyComponent={ {loadingWallets ? '加载中…' : '暂无绑定钱包,点右上角 + Add 添加'} } /> {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 }, });