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": "0.72.10",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-webview": "13.6.2",
|
"react-native-webview": "13.6.2",
|
||||||
"rnwalletman": "./rnwalletman"
|
"rnwalletman": "./libs/rnwalletman"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"@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