diff --git a/App.tsx b/App.tsx index 4d58b5d..8e44486 100644 --- a/App.tsx +++ b/App.tsx @@ -1,11 +1,32 @@ import React from 'react'; -import { Text } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import Svg, { Path, Circle, Rect } from 'react-native-svg'; import HomeScreen from './screens/HomeScreen'; +import TestScreen from './screens/TestScreen'; import MessageScreen from './screens/MessageScreen'; +const WalletIcon = ({ color, size }: { color: string; size: number }) => ( + + + + + +); + +const ToolIcon = ({ color, size }: { color: string; size: number }) => ( + + + +); + +const MessageIcon = ({ color, size }: { color: string; size: number }) => ( + + + +); + const Tab = createBottomTabNavigator(); export default function App() { @@ -30,11 +51,18 @@ export default function App() { name="Home" component={HomeScreen} options={{ - title: '首页', - tabBarLabel: '首页', - tabBarIcon: ({ color, size }) => ( - 🏠 - ), + title: '钱包列表', + tabBarLabel: '钱包列表', + tabBarIcon: ({ color, size }) => , + }} + /> + , }} /> ( - 💬 - ), + tabBarIcon: ({ color, size }) => , }} /> diff --git a/declarations.d.ts b/declarations.d.ts new file mode 100644 index 0000000..dcae957 --- /dev/null +++ b/declarations.d.ts @@ -0,0 +1,6 @@ +declare module '*.svg' { + import React from 'react'; + import { SvgProps } from 'react-native-svg'; + const content: React.FC; + export default content; +} diff --git a/metro.config.js b/metro.config.js index ad8f87b..87f8ee2 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,11 +1,16 @@ -const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); -/** - * Metro configuration - * https://facebook.github.io/metro/docs/configuration - * - * @type {import('metro-config').MetroConfig} - */ -const config = {}; +const defaultConfig = getDefaultConfig(__dirname); +const { assetExts, sourceExts } = defaultConfig.resolver; -module.exports = mergeConfig(getDefaultConfig(__dirname), config); +const config = { + transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer'), + }, + resolver: { + assetExts: assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], + }, +}; + +module.exports = mergeConfig(defaultConfig, config); diff --git a/package.json b/package.json index 119a351..342480d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "react-native-gesture-handler": "~2.9.0", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "~3.36.0", + "react-native-svg": "^14.1.0", + "react-native-svg-transformer": "^1.5.3", "react-native-tcp-socket": "^6.4.1", "react-native-webview": "13.6.2", "rnauto": "./libs/rnauto", diff --git a/res/bharatpe-business.webp b/res/bharatpe-business.webp new file mode 100644 index 0000000..729871b Binary files /dev/null and b/res/bharatpe-business.webp differ diff --git a/res/freecharge.png b/res/freecharge.png new file mode 100644 index 0000000..08fc60b Binary files /dev/null and b/res/freecharge.png differ diff --git a/res/googlepay-business.webp b/res/googlepay-business.webp new file mode 100644 index 0000000..04165e9 Binary files /dev/null and b/res/googlepay-business.webp differ diff --git a/res/mobikwik.png b/res/mobikwik.png new file mode 100644 index 0000000..32908f5 Binary files /dev/null and b/res/mobikwik.png differ diff --git a/res/paytm-business.png b/res/paytm-business.png new file mode 100644 index 0000000..b8bd773 Binary files /dev/null and b/res/paytm-business.png differ diff --git a/res/paytm.png b/res/paytm.png new file mode 100644 index 0000000..1803ce3 Binary files /dev/null and b/res/paytm.png differ diff --git a/res/phonepe-business.webp b/res/phonepe-business.webp new file mode 100644 index 0000000..9cb44f4 Binary files /dev/null and b/res/phonepe-business.webp differ diff --git a/res/phonepe.webp b/res/phonepe.webp new file mode 100644 index 0000000..c6111c8 Binary files /dev/null and b/res/phonepe.webp differ diff --git a/screens/HomeScreen.tsx b/screens/HomeScreen.tsx index a4c7144..41f8440 100644 --- a/screens/HomeScreen.tsx +++ b/screens/HomeScreen.tsx @@ -1,5 +1,9 @@ import React, { Component } from "react"; -import { Alert, AppState, AppStateStatus, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native"; +import { + Alert, AppState, AppStateStatus, FlatList, Image, Modal, + ScrollView, StyleSheet, Text, TextInput, + TouchableOpacity, View, ActivityIndicator, +} from "react-native"; import DeviceInfo from 'react-native-device-info'; import { PhonePeBusinessBind, @@ -22,9 +26,6 @@ import { PhonePePersonalBind, SmsMessage, proxyBackgroundService, - proxySendMessage, - onProxyMessage, - paytmPay } from "rnwalletman"; import { @@ -36,17 +37,79 @@ import { PaytmBusinessOTPBind, } from '../components/WalletBindComponents'; -import Api, { loadServerDomain, saveServerDomain, getServerDomain } from '../services/api'; -import { WalletmanAppState } from '../types'; +import Api, { WalletItem, loadServerDomain, saveServerDomain, getServerDomain } from '../services/api'; -interface HomeScreenState extends WalletmanAppState {} +// 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; + expandedWalletId: string | null; + walletVpas: Record; + loadingVpas: Record; + // add wallet + showAddWallet: boolean; +} export default class HomeScreen extends Component { private deviceId: string; private tuneUserId: string; private clientId: string = ''; private appStateSubscription?: any; - private onProxyMessageSub?: ReturnType; constructor(props: any) { super(props); @@ -65,8 +128,13 @@ export default class HomeScreen extends Component { showServerSettings: false, settingsHost: '', settingsPort: '', + wallets: [], + loadingWallets: false, + expandedWalletId: null, + walletVpas: {}, + loadingVpas: {}, + showAddWallet: false, }; - this.deviceId = DeviceInfo.getUniqueIdSync(); this.tuneUserId = Math.random().toString(36).substring(2, 15); } @@ -75,21 +143,11 @@ export default class HomeScreen extends Component { await loadServerDomain(); await this.setupPermissions(); - this.onProxyMessageSub = onProxyMessage((msg) => { - switch (msg.type) { - case 'echo': - Alert.alert('Echo 回来了', JSON.stringify(msg.data)); - break; - default: - break; - } - }); - const doLogin = () => { Api.instance.login('test123', '123456') - .then(async (userId) => { - console.log('[Login] userId:', userId); + .then(async () => { await this.startProxyClient(); + this.fetchWallets(); }) .catch((error) => { console.log('[Login] retry in 3s:', error); @@ -103,20 +161,12 @@ export default class HomeScreen extends Component { componentWillUnmount() { this.stopProxyClient(); stopSmsListener(); - this.onProxyMessageSub?.remove(); this.appStateSubscription?.remove(); } - sendEcho = () => { - const text = `hello_${Date.now()}`; - proxySendMessage({ type: 'echo', messageId: `echo_${Date.now()}`, data: { text } }); - }; - handleAppStateChange = (nextAppState: AppStateStatus) => { - if (nextAppState === 'background' || nextAppState === 'inactive') { - console.log('[AppState] 应用进入后台,代理服务继续运行'); - } else if (nextAppState === 'active') { - console.log('[AppState] 应用回到前台'); + if (nextAppState === 'active') { + this.fetchWallets(); } } @@ -133,28 +183,18 @@ export default class HomeScreen extends Component { try { this.clientId = DeviceInfo.getUniqueIdSync(); const userId = Api.instance.getUserId(); - console.log('[Proxy] 初始化后台服务:', this.clientId, 'userId:', userId); this.setState({ proxyStatus: 'connecting' }); await proxyBackgroundService.start({ wsUrl: Api.WS_URL, clientId: this.clientId || '', - userId: userId, + userId, debug: true, heartbeatInterval: 10000, reconnectInterval: 5000, reconnectMaxAttempts: Infinity, - onConnected: () => { - console.log('[Proxy] 后台服务已连接'); - this.setState({ proxyStatus: 'connected' }); - }, - onDisconnected: () => { - console.log('[Proxy] 后台服务已断开'); - this.setState({ proxyStatus: 'disconnected' }); - }, - onError: (error: string) => { - console.log('[Proxy] 错误:', error); - this.setState({ proxyStatus: 'error', proxyError: error }); - }, + 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); @@ -162,423 +202,228 @@ export default class HomeScreen extends Component { } stopProxyClient() { - try { - proxyBackgroundService.stop(); - console.log('[Proxy] 后台服务已停止'); - } catch (error) { - console.error('[Proxy] 停止失败:', error); - } + try { proxyBackgroundService.stop(); } catch {} } - handleUploadPaytmPersonalToken = async (result: PaytmPersonalBindResult) => { + fetchWallets = async () => { + this.setState({ loadingWallets: true }); try { - await Api.instance.register(WalletType.PAYTM_PERSONAL, result); - this.setState({ showPaytmPersonalBind: false }); - Alert.alert('绑定成功', 'Paytm Personal Token 绑定成功'); + const wallets = await Api.instance.listWallets(); + this.setState({ wallets }); + } catch (e) { + console.log('[fetchWallets]', e); + } finally { + this.setState({ loadingWallets: false }); + } + }; + + 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 } })); + } + }; + + handleSetVpa = async (walletId: string, vpaIndex: number) => { + try { + const vpa = await Api.instance.setCurrentVpa(walletId, vpaIndex); + Alert.alert('已设置', `当前 VPA: ${vpa}`); + this.fetchWallets(); + } 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); - this.setState({ showPaytmPersonalBind: false }); } - } + }; - handleUploadPhonePePersonalToken = async (result: PhonePePersonalBindResult) => { - try { - await Api.instance.register(WalletType.PHONEPE_PERSONAL, result); - this.setState({ showPhonePePersonalBind: false }); - Alert.alert('绑定成功', 'PhonePe Personal Token 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showPhonePePersonalBind: false }); - } - } - - handleUploadPaytmBusiness = async (result: PaytmBusinessBindResult) => { - try { - console.log(result); - this.setState({ showPaytmBusinessBind: false }); - Alert.alert('绑定成功', 'Paytm Business 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showPaytmBusinessBind: false }); - } - } - - handleUploadPhonePeBusiness = async (result: PhonePeBusinessBindResult) => { - try { - await Api.instance.register(WalletType.PHONEPE_BUSINESS, result); - this.setState({ showPhonePeBusinessBind: false }); - Alert.alert('绑定成功', 'PhonePe Business 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showPhonePeBusinessBind: false }); - } - } - - handleUploadGooglePayBusiness = async (result: GooglePayBusinessBindResult) => { - try { - await Api.instance.register(WalletType.GOOGLEPAY_BUSINESS, result); - this.setState({ showGooglePayBusinessBind: false }); - Alert.alert('绑定成功', 'Google Pay Business 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showGooglePayBusinessBind: false }); - } - } - - handleUploadBharatPeBusiness = async (result: BharatPeBusinessBindResult) => { - try { - console.log(JSON.stringify(result)); - this.setState({ showBharatPeBusinessBind: false }); - Alert.alert('绑定成功', 'BharatPe Business 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showBharatPeBusinessBind: false }); - } - } - - handleUploadMobikwikPersonal = async (result: MobikwikPersonalBindResult) => { - try { - console.log(JSON.stringify(result)); - this.setState({ showMobikwikPersonalBind: false }); - Alert.alert('绑定成功', 'Mobikwik Personal 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showMobikwikPersonalBind: false }); - } - } - - handleUploadFreechargePersonal = async (result: FreechargePersonalBindResult) => { - try { - console.log(JSON.stringify(result)); - this.setState({ showFreechargePersonalBind: false }); - Alert.alert('绑定成功', 'Freecharge Personal 绑定成功'); - } catch (error) { - Alert.alert('绑定失败', (error as Error).message); - this.setState({ showFreechargePersonalBind: false }); - } - } - - renderPaytmPersonalTokenBind = () => ( - this.setState({ showPaytmPersonalBind: false })}> - { - Alert.alert('绑定失败', error); - this.setState({ showPaytmPersonalBind: false }); - }} - /> - - ) - - renderPaytmPersonalOTPBind = () => ( - this.setState({ showPaytmPersonalBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile, {}); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { sessionId: params.sessionId }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={(result: PaytmPersonalBindResult) => { - Alert.alert('绑定成功', 'Paytm Personal OTP 绑定成功'); - this.setState({ showPaytmPersonalBind: false }); - }} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showPaytmPersonalBind: false }); - }} - /> - - ) - - renderPhonePePersonalTokenBind = () => ( - this.setState({ showPhonePePersonalBind: false })}> - { - Alert.alert('绑定失败', error); - this.setState({ showPhonePePersonalBind: false }); - }} - /> - - ) - - renderPhonePePersonalOTPBind = () => ( - this.setState({ showPhonePePersonalBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile, {}); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { sessionId: params.sessionId }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={(result: PhonePePersonalBindResult) => { - Alert.alert('绑定成功', 'PhonePe Personal OTP 绑定成功'); - this.setState({ showPhonePePersonalBind: false }); - }} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showPhonePePersonalBind: false }); - }} - /> - - ) - - renderPaytmBusinessBind = () => ( - this.setState({ showPaytmBusinessBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile, { password: params.password }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { sessionId: params.sessionId }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={this.handleUploadPaytmBusiness} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showPaytmBusinessBind: false }); - }} - /> - - ) - - renderPhonePeBusinessBind = () => ( - this.setState({ showPhonePeBusinessBind: false })}> - { - Alert.alert('绑定失败', error); - this.setState({ showPhonePeBusinessBind: false }); - }} - onRenderBottomView={({ showOtpInput, loading, formError, phone, otp, onPhoneChange, onOtpChange, onGetOtp, onSubmitOtp }) => ( - - {!showOtpInput ? ( - <> - - {!!formError && {formError}} - - {loading ? 'Loading...' : 'GET OTP'} - - - ) : ( - <> - - {!!formError && {formError}} - - {loading ? 'Loading...' : 'Verify OTP'} - - - )} - - )} - /> - - ) - - renderGooglePayBusinessBind = () => ( - this.setState({ showGooglePayBusinessBind: false })}> - { - Alert.alert('绑定失败', error); - this.setState({ showGooglePayBusinessBind: false }); - }} - /> - - ) - - renderBharatPeBusinessBind = () => ( - this.setState({ showBharatPeBusinessBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { sessionId: params.sessionId }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={this.handleUploadBharatPeBusiness} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showBharatPeBusinessBind: false }); - }} - /> - - ) - - renderMobikwikPersonalOTPBind = () => ( - this.setState({ showMobikwikPersonalBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile, { - deviceId: params.deviceId, - tuneUserId: params.tuneUserId - }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { - sessionId: params.sessionId, - deviceId: params.deviceId, - tuneUserId: params.tuneUserId, - nid: params.nid - }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={this.handleUploadMobikwikPersonal} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showMobikwikPersonalBind: false }); - }} - /> - - ) - - renderFreechargePersonalOTPBind = () => ( - this.setState({ showFreechargePersonalBind: false })}> - { - try { - return await Api.instance.requestOTP(walletType, params.mobile); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onVerifyOTP={async (walletType, params) => { - try { - return await Api.instance.verifyOTP(walletType, params.mobile, params.otp, { - otpId: params.otpId, - deviceId: params.deviceId, - csrfId: params.csrfId, - appFc: params.appFc - }); - } catch (error) { - return { success: false, message: (error as Error).message }; - } - }} - onSuccess={this.handleUploadFreechargePersonal} - onError={(error: string) => { - Alert.alert('绑定失败', error); - this.setState({ showFreechargePersonalBind: false }); - }} - /> - - ) - - async handlerPaytmPay () { - paytmPay('100', 'Gurvir singh', '296001000405', 'ICIC0002960', 'ABCDEF').then(result => { - console.log(result); - }).catch(error => { - console.log(error); - }); - } + // ---- modals ---- renderBindModal = () => { - if (this.state.showPaytmPersonalBind) { - return this.state.paytmPersonalBindType === 'tokenMode' - ? this.renderPaytmPersonalTokenBind() - : this.renderPaytmPersonalOTPBind(); - } - if (this.state.showPhonePePersonalBind) { - return this.state.phonePePersonalBindType === 'tokenMode' - ? this.renderPhonePePersonalTokenBind() - : this.renderPhonePePersonalOTPBind(); - } - if (this.state.showPaytmBusinessBind) return this.renderPaytmBusinessBind(); - if (this.state.showPhonePeBusinessBind) return this.renderPhonePeBusinessBind(); - if (this.state.showGooglePayBusinessBind) return this.renderGooglePayBusinessBind(); - if (this.state.showBharatPeBusinessBind) return this.renderBharatPeBusinessBind(); - if (this.state.showMobikwikPersonalBind) return this.renderMobikwikPersonalOTPBind(); - if (this.state.showFreechargePersonalBind) return this.renderFreechargePersonalOTPBind(); + 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)}> + {WALLET_ICONS[opt.walletType] + ? + : + } + {opt.label} + + ))} + + + + + ); } - openServerSettings = () => { - 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 }); - }; - - saveDomain = async () => { - const { settingsHost, settingsPort } = this.state; - const domain = settingsPort ? `${settingsHost}:${settingsPort}` : settingsHost; - const useHttps = settingsPort === '443'; - await saveServerDomain(domain, useHttps); - this.setState({ showServerSettings: false }); - Alert.alert('已保存', '重启 App 后生效'); - }; - renderServerSettingsModal() { const { showServerSettings, settingsHost, settingsPort } = this.state; const presets = [ @@ -587,42 +432,32 @@ export default class HomeScreen extends Component { ]; return ( - - - 服务器设置 + + + 服务器设置 {presets.map(p => ( - this.setState({ settingsHost: p.host, settingsPort: p.port })} - style={{ paddingHorizontal: 10, paddingVertical: 5, borderRadius: 6, backgroundColor: settingsHost === p.host && settingsPort === p.port ? '#3498db' : '#f0f0f0' }} - > + 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" - keyboardType="default" - /> - Port - this.setState({ settingsPort: t })} - placeholder="16000" - keyboardType="number-pad" - /> + 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}> 保存 @@ -632,137 +467,214 @@ 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}> + + + {WALLET_ICONS[item.walletType] + ? + : + {item.walletType.split('_')[0]} + + } + + + {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 && } + + )) + )} + + )} + + ); + }; + render() { - const { proxyStatus, proxyError } = this.state; - const cfg: Record = { + 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 } = cfg[proxyStatus]; + const { label, color } = proxyCfg[proxyStatus]; return ( - - - - - Proxy {label}{proxyStatus === 'error' && proxyError ? `:${proxyError}` : ''} - + + {/* 顶栏 */} + + + + + 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 }); + }}> + + + this.setState({ showAddWallet: true })} style={s.addBtn}> + + Add + + + + {/* 钱包列表 */} + + 已绑定钱包 + + {loadingWallets ? '刷新中…' : '刷新'} + + + + item.id} + renderItem={this.renderWalletItem} + ListEmptyComponent={ + + {loadingWallets ? '加载中…' : '暂无绑定钱包,点右上角 + Add 添加'} + + } + /> + {this.renderBindModal()} {this.renderServerSettingsModal()} - - ⚙ 服务器设置 - - - - { - this.setState({ showPaytmPersonalBind: true, paytmPersonalBindType: 'otpMode' }); - }}> - 绑定 Paytm Personal(OTP) - - { - this.setState({ showPaytmPersonalBind: true, paytmPersonalBindType: 'tokenMode' }); - }}> - 绑定 Paytm Personal(Token) - - - this.setState({ showPaytmBusinessBind: true })}> - 绑定 Paytm Business (OTP) - - - { - this.setState({ showPhonePePersonalBind: true, phonePePersonalBindType: 'otpMode' }); - }}> - 绑定 PhonePe Personal(OTP) - - { - this.setState({ showPhonePePersonalBind: true, phonePePersonalBindType: 'tokenMode' }); - }}> - 绑定 PhonePe Personal(Token) - - - this.setState({ showPhonePeBusinessBind: true })}> - 绑定 PhonePe Business (OTP-WEB) - - this.setState({ showGooglePayBusinessBind: true })}> - 绑定 GooglePay Business - - this.setState({ showBharatPeBusinessBind: true })}> - 绑定 BharatPe Business (OTP) - - this.setState({ showMobikwikPersonalBind: true })}> - 绑定 Mobikwik Personal (OTP) - - this.setState({ showFreechargePersonalBind: true })}> - 绑定 Freecharge Personal (OTP) - - - Paytm Pay 代付测试 - - - Echo 测试 - - + {this.renderAddWalletModal()} ); } } - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "#f0f0f0", +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', }, - scrollView: { - flex: 1, - // backgroundColor: "lightblue", - width: "100%", + addBtn: { + backgroundColor: '#3498db', borderRadius: 6, + paddingHorizontal: 10, paddingVertical: 4, }, - scrollViewContent: { - flexGrow: 1, - justifyContent: "center", - alignItems: "center", + addBtnText: { color: '#fff', fontSize: 13, fontWeight: '600' }, + listHeader: { + flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + paddingHorizontal: 14, paddingVertical: 10, }, - button: { - padding: 10, - backgroundColor: "lightblue", - borderRadius: 5, - width: 200, - height: 55, + 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 }, }, - text: { - fontSize: 20, - fontWeight: "bold", + walletBadge: { + width: 52, height: 52, borderRadius: 10, overflow: 'hidden', + alignItems: 'center', justifyContent: 'center', + backgroundColor: '#f5f5f5', }, - modal: { - flex: 1, - justifyContent: "center", - alignItems: "center", + 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', }, - modalContent: { - flex: 1, - justifyContent: "center", - alignItems: "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 }, + 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, }, - bindButton: { - marginTop: 10, - marginBottom: 10, - backgroundColor: "#007AFF", - borderRadius: 5, - width: "90%", - height: 45, - alignItems: "center", - justifyContent: "center", + vpaRowActive: { backgroundColor: '#3498db' }, + vpaText: { fontSize: 13, color: '#333' }, + modalOverlay: { + flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', alignItems: 'center', }, - bindButtonText: { - fontSize: 14, - // fontWeight: "bold", - color: "#fff", + addModalBox: { + backgroundColor: '#fff', borderRadius: 12, padding: 20, + width: '88%', maxHeight: '70%', }, -}); \ No newline at end of file + 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 }, +}); diff --git a/screens/TestScreen.tsx b/screens/TestScreen.tsx new file mode 100644 index 0000000..af96af3 --- /dev/null +++ b/screens/TestScreen.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useRef } from 'react'; +import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { onProxyMessage, proxySendMessage, paytmPay } from 'rnwalletman'; + +export default function TestScreen() { + const subRef = useRef | null>(null); + + useEffect(() => { + subRef.current = onProxyMessage((msg) => { + if (msg.type === 'echo') { + Alert.alert('Echo 回来了', JSON.stringify(msg.data)); + } + }); + return () => { + subRef.current?.remove(); + }; + }, []); + + const handlePaytmPay = () => { + paytmPay('100', 'Gurvir singh', '296001000405', 'ICIC0002960', 'ABCDEF') + .then(result => console.log(result)) + .catch(error => Alert.alert('代付失败', String(error))); + }; + + const handleEcho = () => { + proxySendMessage({ type: 'echo', messageId: `echo_${Date.now()}`, data: { text: `hello_${Date.now()}` } }); + }; + + return ( + + 测试工具 + + Paytm Pay 代付测试 + + + Echo 测试 + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f0f0f0', + padding: 20, + alignItems: 'center', + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + color: '#333', + alignSelf: 'flex-start', + marginBottom: 16, + marginTop: 8, + }, + btn: { + width: '100%', + paddingVertical: 14, + borderRadius: 8, + alignItems: 'center', + marginBottom: 12, + }, + btnText: { + color: '#fff', + fontSize: 15, + fontWeight: '600', + }, +}); diff --git a/servers/walletman b/servers/walletman index 96d1cf8..6701de2 160000 --- a/servers/walletman +++ b/servers/walletman @@ -1 +1 @@ -Subproject commit 96d1cf84ba0a86bce1c3e89c7351be4130d37086 +Subproject commit 6701de28bf3305b926281ab149fb36faa6de169d diff --git a/services/api.ts b/services/api.ts index 70e4496..97d8dc8 100644 --- a/services/api.ts +++ b/services/api.ts @@ -1,6 +1,14 @@ import { WalletType } from 'rnwalletman'; import AsyncStorage from '@react-native-async-storage/async-storage'; +export interface WalletItem { + id: string; + walletType: string; + upi?: string; + phone?: string; + status?: string; +} + const DEFAULT_DOMAIN = 'aa.pfgame.org'; const STORAGE_KEY = 'server_domain'; const HTTPS_KEY = 'server_https'; @@ -117,6 +125,44 @@ class Api { if (!data.success) throw new Error(data.message); return data; } + + public async listWallets(): Promise { + const res = await fetch(`${Api.BASE_URL}/wallets`, { headers: this.headers() }); + const data = await res.json(); + if (!data.success) throw new Error(data.message); + return data.data?.wallets ?? []; + } + + public async getWalletVpas(walletId: string): Promise { + const res = await fetch(`${Api.BASE_URL}/wallet/vpas?walletId=${encodeURIComponent(walletId)}`, { + headers: this.headers(), + }); + const data = await res.json(); + if (!data.success) throw new Error(data.message); + return data.data?.vpas ?? []; + } + + public async setCurrentVpa(walletId: string, vpaIndex: number): Promise { + const res = await fetch(`${Api.BASE_URL}/wallet/set-vpa`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ walletId, vpaIndex }), + }); + const data = await res.json(); + if (!data.success) throw new Error(data.message); + return data.data?.vpa ?? ''; + } + + public async generateLink(walletId: string, amount: string): Promise<{ link: string; orderId: string }> { + const res = await fetch(`${Api.BASE_URL}/generate-link`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ walletId, amount }), + }); + const data = await res.json(); + if (!data.success) throw new Error(data.message); + return { link: data.data?.link, orderId: data.data?.orderId }; + } } export default Api;