fix bugs
This commit is contained in:
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal 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
8
.vscode/settings.json
vendored
Normal 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
26
.vscode/tasks.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 代码
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
// 不需要处理
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
219
rnwalletman/index.d.ts
vendored
@@ -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>;
|
||||
@@ -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 提供
|
||||
@@ -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';
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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...',
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user