diff --git a/android/app/src/main/java/com/rnpay/IncomingCallActivity.java b/android/app/src/main/java/com/rnpay/IncomingCallActivity.java index 25eda7f..3621959 100644 --- a/android/app/src/main/java/com/rnpay/IncomingCallActivity.java +++ b/android/app/src/main/java/com/rnpay/IncomingCallActivity.java @@ -126,13 +126,15 @@ public class IncomingCallActivity extends Activity { rowLp.rightMargin = dp(13); btnRow.setLayoutParams(rowLp); - // 红色:挂断 + // 红色:挂断(也进 app,让 JS 处理 decline 逻辑) final String finalCaller = caller; ImageView btnDecline = makeCircleBtn(0xFFe53935, android.R.drawable.ic_menu_close_clear_cancel); btnDecline.setOnClickListener(v -> { resultDelivered = true; + IncomingCallNotificationHelper.cancel(this); IncomingCallNotificationHelper.deliverCallResult(this, "decline"); + launchMainApp(finalCaller); finish(); }); btnRow.addView(btnDecline); @@ -160,8 +162,10 @@ public class IncomingCallActivity extends Activity { @Override public void onFinish() { if (!resultDelivered) { resultDelivered = true; + IncomingCallNotificationHelper.cancel(IncomingCallActivity.this); IncomingCallNotificationHelper.deliverCallResult( IncomingCallActivity.this, "decline"); + launchMainApp(finalCaller); } finish(); } diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..5431ef3 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,395 @@ +# rnwalletman 接入文档 + +## 1. Android 宿主接入 + +### 1.1 新建 Service 子类 + +宿主 App 必须继承 `BaseProxyService` 并实现 `registerWallet`: + +```java +package com.yourapp; + +import android.content.Context; +import android.content.SharedPreferences; +import com.rnwalletman.BaseProxyService; +import org.json.JSONObject; +import okhttp3.*; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class AppProxyService extends BaseProxyService { + + private static final OkHttpClient HTTP = new OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + + @Override + protected void registerWallet(Context ctx, String walletId, String walletType, + String phone, JSONObject params) throws Exception { + SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String baseUrl = prefs.getString("rebind_baseUrl", null); // JS 侧通过 syncRebindConfig 写入 + int userId = prefs.getInt("rebind_userId", 0); + + JSONObject body = new JSONObject(); + body.put("walletType", walletType); + body.put("params", params); + + Request req = new Request.Builder() + .url(baseUrl + "/register") + .header("X-User-ID", String.valueOf(userId)) + .header("Content-Type", "application/json") + .post(RequestBody.create(body.toString(), MediaType.parse("application/json"))) + .build(); + + try (Response resp = HTTP.newCall(req).execute()) { + String body2 = resp.body() != null ? resp.body().string() : ""; + JSONObject json = new JSONObject(body2); + if (!json.optBoolean("success", false)) { + throw new IOException(json.optString("message", "register failed")); + } + } + } +} +``` + +### 1.2 AndroidManifest.xml + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 1.3 Application.onCreate 注册 + +```java +@Override +public void onCreate() { + super.onCreate(); + // 指定宿主 Service 类 + BaseProxyService.setServiceClass(AppProxyService.class); + // 指定来电全屏 Activity(有来电功能时) + BaseProxyService.setCallActivityClass(IncomingCallActivity.class); +} +``` + +--- + +## 2. FCM 消息格式 + +服务端发 **data-only** FCM(无 `notification` 块),data 字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `cmd` | string | 命令类型(见下表) | +| `msg` | string | 可选,通知文案(非空则弹系统通知) | +| `params` | string (JSON) | 命令参数 | + +### 命令列表 + +#### `wake_proxy` +唤醒后台 WebSocket 服务。`params` 为空 `{}`。 + +--- + +#### `rebind_wallet` +触发钱包 token 重绑(AIDL 拉取 → `registerWallet` 回调)。 + +`params` 字段: + +| 字段 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `walletId` | string | ✅ | — | 钱包 ID | +| `walletType` | string | ✅ | — | `phonepe` / `paytm` / `mobikwik` 等 | +| `phone` | string | — | `""` | 绑定手机号 | +| `retry` | bool | — | `false` | 是否允许重试 | +| `tryGtkAnyway` | bool | — | `false` | PhonePe 强制拉 GTK | + +示例: +```json +{ + "cmd": "rebind_wallet", + "params": "{\"walletId\":\"abc123\",\"walletType\":\"phonepe\",\"phone\":\"9876543210\",\"retry\":true}" +} +``` + +--- + +#### `incoming_call` +触发来电通知 / 全屏来电界面。 + +`params` 字段: + +| 字段 | 类型 | 必填 | 默认 | 说明 | +|------|------|------|------|------| +| `caller` | string | — | `"Unknown"` | 来电显示名称 | +| `ringCount` | int | — | `1` | 呼叫次数(≥1) | +| `ringDuration` | int | — | `30` | 每次响铃时长(秒,≥5) | +| `mode` | string | — | `"fullscreen"` | `"fullscreen"` 或 `"notification"` | +| `extra` | object | — | — | 透传给 JS 的任意字段 | + +示例: +```json +{ + "cmd": "incoming_call", + "params": "{\"caller\":\"客服\",\"ringCount\":2,\"ringDuration\":30,\"mode\":\"fullscreen\",\"extra\":{\"orderId\":\"ORD001\"}}" +} +``` + +--- + +## 3. JS/TS 层 API + +### 3.1 启动服务 + +```typescript +import { proxyBackgroundService } from 'rnwalletman'; + +await proxyBackgroundService.start({ + wsUrl: 'wss://your-server/ws', + clientId: 'device-unique-id', + userId: 12345, + heartbeatInterval: 20000, // ms,默认 20000 + reconnectInterval: 5000, // ms + onConnected: () => {}, + onDisconnected: () => {}, + onError: (err) => {}, + onCustomCommand: (msg) => {}, + // 可选:上报 FCM token 到服务端 + registerFcmToken: async (clientId, fcmToken) => { + await api.post('/fcm/register', { clientId, fcmToken }); + }, +}); +``` + +### 3.2 配置重绑(native 侧 AIDL 用) + +```typescript +await proxyBackgroundService.syncRebindConfig({ + baseUrl: 'https://your-server', + userId: 12345, + userToken: 'jwt-token', + onRebound: () => { /* 刷新 UI */ }, +}); +``` + +### 3.3 来电结果回调 + +接听 / 拒接 / 超时 均通过此回调返回,**与通知点击回调相互独立**: + +```typescript +proxyBackgroundService.setCallResultHandler((payload) => { + const { action, caller, mode, ringDuration, ringCount, extra } = payload; + // action: 'accept' | 'decline' + if (action === 'accept') { + // 进入游戏 / 业务逻辑 + } +}); +``` + +payload 类型: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `action` | `'accept' \| 'decline'` | 操作结果 | +| `caller` | string | 来电显示名 | +| `mode` | string | `fullscreen` / `notification` | +| `ringDuration` | number | 响铃时长(秒) | +| `ringCount` | number | 呼叫次数 | +| `extra` | object? | 服务端透传的额外参数 | + +### 3.4 FCM 通知点击回调 + +```typescript +proxyBackgroundService.setNotificationTapHandler((payload) => { + const { cmd, params } = payload; + // cmd: 'rebind_wallet' 等 +}); +``` + +### 3.5 其他方法 + +```typescript +await proxyBackgroundService.stop(); + +// 引导用户忽略电池优化(Android) +ProxyBackgroundService.openBatteryOptimizationSettings(); +``` + +--- + +## 4. JS 事件(DeviceEventEmitter) + +| 事件名 | payload | 说明 | +|--------|---------|------| +| `ProxyServiceConnected` | — | WS 已连接 | +| `ProxyServiceDisconnected` | — | WS 断开 | +| `ProxyServiceError` | `{ data: string }` | 错误信息 | +| `ProxyServiceCustomCommand` | `{ data: string (JSON) }` | 服务端自定义消息 | +| `ProxyServiceTick` | — | 每 ~60s 一次心跳 tick | +| `ProxyServiceRebound` | `{ data: walletId }` | 重绑成功 | +| `ProxyServiceNotificationTap` | `{ data: JSON }` | FCM 通知被点击 | +| `ProxyServiceFcmToken` | `{ data: token }` | FCM token 更新 | +| `ProxyServiceCallResult` | `{ data: JSON }` | 来电结果(accept/decline) | + +--- + +## 5. 服务端发来电接口(phecda_hall gateway) + +``` +POST http://gateway:12881/fcm/send-call +Content-Type: application/json + +{ + "clientId": "device-client-id", // 或 "userId": 12345 + "caller": "客服", + "ringCount": 1, + "ringDuration": 30, + "mode": "fullscreen", // "fullscreen" | "notification" + "extra": { "orderId": "ORD001" } // 可选,透传到 JS +} +``` + +FCM token 注册: +``` +POST http://gateway:12881/fcm/register +Content-Type: application/json + +{ + "clientId": "device-client-id", + "fcmToken": "firebase-device-token" +} +``` + +> **Firebase 服务账号**:放到 `config/fcm-service-account.json`,或设环境变量 `FCM_SERVICE_ACCOUNT=/path/to/file.json` + +--- + +## 6. walletman 服务端 HTTP 接口(rnpay,端口 16000) + +### FCM 管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/fcm/register` | 注册设备 FCM token | +| POST | `/fcm/send-wake` | 发送唤醒指令 | +| POST | `/fcm/send-rebind` | 发送钱包重绑指令 | +| POST | `/fcm/send-call` | 发送来电推送 | +| GET | `/fcm/clients` | 列出所有已注册设备 | + +#### `POST /fcm/register` +```json +{ "clientId": "device-id", "fcmToken": "firebase-token" } +``` + +#### `POST /fcm/send-wake` +```json +{ "clientId": "device-id" } +``` + +#### `POST /fcm/send-rebind` +```json +{ + "clientId": "device-id", // 可选,不传则按 walletId 查找绑定设备 + "walletId": "abc123", // 必填 + "walletType": "phonepe", // 可选,不传则自动查找 + "phone": "9876543210", // 可选 + "retry": false, // 是否重试(phonepe 强制 true) + "notify": false // true=弹系统通知,false=静默重绑 +} +``` + +#### `POST /fcm/send-call` +```json +{ + "clientId": "device-id", // 必填 + "caller": "客服", // 默认 "Customer Service" + "ringCount": 1, // 默认 1 + "ringDuration": 30, // 秒,默认 30 + "mode": "fullscreen", // "fullscreen" | "notification" + "extra": { "key": "val" } // 可选,透传到 JS +} +``` + +--- + +## 7. WebSocket 协议(`wss://server/ws`) + +### 客户端 → 服务端 + +所有消息格式: +```json +{ "type": "<类型>", "messageId": "<唯一ID>", "clientId": "<设备ID>", "data": {} } +``` + +| type | data 字段 | 说明 | +|------|-----------|------| +| `register` | `{ userId, fcmToken }` | 连接后首条消息,注册设备身份 | +| `ping` | — | 心跳,服务端回 `pong` | +| `echo` | 任意 | 测试回显 | +| `proxyReady` | — | 通知服务端 TCP 代理隧道就绪 | +| `proxyData` | `{ data: base64 }` | 转发代理数据(按 `messageId` 路由) | +| `proxyClose` | — | 通知代理数据发送完毕 | + +### 服务端 → 客户端 + +| type | data 字段 | 说明 | +|------|-----------|------| +| `response` | `{ success, message, data }` | 通用应答 | +| `echo` | 原样 | 回显 | +| `proxyRequest` | `{ host, port }` | 服务端请求建立 TCP 代理隧道 | +| `proxyData` | `{ data: base64 }` | 服务端 → 客户端代理数据 | +| `proxyClose` | — | 服务端通知关闭代理隧道 | + +### register 示例 +```json +{ + "type": "register", + "messageId": "init-001", + "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "data": { + "userId": 12345, + "fcmToken": "firebase-device-token" + } +} +```