Otp / Token 模式 OK

This commit is contained in:
2026-05-29 21:32:14 +08:00
parent c79a088597
commit 5c84a4bb89
5 changed files with 448 additions and 240 deletions

View File

@@ -22,7 +22,6 @@ import {
WalletType,
PaytmBusinessBindResult,
PaytmPersonalBind,
MobikwikPersonalBindResult,
FreechargePersonalBindResult,
GooglePayBusinessBindResult,
BharatPeBusinessBindResult,
@@ -42,7 +41,8 @@ import {
import {
FreeChargeBind,
MobikwikOTPBind,
MobikwikPersonalTokenBind,
MobikwikPersonalOTPBind,
PayTmPersonalOTPBind,
PhonePePersonalOTPBind,
BharatPeBusinessOTPBind,
@@ -50,6 +50,7 @@ import {
PhonePeBusinessOTPBind,
AmazonPayOTPBind,
} from '../components/WalletBindComponents';
import { WalletSelectModal, WALLET_ICONS, WALLET_TYPE_COLORS } from '../components/WalletSelectModal';
import Api, {
WalletItem,
@@ -58,113 +59,6 @@ import Api, {
getServerDomain,
} from '../services/api';
// key matches server WalletType string (see types.go)
const WALLET_ICONS: Record<string, any> = {
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'),
amazonpay: require('../res/amazon.png'),
};
const WALLET_TYPE_COLORS: Record<string, string> = {
paytm: '#002970',
'paytm business': '#002970',
phonepe: '#5a2d9c',
'phonepe business': '#5a2d9c',
'googlepay business': '#4285f4',
'bharatpe business': '#e91e63',
mobikwik: '#00bcd4',
freecharge: '#ff5722',
amazonpay: '#ff9900',
};
// 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_otp',
walletType: 'phonepe business',
label: 'PhonePe Business (OTP)',
mode: 'otp',
},
{
key: 'phonepe_business_web',
walletType: 'phonepe business',
label: 'PhonePe Business (Web)',
mode: 'token',
},
{
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',
},
{
key: 'freecharge_personal_token',
walletType: 'freecharge',
label: 'Freecharge Personal (Token)',
mode: 'token',
},
{
key: 'amazonpay_personal',
walletType: 'amazonpay',
label: 'Amazon Pay (OTP)',
mode: 'otp',
},
];
function formatWalletTypeLabel(walletType: string) {
return walletType.replace(/\b\w/g, (c) => c.toUpperCase());
}
@@ -201,7 +95,7 @@ function getBindKeyForWallet(item: WalletItem): string | null {
case 'bharatpe business':
return 'bharatpe_business';
case 'mobikwik':
return 'mobikwik_personal';
return otp ? 'mobikwik_personal' : 'mobikwik_personal_token';
case 'freecharge':
return otp ? 'freecharge_personal' : 'freecharge_personal_token';
case 'amazonpay':
@@ -224,6 +118,7 @@ interface HomeScreenState {
showGooglePayBusinessBind: boolean;
showBharatPeBusinessBind: boolean;
showMobikwikPersonalBind: boolean;
mobikwikPersonalBindType: 'otpMode' | 'tokenMode';
showFreechargePersonalBind: boolean;
showAmazonPayPersonalBind: boolean;
// proxy
@@ -270,6 +165,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
showGooglePayBusinessBind: false,
showBharatPeBusinessBind: false,
showMobikwikPersonalBind: false,
mobikwikPersonalBindType: 'otpMode',
showFreechargePersonalBind: false,
freechargePersonalBindType: 'otpMode',
showAmazonPayPersonalBind: false,
@@ -427,7 +323,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
// ---- bind handlers ----
/** Token 等需客户端 register 的流程(与 OTP 同 modal key 时勿用推断 map */
/** Token bind: client calls register (do not infer modal key from wallet type alone) */
handleBindSuccess = (key: keyof HomeScreenState, walletType: WalletType, msg: string) =>
async (result: any) => {
try {
@@ -441,7 +337,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
}
};
/** OTP:服务端 verify 已注册,只提示并关弹窗 */
/** OTP bind: server registers on verify; only show success and close modal */
onOtpBindSuccess = (key: keyof HomeScreenState, msg: string) => () => {
Alert.alert('Bind Success', msg);
this.setState({ [key]: false } as any);
@@ -454,7 +350,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
const {
showPaytmPersonalBind, paytmPersonalBindType, showPhonePePersonalBind, phonePePersonalBindType,
showPaytmBusinessBind, showPhonePeBusinessBind, phonePeBusinessBindType, showGooglePayBusinessBind, showBharatPeBusinessBind,
showMobikwikPersonalBind, showFreechargePersonalBind, freechargePersonalBindType,
showMobikwikPersonalBind, mobikwikPersonalBindType, showFreechargePersonalBind, freechargePersonalBindType,
showAmazonPayPersonalBind,
bindPrefillMobile,
} = this.state;
@@ -463,7 +359,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
this.setState({ [key]: false, bindPrefillMobile: '' } as any);
if (showPaytmPersonalBind && paytmPersonalBindType === 'tokenMode') {
const remoteVersion = '1'; // 目前版本是 1
const remoteVersion = '1';
return (
<Modal
visible
@@ -475,41 +371,37 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
isDebug
onSuccess={(result: PaytmPersonalBindResult) => {
if (result.chType != 'ipay') {
// 有 AIDL 通信,能拿回数据,但是不是我们的魔改 type
// 视作未安装
Alert.alert('Bind Failed', '未安装Paytm 魔改包,需要重新安装');
Alert.alert('Bind Failed', 'Patched Paytm app not installed. Reinstall required');
return;
}
if (result.chVersion != remoteVersion) {
// 版本有更新
Alert.alert('Bind Failed', '版本有更新,需要重新安装');
Alert.alert('Bind Failed', 'App version outdated. Reinstall required');
return;
}
// 实际请求注册钱包
this.handleBindSuccess('showPaytmPersonalBind', WalletType.PAYTM_PERSONAL, 'Paytm Personal bound successfully')(result);
}}
onError={(code: string, message: string) => {
switch (code) {
case BindErrorCode.NATIVE_MODULE_UNAVAILABLE: // 没有模块
case BindErrorCode.NATIVE_MODULE_UNAVAILABLE:
Alert.alert('Bind Failed', 'Native module not available');
break;
case BindErrorCode.NOT_LOGGED_IN: // 未登录,提示用户登录
case BindErrorCode.NOT_LOGGED_IN:
Alert.alert('Bind Failed', 'Please login in paytm app first');
break;
case BindErrorCode.ERROR: // catch 错误,提示错误即可
case BindErrorCode.ERROR:
Alert.alert('Bind Failed', message);
break;
case BindErrorCode.NO_DATA: // 拿到了错误的信息,提示错误就可以 aidl 给了错误的 resp几率低提示错误就可以
case BindErrorCode.NO_DATA:
Alert.alert('Bind Failed', 'No data received from Paytm');
break;
case BindErrorCode.SERVICE_DISCONNECTED: // 服务不可用 可能aidl 通信失败,几率低)提示错误就可以
case BindErrorCode.SERVICE_DISCONNECTED:
Alert.alert('Bind Failed', 'Paytm service disconnected');
break;
case BindErrorCode.NOT_INSTALLED: // 未安装,提示用户安装
Alert.alert('Bind Failed', '未安装Paytm 魔改包,需要重新安装');
case BindErrorCode.NOT_INSTALLED:
Alert.alert('Bind Failed', 'Patched Paytm app not installed. Reinstall required');
break;
case BindErrorCode.BIND_ERROR: // 绑定错误aidl 通信失败,可能存在 paytm 未打开的情况,出现这个,手动拉起一下
case BindErrorCode.BIND_ERROR:
Alert.alert('Bind Failed', 'Paytm bind error');
break;
default:
@@ -545,7 +437,7 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
);
}
if (showPhonePePersonalBind && phonePePersonalBindType === 'tokenMode') {
const remoteVersion = '1'; // 目前版本是 1
const remoteVersion = '1';
return (
<Modal
visible
@@ -558,45 +450,36 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
isDebug
onSuccess={(result: PhonePePersonalBindResult) => {
if (result.chType != 'ipay') {
// 有 AIDL 通信,能拿回数据,但是不是我们的魔改 type
// 视作未安装
Alert.alert('Bind Failed', '未安装Phonepe 魔改包,需要重新安装');
Alert.alert('Bind Failed', 'Patched PhonePe app not installed. Reinstall required');
return;
}
if (result.chVersion != remoteVersion) {
// 版本有更新
Alert.alert('Bind Failed', '版本有更新,需要重新安装');
Alert.alert('Bind Failed', 'App version outdated. Reinstall required');
return;
}
// 实际请求注册钱包
this.handleBindSuccess('showPhonePePersonalBind', WalletType.PHONEPE_PERSONAL, 'PhonePe Personal bound successfully')(result);
}}
onError={(code: string, message: string) => {
switch (code) {
case BindErrorCode.NOT_INSTALLED:
// 未安装
Alert.alert('Bind Failed', '未安装Phonepe 魔改包,需要重新安装');
Alert.alert('Bind Failed', 'Patched PhonePe app not installed. Reinstall required');
break;
case BindErrorCode.SERVICE_DISCONNECTED:
// 服务不可用 可能aidl 通信失败,几率低)提示错误就可以
Alert.alert('Bind Failed', '服务不可用');
Alert.alert('Bind Failed', 'Service unavailable');
break;
case BindErrorCode.NO_DATA:
// 拿到了错误的信息,提示错误就可以 aidl 给了错误的 resp几率低提示错误就可以
Alert.alert('Bind Failed', '未知错误信息');
Alert.alert('Bind Failed', 'Invalid response from PhonePe');
break;
case BindErrorCode.BIND_ERROR:
// 绑定错误aidl 通信失败,可能存在 paytm / phonepe 未打开的情况,出现这个,手动拉起一下
Alert.alert('Bind Failed', '绑定失败,请手动打开PhonePe后重试');
Alert.alert('Bind Failed', 'Bind failed. Open PhonePe manually and try again');
break;
case BindErrorCode.ERROR:
// catch 错误,通用错误,提示错误即可
Alert.alert('Bind Failed', message);
break;
default:
Alert.alert('Bind Failed', '未知错误');
Alert.alert('Bind Failed', 'Unknown error');
break;
}
@@ -727,6 +610,26 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
</Modal>
);
}
if (showMobikwikPersonalBind && mobikwikPersonalBindType === 'tokenMode') {
return (
<Modal
visible
transparent
onRequestClose={close('showMobikwikPersonalBind')}
>
<MobikwikPersonalTokenBind
userToken={Api.instance.getUserToken()}
isDebug
onClose={close('showMobikwikPersonalBind')}
onSuccess={this.handleBindSuccess(
'showMobikwikPersonalBind',
WalletType.MOBIKWIK_PERSONAL,
'Mobikwik bound successfully',
)}
/>
</Modal>
);
}
if (showMobikwikPersonalBind) {
return (
<Modal
@@ -734,15 +637,15 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
transparent
onRequestClose={close('showMobikwikPersonalBind')}
>
<MobikwikOTPBind
isDebug={true}
<MobikwikPersonalOTPBind
isDebug
initialMobile={bindPrefillMobile}
onRequestOTP={async (wt, p) => {
return this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))
}}
onVerifyOTP={async (wt, p) => {
return this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, p));
}}
onRequestOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.requestOTP(wt, p.mobile))
}
onVerifyOTP={async (wt, p) =>
this.wrapOtpCall(() => Api.instance.verifyOTP(wt, p.mobile, p.otp, p))
}
onSuccess={this.onOtpBindSuccess('showMobikwikPersonalBind', 'Mobikwik bound successfully')}
onError={() => {}}
/>
@@ -850,7 +753,10 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
this.setState({ showBharatPeBusinessBind: true });
break;
case 'mobikwik_personal':
this.setState({ showMobikwikPersonalBind: true });
this.setState({ showMobikwikPersonalBind: true, mobikwikPersonalBindType: 'otpMode' });
break;
case 'mobikwik_personal_token':
this.setState({ showMobikwikPersonalBind: true, mobikwikPersonalBindType: 'tokenMode' });
break;
case 'freecharge_personal':
this.setState({ showFreechargePersonalBind: true, freechargePersonalBindType: 'otpMode' });
@@ -874,51 +780,6 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
}));
};
renderAddWalletModal() {
return (
<Modal
visible={this.state.showAddWallet}
transparent
animationType="none"
onRequestClose={() => this.setState({ showAddWallet: false })}
>
<View style={s.modalOverlay}>
<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}>Select Wallet Type</Text>
<TouchableOpacity onPress={() => this.setState({ showAddWallet: false })}>
<Text style={{ fontSize: 20, color: '#999' }}></Text>
</TouchableOpacity>
</View>
<ScrollView bounces={false}>
{WALLET_TYPE_OPTIONS.map((opt) => (
<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' }]} />
)}
<Text style={s.walletTypeLabel}>{opt.label}</Text>
</TouchableOpacity>
))}
</ScrollView>
</Animatable.View>
</View>
</Modal>
);
}
renderServerSettingsModal() {
const { showServerSettings, settingsHost, settingsPort } = this.state;
const presets = [
@@ -1214,7 +1075,11 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
{this.renderBindModal()}
{this.renderServerSettingsModal()}
{this.renderAddWalletModal()}
<WalletSelectModal
visible={this.state.showAddWallet}
onClose={() => this.setState({ showAddWallet: false })}
onSelectBind={this.openWalletBind}
/>
{this.renderVpaModal()}
</View>
);
@@ -1420,29 +1285,6 @@ const s = StyleSheet.create({
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',
},
walletTypeLabel: {
fontSize: 14,
color: '#333',
},
boundWalletGroup: {
marginBottom: 4,
},
@@ -1476,18 +1318,6 @@ const s = StyleSheet.create({
width: 16,
textAlign: 'right',
},
walletTypeIcon: {
width: 32,
height: 32,
borderRadius: 6,
marginRight: 12,
},
walletTypeDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 12,
},
settingsBox: {
backgroundColor: '#fff',
borderRadius: 10,