fix
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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<string, string[]>;
|
||||
loadingVpas: Record<string, boolean>;
|
||||
// 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<any, HomeScreenState> {
|
||||
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<any, HomeScreenState> {
|
||||
}
|
||||
};
|
||||
|
||||
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<any, HomeScreenState> {
|
||||
|
||||
renderAddWalletModal() {
|
||||
return (
|
||||
<Modal visible={this.state.showAddWallet} transparent animationType="slide" onRequestClose={() => this.setState({ showAddWallet: false })}>
|
||||
<Modal visible={this.state.showAddWallet} transparent animationType="none" onRequestClose={() => this.setState({ showAddWallet: false })}>
|
||||
<View style={s.modalOverlay}>
|
||||
<View style={s.addModalBox}>
|
||||
<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}>选择钱包类型</Text>
|
||||
<TouchableOpacity onPress={() => this.setState({ showAddWallet: false })}>
|
||||
<Text style={{ fontSize: 20, color: '#999' }}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView>
|
||||
<ScrollView bounces={false}>
|
||||
{WALLET_TYPE_OPTIONS.map(opt => (
|
||||
<TouchableOpacity key={opt.key} style={s.walletTypeRow} onPress={() => this.openWalletBind(opt.key)}>
|
||||
<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' }]} />
|
||||
@@ -418,7 +436,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
@@ -468,15 +486,10 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
||||
}
|
||||
|
||||
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 (
|
||||
<TouchableOpacity style={s.walletCard} onPress={() => this.handleToggleExpand(item.id)} activeOpacity={0.8}>
|
||||
<TouchableOpacity style={s.walletCard} onPress={() => this.openVpaModal(item)} activeOpacity={0.8}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<View style={[s.walletBadge, !isActive && s.walletBadgeInactive]}>
|
||||
{WALLET_ICONS[item.walletType]
|
||||
@@ -490,34 +503,70 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
||||
<Text style={s.walletPhone}>{item.phone || '—'}</Text>
|
||||
<Text style={s.walletUpi} numberOfLines={1}>{item.upi || 'No UPI'}</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<View style={[s.statusDot, { backgroundColor: isActive ? '#2ecc71' : '#bbb' }]} />
|
||||
<Text style={{ fontSize: 10, color: '#aaa', marginTop: 4 }}>{isExpanded ? '▲' : '▼'}</Text>
|
||||
</View>
|
||||
<View style={[s.statusDot, { backgroundColor: isActive ? '#2ecc71' : '#bbb' }]} />
|
||||
</View>
|
||||
|
||||
{isExpanded && (
|
||||
<View style={s.vpaSection}>
|
||||
<Text style={s.vpaSectionTitle}>VPAs</Text>
|
||||
{loadingV ? (
|
||||
<ActivityIndicator size="small" color={color} />
|
||||
) : vpas.length === 0 ? (
|
||||
<Text style={{ color: '#aaa', fontSize: 13 }}>无 VPA 数据</Text>
|
||||
) : (
|
||||
vpas.map((vpa, idx) => (
|
||||
<TouchableOpacity key={vpa} style={[s.vpaRow, item.upi === vpa && s.vpaRowActive]}
|
||||
onPress={(e) => { e.stopPropagation?.(); this.handleSetVpa(item.id, idx); }}>
|
||||
<Text style={[s.vpaText, item.upi === vpa && { color: '#fff' }]}>{vpa}</Text>
|
||||
{item.upi === vpa && <Text style={{ color: '#fff', fontSize: 12 }}>✓</Text>}
|
||||
</TouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
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}>选择 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 }}>无 VPA 数据</Text>
|
||||
: vpaModalVpas.map(vpa => {
|
||||
const selected = vpa === vpaModalSelected;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={vpa}
|
||||
style={[s.vpaOptionRow, selected && { borderColor: color, backgroundColor: color + '10' }]}
|
||||
onPress={() => this.setState({ vpaModalSelected: vpa })}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={[s.radioOuter, selected && { borderColor: color }]}>
|
||||
{selected && <View style={[s.radioInner, { backgroundColor: color }]} />}
|
||||
</View>
|
||||
<Text style={[s.vpaOptionText, selected && { color, fontWeight: '600' }]}>{vpa}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ScrollView>
|
||||
|
||||
<View style={s.vpaModalFooter}>
|
||||
<TouchableOpacity style={s.vpaModalCancelBtn} onPress={this.closeVpaModal}>
|
||||
<Text style={{ color: '#666', fontSize: 15 }}>取消</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[s.vpaModalConfirmBtn, { backgroundColor: vpaModalSelected ? color : '#ccc' }]}
|
||||
onPress={this.confirmVpa}
|
||||
disabled={!vpaModalSelected}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontSize: 15, fontWeight: '600' }}>确认</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { proxyStatus, proxyError, wallets, loadingWallets } = this.state;
|
||||
const proxyCfg: Record<string, { label: string; color: string }> = {
|
||||
@@ -579,6 +628,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
||||
{this.renderBindModal()}
|
||||
{this.renderServerSettingsModal()}
|
||||
{this.renderAddWalletModal()}
|
||||
{this.renderVpaModal()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Submodule servers/walletman updated: 6701de28bf...5762febe51
Reference in New Issue
Block a user