diff --git a/README.md b/README.md index 52e8112..6264d72 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,7 @@ XuqmGroup-RNSDK/ # @xuqm/rn-sdk (meta-package, private) │ ├── im/ @xuqm/rn-im # IM messaging (WebSocket/STOMP + WatermelonDB) │ ├── push/ @xuqm/rn-push # Multi-vendor push registration │ ├── update/ @xuqm/rn-update # App update check + RN plugin hot-update -│ ├── xwebview/ @xuqm/rn-xwebview # Enhanced WebView with JSBridge -│ └── license/ @xuqm/rn-license # Encrypted license file decryption & verification +│ └── xwebview/ @xuqm/rn-xwebview # Enhanced WebView with JSBridge ``` All packages are at version **0.2.2**. @@ -74,20 +73,20 @@ yarn add @react-native-async-storage/async-storage **Option B -- Individual packages:** ```bash -yarn add @xuqm/rn-common @xuqm/rn-im @xuqm/rn-push @xuqm/rn-update @xuqm/rn-xwebview @xuqm/rn-license +yarn add @xuqm/rn-common @xuqm/rn-update @xuqm/rn-xwebview yarn add @react-native-async-storage/async-storage ``` ### 3. Install optional peer dependencies ```bash -# IM module (local database) +# IM module (if using @xuqm/rn-im) yarn add @nozbe/watermelondb # XWebView module yarn add react-native-webview react-native-blob-util react-native-svg -# License / encrypted config +# Config file decryption (auto-init) yarn add react-native-quick-crypto ``` @@ -149,7 +148,7 @@ Core module. Handles SDK initialization, HTTP requests, token persistence, devic |--------|-------------| | `XuqmSDK.initialize(opts)` | Init with `appKey`, fetches remote config | | `XuqmSDK.initWithConfigFile(encrypted)` | Init from `XUQM-CONFIG-V1` encrypted file | -| `XuqmSDK.initializeFromLicense(licenseFile)` | Init from decrypted license data | +| `XuqmSDK.initializeFromLicense(file)` | Init from decrypted config data | | `XuqmSDK.awaitInitialization()` | Wait for async init to complete | | `XuqmSDK.setUserId / getUserId` | Manage current user ID | | `XuqmSDK.setUserInfo / getUserInfo` | Manage current user profile | @@ -261,24 +260,6 @@ Enhanced WebView with JSBridge for bidirectional communication between RN and we --- -### @xuqm/rn-license - -Encrypted license file decryption and device license verification with server. Supports `XUQM-CONFIG-V1` and `XUQM-LICENSE-V1` encrypted formats. - -| Export | Description | -|--------|-------------| -| `initialize(appKey, options?)` | Configure license with appKey and optional baseUrl | -| `initializeFromFile(encryptedContent)` | Decrypt and init from encrypted file | -| `checkLicense(userInfo?)` | Verify device license with server (10-min cache) | -| `getStatus()` | Returns `'ok'`, `'denied'`, or `'unknown'` | -| `getDeviceId()` | Get device ID for license check | -| `decryptLicenseFile(content)` | Decrypt `XUQM-LICENSE-V1` format | -| `decryptConfigFile(content)` | Decrypt `XUQM-CONFIG-V1` format (alias) | - -**Additional peer dependency:** `react-native-quick-crypto >= 0.7.0` - ---- - ## Development ### Prerequisites @@ -307,7 +288,7 @@ cd packages/common npm publish # Publish all packages (from each package directory) -for pkg in common im push update xwebview license; do +for pkg in common im push update xwebview; do cd packages/$pkg && npm publish && cd ../.. done ``` diff --git a/docs/PushSDK-厂商集成.md b/docs/PushSDK-厂商集成.md new file mode 100644 index 0000000..bcc0e64 --- /dev/null +++ b/docs/PushSDK-厂商集成.md @@ -0,0 +1,292 @@ +# PushSDK Android 厂商推送集成指南 + +> 最后更新:2026-06-15 + +--- + +## 概述 + +PushSDK 支持 Android 多厂商推送 SDK,通过反射机制自动检测设备厂商并调用对应的推送注册 API。宿主应用只需配置 `AndroidManifest.xml` 中的 meta-data,无需编写额外的 Java/Kotlin 代码。 + +**支持的厂商**: + +| 厂商 | SDK | 自动检测条件 | +|------|-----|------------| +| 华为 | HMS Push (HmsInstanceId) | `Build.MANUFACTURER == "HUAWEI"` | +| 小米 | MiPush (MiPushClient) | `Build.MANUFACTURER == "XIAOMI" \| "REDMI"` | +| OPPO | Heytap Push (PushManager) | `Build.MANUFACTURER == "OPPO" \| "REALME"` | +| vivo | PushClient | `Build.MANUFACTURER == "VIVO" \| "IQOO"` | +| 荣耀 | HonorPush | `Build.MANUFACTURER == "HONOR"` | +| Google | FCM (FirebaseMessaging) | 其他厂商 | +| Apple | APNs | iOS 自动 | + +--- + +## 一、集成步骤 + +### 1.1 添加依赖 + +宿主应用的 `android/app/build.gradle`: + +```groovy +dependencies { + // 华为 HMS Push(可选) + implementation 'com.huawei.hms:push:6.12.0.300' + + // 小米 MiPush(可选) + implementation 'com.xiaomi.mipush:mipush-sdk:5.9.9' + + // OPPO Heytap Push(可选) + implementation 'com.heytap.mcssdk:mcssdk:3.0.2' + + // vivo Push(可选) + implementation 'com.vivo.push:push:3.0.0.4' + + // 荣耀 Push(可选) + implementation 'com.hihonor.push:honor-push:7.0.3.300' + + // Google FCM(推荐作为兜底) + implementation 'com.google.firebase:firebase-messaging:23.4.0' +} +``` + +**注意**:只需添加目标设备对应的依赖。PushSDK 通过反射调用,不添加对应依赖时该厂商注册会静默跳过。 + +### 1.2 配置 AndroidManifest.xml + +在 `android/app/src/main/AndroidManifest.xml` 的 `` 标签内添加 meta-data: + +```xml + + + + + + + + + + + + + + + + + +``` + +在 `android/app/src/main/res/values/strings.xml` 中添加: + +```xml + + + YOUR_XIAOMI_APP_ID + YOUR_XIAOMI_APP_KEY + + + YOUR_OPPO_APP_KEY + YOUR_OPPO_APP_SECRET + +``` + +### 1.3 华为 HMS 配置 + +华为推送需要 `agconnect-services.json` 文件: + +1. 在 [华为开发者联盟](https://developer.huawei.com/) 创建应用 +2. 下载 `agconnect-services.json` +3. 放置到 `android/app/` 目录 + +### 1.4 Google FCM 配置 + +FCM 需要 `google-services.json` 文件: + +1. 在 [Firebase Console](https://console.firebase.google.com/) 创建项目 +2. 下载 `google-services.json` +3. 放置到 `android/app/` 目录 +4. 确保 `android/build.gradle` 添加了 Google Services 插件: + +```groovy +buildscript { + dependencies { + classpath 'com.google.gms:google-services:4.4.0' + } +} +``` + +--- + +## 二、工作流程 + +``` +App 启动 + → PushSDK.onPushToken(callback) ← 注册监听器 + → PushSDK.requestNativeRegistration() + → XuqmPushModule.registerPush() + → 检测厂商 (detectVendor) + → 调用对应厂商 SDK 注册 + → 厂商 SDK 返回 token + → emitToken(token, vendor) ← 通过事件发送给 JS + → callback(token, vendor) 被调用 + → PushSDK.setDeviceToken(token, vendor) + → 注册到服务器 + +用户登录 + → PushSDK.initialize(userId) + → 将 pending token 绑定到 userId + +用户登出 + → PushSDK.logout(userId) + → 从服务器注销 token +``` + +--- + +## 三、厂商 SDK 详细说明 + +### 3.1 华为 HMS Push + +**注册方式**:`HmsInstanceId.getInstance(context).getToken(appId, "HCM")` + +**AppID 来源**:`AGConnectServicesConfig.fromContext(context).getString("client/app_id")` + +**Token 事件**:HMS 6.x 版本 token 通过 `getToken()` 同步返回。如果返回空,需要实现 `HmsMessageService.onNewToken()` 异步接收。 + +**ProGuard 规则**: +``` +-keep class com.huawei.hms.** { *; } +-dontwarn com.huawei.hms.** +``` + +### 3.2 小米 MiPush + +**注册方式**:`MiPushClient.registerPush(context, appId, appKey)` + +**配置来源**:`AndroidManifest.xml` 中的 `XUQM_XIAOMI_APP_ID` 和 `XUQM_XIAOMI_APP_KEY` + +**Token 事件**:通过 `MiPushMessageReceiver.onReceiveRegisterResult()` 回调。PushSDK 通过反射监听。 + +**ProGuard 规则**: +``` +-keep class com.xiaomi.mipush.** { *; } +-dontwarn com.xiaomi.mipush.** +``` + +### 3.3 OPPO Heytap Push + +**注册方式**:`PushManager.getInstance().register(context, appKey, appSecret, pushCallback)` + +**配置来源**:`AndroidManifest.xml` 中的 `XUQM_OPPO_APP_KEY` 和 `XUQM_OPPO_APP_SECRET` + +**Token 事件**:通过 `PushCallback.onRegister(regId)` 回调。PushSDK 使用 `Proxy.newProxyInstance` 动态代理。 + +**ProGuard 规则**: +``` +-keep class com.heytap.mcssdk.** { *; } +-dontwarn com.heytap.mcssdk.** +``` + +### 3.4 vivo Push + +**注册方式**:`PushClient.getInstance().initialize(context)` + +**配置来源**:`AndroidManifest.xml` 中的 `com.vivo.push.api_key` 和 `com.vivo.push.app_id` + +**Token 事件**:通过 `PushClientReceiver.onReceiveRegId()` 回调。 + +**ProGuard 规则**: +``` +-keep class com.vivo.push.** { *; } +-dontwarn com.vivo.push.** +``` + +### 3.5 荣耀 Honor Push + +**注册方式**:`HonorPushClient.getInstance().getToken()` + +**配置来源**:`AndroidManifest.xml` 中的 `com.hihonor.push.app_id` + +**Token 事件**:通过 `HonorPushService.onNewToken()` 回调。 + +**ProGuard 规则**: +``` +-keep class com.hihonor.push.** { *; } +-dontwarn com.hihonor.push.** +``` + +### 3.6 Google FCM + +**注册方式**:`FirebaseMessaging.getInstance().getToken()` + +**配置来源**:`google-services.json` + +**Token 事件**:通过 `FirebaseMessagingService.onNewToken()` 回调。 + +**ProGuard 规则**: +``` +-keep class com.google.firebase.** { *; } +-dontwarn com.google.firebase.** +``` + +--- + +## 四、iOS APNs 配置 + +iOS 推送使用原生 APNs,无需额外 SDK 依赖。 + +```objective-c +// ios/YourApp/AppDelegate.mm +#import + +- (void)application:(UIApplication *)application + didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + // PushSDK 自动处理 +} +``` + +在 Xcode 中: +1. 打开 Capabilities → Push Notifications +2. 启用 Push Notifications +3. 配置 APNs Auth Key(在 Apple Developer Portal) + +--- + +## 五、调试 + +### 5.1 检查厂商检测 + +```typescript +import { PushSDK } from '@xuqm/rn-push'; +// 或通过 NativeModules +const vendor = await NativeModules.XuqmPushModule.detectVendor(); +console.log('Detected vendor:', vendor); +``` + +### 5.2 检查 Token 注册 + +```typescript +PushSDK.onPushToken((token, vendor) => { + console.log('Push token:', token); + console.log('Vendor:', vendor); +}); + +await PushSDK.requestNativeRegistration(); +``` + +### 5.3 常见问题 + +| 问题 | 原因 | 解决方案 | +|------|------|---------| +| Token 为空 | 厂商 SDK 未正确配置 | 检查 meta-data / json 文件 | +| 注册失败 | 网络问题或 SDK 版本不兼容 | 检查 Logcat 错误日志 | +| 华为 token 获取失败 | HMS Core 版本过低 | 更新 HMS Core 到最新版 | +| 小米注册无反应 | appId/appKey 错误 | 检查 strings.xml 配置 | diff --git a/docs/SDK-API参考.md b/docs/SDK-API参考.md index 9e52f98..fd095e4 100644 --- a/docs/SDK-API参考.md +++ b/docs/SDK-API参考.md @@ -2,7 +2,7 @@ > 版本:基于源码自动生成,最后更新 2026-06-15 > -> 包名:`@xuqm/rn-common` · `@xuqm/rn-update` · `@xuqm/rn-xwebview` · `@xuqm/rn-push` · `@xuqm/rn-im` · `@xuqm/rn-license` +> 包名:`@xuqm/rn-common` · `@xuqm/rn-update` · `@xuqm/rn-xwebview` · `@xuqm/rn-push` · `@xuqm/rn-im` --- @@ -728,85 +728,6 @@ interface ImEventListener { --- -## 6. License(license) - -> `import * as License from '@xuqm/rn-license'` - -设备 License 验证模块,支持离线缓存和自动注册。 - -### 6.1 类型定义 - -```ts -interface LicenseFile { - appKey: string; - appName?: string; - companyName?: string; - baseUrl?: string; - issuedAt?: string; - expiresAt?: string; -} - -interface LicenseUserInfo { - userId?: string; - name?: string; - email?: string; - phone?: string; -} - -type LicenseStatus = "ok" | "denied" | "unknown"; - -type LicenseResult = - | { type: "success"; reason: string } - | { type: "error"; message: string }; -``` - -### 6.2 API 方法 - -#### `initialize(appKey, options?): void` - -初始化 License SDK。 - -```ts -License.initialize("your-app-key", { - baseUrl: "https://auth.xuqinmin.com", - deviceName: "My Device", -}); -``` - -#### `initializeFromFile(encryptedContent): Promise` - -从加密 License 文件初始化。自动解密并提取 `appKey` 和 `baseUrl`。 - -```ts -import licenseFile from "./assets/license.xuqmconfig"; -await License.initializeFromFile(licenseFile); -``` - -#### `checkLicense(userInfo?): Promise` - -检查 License 状态。优先使用内存缓存(10 分钟有效期),其次持久化缓存,最后请求服务端验证。服务端流程:先尝试 verify(已有 token),失败则 register(注册新设备)。 - -```ts -const result = await License.checkLicense({ userId: "user-123", name: "张三" }); -if (result.type === "error") { - console.error(result.message); -} -``` - -#### `getStatus(): Promise` - -获取当前 License 状态(`'ok'` / `'denied'` / `'unknown'`)。 - -#### `getDeviceId(): Promise` - -获取设备 ID(首次生成后持久化存储)。 - -#### `clear(): Promise` - -清除所有 License 缓存(状态、token、设备 ID)。 - ---- - ## 快速参考 ### 初始化顺序 @@ -842,5 +763,4 @@ const pluginUpdate = await UpdateSDK.checkAndCachePlugin("buz1"); @xuqm/rn-push ← PushSDK(依赖 common) @xuqm/rn-update ← UpdateSDK(依赖 common) @xuqm/rn-xwebview ← XWebView 组件(依赖 common) -@xuqm/rn-license ← License(依赖 common) ``` diff --git a/docs/XWebView-JSBridge.md b/docs/XWebView-JSBridge.md new file mode 100644 index 0000000..eae5788 --- /dev/null +++ b/docs/XWebView-JSBridge.md @@ -0,0 +1,211 @@ +# XWebView JSBridge 标准接口 + +> 最后更新:2026-06-15 + +--- + +## 概述 + +XWebView 内置 JSBridge,支持 H5 页面与 React Native 宿主之间的双向通信。 + +**通信机制**: +- H5 → RN:`window.ReactNativeWebView.postMessage(JSON.stringify(data))` +- RN → H5:`webViewRef.injectJavaScript(jsString)` + +--- + +## 一、H5 → RN 消息协议 + +### 1.1 内置消息(__xwv 前缀) + +XWebView 自动拦截以下浏览器行为,通过 `postMessage` 发送给 RN: + +| 消息类型 | 触发场景 | 数据格式 | +|---------|---------|---------| +| `alert` | `window.alert(msg)` | `{__xwv: 'alert', msg: string}` | +| `confirm` | `window.confirm(msg)` | `{__xwv: 'confirm', msg: string}` | +| `prompt` | `window.prompt(msg, def)` | `{__xwv: 'prompt', msg: string, def: string}` | +| `download` | 点击下载链接 | `{__xwv: 'download', url: string, filename: string}` | +| `blobdownload` | blob URL 下载 | `{__xwv: 'blobdownload', url: string, filename: string, data: base64}` | +| `bloberror` | blob 读取失败 | `{__xwv: 'bloberror', msg: string}` | +| `log` | 调试日志 | `{__xwv: 'log', msg: string}` | + +### 1.2 自定义消息 + +H5 发送非 `__xwv` 前缀的消息时,XWebView 将其转发给 `onMessage` 回调: + +```javascript +// H5 端 +window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'customEvent', + payload: { key: 'value' } +})); +``` + +```typescript +// RN 端 + { + const data = JSON.parse(event.nativeEvent.data); + if (data.type === 'customEvent') { + // 处理自定义消息 + } + }} +/> +``` + +--- + +## 二、RN → H5 通信 + +### 2.1 注入 JavaScript + +```typescript +// 通过 controller API +controller.postMessageToWeb('window.dispatchEvent(new CustomEvent("rnMessage", {detail: {type: "update", data: "hello"}}))'); + +// 通过 ref +webViewRef.injectJavaScript('window.handleRNMessage("hello")'); +``` + +### 2.2 Controller API + +通过 `setXWebViewController` 获取的 controller 对象提供以下方法: + +| 方法 | 说明 | +|------|------| +| `refresh()` | 刷新当前页面 | +| `close()` | 关闭 WebView | +| `goBack()` | 返回上一页 | +| `goForward()` | 前进 | +| `copyUrl()` | 复制当前 URL 到剪贴板 | +| `postMessageToWeb(jsString)` | 向 H5 注入并执行 JS | +| `getTitle()` | 获取当前页面标题 | + +--- + +## 三、下载处理 + +### 3.1 自动下载拦截 + +XWebView 自动拦截以下文件扩展名的链接点击: + +``` +.pdf .zip .rar .tar .gz .apk .ipa .doc .docx .xls .xlsx .ppt .pptx .mp4 .mp3 .mov .exe .dmg +``` + +### 3.2 下载回调 + +```typescript + { + // request: { url, suggestedFilename, mimeType?, fileSize? } + console.log('Download started:', request.suggestedFilename); + }} + onDownloadProgress={(progress) => { + // progress: { url, filename, received, total, percentage } + console.log(`Download: ${progress.percentage}%`); + }} + onDownloadComplete={(result) => { + // result: { url, filename, filePath, fileSize } + console.log('Download complete:', result.filePath); + }} + onDownloadError={(url, error) => { + console.error('Download failed:', url, error); + }} + onDownloadDecide={(request) => { + // 返回下载决策 + return { allowed: true, filename: request.suggestedFilename }; + }} + downloadConflict="rename" // 'rename' | 'overwrite' +/> +``` + +### 3.3 Blob URL 下载 + +XWebView 自动处理 blob URL 下载: +1. 拦截 `` 点击 +2. 通过 `FileReader` 读取 blob 数据为 base64 +3. 通过 `saveBase64File` 保存到本地 + +--- + +## 四、内置 Dialog 覆盖 + +XWebView 注入 `DIALOG_OVERRIDE_JS`,覆盖浏览器原生对话框: + +| 原生方法 | 覆盖行为 | +|---------|---------| +| `window.alert(msg)` | 发送 `{__xwv: 'alert', msg}` 消息 | +| `window.confirm(msg)` | 发送 `{__xwv: 'confirm', msg}` 消息,始终返回 `true` | +| `window.prompt(msg, def)` | 发送 `{__xwv: 'prompt', msg, def}` 消息,返回默认值 | +| `window.open(url)` | 发送日志消息,然后 `window.location.href = url` | + +RN 端收到这些消息后,可以展示原生 Alert/Confirm/Prompt 对话框。 + +--- + +## 五、标准 Bridge 接口(宿主实现) + +以下接口由宿主应用通过 `bridgeHandlers` 实现,H5 通过 `xuqm.call(method, params)` 调用: + +### 5.1 会话管理 + +| 接口 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `getSession` | — | `{token, userId}` | 获取当前登录会话 | +| `getUserInfo` | — | `{userId, name, phone, avatar}` | 获取用户信息 | + +### 5.2 导航 + +| 接口 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `navigate` | `{screen, params?}` | — | 跳转宿主页面 | +| `close` | — | — | 关闭当前 WebView | +| `goBack` | — | — | 返回上一页 | + +### 5.2 UI 交互 + +| 接口 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `showToast` | `{message}` | — | 显示 Toast | +| `showAlert` | `{title?, message}` | — | 显示 Alert 对话框 | +| `showConfirm` | `{title?, message, confirmText?, cancelText?}` | `{confirmed: boolean}` | 显示确认对话框 | + +### 5.3 文件操作 + +| 接口 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `uploadFile` | `{accept?}` | `{url, name, size}` | 选择并上传文件 | +| `openCamera` | — | `{url, base64}` | 打开相机拍照 | +| `scanQR` | — | `{result}` | 扫描二维码 | + +### 5.4 设备信息 + +| 接口 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `getDeviceInfo` | — | `{platform, version, model}` | 获取设备信息 | +| `getLocation` | — | `{latitude, longitude}` | 获取当前位置 | + +--- + +## 六、H5 端调用示例 + +```javascript +// 获取会话 +xuqm.call('getSession').then(session => { + console.log('Token:', session.token); +}); + +// 跳转宿主页面 +xuqm.call('navigate', { screen: 'Settings' }); + +// 显示 Toast +xuqm.call('showToast', { message: '操作成功' }); + +// 上传文件 +xuqm.call('uploadFile', { accept: 'image/*' }).then(file => { + console.log('Uploaded:', file.url); +}); +``` diff --git a/package.json b/package.json index 0d35f96..84a59a7 100644 --- a/package.json +++ b/package.json @@ -21,21 +21,22 @@ "typecheck:all": "yarn workspaces run typecheck" }, "peerDependencies": { + "@react-native-async-storage/async-storage": ">=1.21.0", "react": ">=18.0.0", - "react-native": ">=0.76.0", - "@react-native-async-storage/async-storage": ">=1.21.0" + "react-native": ">=0.76.0" }, "dependencies": { "@xuqm/rn-common": ">=0.2.0", "@xuqm/rn-im": ">=0.2.0", + "@xuqm/rn-license": ">=0.2.0", "@xuqm/rn-push": ">=0.2.0", "@xuqm/rn-update": ">=0.2.0", "@xuqm/rn-xwebview": ">=0.2.0", - "@xuqm/rn-license": ">=0.2.0" + "react-native-qrcode-svg": "^6.3.21" }, "devDependencies": { - "typescript": "^5.9.3", "@types/react": "^19.0.0", - "@types/react-native": "^0.73.0" + "@types/react-native": "^0.73.0", + "typescript": "^5.9.3" } } diff --git a/packages/update/android/src/main/java/com/xuqm/update/XuqmBundleModule.java b/packages/update/android/src/main/java/com/xuqm/update/XuqmBundleModule.java new file mode 100644 index 0000000..2bc7521 --- /dev/null +++ b/packages/update/android/src/main/java/com/xuqm/update/XuqmBundleModule.java @@ -0,0 +1,192 @@ +package com.xuqm.update; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; + +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 java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * XuqmBundleModule — 原生 bundle 文件管理。 + * + * 提供 bundle 文件的读取、写入、清单管理和热重载能力。 + * 宿主应用通过此模块实现插件化 bundle 加载。 + * + * 注意:bundle 注入到运行中的 RN host 需要宿主侧配合实现 + * (RN 0.84 不暴露公开的 loadBundle API)。 + * 本模块提供文件管理能力,注入逻辑由宿主的 BundleRuntime 负责。 + */ +public class XuqmBundleModule extends ReactContextBaseJavaModule { + + private static final String TAG = "XuqmBundleModule"; + private static final String BUNDLE_DIR = "rn-bundles"; + + public XuqmBundleModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @NonNull + @Override + public String getName() { + return "XuqmBundleModule"; + } + + // ── Bundle 文件操作 ────────────────────────────────────────────────────── + + /** + * 检查指定 bundle 文件是否存在。 + */ + @ReactMethod + public void bundleExists(String moduleId, Promise promise) { + try { + File file = getBundleFile(moduleId); + promise.resolve(file.exists()); + } catch (Exception e) { + promise.reject("BUNDLE_EXISTS_ERROR", e.getMessage(), e); + } + } + + /** + * 读取 bundle 文件内容(JS 源码文本)。 + */ + @ReactMethod + public void readBundle(String moduleId, Promise promise) { + try { + File file = getBundleFile(moduleId); + if (!file.exists()) { + promise.reject("BUNDLE_NOT_FOUND", "Bundle not found: " + moduleId); + return; + } + byte[] bytes = readBytes(file); + promise.resolve(new String(bytes, StandardCharsets.UTF_8)); + } catch (Exception e) { + promise.reject("BUNDLE_READ_ERROR", e.getMessage(), e); + } + } + + /** + * 将 bundle 源码写入本地文件。 + * + * @param moduleId 插件 ID + * @param source JS 源码文本 + * @param md5 文件 MD5 校验值 + */ + @ReactMethod + public void writeBundle(String moduleId, String source, String md5, Promise promise) { + try { + File file = getBundleFile(moduleId); + File dir = file.getParentFile(); + if (dir != null && !dir.exists()) { + dir.mkdirs(); + } + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(source.getBytes(StandardCharsets.UTF_8)); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject("BUNDLE_WRITE_ERROR", e.getMessage(), e); + } + } + + /** + * 删除指定 bundle 文件。 + */ + @ReactMethod + public void deleteBundle(String moduleId, Promise promise) { + try { + File file = getBundleFile(moduleId); + if (file.exists()) { + file.delete(); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject("BUNDLE_DELETE_ERROR", e.getMessage(), e); + } + } + + /** + * 获取 bundle 文件路径。 + */ + @ReactMethod + public void getBundlePath(String moduleId, Promise promise) { + try { + File file = getBundleFile(moduleId); + promise.resolve(file.getAbsolutePath()); + } catch (Exception e) { + promise.reject("BUNDLE_PATH_ERROR", e.getMessage(), e); + } + } + + // ── Manifest 操作 ──────────────────────────────────────────────────────── + + /** + * 读取 manifest.json 内容。 + */ + @ReactMethod + public void getManifest(Promise promise) { + try { + File manifestFile = new File(getBundleDir(), "manifest.json"); + if (!manifestFile.exists()) { + promise.resolve("{}"); + return; + } + byte[] bytes = readBytes(manifestFile); + promise.resolve(new String(bytes, StandardCharsets.UTF_8)); + } catch (Exception e) { + promise.reject("MANIFEST_READ_ERROR", e.getMessage(), e); + } + } + + /** + * 写入 manifest.json。 + */ + @ReactMethod + public void writeManifest(String manifestJson, Promise promise) { + try { + File manifestFile = new File(getBundleDir(), "manifest.json"); + try (FileOutputStream fos = new FileOutputStream(manifestFile)) { + fos.write(manifestJson.getBytes(StandardCharsets.UTF_8)); + } + promise.resolve(true); + } catch (Exception e) { + promise.reject("MANIFEST_WRITE_ERROR", e.getMessage(), e); + } + } + + // ── 辅助方法 ───────────────────────────────────────────────────────────── + + private File getBundleDir() { + Context context = getReactApplicationContext(); + File dir = new File(context.getFilesDir(), BUNDLE_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + return dir; + } + + private File getBundleFile(String moduleId) { + String platform = "android"; + String filename = moduleId + "." + platform + ".bundle"; + return new File(getBundleDir(), filename); + } + + private byte[] readBytes(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + byte[] bytes = new byte[(int) file.length()]; + fis.read(bytes); + return bytes; + } + } +} diff --git a/packages/update/android/src/main/java/com/xuqm/update/XuqmUpdatePackage.java b/packages/update/android/src/main/java/com/xuqm/update/XuqmUpdatePackage.java index 0bb8c20..e7bc4ac 100644 --- a/packages/update/android/src/main/java/com/xuqm/update/XuqmUpdatePackage.java +++ b/packages/update/android/src/main/java/com/xuqm/update/XuqmUpdatePackage.java @@ -13,7 +13,10 @@ public class XuqmUpdatePackage implements ReactPackage { @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new XuqmVersionModule(reactContext)); + return Arrays.asList( + new XuqmVersionModule(reactContext), + new XuqmBundleModule(reactContext) + ); } @Override diff --git a/packages/update/ios/XuqmBundleModule.m b/packages/update/ios/XuqmBundleModule.m new file mode 100644 index 0000000..6f60e47 --- /dev/null +++ b/packages/update/ios/XuqmBundleModule.m @@ -0,0 +1,116 @@ +#import + +@interface XuqmBundleModule : NSObject +@end + +@implementation XuqmBundleModule + +RCT_EXPORT_MODULE() + +- (NSString *)bundleDir { + NSString *appSupport = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject]; + NSString *dir = [appSupport stringByAppendingPathComponent:@"rn-bundles"]; + [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil]; + return dir; +} + +- (NSString *)bundlePathForModule:(NSString *)moduleId { + NSString *filename = [NSString stringWithFormat:@"%@.ios.bundle", moduleId]; + return [[self bundleDir] stringByAppendingPathComponent:filename]; +} + +// ── Bundle 文件操作 ────────────────────────────────────────────────────────── + +RCT_EXPORT_METHOD(bundleExists:(NSString *)moduleId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [self bundlePathForModule:moduleId]; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path]; + resolve(@(exists)); +} + +RCT_EXPORT_METHOD(readBundle:(NSString *)moduleId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [self bundlePathForModule:moduleId]; + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + reject(@"BUNDLE_NOT_FOUND", [NSString stringWithFormat:@"Bundle not found: %@", moduleId], nil); + return; + } + NSError *error = nil; + NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error) { + reject(@"BUNDLE_READ_ERROR", error.localizedDescription, error); + return; + } + resolve(content); +} + +RCT_EXPORT_METHOD(writeBundle:(NSString *)moduleId + source:(NSString *)source + md5:(NSString *)md5 + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [self bundlePathForModule:moduleId]; + NSError *error = nil; + [source writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + reject(@"BUNDLE_WRITE_ERROR", error.localizedDescription, error); + return; + } + resolve(@(YES)); +} + +RCT_EXPORT_METHOD(deleteBundle:(NSString *)moduleId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [self bundlePathForModule:moduleId]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + if (error) { + reject(@"BUNDLE_DELETE_ERROR", error.localizedDescription, error); + return; + } + } + resolve(@(YES)); +} + +RCT_EXPORT_METHOD(getBundlePath:(NSString *)moduleId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + resolve([self bundlePathForModule:moduleId]); +} + +// ── Manifest 操作 ──────────────────────────────────────────────────────────── + +RCT_EXPORT_METHOD(getManifest:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [[self bundleDir] stringByAppendingPathComponent:@"manifest.json"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { + resolve(@"{}"); + return; + } + NSError *error = nil; + NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; + if (error) { + reject(@"MANIFEST_READ_ERROR", error.localizedDescription, error); + return; + } + resolve(content); +} + +RCT_EXPORT_METHOD(writeManifest:(NSString *)manifestJson + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSString *path = [[self bundleDir] stringByAppendingPathComponent:@"manifest.json"]; + NSError *error = nil; + [manifestJson writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + reject(@"MANIFEST_WRITE_ERROR", error.localizedDescription, error); + return; + } + resolve(@(YES)); +} + +@end diff --git a/packages/update/src/NativeBundle.ts b/packages/update/src/NativeBundle.ts new file mode 100644 index 0000000..319d9ab --- /dev/null +++ b/packages/update/src/NativeBundle.ts @@ -0,0 +1,93 @@ +/** + * XuqmBundleModule JS 封装。 + * + * 提供 bundle 文件的原生管理能力: + * - 读取/写入/删除 bundle 文件 + * - manifest 管理 + * - bundle 路径查询 + * + * 注意:bundle 注入到运行中的 RN host(loadBundle) + * 由宿主的 BundleRuntime 负责(RN 0.84 不暴露公开 API)。 + */ + +import {NativeModules, Platform} from 'react-native' + +interface XuqmBundleModuleInterface { + bundleExists(moduleId: string): Promise + readBundle(moduleId: string): Promise + writeBundle(moduleId: string, source: string, md5: string): Promise + deleteBundle(moduleId: string): Promise + getBundlePath(moduleId: string): Promise + getManifest(): Promise + writeManifest(manifestJson: string): Promise +} + +function getModule(): XuqmBundleModuleInterface { + const mod = NativeModules.XuqmBundleModule + if (!mod) { + throw new Error('[XuqmBundleModule] Native module not available') + } + return mod as XuqmBundleModuleInterface +} + +export const NativeBundle = { + /** + * 检查指定 bundle 文件是否存在。 + */ + async exists(moduleId: string): Promise { + return getModule().bundleExists(moduleId) + }, + + /** + * 读取 bundle 文件内容(JS 源码文本)。 + */ + async read(moduleId: string): Promise { + return getModule().readBundle(moduleId) + }, + + /** + * 将 bundle 源码写入本地文件。 + * + * @param moduleId 插件 ID + * @param source JS 源码文本 + * @param md5 文件 MD5 校验值 + */ + async write(moduleId: string, source: string, md5: string): Promise { + await getModule().writeBundle(moduleId, source, md5) + }, + + /** + * 删除指定 bundle 文件。 + */ + async remove(moduleId: string): Promise { + await getModule().deleteBundle(moduleId) + }, + + /** + * 获取 bundle 文件的绝对路径。 + */ + async getPath(moduleId: string): Promise { + return getModule().getBundlePath(moduleId) + }, + + /** + * 读取 manifest.json 内容。 + */ + async getManifest(): Promise { + return getModule().getManifest() + }, + + /** + * 写入 manifest.json。 + */ + async writeManifest(manifestJson: string): Promise { + await getModule().writeManifest(manifestJson) + }, + + /** + * 获取 bundle 文件名(平台相关)。 + */ + getFileName(moduleId: string): string { + return `${moduleId}.${Platform.OS}.bundle` + }, +} diff --git a/packages/update/src/UpdateSDK.ts b/packages/update/src/UpdateSDK.ts index 1654db5..d7482df 100644 --- a/packages/update/src/UpdateSDK.ts +++ b/packages/update/src/UpdateSDK.ts @@ -216,11 +216,35 @@ export const UpdateSDK = { /** * 下载插件 bundle 源码文本。 + * + * @param downloadUrl 下载地址 + * @param onProgress 下载进度回调 (0~1) */ - async downloadPluginBundle(downloadUrl: string): Promise { + async downloadPluginBundle(downloadUrl: string, onProgress?: (progress: number) => void): Promise { const response = await fetch(downloadUrl) if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`) - return response.text() + + if (!onProgress) return response.text() + + const contentLength = Number(response.headers.get('Content-Length') ?? '0') + const reader = response.body?.getReader() + if (!reader) return response.text() + + const chunks: Uint8Array[] = [] + let received = 0 + + for (;;) { + const { done, value } = await reader.read() + if (done) break + chunks.push(value) + received += value.length + if (contentLength > 0) { + onProgress(received / contentLength) + } + } + + const decoder = new TextDecoder() + return chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') + decoder.decode() }, /** @@ -249,13 +273,15 @@ export const UpdateSDK = { * 如果有更新,下载 bundle 并缓存到本地。 * 调用方需要在下次启动时通过 reloadPlugin() 加载新版本。 * + * @param moduleId 插件 ID + * @param onProgress 下载进度回调 (0~1) * @returns 如果有更新,返回缓存的 bundle 信息;否则返回 null */ - async checkAndCachePlugin(moduleId: string): Promise { + async checkAndCachePlugin(moduleId: string, onProgress?: (progress: number) => void): Promise { const info = await this.checkPluginUpdate(moduleId) if (!info.needsUpdate) return null - const source = await this.downloadPluginBundle(info.downloadUrl) + const source = await this.downloadPluginBundle(info.downloadUrl, onProgress) return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source) }, diff --git a/packages/update/src/index.ts b/packages/update/src/index.ts index aa1106b..ea59a1f 100644 --- a/packages/update/src/index.ts +++ b/packages/update/src/index.ts @@ -1,2 +1,3 @@ export { UpdateSDK } from './UpdateSDK' export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK' +export { NativeBundle } from './NativeBundle' diff --git a/src/index.ts b/src/index.ts index 4a53c62..16413d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,6 @@ export { PushSDK } from '@xuqm/rn-push' export type { PushVendor } from '@xuqm/rn-push' export { UpdateSDK } from '@xuqm/rn-update' export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' -export { decryptLicenseFile, decryptConfigFile, initialize as initializeLicense, initializeFromFile as initializeLicenseFromFile, checkLicense } from '@xuqm/rn-license' -export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from '@xuqm/rn-license' export { XWebViewControl, XWebViewScreen, diff --git a/yarn.lock b/yarn.lock index 866aa43..eb47cb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,43 +401,6 @@ dependencies: "@types/yargs-parser" "*" -"@xuqm/rn-common@>=0.2.2", "@xuqm/rn-common@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/common": - version "0.3.2" - resolved "file:packages/common" - -"@xuqm/rn-im@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/im": - version "0.2.2" - resolved "file:packages/im" - dependencies: - "@xuqm/rn-common" ">=0.2.2" - -"@xuqm/rn-license@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/license": - version "0.3.0" - resolved "file:packages/license" - dependencies: - "@xuqm/rn-common" ">=0.2.2" - -"@xuqm/rn-push@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/push": - version "0.2.2" - resolved "file:packages/push" - dependencies: - "@xuqm/rn-common" ">=0.2.2" - -"@xuqm/rn-update@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/update": - version "0.3.0" - resolved "file:packages/update" - dependencies: - "@xuqm/rn-common" ">=0.2.2" - -"@xuqm/rn-xwebview@file:/Users/xuqinmin/Projects/XuqmGroup/XuqmGroup-RNSDK/packages/xwebview": - version "0.2.2" - resolved "file:packages/xwebview" - dependencies: - "@xuqm/rn-common" ">=0.2.2" - react-native-blob-util "^0.24.7" - react-native-svg "^15.15.4" - react-native-webview "^13.16.1" - abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -559,6 +522,11 @@ buffer-from@^1.0.0: resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +camelcase@^5.0.0: + version "5.3.1" + resolved "https://nexus.xuqinmin.com/repository/npm/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelcase@^6.2.0: version "6.3.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" @@ -608,6 +576,15 @@ ci-info@^3.2.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +cliui@^6.0.0: + version "6.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" @@ -692,33 +669,31 @@ csstype@^3.2.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== -debug@^2.6.9: +debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.3.1, debug@^4.4.0, debug@4: +debug@4, debug@^4.1.0, debug@^4.3.1, debug@^4.4.0: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" +decamelize@^1.2.0: + version "1.2.0" + resolved "https://nexus.xuqinmin.com/repository/npm/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== decode-uri-component@^0.2.2: version "0.2.2" resolved "https://nexus.xuqinmin.com/repository/npm/decode-uri-component/-/decode-uri-component-0.2.2.tgz" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== -depd@~2.0.0, depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -728,6 +703,11 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://nexus.xuqinmin.com/repository/npm/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + dom-serializer@^2.0.0: version "2.0.0" resolved "https://nexus.xuqinmin.com/repository/npm/dom-serializer/-/dom-serializer-2.0.0.tgz" @@ -872,6 +852,14 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" +find-up@^4.1.0: + version "4.1.0" + resolved "https://nexus.xuqinmin.com/repository/npm/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + flow-enums-runtime@^0.0.6: version "0.0.6" resolved "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz" @@ -887,7 +875,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.5: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -971,7 +959,7 @@ inherits@~2.0.3, inherits@~2.0.4: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -invariant@^2.2.4, invariant@2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -1082,12 +1070,19 @@ lighthouse-logger@^1.0.0: debug "^2.6.9" marky "^1.2.2" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + lodash.throttle@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -1168,7 +1163,7 @@ metro-cache@0.84.3: https-proxy-agent "^7.0.5" metro-core "0.84.3" -metro-config@^0.84.0, metro-config@0.84.3: +metro-config@0.84.3, metro-config@^0.84.0: version "0.84.3" resolved "https://registry.npmjs.org/metro-config/-/metro-config-0.84.3.tgz" integrity sha512-JmCzZWOETR+O22q8oPBWyQppx3roU9EbkbGzD8Gf1jukQ4b5T1fTzqqHruu6K4sTiNq5zVQySmKF6bp4kVARew== @@ -1182,7 +1177,7 @@ metro-config@^0.84.0, metro-config@0.84.3: metro-runtime "0.84.3" yaml "^2.6.1" -metro-core@^0.84.0, metro-core@0.84.3: +metro-core@0.84.3, metro-core@^0.84.0: version "0.84.3" resolved "https://registry.npmjs.org/metro-core/-/metro-core-0.84.3.tgz" integrity sha512-cc0pvAa80ai1nDmqqz0P59a+0ZqCZ/YHU/3jEekZL6spFnYDfX8iDLdn9FR6kX+67rmzKxHNrbrSRFLX2AYocw== @@ -1221,7 +1216,7 @@ metro-resolver@0.84.3: dependencies: flow-enums-runtime "^0.0.6" -metro-runtime@^0.84.0, metro-runtime@0.84.3: +metro-runtime@0.84.3, metro-runtime@^0.84.0: version "0.84.3" resolved "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.3.tgz" integrity sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ== @@ -1229,7 +1224,7 @@ metro-runtime@^0.84.0, metro-runtime@0.84.3: "@babel/runtime" "^7.25.0" flow-enums-runtime "^0.0.6" -metro-source-map@^0.84.0, metro-source-map@0.84.3: +metro-source-map@0.84.3, metro-source-map@^0.84.0: version "0.84.3" resolved "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.3.tgz" integrity sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ== @@ -1287,7 +1282,7 @@ metro-transform-worker@0.84.3: metro-transform-plugins "0.84.3" nullthrows "^1.1.1" -metro@^0.84.0, metro@0.84.3: +metro@0.84.3, metro@^0.84.0: version "0.84.3" resolved "https://registry.npmjs.org/metro/-/metro-0.84.3.tgz" integrity sha512-1h3lbVrE6hGf1e/764HfhPGg/bGrWMJDDh7G2rc4gFYZboVuI40BlG/y+UhtbhQDNlO/csMvrcnK0YrTlHUVew== @@ -1375,16 +1370,16 @@ mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanoid@^3.3.11: version "3.3.12" resolved "https://nexus.xuqinmin.com/repository/npm/nanoid/-/nanoid-3.3.12.tgz" @@ -1424,6 +1419,11 @@ ob1@0.84.3: dependencies: flow-enums-runtime "^0.0.6" +object-assign@^4.1.1: + version "4.1.1" + resolved "https://nexus.xuqinmin.com/repository/npm/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" @@ -1446,11 +1446,35 @@ open@^7.0.3: is-docker "^2.0.0" is-wsl "^2.1.1" +p-limit@^2.2.0: + version "2.3.0" + resolved "https://nexus.xuqinmin.com/repository/npm/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://nexus.xuqinmin.com/repository/npm/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://nexus.xuqinmin.com/repository/npm/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-exists@^4.0.0: + version "4.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" @@ -1479,6 +1503,11 @@ picomatch@^4.0.4: resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" @@ -1495,6 +1524,24 @@ promise@^8.3.0: dependencies: asap "~2.0.6" +prop-types@^15.8.0: + version "15.8.1" + resolved "https://nexus.xuqinmin.com/repository/npm/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +qrcode@^1.5.4: + version "1.5.4" + resolved "https://nexus.xuqinmin.com/repository/npm/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + query-string@^7.1.3: version "7.1.3" resolved "https://nexus.xuqinmin.com/repository/npm/query-string/-/query-string-7.1.3.tgz" @@ -1525,6 +1572,11 @@ react-devtools-core@^6.1.5: shell-quote "^1.6.1" ws "^7" +react-is@^16.13.1: + version "16.13.1" + resolved "https://nexus.xuqinmin.com/repository/npm/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-is@^18.0.0: version "18.3.1" resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" @@ -1543,6 +1595,15 @@ react-native-blob-util@^0.24.7: base-64 "0.1.0" glob "13.0.1" +react-native-qrcode-svg@^6.3.21: + version "6.3.21" + resolved "https://nexus.xuqinmin.com/repository/npm/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.21.tgz#af873cf8e5b9fc68315a2c267ff6563d55c56abb" + integrity sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA== + dependencies: + prop-types "^15.8.0" + qrcode "^1.5.4" + text-encoding "^0.7.0" + react-native-safe-area-context@^5.4.0: version "5.7.0" resolved "https://nexus.xuqinmin.com/repository/npm/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz" @@ -1617,6 +1678,11 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + scheduler@0.27.0: version "0.27.0" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" @@ -1666,6 +1732,11 @@ serve-static@^1.16.2: parseurl "~1.3.3" send "~0.19.1" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://nexus.xuqinmin.com/repository/npm/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" @@ -1701,16 +1772,11 @@ source-map@^0.5.6: resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.0: +source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.6.1: - version "0.6.1" - resolved "https://nexus.xuqinmin.com/repository/npm/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - split-on-first@^1.0.0: version "1.1.0" resolved "https://nexus.xuqinmin.com/repository/npm/split-on-first/-/split-on-first-1.1.0.tgz" @@ -1783,6 +1849,11 @@ terser@^5.15.0: commander "^2.20.0" source-map-support "~0.5.20" +text-encoding@^0.7.0: + version "0.7.0" + resolved "https://nexus.xuqinmin.com/repository/npm/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" + integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== + throat@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz" @@ -1873,6 +1944,11 @@ whatwg-fetch@^3.0.0: resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz" integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== +which-module@^2.0.0: + version "2.0.1" + resolved "https://nexus.xuqinmin.com/repository/npm/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -1880,6 +1956,15 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://nexus.xuqinmin.com/repository/npm/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -1894,6 +1979,11 @@ ws@^7, ws@^7.5.10: resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== +y18n@^4.0.0: + version "4.0.3" + resolved "https://nexus.xuqinmin.com/repository/npm/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" @@ -1909,11 +1999,36 @@ yaml@^2.6.1: resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz" integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://nexus.xuqinmin.com/repository/npm/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs@^15.3.1: + version "15.4.1" + resolved "https://nexus.xuqinmin.com/repository/npm/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + yargs@^17.6.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"