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

@@ -13,6 +13,7 @@ import { useOTPBind } from '../hooks/useOTPBind';
interface OTPBindUIProps {
walletType: WalletType;
title: string;
subtitle?: string;
otpLength?: number;
mobileLength?: number;
passwordBeforeOtp?: boolean;
@@ -32,6 +33,7 @@ interface OTPBindUIProps {
export const OTPBindUI: React.FC<OTPBindUIProps> = ({
walletType,
title,
subtitle,
otpLength = 6,
mobileLength = 10,
passwordBeforeOtp = false,
@@ -78,6 +80,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
<View style={styles.overlay}>
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
{!!subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
{!!state.errorMessage && (
<Text style={styles.errorText}>{state.errorMessage}</Text>
)}
@@ -182,6 +185,7 @@ export const OTPBindUI: React.FC<OTPBindUIProps> = ({
<View style={styles.overlay}>
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
{!!subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
{!showOtp && (
<>
@@ -287,6 +291,14 @@ const styles = StyleSheet.create({
color: '#111',
marginBottom: 18,
},
subtitle: {
fontSize: 12,
color: '#888',
textAlign: 'center',
marginTop: -10,
marginBottom: 14,
lineHeight: 17,
},
label: {
fontSize: 13,
color: '#666',

View File

@@ -1,8 +1,45 @@
import React, { Component, useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult, PhonePeBusinessBindResult, BharatPeBusinessBindResult, PaytmBusinessBindResult } from 'rnwalletman';
import { Alert, View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import {
WalletType,
BindErrorCode,
MobikwikPersonalBind,
FreechargePersonalBindResult,
MobikwikPersonalBindResult,
PaytmPersonalBindResult,
PhonePePersonalBindResult,
PhonePeBusinessBindResult,
BharatPeBusinessBindResult,
PaytmBusinessBindResult,
} from 'rnwalletman';
import { OTPBindUI } from './OTPBindUI';
export function alertMobikwikAidlBindError(code: string, message: string, onClose?: () => void) {
let msg = message || 'Bind failed';
switch (code) {
case BindErrorCode.NOT_INSTALLED:
msg = 'Patched Mobikwik app not installed. Install mobikwik_ipay_2365.apk';
break;
case BindErrorCode.NOT_LOGGED_IN:
msg = 'Please log in to the Mobikwik app first';
break;
case BindErrorCode.SERVICE_DISCONNECTED:
msg = 'Mobikwik service unavailable. Open the app and try again';
break;
case BindErrorCode.NO_DATA:
msg = 'No login data received';
break;
case BindErrorCode.BIND_ERROR:
msg = 'Bind failed. Open Mobikwik manually and try again';
break;
case BindErrorCode.NATIVE_MODULE_UNAVAILABLE:
msg = 'Native module not available';
break;
}
Alert.alert('Bind Failed', msg);
onClose?.();
}
export class FreeChargeBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
@@ -28,10 +65,38 @@ export class FreeChargeBind extends Component<{
}
}
export class MobikwikOTPBind extends Component<{
/** Mobikwik 2365 AIDL bind */
export class MobikwikPersonalTokenBind extends Component<{
userToken: string;
onSuccess: (result: MobikwikPersonalBindResult) => void;
onClose?: () => void;
isDebug?: boolean;
}> {
render() {
const { userToken, onSuccess, onClose, isDebug = false } = this.props;
return (
<MobikwikPersonalBind
processString="Binding Mobikwik..."
userToken={userToken}
isDebug={isDebug}
onSuccess={(result: MobikwikPersonalBindResult) => {
if (!result.hashId) {
Alert.alert('Bind Failed', 'Please log in to the Mobikwik app first');
return;
}
onSuccess(result);
}}
onError={(code: string, message: string) => alertMobikwikAidlBindError(code, message, onClose)}
/>
);
}
}
/** Mobikwik web OTP bind */
export class MobikwikPersonalOTPBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: MobikwikPersonalBindResult) => void;
onSuccess: (result: any) => void;
onError: (error: string) => void;
isDebug: boolean;
initialMobile?: string;
@@ -40,7 +105,8 @@ export class MobikwikOTPBind extends Component<{
return (
<OTPBindUI
walletType={WalletType.MOBIKWIK_PERSONAL}
title="Bind Mobikwik"
title="Bind Mobikwik (OTP)"
subtitle="Web OTP login. If you have the 2365 patched app, use Token Mode instead"
otpLength={6}
onRequestOTP={this.props.onRequestOTP}
onVerifyOTP={this.props.onVerifyOTP}
@@ -53,6 +119,9 @@ export class MobikwikOTPBind extends Component<{
}
}
/** @deprecated Use MobikwikPersonalOTPBind */
export const MobikwikOTPBind = MobikwikPersonalOTPBind;
export class PayTmPersonalOTPBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;

View File

@@ -0,0 +1,255 @@
import React from 'react';
import {
Image,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import * as Animatable from 'react-native-animatable';
export type WalletBindAction = {
key: string;
label: string;
};
export type WalletSelectRow = {
walletType: string;
label: string;
actions: WalletBindAction[];
};
export const WALLET_ICONS: Record<string, number> = {
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'),
};
export 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',
};
const TOKEN_MODE = 'Token Mode';
const OTP_MODE = 'Otp Mode';
export const WALLET_SELECT_ROWS: WalletSelectRow[] = [
{
walletType: 'paytm',
label: 'Paytm Personal',
actions: [
{ key: 'paytm_personal_token', label: TOKEN_MODE },
{ key: 'paytm_personal_otp', label: OTP_MODE },
],
},
{
walletType: 'paytm business',
label: 'Paytm Business',
actions: [{ key: 'paytm_business', label: OTP_MODE }],
},
{
walletType: 'phonepe',
label: 'PhonePe Personal',
actions: [
{ key: 'phonepe_personal_token', label: TOKEN_MODE },
{ key: 'phonepe_personal_otp', label: OTP_MODE },
],
},
{
walletType: 'phonepe business',
label: 'PhonePe Business',
actions: [
{ key: 'phonepe_business_web', label: TOKEN_MODE },
{ key: 'phonepe_business_otp', label: OTP_MODE },
],
},
{
walletType: 'googlepay business',
label: 'GooglePay Business',
actions: [{ key: 'googlepay_business', label: TOKEN_MODE }],
},
{
walletType: 'bharatpe business',
label: 'BharatPe Business',
actions: [{ key: 'bharatpe_business', label: OTP_MODE }],
},
{
walletType: 'mobikwik',
label: 'Mobikwik Personal',
actions: [
{ key: 'mobikwik_personal_token', label: TOKEN_MODE },
{ key: 'mobikwik_personal', label: OTP_MODE },
],
},
{
walletType: 'freecharge',
label: 'Freecharge Personal',
actions: [
{ key: 'freecharge_personal_token', label: TOKEN_MODE },
{ key: 'freecharge_personal', label: OTP_MODE },
],
},
{
walletType: 'amazonpay',
label: 'Amazon Pay',
actions: [{ key: 'amazonpay_personal', label: OTP_MODE }],
},
];
interface Props {
visible: boolean;
onClose: () => void;
onSelectBind: (bindKey: string) => void;
rows?: WalletSelectRow[];
}
export const WalletSelectModal: React.FC<Props> = ({
visible,
onClose,
onSelectBind,
rows = WALLET_SELECT_ROWS,
}) => (
<Modal visible={visible} transparent animationType="none" onRequestClose={onClose}>
<View style={styles.overlay}>
<Animatable.View
animation="zoomIn"
duration={220}
easing="ease-out-back"
useNativeDriver
style={styles.box}
>
<View style={styles.header}>
<Text style={styles.title}>Select Wallet Type</Text>
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<Text style={styles.close}></Text>
</TouchableOpacity>
</View>
<ScrollView bounces={false}>
{rows.map((row) => (
<View key={row.label} style={styles.row}>
{WALLET_ICONS[row.walletType] ? (
<Image
source={WALLET_ICONS[row.walletType]}
style={styles.icon}
resizeMode="contain"
/>
) : (
<View
style={[
styles.dot,
{ backgroundColor: WALLET_TYPE_COLORS[row.walletType] ?? '#888' },
]}
/>
)}
<Text style={styles.label} numberOfLines={1}>
{row.label}
</Text>
<View style={styles.actions}>
{row.actions.map((action) => (
<TouchableOpacity
key={action.key}
style={styles.actionBtn}
onPress={() => onSelectBind(action.key)}
activeOpacity={0.7}
>
<Text style={styles.actionBtnText}>{action.label}</Text>
</TouchableOpacity>
))}
</View>
</View>
))}
</ScrollView>
</Animatable.View>
</View>
</Modal>
);
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
box: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
width: '88%',
maxHeight: '80%',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
title: {
fontSize: 17,
fontWeight: '700',
color: '#222',
},
close: {
fontSize: 20,
color: '#999',
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
icon: {
width: 32,
height: 32,
borderRadius: 6,
marginRight: 10,
},
dot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 10,
},
label: {
flex: 1,
fontSize: 14,
color: '#333',
marginRight: 8,
},
actions: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
actionBtn: {
paddingHorizontal: 8,
paddingVertical: 6,
borderRadius: 6,
backgroundColor: '#f0f4ff',
borderWidth: 1,
borderColor: '#d0daf0',
alignItems: 'center',
},
actionBtnText: {
fontSize: 11,
fontWeight: '600',
color: '#3355aa',
},
});