Compare commits

...

10 Commits

Author SHA1 Message Date
c42d89d585 fix bugs 2026-02-10 19:24:00 +08:00
cc6a4443ee fix 2026-02-06 18:14:43 +08:00
9477c1fed0 内部直接封装 bhartpe business 的接码和转类型 2026-02-06 18:13:07 +08:00
a113805b51 fix 2026-02-06 17:53:40 +08:00
2f4241951d 代理优化 2026-02-06 17:48:43 +08:00
57928cd97d 1 2026-02-06 12:46:17 +08:00
0db8a9254a 增加 qr 解析 2026-02-05 19:39:30 +08:00
d0f60b5ece phonepe personal otp / token 模式 ok 2026-02-05 03:01:55 +08:00
bb215fd492 fix ui 2026-02-05 02:29:50 +08:00
01e597ac93 重构 demo 2026-02-05 00:09:57 +08:00
13 changed files with 1075 additions and 652 deletions

883
App.tsx

File diff suppressed because it is too large Load Diff

1
android/.idea/vcs.xml generated
View File

@@ -7,6 +7,7 @@
<mapping directory="$PROJECT_DIR$/../libs/rnwalletman" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../node_modules/rnauto" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../node_modules/rnwalletman" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../servers/usdtman" vcs="Git" />
<mapping directory="$PROJECT_DIR$/../servers/walletman" vcs="Git" />
</component>
</project>

View File

@@ -1,6 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".MainApplication"

206
components/OTPBindUI.tsx Normal file
View File

@@ -0,0 +1,206 @@
import React from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { WalletType } from 'rnwalletman';
import { useOTPBind } from '../hooks/useOTPBind';
interface OTPBindUIProps {
walletType: WalletType;
title: string;
otpLength?: number;
mobileLength?: number;
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: any) => void;
onError: (error: string) => void;
isDebug: boolean;
additionalParams?: any;
}
export const OTPBindUI: React.FC<OTPBindUIProps> = ({
walletType,
title,
otpLength = 6,
mobileLength = 10,
onRequestOTP,
onVerifyOTP,
onSuccess,
onError,
isDebug,
additionalParams = {},
}) => {
const [state, actions] = useOTPBind(
walletType,
{
onRequestOTP,
onVerifyOTP,
onSuccess,
onError,
isDebug,
},
{
otpLength,
mobileLength,
additionalParams,
}
);
if (state.step === 'processing') {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
<Text style={styles.processingText}>...</Text>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.form}>
<Text style={styles.title}>{title}</Text>
{state.step === 'mobile' && (
<>
{state.errorMessage ? (
<Text style={styles.errorText}>{state.errorMessage}</Text>
) : null}
<TextInput
style={[styles.input, state.errorMessage ? styles.inputError : {}]}
placeholder="请输入手机号"
placeholderTextColor="#999"
keyboardType="phone-pad"
maxLength={mobileLength}
value={state.mobile}
onChangeText={(text) => {
actions.setMobile(text);
if (state.errorMessage) actions.clearError();
}}
editable={!state.loading}
/>
<TouchableOpacity
style={[styles.button, state.loading && styles.buttonDisabled]}
onPress={actions.requestOTP}
disabled={state.loading}
>
{state.loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}></Text>
)}
</TouchableOpacity>
</>
)}
{state.step === 'otp' && (
<>
<Text style={styles.hint}> {state.mobile}</Text>
{state.errorMessage ? (
<Text style={styles.errorText}>{state.errorMessage}</Text>
) : null}
<TextInput
style={[styles.input, state.errorMessage ? styles.inputError : {}]}
placeholder={`请输入 ${otpLength} 位验证码`}
placeholderTextColor="#999"
keyboardType="number-pad"
maxLength={otpLength}
value={state.otp}
onChangeText={(text) => {
actions.setOtp(text);
if (state.errorMessage) actions.clearError();
}}
editable={!state.loading}
/>
<TouchableOpacity
style={[styles.button, state.loading && styles.buttonDisabled]}
onPress={actions.verifyOTP}
disabled={state.loading}
>
{state.loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}></Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={actions.resetToMobile}
disabled={state.loading}
>
<Text style={styles.linkText}></Text>
</TouchableOpacity>
</>
)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.8)',
justifyContent: 'center',
alignItems: 'center',
},
form: {
width: '80%',
backgroundColor: '#fff',
borderRadius: 10,
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 5,
padding: 12,
fontSize: 16,
marginBottom: 15,
},
inputError: {
borderColor: '#ff3b30',
},
errorText: {
color: '#ff3b30',
fontSize: 14,
marginBottom: 10,
textAlign: 'center',
},
button: {
backgroundColor: '#007AFF',
borderRadius: 5,
padding: 15,
alignItems: 'center',
},
buttonDisabled: {
backgroundColor: '#ccc',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
linkButton: {
marginTop: 10,
alignItems: 'center',
},
linkText: {
color: '#007AFF',
fontSize: 14,
},
hint: {
fontSize: 14,
color: '#666',
marginBottom: 10,
textAlign: 'center',
},
processingText: {
color: '#fff',
fontSize: 16,
marginTop: 10,
},
});

View File

@@ -0,0 +1,101 @@
import React, { Component } from 'react';
import { WalletType, FreechargePersonalBindResult, MobikwikPersonalBindResult, PaytmPersonalBindResult, PhonePePersonalBindResult } from 'rnwalletman';
import { OTPBindUI } from './OTPBindUI';
export class FreeChargeBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: FreechargePersonalBindResult) => void;
onError: (error: string) => void;
isDebug: boolean;
}> {
render() {
return (
<OTPBindUI
walletType={WalletType.FREECHARGE_PERSONAL}
title="Freecharge 绑定"
otpLength={4}
onRequestOTP={this.props.onRequestOTP}
onVerifyOTP={this.props.onVerifyOTP}
onSuccess={this.props.onSuccess}
onError={this.props.onError}
isDebug={this.props.isDebug}
/>
);
}
}
export class MobikwikOTPBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: MobikwikPersonalBindResult) => void;
onError: (error: string) => void;
isDebug: boolean;
deviceId: string;
tuneUserId: string;
}> {
render() {
return (
<OTPBindUI
walletType={WalletType.MOBIKWIK_PERSONAL}
title="Mobikwik 绑定"
otpLength={6}
onRequestOTP={this.props.onRequestOTP}
onVerifyOTP={this.props.onVerifyOTP}
onSuccess={this.props.onSuccess}
onError={this.props.onError}
isDebug={this.props.isDebug}
additionalParams={{
deviceId: this.props.deviceId,
tuneUserId: this.props.tuneUserId,
}}
/>
);
}
}
export class PayTmPersonalOTPBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: PaytmPersonalBindResult) => void;
onError: (error: string) => void;
isDebug: boolean;
}> {
render() {
return (
<OTPBindUI
walletType={WalletType.PAYTM_PERSONAL}
title="Paytm 绑定"
otpLength={6}
onRequestOTP={this.props.onRequestOTP}
onVerifyOTP={this.props.onVerifyOTP}
onSuccess={this.props.onSuccess}
onError={this.props.onError}
isDebug={this.props.isDebug}
/>
);
}
}
export class PhonePePersonalOTPBind extends Component<{
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: PhonePePersonalBindResult) => void;
onError: (error: string) => void;
isDebug: boolean;
}> {
render() {
return (
<OTPBindUI
walletType={WalletType.PHONEPE_PERSONAL}
title="PhonePe 绑定"
otpLength={5}
onRequestOTP={this.props.onRequestOTP}
onVerifyOTP={this.props.onVerifyOTP}
onSuccess={this.props.onSuccess}
onError={this.props.onError}
isDebug={this.props.isDebug}
/>
);
}
}

151
hooks/useOTPBind.ts Normal file
View File

@@ -0,0 +1,151 @@
import { useState } from 'react';
import { WalletType } from 'rnwalletman';
export interface OTPBindCallbacks {
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
onSuccess: (result: any) => void;
onError: (error: string) => void;
isDebug?: boolean;
}
export interface OTPBindState {
mobile: string;
otp: string;
step: 'mobile' | 'otp' | 'processing';
loading: boolean;
otpData: any;
errorMessage: string;
}
export interface OTPBindActions {
setMobile: (mobile: string) => void;
setOtp: (otp: string) => void;
requestOTP: () => Promise<void>;
verifyOTP: () => Promise<void>;
resetToMobile: () => void;
clearError: () => void;
}
export function useOTPBind(
walletType: WalletType,
callbacks: OTPBindCallbacks,
config?: {
otpLength?: number;
mobileLength?: number;
additionalParams?: any;
}
): [OTPBindState, OTPBindActions] {
const [mobile, setMobile] = useState('');
const [otp, setOtp] = useState('');
const [step, setStep] = useState<'mobile' | 'otp' | 'processing'>('mobile');
const [loading, setLoading] = useState(false);
const [otpData, setOtpData] = useState<any>(null);
const [errorMessage, setErrorMessage] = useState('');
const { onRequestOTP, onVerifyOTP, onSuccess, onError, isDebug = false } = callbacks;
const { otpLength = 6, mobileLength = 10, additionalParams = {} } = config || {};
const clearError = () => setErrorMessage('');
const log = (...args: any[]) => {
if (isDebug) console.log(`[${walletType}]`, ...args);
};
const error = (...args: any[]) => {
if (isDebug) console.error(`[${walletType}]`, ...args);
};
const requestOTP = async () => {
if (!mobile || mobile.length !== mobileLength) {
const msg = 'Invalid mobile number';
setErrorMessage(msg);
onError(msg);
return;
}
setLoading(true);
setErrorMessage('');
log('Requesting OTP for:', mobile);
try {
const response = await onRequestOTP(walletType, {
mobile,
...additionalParams,
});
log('OTP response:', response);
if (response.success) {
setOtpData(response.data);
setStep('otp');
setErrorMessage('');
} else {
error('OTP request failed:', response.message);
const msg = response.message || 'Failed to request OTP';
setErrorMessage(msg);
onError(msg);
}
} catch (e) {
error('Request OTP error:', e);
const msg = e instanceof Error ? e.message : 'Failed to request OTP';
setErrorMessage(msg);
onError(msg);
} finally {
setLoading(false);
}
};
const verifyOTP = async () => {
if (!otp || otp.length !== otpLength) {
const msg = `请输入 ${otpLength} 位验证码`;
setErrorMessage(msg);
onError(msg);
return;
}
setLoading(true);
setErrorMessage('');
setStep('processing');
log('Verifying OTP:', otp);
try {
const response = await onVerifyOTP(walletType, {
mobile,
otp,
...additionalParams,
...(otpData || {}),
});
log('Verify response:', response);
if (response.success) {
setErrorMessage('');
onSuccess(response.data);
} else {
error('Verify failed:', response.message);
const msg = response.message || 'Failed to verify OTP';
setStep('otp');
setErrorMessage(msg);
onError(msg);
}
} catch (e) {
error('Verify OTP error:', e);
const msg = e instanceof Error ? e.message : 'Failed to verify OTP';
setStep('otp');
setErrorMessage(msg);
onError(msg);
} finally {
setLoading(false);
}
};
const resetToMobile = () => {
setStep('mobile');
setOtp('');
setErrorMessage('');
};
return [
{ mobile, otp, step, loading, otpData, errorMessage },
{ setMobile, setOtp, requestOTP, verifyOTP, resetToMobile, clearError },
];
}

View File

@@ -7,7 +7,8 @@
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
"test": "jest",
"postinstall": "patch-package"
},
"dependencies": {
"@react-native-cookies/cookies": "^6.2.1",
@@ -15,6 +16,7 @@
"buffer": "^6.0.3",
"react": "18.2.0",
"react-native": "0.72.10",
"react-native-background-actions": "^4.0.1",
"react-native-device-info": "14.0.4",
"react-native-fs": "^2.20.0",
"react-native-tcp-socket": "^6.4.1",
@@ -35,6 +37,7 @@
"eslint": "^8.19.0",
"jest": "^29.2.1",
"metro-react-native-babel-preset": "0.76.8",
"patch-package": "^8.0.1",
"prettier": "^2.4.1",
"react-test-renderer": "18.2.0",
"typescript": "4.8.4"

View File

@@ -0,0 +1,15 @@
diff --git a/node_modules/react-native-background-actions/android/src/main/java/com/asterinet/react/bgactions/RNBackgroundActionsTask.java b/node_modules/react-native-background-actions/android/src/main/java/com/asterinet/react/bgactions/RNBackgroundActionsTask.java
index 9900fc0..d810b1c 100644
--- a/node_modules/react-native-background-actions/android/src/main/java/com/asterinet/react/bgactions/RNBackgroundActionsTask.java
+++ b/node_modules/react-native-background-actions/android/src/main/java/com/asterinet/react/bgactions/RNBackgroundActionsTask.java
@@ -41,8 +41,8 @@ final public class RNBackgroundActionsTask extends HeadlessJsTaskService {
notificationIntent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
}
final PendingIntent contentIntent;
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- contentIntent = PendingIntent.getActivity(context,0, notificationIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT);
+ if (Build.VERSION.SDK_INT >= 34) { // Android 14 (UPSIDE_DOWN_CAKE)
+ contentIntent = PendingIntent.getActivity(context,0, notificationIntent, PendingIntent.FLAG_MUTABLE | 0x01000000); // FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_MUTABLE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

84
services/api.ts Normal file
View File

@@ -0,0 +1,84 @@
import { WalletType } from 'rnwalletman';
const DOMAIN = '192.168.0.101:16000';
const BASE_URL = `http://${DOMAIN}`;
const WS_URL = `ws://${DOMAIN}/ws`;
class Api {
public static readonly BASE_URL = BASE_URL;
public static readonly WS_URL = WS_URL;
private static _instance: Api | null = null;
private userId: number = 0;
private constructor() {}
public setUserId(userId: number) {
this.userId = userId;
}
public getUserId(): number {
return this.userId;
}
public static get instance() {
if (Api._instance === null) {
Api._instance = new Api();
}
return Api._instance;
}
private headers(): Record<string, string> {
const h: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.userId > 0) {
h['X-User-ID'] = String(this.userId);
}
return h;
}
public async login(username: string, password: string): Promise<number> {
const res = await fetch(`${Api.BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
this.userId = data.data.userId;
return this.userId;
}
public async register(walletType: WalletType, params: any) {
const res = await fetch(`${Api.BASE_URL}/register`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ walletType, params }),
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
return data;
}
public async requestOTP(walletType: WalletType, mobile: string, params: any = {}) {
const res = await fetch(`${Api.BASE_URL}/request-otp`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ walletType, mobile, ...params }),
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
return data;
}
public async verifyOTP(walletType: WalletType, mobile: string, otp: string, params: any = {}) {
const res = await fetch(`${Api.BASE_URL}/verify-otp`, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ walletType, mobile, otp, params }),
});
const data = await res.json();
if (!data.success) throw new Error(data.message);
return data;
}
}
export default Api;

45
styles.ts Normal file
View File

@@ -0,0 +1,45 @@
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
button: {
padding: 10,
backgroundColor: "lightblue",
borderRadius: 5,
width: 200,
height: 55,
},
text: {
fontSize: 20,
fontWeight: "bold",
},
modal: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
modalContent: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
bindButton: {
marginTop: 10,
marginBottom: 10,
backgroundColor: "#007AFF",
borderRadius: 5,
width: "90%",
height: 45,
alignItems: "center",
justifyContent: "center",
},
bindButtonText: {
fontSize: 14,
// fontWeight: "bold",
color: "#fff",
},
});

25
types.ts Normal file
View File

@@ -0,0 +1,25 @@
export interface AppProps {}
export interface WalletmanAppState {
/* Paytm Personal */
showPaytmPersonalBind: boolean;
paytmPersonalBindType: 'otpMode' | 'tokenMode';
showPaytmBusinessBind: boolean;
/* PhonePe Personal */
showPhonePePersonalBind: boolean;
phonePePersonalBindType: 'otpMode' | 'tokenMode';
showPhonePeBusinessBind: boolean;
/* GooglePay Business */
showGooglePayBusinessBind: boolean;
/* BharatPe Business */
showBharatPeBusinessBind: boolean;
/* Mobikwik Personal */
showMobikwikPersonalBind: boolean;
/* Freecharge Personal */
showFreechargePersonalBind: boolean;
}