diff --git a/package.json b/package.json index 342480d..d9666ec 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "buffer": "^6.0.3", "react": "18.2.0", "react-native": "0.72.10", + "react-native-animatable": "^1.4.0", "react-native-background-actions": "^4.0.1", "react-native-device-info": "14.0.4", "react-native-fs": "^2.20.0", diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx index 41f8440..7a3d2ca 100644 --- a/screens/HomeScreen.tsx +++ b/screens/HomeScreen.tsx @@ -4,6 +4,7 @@ import { 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, @@ -98,9 +99,11 @@ interface HomeScreenState { // wallet list wallets: WalletItem[]; loadingWallets: boolean; - expandedWalletId: string | null; - walletVpas: Record; - loadingVpas: Record; + // vpa modal + vpaModalWallet: WalletItem | null; + vpaModalVpas: string[]; + vpaModalLoading: boolean; + vpaModalSelected: string; // add wallet showAddWallet: boolean; } @@ -130,9 +133,10 @@ export default class HomeScreen extends Component { settingsPort: '', wallets: [], loadingWallets: false, - expandedWalletId: null, - walletVpas: {}, - loadingVpas: {}, + vpaModalWallet: null, + vpaModalVpas: [], + vpaModalLoading: false, + vpaModalSelected: '', showAddWallet: false, }; this.deviceId = DeviceInfo.getUniqueIdSync(); @@ -217,28 +221,36 @@ export default class HomeScreen extends Component { } }; - handleToggleExpand = async (walletId: string) => { - const { expandedWalletId, walletVpas } = this.state; - if (expandedWalletId === walletId) { - this.setState({ expandedWalletId: null }); - return; - } - this.setState({ expandedWalletId: walletId }); - if (!walletVpas[walletId]) { - this.setState(s => ({ loadingVpas: { ...s.loadingVpas, [walletId]: true } })); - try { - const vpas = await Api.instance.getWalletVpas(walletId); - this.setState(s => ({ walletVpas: { ...s.walletVpas, [walletId]: vpas } })); - } catch {} - this.setState(s => ({ loadingVpas: { ...s.loadingVpas, [walletId]: 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 }); } }; - handleSetVpa = async (walletId: string, vpaIndex: number) => { + 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 { - const vpa = await Api.instance.setCurrentVpa(walletId, vpaIndex); - Alert.alert('已设置', `当前 VPA: ${vpa}`); - this.fetchWallets(); + 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); } @@ -398,18 +410,24 @@ export default class HomeScreen extends Component { renderAddWalletModal() { return ( - this.setState({ showAddWallet: false })}> + this.setState({ showAddWallet: false })}> - + 选择钱包类型 this.setState({ showAddWallet: false })}> - + {WALLET_TYPE_OPTIONS.map(opt => ( - this.openWalletBind(opt.key)}> + this.openWalletBind(opt.key)} activeOpacity={0.7}> {WALLET_ICONS[opt.walletType] ? : @@ -418,7 +436,7 @@ export default class HomeScreen extends Component { ))} - + ); @@ -468,15 +486,10 @@ export default class HomeScreen extends Component { } renderWalletItem = ({ item }: { item: WalletItem }) => { - const { expandedWalletId, walletVpas, loadingVpas } = this.state; - const isExpanded = expandedWalletId === item.id; const color = WALLET_TYPE_COLORS[item.walletType] ?? '#888'; - const vpas = walletVpas[item.id] ?? []; - const loadingV = loadingVpas[item.id]; const isActive = item.status === 'ACTIVE'; - return ( - this.handleToggleExpand(item.id)} activeOpacity={0.8}> + this.openVpaModal(item)} activeOpacity={0.8}> {WALLET_ICONS[item.walletType] @@ -490,34 +503,70 @@ export default class HomeScreen extends Component { {item.phone || '—'} {item.upi || 'No UPI'} - - - {isExpanded ? '▲' : '▼'} - + - - {isExpanded && ( - - VPAs - {loadingV ? ( - - ) : vpas.length === 0 ? ( - 无 VPA 数据 - ) : ( - vpas.map((vpa, idx) => ( - { e.stopPropagation?.(); this.handleSetVpa(item.id, idx); }}> - {vpa} - {item.upi === vpa && } - - )) - )} - - )} ); }; + 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 = { @@ -579,6 +628,7 @@ export default class HomeScreen extends Component { {this.renderBindModal()} {this.renderServerSettingsModal()} {this.renderAddWalletModal()} + {this.renderVpaModal()} ); } @@ -622,15 +672,37 @@ const s = StyleSheet.create({ walletPhone: { fontSize: 15, fontWeight: '600', color: '#222' }, walletUpi: { fontSize: 12, color: '#888', marginTop: 2 }, statusDot: { width: 8, height: 8, borderRadius: 4 }, - vpaSection: { marginTop: 12, paddingTop: 10, borderTopWidth: 1, borderTopColor: '#f0f0f0' }, - vpaSectionTitle: { fontSize: 12, color: '#999', marginBottom: 6, fontWeight: '600' }, - vpaRow: { - flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingVertical: 8, paddingHorizontal: 10, borderRadius: 6, - backgroundColor: '#f5f5f5', marginBottom: 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, }, - vpaRowActive: { backgroundColor: '#3498db' }, - vpaText: { fontSize: 13, color: '#333' }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', diff --git a/servers/walletman b/servers/walletman index 6701de2..5762feb 160000 --- a/servers/walletman +++ b/servers/walletman @@ -1 +1 @@ -Subproject commit 6701de28bf3305b926281ab149fb36faa6de169d +Subproject commit 5762febe5124fca9b7207545caff7d5f3dfedfde