# SuperSDK 使用指南 SuperSDK 是面向前端业务开发的 JavaScript SDK,提供与原生 App 交互的能力。 ## 快速开始 ### 引入 SDK ```html ``` 引入后,SDK 会自动挂载到 `window.SuperSDK`。 ### 环境检测 ```javascript if (SuperSDK.isInApp()) { // 在 App 内运行 } else { // 在浏览器中运行 } ``` ## 非端环境降级(浏览器兼容) SuperSDK 支持在非 App 环境(如浏览器)中运行,提供合理的降级行为,使前端项目在浏览器中也能基本运行和调试。 ### initFallback(config) 在非端环境下初始化降级配置。配置会先校验,端内(有 appbridge)的合法调用才会被忽略。 ```javascript SuperSDK.initFallback({ baseUrls: ['https://api.supersdk.com/superapi'], // 服务端地址列表(用于 reportEvent 上报;业务 path 追加到 baseUrl 后) baseUrl: 'https://api.supersdk.com/superapi', // 兼容旧字段;未传 baseUrls 时使用,可省略 /superapi spid: 'ab12cd34', // 替换为控制台签发的真实 spid(8 位小写字母数字) environment: 'staging', // 可选,缺省 prod;1..10 位小写字母数字 appName: 'MyApp', // 应用名称 appIcon: '', // 应用图标 Base64(可选) deviceId: '', // 设备标识(可选,不传则自动生成) }); ``` **行为规则**: - `config` 必须是 plain object,`spid` 必须匹配 `^[a-z0-9]{8}$`;`environment` 省略时为 `prod`,显式传入时必须匹配 `^[a-z0-9]{1,10}$` - 旧 `aid` 字段不兼容;不 trim、不转换大小写,非法输入立即抛错且不修改已生效配置 - 端内(有 appbridge)也会先执行上述校验;合法时忽略 fallback 配置,所有 API 走原有端内逻辑 - 可多次调用,采用 merge 语义(只更新本次传入的字段,未传入字段保留前次值) - `baseUrls` 优先于 `baseUrl`;未配置时默认 `['https://api.supersdk.com/superapi']`;旧 `baseUrl` 字段会兼容补齐 `/superapi` - 请求会优先使用上次成功的 baseUrl,其余候选随机排序;HTTP 非 2xx 或网络错误时切换下一个 baseUrl - 不调用 `initFallback` 时,环境配置类 API 返回默认空值(不会 reject) **deviceId 说明**: - 未传入时自动生成 UUID 并存入 `localStorage`(key: `supersdk_device_id`),跨会话保持一致 - 清除方式:`localStorage.removeItem('supersdk_device_id')` - 隐私模式下 localStorage 可能受限,deviceId 退化为内存存储(每次会话不同) ### isFallbackMode() 判断当前是否处于降级模式。 ```javascript if (SuperSDK.isFallbackMode()) { // 非端环境且已调用 initFallback // 可用于条件渲染(如隐藏不支持的功能按钮) } ``` 返回 `true` 的条件:非端环境(无 appbridge)且已调用 `initFallback`。 ### 各 API 降级行为 | 分类 | API | 降级行为 | 需要 initFallback | |------|-----|----------|:-----------------:| | **页面导航** | `openPage` / `openExternal` / `openWithBrowser` / `openBrowser` | `window.open(url, '_blank')` | 否 | | | `closePage` | `window.close()` 或 `history.back()` | 否 | | **UI 反馈** | `showToast(msg)` | `console.log('[SuperSDK Toast]:', msg)` | 否 | | | `share(content)` | `console.log('[SuperSDK Share]:', content)` | 否 | | **环境配置** | `getEnv()` | resolve `{ os:'web', spid, environment, appName, deviceId, ... }` | 否(有配置返回更多字段) | | | `setAPIEnvironment(env)` / `getAPIEnvironment()` | web 降级下可运行时切换项目环境;端内 set reject(环境由原生宿主控制)、get 透传原生值 | set 是(需先 initFallback) | | | `getAppName()` / `getAppIcon()` | resolve 配置值或 `""` | 否 | | | Settings 系列 | resolve `{}` 或 `null` | 否 | | **埋点上报** | `reportEvent(name, params)` | `fetch` POST 到 `{baseUrl}/v2/reportEvent`;body 带 `basic_data.host_spid/env`,header 带 `X-Aid=` 与 `X-Super-API-ENV` | 是(需 baseUrl/baseUrls + spid) | | **用户反馈** | `submitFeedback(params)` | `fetch` POST 到 `{baseUrl}/v2/feedback`;body 带 `spid/env/platform=web`,header 带 `X-Aid=` 与 `X-Super-API-ENV`;业务拒绝(`INVALID_REQUEST`/`NOT_FOUND`)不换线路 | 是(需 baseUrl/baseUrls + spid) | | **事件监听** | `subscribe` / `onForeground` / `onBackground` 等 | 正常注册(但不会收到 Native 推送) | 否 | | **不可降级** | `callHostMethod` / `requestShortUrl` / `downloadWithCache` / 文件操作 / ZIP / 归因 / 支付 / nativeBlock 等 | reject,错误信息 `"Not in app environment..."` | - | --- ## API 参考 ### API 概览 | 分类 | 方法 | 说明 | 异步 | |------|------|------|:----:| | **降级配置** | `initFallback(config)` | 初始化非端环境降级配置 | - | | | `isFallbackMode()` | 判断是否处于降级模式 | - | | **环境与配置** | `isInApp()` | 检测是否在 App 内运行 | - | | | `getEnv()` | 获取运行环境信息(版本、设备、离线包等) | ✓ | | | `setAPIEnvironment(env)` | 设置项目环境(web 降级模式生效;端内 reject) | ✓ | | | `getAPIEnvironment()` | 获取当前项目环境(端内透传原生值) | ✓ | | | `getAppName()` | 获取宿主 App 名称 | ✓ | | | `getAppIcon()` | 获取宿主 App 图标(Base64) | ✓ | | | `getHostSettings(configName?)` | 获取宿主云控配置 | ✓ | | | `getHostSettingsByKey(key, configName?)` | 根据 key 获取宿主配置 | ✓ | | **宿主方法** | `callHostMethod(name, params)` | 调用宿主自定义方法 | ✓ | | | `reportEvent(name, params)` | 上报埋点事件 | ✓ | | | `addEventCommonParam(key, value)` | 设置埋点公共参数 | - | | | `submitFeedback(params)` | 提交用户反馈(控制台「业务运营 → 用户反馈」可查) | ✓ | | | `openBrowser(url, title)` | 打开浏览器页面 | - | | | `showToast(message)` | 显示 Toast 提示 | - | | | `setStatusBarDarkMode(isDark)` | 设置状态栏深/浅色 | - | | | `showKeyboard(selector)` | 唤起软键盘(Android 空实现) | - | | **页面导航** | `openPage(url, options)` | 新页面打开 URL | - | | | `closePage()` | 关闭当前页面 | - | | | `openExternal(url)` | 系统方式打开 URL(应用商店、电话等) | - | | | `openWithBrowser(url)` | 系统浏览器打开 URL | - | | **安全区域** | `getSafeArea()` | 获取安全区域(状态栏/导航栏高度) | - | | **分享** | `share(content)` | 调用系统分享 | - | | | `requestShortUrl(url, baseUrl?)` | 长链接生成短链 | ✓ | | | `requestShortUrlWithHtml(html, baseUrl?)` | HTML 内容生成短链(直出渲染) | ✓ | | **生命周期** | `onForeground(callback)` | 监听进入前台事件 | - | | | `onBackground(callback)` | 监听进入后台事件 | - | | **WebView 通信** | `publish(key, data, targetId)` | 向其他 WebView 发送消息 | - | | | `subscribe(key, callback)` | 监听广播消息 | - | | **返回键** | `onBackPressed(callback)` | 监听返回键(可拦截) | - | | | `offBackPressed()` | 取消返回键监听 | - | | **文件下载** | `downloadFile(options)` | 下载文件(支持进度回调) | ✓ | | | `downloadWithCache(fileUrl, fileMd5, cacheTts, fileCacheKey?)` | 下载并缓存文件,命中缓存直接返回本地路径 | ✓ | | **文件操作** | `fileExists(path)` | 检查文件是否存在 | ✓ | | | `getFileMd5(path)` | 获取文件 MD5 | ✓ | | | `deleteFile(path)` | 删除文件或目录 | ✓ | | | `moveFile(src, dest)` | 移动/重命名文件 | ✓ | | | `createDir(path)` | 创建目录 | ✓ | | | `listDir(path)` | 列出目录内容 | ✓ | | | `readFile(path)` | 读取文本文件 | ✓ | | | `writeFile(path, content)` | 写入文本文件 | ✓ | | **ZIP** | `unzipFile(zipPath, destPath)` | 解压 ZIP 文件 | ✓ | | | `zipDir(sourcePath, zipPath)` | 压缩目录为 ZIP | ✓ | | **登录** | `getAuthProviders()` | 获取支持的三方登录方式列表 | ✓ | | | `requestLogin(options)` | 触发三方登录 | ✓ | | **支付**
`SuperSDK.pay.*` | `pay.purchase(payConfig)` | 发起购买(唯一入口 · 聚合 createOrder→pay · 返回三态) | ✓ | | | `pay.getProviders()` | 探测宿主支持的支付方式列表 | ✓ | | | `pay.registerProvider(name, provider)` | 注册 Provider(Web 收银台 / Native IAP override) | - | | | `pay.restorePurchases(options)` | 客户端恢复购买(F036 PurchaseResult[] · provider 可选实现) | ✓ | | **归因** | `getAttribution(onlyFirst)` | 获取归因数据 | ✓ | | **Native Block** | `nativeBlock.init()` | 初始化,扫描静态 `` | - | | | `nativeBlock.create(options)` | 动态创建原生组件 | ✓ | | | `nativeBlock.destroy(blockId)` | 销毁指定组件 | - | | | `nativeBlock.destroyAll()` | 销毁所有组件 | - | | | `nativeBlock.updatePositions()` | 手动更新组件位置 | - | | **响应编码** | `getSuEncCapabilities()` | 查询本端支持的 suenc codec | - | | | `decodeSuEnc(encodedData, codec)` | 解码 suenc 响应体 | - | --- ### 环境与配置 #### getEnv() 获取当前运行环境信息。 ```javascript const env = await SuperSDK.getEnv(); console.log(env); // { // webviewId: "a1b2c3d4", // versionName: "1.0.0", // versionCode: 1010203, // channel: "local_test", // spid: "ab12cd34", // 替换为控制台签发的真实 spid // environment: "prod", // deviceId: "xxx-xxx-xxx", // platform: "android", // osVersion: "14", // deviceModel: "Pixel 8", // wappVersion: "1.0.0", // 当前 WApp(离线包)版本,非离线包页面为 "" // wappBuildTime: 1740198896 // 离线包构建时间(秒级时间戳),非离线包页面为 0 // } ``` #### setAPIEnvironment(environment) / getAPIEnvironment() 项目环境对应控制台顶栏的 env(如 `prod` / `staging`),决定 settings 拉取与埋点上报归属的环境。 ```javascript // web 降级模式:运行时切换项目环境(需先 initFallback) await SuperSDK.setAPIEnvironment('staging'); // 切换后 getHostSettings 请求的 env 参数、reportEvent 的 // basic_data.env 与 X-Super-API-ENV header 均使用新值; // settings 请求缓存按环境隔离,切回旧环境命中原缓存。 // 读取当前项目环境(Promise;web 未初始化时 resolve "prod") const env = await SuperSDK.getAPIEnvironment(); ``` **规则:** - 取值限 1–10 位小写字母/数字(`^[a-z0-9]{1,10}$`),非法值 reject `INVALID_ARGUMENT` 且状态不变;web 未 `initFallback` 时 set reject `NOT_INITIALIZED` - 切换前会尽力冲刷旧环境的待发埋点事件,再换值 - **端内(App 内)**:`setAPIEnvironment` 一律 reject `UNSUPPORTED` —— 端内环境由原生宿主经 `SuperSDK.setAPIEnvironment`(Android)/ `SuperSDKManager.setAPIEnvironment`(iOS)控制;`getAPIEnvironment` 经 `getEnv` 透传原生当前值 - `initFallback(config)` 的 `environment` 字段仍作为 web 模式的初始环境 #### getAppName() 获取宿主 App 名称。 ```javascript const appName = await SuperSDK.getAppName(); // "MyApp" ``` #### getAppIcon() 获取宿主 App 图标(Base64 格式)。 ```javascript const iconBase64 = await SuperSDK.getAppIcon(); // "data:image/png;base64,iVBOR..." // 可直接用于 ``` #### getHostSettings(configName?) 获取宿主配置信息。`configName` 是配置名称,不传则端上请求不带 `configName` 参数。 ```javascript const hostSettings = await SuperSDK.getHostSettings(); const featureSettings = await SuperSDK.getHostSettings('feature_flags'); // { code: 'SUCCESS', msg: '', data: { ... } } 与服务端 settings 响应形态一致 ``` **参数**:`configName` (string, 可选) — 配置名称;不传则请求不带 `configName` 参数。 **返回值**:`Promise` — 宿主云控响应对象,成功时为 `{ code, msg, data }`;`data` 是配置内容。非端环境下 resolve 空对象 `{}`,不会 reject。 #### getHostSettingsByKey(key, configName?) 根据 key 获取宿主特定配置。 ```javascript const config = await SuperSDK.getHostSettingsByKey('feature_flags'); const config = await SuperSDK.getHostSettingsByKey('enabled', 'feature_flags'); ``` **参数**:`key` (string) — 配置项的键。 **参数**:`configName` (string, 可选) — 配置名称;不传则请求不带 `configName` 参数。 **返回值**:`Promise<*>` — 该 key 对应的宿主配置值;端上会优先查响应根对象,未命中时兼容查 `data[key]`。非端环境下 resolve `null`。 --- ### 宿主方法调用 #### callHostMethod(methodName, params) 调用宿主 App 自定义的方法,实现业务扩展。 ```javascript const result = await SuperSDK.callHostMethod('getUserInfo', { id: '123' }); console.log(result); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | methodName | string | 方法名 | | params | object | 参数对象 | > **注意:** 此方法**无超时限制**,Promise 会一直等待宿主返回结果。如需超时控制,请在业务层自行实现。 > > 宿主 App 需实现 `HostMethodService` 接口注册自定义方法。 #### reportEvent(eventName, params) 上报事件到 SDK 内置埋点(通过 `SReporter` 实现)。 ```javascript await SuperSDK.reportEvent('page_view', { page: 'home', duration: 1200 }); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | eventName | string | 事件名称 | | params | object | 事件参数(可选) | #### openBrowser(url, title) 打开浏览器页面(SDK 内置实现,Android 使用系统浏览器,iOS 使用 SFSafariViewController)。 ```javascript SuperSDK.openBrowser('https://example.com', '示例页面'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | url | string | 要打开的 URL | | title | string | 页面标题 | #### showToast(message) 显示 Toast 提示(SDK 内置实现,Android 使用系统 Toast,iOS 使用自定义浮层)。 ```javascript SuperSDK.showToast('操作成功'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | message | string | 消息内容 | > `reportEvent`、`showToast`、`openBrowser` 均为 SDK 内置实现,无需宿主额外处理。 #### addEventCommonParam(key, value) 设置埋点公共参数。设置后,本 WebView 后续所有 `reportEvent` 上报都会自动带上该公共参数(常用于统一注入页面来源、AB 实验分组等上下文)。 ```javascript SuperSDK.addEventCommonParam('source', 'home_banner'); SuperSDK.addEventCommonParam('ab_group', 'B'); // 之后的 reportEvent 都会带上 source / ab_group ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | key | string | 公共参数键 | | value | string | 公共参数值(非字符串会被转为字符串,`null`/`undefined` 转为空串) | > 同步方法,**无返回值**。非端环境(无 appbridge)下为 no-op,不抛错。 #### submitFeedback(params) 提交一条用户反馈。端内经 JSB 由 Native 补齐 `spid / env / device_id / platform / app_version / sdk_version / os_version / device_model` 后上报到 `POST /superapi/v2/feedback`;非端环境(已 `init`/`initFallback` 提供 `baseUrl(s)` + `spid`)由 JSSDK 直接 `fetch`。服务端记录提交时间与用户 IP,控制台「业务运营 → 用户反馈」按项目 × 环境查看、搜索并标记处理状态。 ```javascript const { feedbackId } = await SuperSDK.submitFeedback({ scene: 'settings', // 必填 · 反馈场景 subScene: 'about', // 可选 · 子场景 title: '打开设置页闪退', // 必填 content: '进入设置页后约 1 秒必现闪退,Pixel 8 / Android 14', // 必填 userId: 'u_123456', // 可选 · 业务用户 ID(SDK 不维护用户身份,由调用方按次传入) extra: { page: 'settings', build: 42 } // 可选 · 附加信息 }); ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|:----:|------| | scene | string | ✓ | 反馈场景,≤64 字符 | | subScene | string | | 子场景,≤64 字符 | | title | string | ✓ | 标题,≤200 字符 | | content | string | ✓ | 反馈内容,≤5000 字符 | | userId | string | | 业务用户 ID,≤128 字符;未传则控制台 user_id 为空 | | extra | object | | 附加信息,JSON object,序列化后 ≤4KB | **返回值:** `Promise<{ feedbackId: number }>` —— 服务端反馈记录 ID。 **错误:** | 场景 | 端内(JSB) | web | |------|------------|-----| | 参数不合法(长度/类型) | reject `Error('INVALID_PARAMS')`,`err.detail` 为原因 | `SuperSDKError` `INVALID_ARGUMENT` | | 未初始化 | - | `NOT_INITIALIZED` | | 服务端业务拒绝 | 字段被服务端拒绝 → reject `Error('INVALID_ARGUMENT')`;项目/环境不存在 → reject `Error('BRIDGE_ERROR')`(Native 按 JSB 注册表归一 code,`err.detail` 为脱敏 message) | `INVALID_ARGUMENT`,`err.nativeCode` 为服务端 code(`INVALID_REQUEST` / `NOT_FOUND` …) | | 触发限流(每 IP 10 次/分钟 · 每项目 600 次/分钟) | reject `Error('RATE_LIMITED')` | `RATE_LIMITED` | | 网络/超时 | reject `Error('NETWORK_ERROR' / 'TIMEOUT')` | `NETWORK_ERROR` / `TIMEOUT` / `UNAVAILABLE`(自动轮换 baseUrls) | | Native 过旧(无此 JSB) | reject `Error('SDK_TOO_OLD')` | - | > 字段长度以服务端校验为准(JSSDK 本地校验只是同一规则的镜像);所有字符串会被 trim。提交**不是幂等**的,请勿在业务侧对同一内容重复重试。 #### setStatusBarDarkMode(isDark) 设置状态栏深色/浅色模式。 ```javascript // 深色背景页面:状态栏图标显示为白色 SuperSDK.setStatusBarDarkMode(true); // 浅色背景页面:状态栏图标显示为黑色 SuperSDK.setStatusBarDarkMode(false); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | isDark | boolean | `true`=深色背景(状态栏图标白色);`false`=浅色背景(图标黑色) | > 同步方法,**无返回值**。非端环境下为 no-op,不抛错。 #### showKeyboard(selector) 唤起软键盘。 ```javascript SuperSDK.showKeyboard('#search-input'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | selector | string | 目标输入框的 CSS 选择器(可选,iOS 使用) | > 同步方法,**无返回值**。非端环境下为 no-op,不抛错。 > > **平台差异**:Android 为**空实现**(WebView 中对输入框调用 JS `focus()` 即可自动唤起键盘,无需此方法);iOS 转发到 Native 处理。 #### onJSBridgeReady(内部接口) > ⚠️ **内部接口,业务代码请勿调用**。`onJSBridgeReady` 是 SDK 在脚本加载时自动调用的就绪通知,用于让 Native 端 flush 在 JSBridge 就绪前缓冲的消息。业务无需也不应主动调用。 --- ### 页面导航 #### openPage(url, options) 在新的全屏 WebView 页面中打开 URL。 ```javascript // 打开新页面 SuperSDK.openPage('https://example.com'); // 带配置(当前 config 暂不配置任何功能) SuperSDK.openPage('https://example.com', {}); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | url | string | 要打开的 URL | | options | object | 配置对象(当前暂无可用配置,预留扩展) | > **说明:** 打开的新页面是完整的全屏 WebView,无原生标题栏遮挡。页面可通过 `closePage()` 关闭。 #### closePage() 关闭当前页面。 ```javascript SuperSDK.closePage(); ``` > 注意:在 TAB 页面中调用此方法无效果。 #### openExternal(url) 使用系统方式打开 URL(跳转到外部应用)。 ```javascript // 打开应用商店 SuperSDK.openExternal('market://details?id=com.example.app'); // 拨打电话 SuperSDK.openExternal('tel:10086'); // 发送邮件 SuperSDK.openExternal('mailto:test@example.com'); ``` #### openWithBrowser(url) 使用系统浏览器打开 URL。 ```javascript SuperSDK.openWithBrowser('https://www.google.com'); ``` --- ### 安全区域 #### getSafeArea() 获取安全区域信息(从 UserAgent 解析),用于适配刘海屏、全面屏等设备。 ```javascript const safeArea = SuperSDK.getSafeArea(); console.log(safeArea); // { top: 88, bottom: 34 } // top: 状态栏高度(物理像素) // bottom: 导航栏高度(物理像素) ``` **返回值:** | 参数 | 类型 | 说明 | |------|------|------| | top | number | 状态栏安全区域高度(物理像素) | | bottom | number | 导航栏安全区域高度(物理像素) | > 注意:此方法为同步方法,直接返回结果。安全区域信息由客户端通过 UserAgent 注入(`SafeAreaTop/xx SafeAreaBottom/xx`),需要客户端调用 `setSafeArea` 或 `fitSystemWindows` 后才能获取到正确的值。 --- ### 分享 #### share(content) 调用系统分享功能。 ```javascript SuperSDK.share('分享内容文本'); ``` #### requestShortUrl(url, baseUrl?) 生成短链。将长链接转换为短链,便于分享。 ```javascript // 使用默认前缀 const shortUrl = await SuperSDK.requestShortUrl('https://example.com/very/long/path?param=value'); console.log(shortUrl); // "https://s.supersdk.com/AbCd12" // 使用自定义前缀 const shortUrl2 = await SuperSDK.requestShortUrl('https://example.com/very/long/path', 'https://s.a.com/'); console.log(shortUrl2); // "https://s.a.com/AbCd12" ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | url | string | 是 | 原始长链接 | | baseUrl | string | 否 | 自定义短链前缀,如 `https://s.a.com/`。不传使用默认值 | **返回值:** `Promise` - 短链字符串 **特性:** - 相同 URL 重复调用返回相同短链(幂等) - 短链永久有效 - 超时时间 10 秒 **错误处理:** ```javascript try { const shortUrl = await SuperSDK.requestShortUrl('https://example.com/page'); // 使用短链 } catch (error) { console.error('生成短链失败:', error.message); // 可能的错误:URL is required, 网络异常, appbridge not available } ``` #### requestShortUrlWithHtml(html, baseUrl?) 通过 HTML 内容生成短链。访问生成的短链时直接渲染 HTML 页面,不做 302 跳转。适用于生成动态分享页面、活动海报等场景。 ```javascript // 使用默认前缀 const shortUrl = await SuperSDK.requestShortUrlWithHtml('

Hello World

'); console.log(shortUrl); // "https://s.supersdk.com/XyZ789" // 使用自定义前缀 const shortUrl2 = await SuperSDK.requestShortUrlWithHtml('

活动页面

', 'https://s.a.com/'); console.log(shortUrl2); // "https://s.a.com/XyZ789" ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | html | string | 是 | HTML 内容(完整文档或片段) | | baseUrl | string | 否 | 自定义短链前缀,如 `https://s.a.com/`。不传使用默认值 | **返回值:** `Promise` - 短链字符串 **特性:** - 每次调用生成新短链(不做幂等去重) - 短链永久有效 - HTML 内容大小限制 1MB - 超时时间 10 秒 **错误处理:** ```javascript try { const shortUrl = await SuperSDK.requestShortUrlWithHtml('

活动页面

活动详情...

'); // 使用短链分享 } catch (error) { console.error('生成短链失败:', error.message); // 可能的错误:HTML content is required, 内容超过大小限制, 网络异常 } ``` --- ### 生命周期事件 #### onForeground(callback) 监听页面进入前台事件。 ```javascript const unsubscribe = SuperSDK.onForeground((data) => { console.log('进入前台', data.triggerFrom); // triggerFrom: 'window' | 'view' | 'activity' }); // 取消监听 unsubscribe(); ``` **triggerFrom 说明:** | 值 | 说明 | |------|------| | window | 窗口可见性变化(如从其他应用切回) | | view | View 可见性变化(如 TAB 切换) | | activity | Activity 生命周期变化(onResume) | #### onBackground(callback) 监听页面进入后台事件。 ```javascript const unsubscribe = SuperSDK.onBackground((data) => { console.log('进入后台', data.triggerFrom); }); ``` --- ### WebView 间通信 #### publish(eventKey, data, targetWebviewId) 向其他 WebView 发送消息。 ```javascript // 广播到所有 WebView SuperSDK.publish('user_login', { userId: '123' }); // 发送到指定 WebView SuperSDK.publish('refresh_data', { type: 'order' }, 'a1b2c3d4'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | eventKey | string | 事件标识符 | | data | object | 消息数据 | | targetWebviewId | string | 目标 WebView ID,省略或传 '*' 广播到所有 | #### subscribe(eventKey, callback) 监听来自其他 WebView 或客户端的广播消息。 ```javascript const unsubscribe = SuperSDK.subscribe('user_login', (data) => { console.log('收到消息:', data); }); // 取消监听 unsubscribe(); ``` > **提示:** `subscribe` 既可以接收来自其他 WebView 的 `publish` 消息,也可以接收客户端通过 `sendBroadcast` / `broadcastToAll` 发送的广播。 #### 客户端广播(Native → 前端) 客户端(宿主 App)可以通过 SDK API 向 WebView 发送广播消息,前端使用 `subscribe` 接收。 **客户端调用方式:** ```java // Android - 向当前 WebView 发送广播 pageView.sendBroadcast("login_changed", "{\"isLogin\":true}"); // Android - 向所有 WebView 广播 SuperSDK.broadcastToAll("config_updated", "{\"key\":\"value\"}"); ``` ```swift // iOS - 向当前 WebView 发送广播 pageView.sendBroadcast(eventKey: "login_changed", jsonData: "{\"isLogin\":true}") // iOS - 向所有 WebView 广播 SuperSDK.broadcastToAll(eventKey: "config_updated", jsonData: "{\"key\":\"value\"}") ``` **前端接收方式:** ```javascript // 监听客户端广播(使用 subscribe,与 WebView 间通信相同) SuperSDK.subscribe('login_changed', (data) => { console.log('登录状态变化:', data); // data = { isLogin: true } }); SuperSDK.subscribe('config_updated', (data) => { console.log('配置更新:', data); }); ``` > **注意:** 如果广播在 JSBridge 就绪之前发送,消息会被缓冲,JSBridge 就绪后自动送达。 --- ### 返回键监听与拦截 #### onBackPressed(callback) 监听用户按下返回键事件,并可以选择拦截。同一时间只有一个监听器生效,后注册的覆盖前面的。 ```javascript SuperSDK.onBackPressed(() => { if (hasUnsavedData) { showConfirmDialog(); return true; // 返回 true:拦截返回,页面不关闭 } return false; // 返回 false:不拦截,执行默认返回逻辑 }); ``` **callback 返回值说明:** | 返回值 | 行为 | |--------|------| | `true` | 拦截返回键,页面停留在当前页,不执行默认返回逻辑 | | `false` | 不拦截,执行默认返回逻辑(有历史则后退,否则关闭页面) | | `undefined`(无返回值) | 等同于 `false`,不拦截 | **使用场景示例:** ```javascript // 场景1:离开页面前确认 SuperSDK.onBackPressed(() => { if (formModified) { const confirmed = confirm('有未保存的修改,确定离开吗?'); if (!confirmed) { return true; // 用户选择留下,拦截返回 } } return false; // 允许返回 }); // 场景2:自定义返回逻辑(如关闭弹窗) SuperSDK.onBackPressed(() => { if (isModalOpen) { closeModal(); return true; // 关闭弹窗,拦截返回 } return false; // 无弹窗,正常返回 }); ``` #### offBackPressed() 取消返回键监听,恢复默认返回行为。 ```javascript SuperSDK.offBackPressed(); ``` > **注意事项:** > 1. callback 必须**同步返回**结果,不支持异步(async/await) > 2. callback 执行超时(>300ms)时,Native 自动执行默认返回逻辑,防止卡死 > 3. Android 物理返回键和手势返回均会触发 > 4. iOS 目前仅导航栏返回按钮会触发,侧滑返回手势暂不支持拦截 --- ### 文件下载 #### downloadFile(options) 下载文件到本地存储。 ```javascript const cancel = SuperSDK.downloadFile({ url: 'https://example.com/file.zip', savePath: 'downloads/file.zip', headers: { 'Authorization': 'Bearer token123' }, onStart: () => { console.log('开始下载'); }, onProgress: ({ progress, downloadedBytes, totalBytes }) => { console.log(`下载进度: ${progress}%`); }, onSuccess: ({ filePath, downloadedBytes, totalBytes }) => { console.log('下载完成:', filePath); }, onFail: ({ error }) => { console.error('下载失败:', error); } }); // 取消下载 cancel(); ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | url | string | 是 | 文件下载地址 | | savePath | string | 是 | 保存路径(相对于 SDK 文件存储目录) | | headers | object | 否 | 请求头 | | onStart | function | 否 | 开始下载回调 | | onProgress | function | 否 | 进度回调,约 300ms 触发一次 | | onSuccess | function | 否 | 成功回调 | | onFail | function | 否 | 失败回调 | **onProgress 回调参数:** | 参数 | 类型 | 说明 | |------|------|------| | progress | number | 下载进度 0-100 | | downloadedBytes | number | 已下载字节数 | | totalBytes | number | 文件总字节数 | **onSuccess 回调参数:** | 参数 | 类型 | 说明 | |------|------|------| | filePath | string | 文件绝对路径 | | downloadedBytes | number | 已下载字节数 | | totalBytes | number | 文件总字节数 | **onFail 回调参数:** | 参数 | 类型 | 说明 | |------|------|------| | error | string | 错误信息 | #### downloadWithCache(fileUrl, fileMd5, cacheTts, fileCacheKey) 下载文件并写入 SDK 缓存目录。缓存命中时不会重新下载,直接返回本地文件路径。 ```javascript const result = await SuperSDK.downloadWithCache( 'https://example.com/assets/file.zip?token=abc', 'd41d8cd98f00b204e9800998ecf8427e', 86400, 'asset-file-v1' ); console.log(result); // { filePath: '/path/to/download_cache/xxx.zip', cacheHit: true, cacheKey: 'asset-file-v1' } ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | fileUrl | string | 是 | 文件下载地址 | | fileMd5 | string | 否 | 文件 MD5;为空时跳过 MD5 校验 | | cacheTts | number | 否 | 缓存有效期,单位秒;`<= 0` 时强制重新下载 | | fileCacheKey | string | 否 | 自定义缓存 key;为空或不传时使用去掉 query 的 `fileUrl` | **缓存规则:** - 缓存 key 优先使用非空 `fileCacheKey`,否则使用去掉 query 的 `fileUrl`。 - Native 会把缓存 key 转成安全文件名,不直接拼接 URL 到文件路径。 - `fileMd5` 非空时会校验缓存文件和新下载文件;新下载文件 MD5 不匹配时失败,且不会覆盖旧缓存。 **返回值:** | 参数 | 类型 | 说明 | |------|------|------| | filePath | string | 本地缓存文件绝对路径 | | cacheHit | boolean | 是否命中缓存 | | cacheKey | string | 本次实际使用的原始缓存 key | --- ### 文件操作 > 所有路径均为相对于 SDK 文件存储目录的相对路径 #### fileExists(path) 检查文件或目录是否存在。 ```javascript const info = await SuperSDK.fileExists('downloads/file.zip'); console.log(info); // { exists: true, isFile: true, isDirectory: false } ``` **返回值:** | 参数 | 类型 | 说明 | |------|------|------| | exists | boolean | 是否存在 | | isFile | boolean | 是否为文件 | | isDirectory | boolean | 是否为目录 | #### getFileMd5(path) 获取文件的 MD5 值。 ```javascript const { md5 } = await SuperSDK.getFileMd5('downloads/file.zip'); console.log('MD5:', md5); ``` #### deleteFile(path) 删除文件或目录(递归删除)。 ```javascript await SuperSDK.deleteFile('downloads/old_file.zip'); ``` #### moveFile(srcPath, destPath) 移动或重命名文件。 ```javascript await SuperSDK.moveFile('downloads/temp.zip', 'cache/final.zip'); ``` #### createDir(path) 创建目录(支持递归创建)。 ```javascript await SuperSDK.createDir('data/cache/images'); ``` > 如果目录已存在,不会报错。 #### listDir(path) 列出目录内容。 ```javascript const { files } = await SuperSDK.listDir('downloads'); files.forEach(file => { console.log(file.name, file.isFile ? '文件' : '目录', file.size); }); ``` **返回值 files 数组元素:** | 参数 | 类型 | 说明 | |------|------|------| | name | string | 文件名 | | isFile | boolean | 是否为文件 | | isDirectory | boolean | 是否为目录 | | size | number | 文件大小(字节) | | lastModified | number | 最后修改时间(时间戳) | #### readFile(path) 读取文本文件内容。 ```javascript const { content } = await SuperSDK.readFile('config/settings.json'); const settings = JSON.parse(content); ``` #### writeFile(path, content) 写入文本文件内容。 ```javascript const settings = { theme: 'dark', lang: 'zh' }; await SuperSDK.writeFile('config/settings.json', JSON.stringify(settings)); ``` --- ### ZIP 压缩与解压 #### unzipFile(zipPath, destPath) 解压 ZIP 文件到指定目录。 ```javascript await SuperSDK.unzipFile('downloads/archive.zip', 'extracted/'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | zipPath | string | ZIP 文件路径 | | destPath | string | 解压目标目录 | > **安全限制**:含恶意路径条目(如 `../`)的 zip 将解压失败,不会写出到目标目录之外。 #### zipDir(sourcePath, zipPath) 将目录压缩为 ZIP 文件。 ```javascript await SuperSDK.zipDir('data/logs/', 'backup/logs.zip'); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | sourcePath | string | 要压缩的目录路径 | | zipPath | string | 生成的 ZIP 文件路径 | > 注意:压缩和解压操作可能需要较长时间,超时时间为 60 秒。 --- ### 登录 SDK 提供三方登录桥接能力。H5 通过 SDK 触发原生三方登录(Google / Apple 等),拿到三方凭证后自行调业务后端换 token。 #### getAuthProviders() 获取宿主 App 支持的三方登录方式列表。 ```javascript const providers = await SuperSDK.getAuthProviders(); // ['google', 'apple'] ``` **返回值:** `string[]` — Provider 名称数组 #### requestLogin(options) 触发指定方式的三方登录。 ```javascript try { const credential = await SuperSDK.requestLogin({ provider: 'google' }); console.log(credential.provider); // 'google' console.log(credential.token); // 主凭证(Google ID Token / Apple Identity Token) console.log(credential.authorizationCode); // 授权码 console.log(credential.email); // 用户邮箱 console.log(credential.displayName); // 用户名 // 拿到凭证后,H5 自行调后端换业务 token const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credential) }); const { token } = await res.json(); } catch (e) { if (e.message === 'CANCELLED') { // 用户取消,不弹错误 } else { console.error('登录失败:', e.message); } } ``` **参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | options.provider | string | 是 | 登录方式名称(从 getAuthProviders 获取) | **返回值:** `ThirdPartyCredential` | 字段 | 类型 | 说明 | |------|------|------| | provider | string | 登录方式名称(始终存在) | | token | string | 主凭证(Google=ID Token, Apple=Identity Token) | | authorizationCode | string | 授权码(部分 Provider) | | email | string | 用户邮箱 | | displayName | string | 用户显示名 | | photoUrl | string | 用户头像 URL | | raw | object | Provider 原始数据 | > 不同 Provider 返回的字段不同,仅 `provider` 保证存在。 **错误码:** | code | 含义 | 建议处理 | |------|------|---------| | CANCELLED | 用户取消登录 | 静默处理,不弹错误提示 | | PROVIDER_NOT_FOUND | 未注册的 Provider | 检查 getAuthProviders 返回值 | | NOT_IMPLEMENTED | 宿主未实现 LoginService | 联系宿主开发者 | | ERROR | 其他错误 | 展示错误信息 | --- ### 支付(SuperSDK.pay namespace) SDK 提供应用内支付桥接能力 · H5 通过 `SuperSDK.pay.*` 调用。 > 🔴 **F038 破坏式变更(继 F036 后第二次)**:`pay.purchase` 返回从 F036 的 `PurchaseResult` 凭证对象改为 **PayResult3State 三态对象**。统一为「**provider.createOrder → provider.pay → 三态回调**」契约;凭证不再经 H5 业务 JS(由 SDK Native `PayBridge` 直投后端回执端点)。**现网 H5 必须配套升级**(无兼容期 · 无 alias)。详见下方 [F038 迁移指南](#f038-迁移指南从-f036-purchaseresult-迁到三态)。 > 🔴 **SDK 是哑管道(v0.3 设计原则)**:provider = **两个方法** `{ createOrder(payParams)→json, pay(json)→三态 }`。createOrder 产出的 `json` 对 SDK **完全不透明** —— SDK 不解析其中任何字段(productId / accountId / profileId / orderToken 都不碰),只按 `provider` 名路由、把 `json` 原样透传给 `pay`。字段语义由 provider 实现方(createOrder 产出 ↔ pay 消费)自洽保障。 **业务方前提**(必读): 本能力假定业务方**已具备服务端主导 IAP 架构**: - 创单 / 凭证验签(verify) / acknowledge / consume / 查单:全部由业务后端实现(调 Google Play Developer API + App Store Server API)· SDK 不提供 - SDK 仅负责 ① 按 provider 名路由 + 把 createOrder 产出的不透明 json 透传给 pay ② 内置 `google_play` / 宿主 Apple provider 的原生扣款 ③ 由 Native `PayBridge` 把凭证直投后端回执端点(凭证不出 Native)④ 回调白名单三态 ⑤ 探测能力 ⑥ 客户端 restore(可选) - 详细架构见下方 [IAP 推荐架构](#iap-推荐架构)章节(v0.3 已重写) ⚠️ **Apple Guideline 3.1.1 合规提醒**:订阅 / 非消耗型商品 App 必须提供 "Restore Purchases" 按钮。可走 SDK `pay.restorePurchases`(provider 不实现时返回 NOT_SUPPORTED · 业务方 fallback 到业务后端 `/api/pay/my-purchases`)。漏做审核会 reject。 #### pay.querySubscriptionOffers(options) 查询 Google Play **当前账号此刻可购买**的订阅 offers。数据只来自客户端 `queryProductDetailsAsync(ProductType.SUBS)` 返回的 `SubscriptionOfferDetails`;SDK 不查询 Play Console 完整目录、不读 Settings、不合并业务文案,也不负责 UI 排序。 ```javascript const offers = await SuperSDK.pay.querySubscriptionOffers({ provider: 'google_play', productIds: ['matrixvplan01'] }); // 普通 base plan 的 offerId 明确为 null // offerToken 永远不会暴露给 H5 console.log(offers[0]); // { // productId: 'matrixvplan01', // basePlanId: 'weekly01', // offerId: null, // offerTags: [], // title: 'Go VPN', // description: '...', // pricingPhases: [{ // formattedPrice: 'US$2.69', // currencyCode: 'USD', // priceAmountMicros: 2690000, // billingPeriod: 'P1W', // billingCycleCount: 0, // recurrenceMode: 1 // }] // } ``` | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `options.provider` | string | 否 | 单 provider 可省;Google 内置名为 `google_play` | | `options.productIds` | string[] | **是** | 非空订阅 product ID 列表 | 返回 `Promise`。同一 base plan 可以返回多条当前 eligible offer;Play 未向当前账号返回的促销不会被 SDK 拼接出来。Android 支持;iOS 明确返回 `NOT_SUPPORTED`。 查询错误会 reject:`PRODUCT_NOT_FOUND`(请求商品均未返回)、`BILLING_UNAVAILABLE`(Billing 服务不可用)、`QUERY_FAILED`(其他查询失败)、`SDK_TOO_OLD`(Native 尚无该 JSB)或 `NOT_IN_APP`。 #### pay.purchase(payConfig) 唯一支付入口。**内部聚合 `provider.createOrder → provider.pay` 两段**(公共面仍仅此一个入口 · 不新增 `createAndPay`)。 执行编排(v0.3 哑管道): ``` 校验 payConfig(SDK 仅校验 payConfig/payParams 是对象 · 不碰业务字段) → json = provider.createOrder(payParams) [provider 实现 · 缺省=identity 透传 payParams] → provider.pay(json) [IAP=SDK 内置原生自读 json · 三方=业务 JS 自写] → 回调三态 PayResult3State { status: finished | cancelled | error } ``` ```javascript // IAP(provider 注册 createOrder-only override · pay 走 SDK 内置原生 · 见 registerProvider) const result = await SuperSDK.pay.purchase({ provider: 'google_play', // 🔴 内置 Google provider 名(非 'google')· 以 getProviders() 返回为准 · 单 Provider 可省 payParams: { // createOrder 入参 · 由你的 createOrder 消费 · SDK 不解析 productId: 'sku_premium', userId: currentUser.id } }); // result 是三态对象 PayResult3State(不是凭证!) switch (result.status) { case 'finished': // 平台已受理 ≠ 支付成功 · 去业务查单定真相 · 不在客户端发货 pollBusinessOrder(result.orderToken); break; case 'cancelled': // 用户取消 break; case 'error': // 失败 · result.phase('create'|'pay') + result.code console.error(result.phase, result.code, result.message); break; } ``` **三方支付 inline 模式**(无需 registerProvider · 单次性 · 两方法都内联): ```javascript const result = await SuperSDK.pay.purchase({ payParams: { productId: pkg.id, userId: uid }, createOrder: async (payParams) => { // 可选 · 产出对 SDK 不透明的 json(你的 pay 自己消费) const res = await myApi.createOrder(payParams); return { prepayId: res.prepay_id, orderToken: res.order_token }; }, pay: async (json) => { // 必填(inline) · 自行 checkout + 轮询 · 返回三态 // json 是你 createOrder 产出的对象(orderToken 是你自己产的 · 当场就持有 · 无需 SDK 回传) const { payUrl } = await myApi.checkout(json.prepayId); window.open(payUrl, '_blank'); const status = await pollChannel(json.orderToken); // 业务自实现轮询 if (status === 'completed') return { status: 'finished', orderToken: json.orderToken }; if (status === 'cancelled') return { status: 'cancelled' }; return { status: 'error', phase: 'pay', code: status, orderToken: json.orderToken }; } }); ``` **payConfig 参数**: | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | provider | string | 否 | 支付方式名(单 Provider 自动选 · 多 Provider 必填)· 以 `getProviders()` 返回为准 | | payParams | object | **是** | createOrder 入参 · **任意对象**(SDK 仅校验是对象 · 字段由你的 createOrder 消费 · SDK 不解析) | | createOrder | function | 否 | inline 模式 · `(payParams) => Promise` · 缺省时 json=payParams(identity 透传) | | pay | function | 否 | inline 模式 · `(json) => Promise` · 有此则走 inline JS provider;无此则走内置原生 IAP | > 🔴 **删除 v0.2 的 `onOrderCreated` / `checkout` / `queryStatus` / `pollInterval` / `maxPolls` / `onProgress`**:v0.3 哑管道下,这些都是 provider **自己实现**的事(三方在自己的 `pay` 里 checkout+轮询;orderToken 是业务 createOrder 自己产的 · 当场持有 · 无需 SDK 回调还给业务)。 **输入契约(无法开始 → reject)**: > 仅「无法开始」reject(`INVALID_PARAMS` / `NOT_IN_APP` / `SDK_TOO_OLD` / `PURCHASE_IN_PROGRESS` / `PROVIDER_REQUIRED` / `NO_PROVIDER`);**已开始流程的终态(create/pay 失败、取消、结束)一律 resolve 三态**。 | 输入 | 行为 | |---|---| | `pay.purchase(undefined/null/'x')` | reject `INVALID_PARAMS` · err.detail: "payConfig must be object" | | `pay.purchase({})` 或 payParams 非对象 | reject `INVALID_PARAMS` · err.detail: "payParams must be object" | | Native 路径但非端环境 | reject `NOT_IN_APP` | | Native 路径但老 SDK 无 `appbridge.nativePay` | reject `SDK_TOO_OLD`(明示升级 · 不静默 fallback 老 purchase) | | 多 Provider 不传 provider | reject `PROVIDER_REQUIRED` | | 无任何 provider | reject `NO_PROVIDER` | | 一笔 inline 进行中再调 | reject `PURCHASE_IN_PROGRESS` | > 🔴 **v0.3 不再校验 productId**:SDK 不碰业务字段。`productId` 缺失/类型错由 **provider 自己**处理(内置 `google_play` 的 pay 读不到 productId → 返回 `phase:'pay'` error)。 **返回值**:`Promise` — **统一三态对象**(IAP 与三方路径返回结构一致 · 由 Native `PayBridge` 白名单构造): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | status | enum | 是 | `finished` / `cancelled` / `error` | | phase | enum | error 时 | `create`(createOrder 阶段失败 · 在 JSSDK 内捕获 · 未调 pay) / `pay`(pay 阶段平台计费错误) | | code | string | error 时 | 平台错误码映射(如 `BILLING_UNAVAILABLE` / `CREATE_FAILED`) | | message | string | 否 | 调试信息(非敏感) | | orderToken | string | 否 | provider 从其 pay 内部结果回带的关联键(**唯一业务键** · 供 H5 查单) | > 🔴 **闭合白名单(凭证不出 Native)**:发给 H5 的对象**只含上表字段**,由 Native `PayBridge` 显式构造 —— **不**把 provider 内部结果原样透传。`purchaseToken` / `signature` / `originalJson` 等凭证**绝不**到达 H5(由 PayBridge 投回执端点)。 > > 🔴 **`finished` ≠ 支付成功**:语义为「客户端流程正常结束 · 去查单」。Google `PENDING`(现金/家长 Ask-to-Buy/慢速支付)与 iOS 等价待定态映射为 `finished`;Google `ITEM_ALREADY_OWNED` 也映射为 `finished`(去查单 · 后端对账补发)而非 error。**H5 一律以业务查单结果判定成功/待定/失败 · 不在客户端发货/解锁**(见下方[业务查单四态](#业务查单四态finished-后)) 。 #### pay.getProviders() 探测宿主支持的支付方式列表(合并 JS 注册的 + Native 注册的)。 ```javascript const providers = await SuperSDK.pay.getProviders(); // 例如:['google_play', 'alipay'] ← 实际值以本方法返回为准 ``` > 🔴 **provider 名以 `pay.getProviders()` 返回为准 · 不要硬编码**。内置 Google provider 注册名是 **`'google_play'`**(`GooglePayProvider.getProviderName()` 硬编码 · **不是** `'google'`)。iOS 无内置支付 provider · Apple 的 provider 名由宿主 `ApplePayProvider` 实现时自定义。**推荐先调 `getProviders()` 拿返回值再传给 `purchase` / `registerProvider`**,避免硬编码错名导致 `PROVIDER_NOT_FOUND`。下文示例为说明清晰用 `'google_play'` 字面量,生产代码应用 `getProviders()` 的返回值。 #### pay.registerProvider(name, provider) 注册支付 Provider。v0.3 起 provider = **两个方法** `{ createOrder?, pay? }`(至少有一个)· **由 `pay` 是否存在决定路由**: **形态 A · 三方支付**(支付宝 / 微信 / Stripe 等非 IAP):**有 `pay`**(可附 `createOrder`)。`pay` 自行 checkout + 轮询 · 返回三态。 ```javascript SuperSDK.pay.registerProvider('alipay', { createOrder: async (payParams) => { // 可选 · 产出对 SDK 不透明的 json(你的 pay 消费) const res = await myApi.createOrder(payParams); return { prepayId: res.prepay_id, orderToken: res.order_token }; }, pay: async (json) => { // 必填(三方) · 自行拉起渠道 + 轮询 · 返回 PayResult3State const { payUrl } = await myApi.checkout(json.prepayId); window.open(payUrl, '_blank'); const status = await pollChannel(json.orderToken); // 业务自实现 if (status === 'completed') return { status: 'finished', orderToken: json.orderToken }; if (status === 'cancelled') return { status: 'cancelled' }; return { status: 'error', phase: 'pay', code: status, orderToken: json.orderToken }; } }); ``` **形态 B · IAP createOrder override**(内置 Google = `'google_play'` / 宿主 Apple provider 名见 `getProviders()`):**仅 `createOrder`**(无 `pay`)· pay 走 SDK 内置原生扣款(`appbridge.nativePay`)。用于让 IAP 的 createOrder 产出你约定的 json(供内置原生 pay 自读)。 > 🔴 **注册名必须与内置/宿主 provider 名完全一致**(内置 Google 是 `'google_play'`,**不是** `'google'`)· 否则 `purchase` 时 `PROVIDER_NOT_FOUND`。建议用 `(await SuperSDK.pay.getProviders())` 的返回值作 name,不要硬编码。 ```javascript const [googleName] = (await SuperSDK.pay.getProviders()) .filter(n => n === 'google_play'); // 取真实内置名 · 不硬编码 SuperSDK.pay.registerProvider(googleName /* 'google_play' */, { createOrder: async (payParams) => { // 业务自行 fetch 创单端点建单 · 返回内置 google_play provider 的 pay 输入 json(见下「google provider 输入键」) const res = await myApi.createOrder(payParams); // 🔴 后端创单端点签发 UUID orderToken return { productId: payParams.productId, // 必填 productType: payParams.productType ?? 'inapp', // 'inapp'(默认)或'subs' basePlanId: payParams.basePlanId, // productType='subs'时必填 offerId: payParams.offerId, // 订阅优惠可选;省略时购买 base plan accountId: payParams.accountId, // 可选 → obfuscatedAccountId(用户标识) profileId: res.order_token // 可选 → obfuscatedProfileId(订单标识)· 🔴 必须 UUID }; } // 无 pay → SDK 内置 google_play 原生 pay 消费上面的 json(SDK 不解析 · 由内置 provider.pay 自读) }); ``` > 🔴 **orderToken 必须 UUID**(`profileId` / iOS `appAccountToken` 同值):若纯前端生成,用 `crypto.randomUUID()`;若后端签发,后端创单端点须返回 UUID 形态。非 UUID → 内置 `google_play` / iOS host provider 返回 `{ phase:'pay', code:'INVALID_ORDER_TOKEN' }`。 > > ```javascript > // 前端生成 orderToken 示例(无后端创单时) > createOrder: async (payParams) => ({ > productId: payParams.productId, > profileId: crypto.randomUUID() // 🔴 UUID · 双端(Android obfuscatedProfileId / iOS appAccountToken)通吃 > }) > ``` **provider 字段**: | 字段 | 形态 | 说明 | |---|---|---| | createOrder | A 可选 / B 必填 | `(payParams) => Promise` · 产出对 SDK 不透明的 json · 缺省=identity(json=payParams) | | pay | A 必填 / B 无 | `(json) => Promise` · 有它=三方 JS pay;无它=内置原生 IAP pay | > 🔴 **provider 契约(v0.3 哑管道)**:`pay.purchase` 内部 `json = createOrder(payParams)` → `pay(json)`。SDK **不解析 json 任何字段**(productId/accountId/profileId/orderToken 都不碰)· 由 provider 实现方(createOrder 产出 ↔ pay 消费)自洽保障。 > > **google provider 输入键(文档化 · 非 SDK 传输层)**:内置 `google_play` 的 pay 自读 json:`productId`(必填)、`productType`(可选,`'inapp'` 默认值或 `'subs'`)、`basePlanId`(`subs` 必填)、`offerId`(`subs` 可选)、`accountId`(可选 → `obfuscatedAccountId`)、`profileId`(可选 → `obfuscatedProfileId`)。订阅只从 Google 返回的用户可用 `SubscriptionOfferDetails` 中精确匹配 `basePlanId + offerId`;不传 `offerId` 时匹配该 base plan 自身的 offer,不会静默回退到其他优惠。无匹配返回 `OFFER_UNAVAILABLE`。这是 **google provider 的输入契约**(谁 override 其 createOrder 就照此产)· **不是** SDK 传输层契约。宿主 Apple provider 自定义其 pay 输入键。 > > **未 override createOrder 的纯 IAP**:provider 既未注册 createOrder、payConfig 也无 createOrder → SDK 用 identity(json=payParams)· 即 `payParams` 直接作内置 pay 的输入(payParams 须自带 google provider 输入键)。 #### pay.restorePurchases(options) 客户端恢复购买 · 走 Native PayService.restorePurchases · **provider 可选实现**。 > 🔵 **F038 不改本契约**:`restorePurchases` 仍返回 **F036 的 `PurchaseResult[]`**(客户端 StoreKit/BillingClient 凭证恢复)· 与 `purchase` 的「三态 + 凭证回执直投」属**不同路径**(restore 是客户端凭证恢复 · 不走回执直投)。统一化留后续 Feature(已知不一致)。 > > **Google 订阅约束**:内置 `google_play` provider 的 restore 仅查询 `ProductType.SUBS`,不返回一次性商品。 ```javascript try { // provider 名以 getProviders() 返回为准:内置 Google 是 'google_play';iOS Apple 名由宿主注册 const purchases = await SuperSDK.pay.restorePurchases({ provider: 'apple' /* 宿主注册名 · 以 getProviders() 为准 */ }); // purchases: [{ orderId, purchaseToken, productId, purchaseState }] } catch (e) { if (e.message === 'NOT_SUPPORTED') { // provider 未实现 restore · fallback 业务后端 const res = await fetch('/api/pay/my-purchases?userId=' + userId); const purchases = await res.json(); } else if (e.message === 'PROVIDER_NOT_FOUND') { // 错误的 provider 名 } } ``` | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | options.provider | string | 否 | 单 Provider 时可省 · 多 Provider 必填 | **返回值**:`Promise` — 购买记录数组,每个元素结构同 `pay.purchase` 的 Native IAP 返回对象(含 `orderId / productId / purchaseToken / purchaseTime / purchaseState / isAcknowledged / signature / originalJson / provider / raw`)。无可恢复购买时返回空数组 `[]`。 #### 错误码 > F038 `pay.purchase` 的失败分两类:**reject 的 code**(无法开始 · `e.message`)与 **error 三态的 code**(已开始的支付阶段失败 · `result.code` + `result.phase`)。 **reject code(无法开始 · `pay.purchase` 抛出)**: | code | 含义 | 建议处理 | |------|------|---------| | INVALID_PARAMS | 入参错误(payConfig/payParams 非对象)· err.detail 含具体字段 | 检查传入参数 | | NOT_IN_APP | Native 路径但非端环境 | 检查 isInApp() | | SDK_TOO_OLD | 老 SDK 无 `appbridge.nativePay`(F038 统一支付需 SDK 升级) | 升级宿主 SDK | | PROVIDER_REQUIRED | 多 Provider 时未指定 | 传入 provider 参数 | | NO_PROVIDER | 无任何 provider 可用 | 提示宿主未实现支付 | | PURCHASE_IN_PROGRESS | 有 inline 购买正在进行中 | 等待当前购买完成 | **error 三态 code(`result.status==='error'` · 带 `phase`)**: | code | phase | 含义 | 建议处理 | |------|------|------|---------| | CREATE_FAILED | create | createOrder reject/throw(在 JSSDK 内捕获 · 未调 pay) | 提示重试(未拉起 · 可安全重发) | | INVALID_JSON | create | createOrder 产出的 json 无法 JSON 序列化(IAP 路径) | 检查 createOrder 产出对象 | | INVALID_PARAMS | pay | 内置 `google_play` 缺少 `productId` / 订阅缺少 `basePlanId` / `productType` 非 `inapp/subs` | 修正 Google provider 输入键 | | INVALID_ORDER_TOKEN | pay | provider 校验输入契约:orderToken(profileId / appAccountToken)**非 UUID 格式**(内置 `google_play` 与 iOS host provider 均会返回此 code) | createOrder 产 orderToken 时用 `crypto.randomUUID()`(后端创单端点亦同) | | PRODUCT_NOT_FOUND | pay | 购买前重查后未找到所选订阅商品 | 刷新 eligible offer 列表 | | OFFER_UNAVAILABLE | pay | 内置 `google_play` 返回的用户可用订阅 offer 中没有匹配的 `basePlanId + offerId` | 刷新商品配置,确认用户资格及 Play Console base plan/offer | | BILLING_UNAVAILABLE | pay | 订阅查询或购买时 Google Billing 服务不可用 | 稍后重试 | | QUERY_FAILED | pay | 订阅购买前查询失败(非 Billing unavailable) | 稍后重试并保留原选择 | | PURCHASE_FAILED | pay | 订阅 Billing UI 拉起或购买回调失败 | 展示错误并允许重试 | | FAILED / EXPIRED / CANCELLED... | pay | 三方 provider 的 pay 自定义返回的 code(业务自定) | 按业务语义处理 | | BILLING_UNAVAILABLE / SERVICE_UNAVAILABLE / DEVELOPER_ERROR / ITEM_UNAVAILABLE / NO_ACTIVITY / ERROR ... | pay | 内置 IAP provider.pay 平台计费错误(映射自 BillingResponseCode / StoreKit Error) | 展示错误信息 | > 🔴 注意:`cancelled` 不在 error 列(单独是 `status==='cancelled'`);`PENDING` / `ITEM_ALREADY_OWNED` 也不是 error(归 `finished` · 去查单 · 见下方架构章节)。`phase:'pay'` 的具体 code 由 provider.pay 返回(IAP=SDK 内置映射;三方=业务自定),SDK 不收敛枚举(哑管道)。 #### 业务查单四态(finished 后) `finished` ≠ 支付成功。**SDK 不提供查单 API**(与支付通道解耦)。H5 用 `orderToken` 轮询**业务自有查单接口**判定真实结果。业务查单接口约定返回**四态**: | status | 含义 | H5 处理 | |---|---|---| | `success` | 后端已验签 + 发货完成 | 解锁 / 提示成功 | | `processing` | 受理中(SDK 回执未达 / RTDN 未达 / 重试中) | 继续轮询(自定间隔/上限) | | `failed` | 后端判定失败(验签不过 / 退款 / 平台拒绝) | 提示失败 | | `unknown` | 暂不确定(超轮询上限仍未定) | 留后台兜底(RTDN/ASSN 终会落地)· 不在客户端发货 | ```javascript async function pollBusinessOrder(orderToken, maxPolls = 6, interval = 2000) { for (let i = 0; i < maxPolls; i++) { const { status } = await fetch('/api/pay/order-status?orderToken=' + orderToken).then(r => r.json()); if (status === 'success' || status === 'failed') return status; await new Promise(r => setTimeout(r, interval)); } return 'unknown'; // 超上限仍未定 · 依赖后端 RTDN/ASSN 兜底 } ``` > 凭证已由 SDK Native(PayBridge)直投后端回执端点 · 后端独立验签发货。即使前端被杀,RTDN/ASSN 兜底也能让订单最终落地 success(见[凭证回执直投契约 PayBridge 守门](#6-凭证回执直投契约paybridge-守门))。 #### 完整支付流程示例(v0.3) ```javascript let PAY_PROVIDER; // 真实 provider 名 · 来自 getProviders() · 不硬编码 async function init() { // 1. 探测能力(provider 名以此返回为准 · 内置 Google 为 'google_play') const providers = await SuperSDK.pay.getProviders(); if (providers.length === 0) return; PAY_PROVIDER = providers[0]; // 或按业务挑选 · 如 providers.find(n => n === 'google_play') // 2. 给 IAP provider 注册 createOrder override(可选 · 让 createOrder 产出 google provider 输入键) SuperSDK.pay.registerProvider(PAY_PROVIDER, { createOrder: async (payParams) => { const res = await fetch('/api/pay/create-order', { // 业务创单(后端签发 UUID orderToken · 你当场持有) method: 'POST', body: JSON.stringify(payParams) }).then(r => r.json()); // ⬇️ google provider 输入键;订阅还需 productType='subs' + basePlanId,offerId 可选 return { productId: payParams.productId, productType: payParams.productType ?? 'inapp', basePlanId: payParams.basePlanId, offerId: payParams.offerId, accountId: res.accountId, profileId: res.orderToken // 🔴 UUID }; } }); } async function buyProduct(productId, userId) { // 3. 发起购买(SDK 内部 createOrder→pay · 返回三态) const result = await SuperSDK.pay.purchase({ provider: PAY_PROVIDER, // 🔴 用 getProviders() 返回值 · 勿硬编码 'google'(内置实为 'google_play') payParams: { productId, userId } // 任意对象 · 由你的 createOrder 消费 · SDK 不解析 }); // 4. 按三态分支 switch (result.status) { case 'finished': // ≠ 成功 · 去查单(orderToken 由 provider 内部回带) const orderStatus = await pollBusinessOrder(result.orderToken); if (orderStatus === 'success') unlockEntitlement(); else if (orderStatus === 'failed') showFailed(); else showPending(); // processing / unknown break; case 'cancelled': // 用户取消 · 静默 break; case 'error': // 失败 · phase + code if (result.phase === 'create') showRetry(); // createOrder 失败 · 未拉起 · 可安全重发 else console.error('支付失败:', result.code, result.message); break; } // 🔴 凭证不经 H5:purchaseToken/signature 等由 SDK Native(PayBridge)直投后端回执端点 · H5 不再 POST /verify } async function restoreUserPurchases(userId) { try { // 5. 客户端 restore(Apple Guideline 3.1.1 必需 · F038 契约不变 · 返回 PurchaseResult[]) return await SuperSDK.pay.restorePurchases({ provider: PAY_PROVIDER }); } catch (e) { if (e.message === 'NOT_SUPPORTED') { const res = await fetch('/api/pay/my-purchases?userId=' + userId); return await res.json(); } throw e; } } ``` #### F038 迁移指南(从 F036 PurchaseResult 迁到三态) > 🔴 **破坏式变更(继 F036 后第二次)**:`pay.purchase` 返回从 `PurchaseResult` 凭证对象改为 **PayResult3State 三态对象**;凭证不再经 H5;JSB 由位置参数改为通用 `nativePay(provider, jsonString, callbackId)`。**现网 H5 必须配套升级**(无 alias · 无兼容期)。`restorePurchases` 不变(仍 F036 `PurchaseResult[]`)。 | F036 写法 | F038 后行为 | 迁移路径 | |---|---|---| | `const r = await pay.purchase(...)`
`r.purchaseToken / r.orderId / r.signature` | `r` 不再含凭证(三态对象) | 删除「读凭证 + POST /verify」逻辑;改为读 `r.status`(finished/cancelled/error) | | `if (r.purchaseState===0) ...`(凭证判成功) | `r.purchaseState` 不存在 | 改为 `r.status==='finished'` → **用 orderToken 查单**定真相 | | try/catch 捕 `CANCELLED` | 取消不再 reject | 改为 `r.status==='cancelled'` 分支 | | try/catch 捕支付失败 | 支付阶段失败不再 reject(归三态 error) | 改为 `r.status==='error'` + `r.phase`/`r.code`;reject 仅剩「无法开始」(INVALID_PARAMS/NOT_IN_APP/SDK_TOO_OLD/PURCHASE_IN_PROGRESS/PROVIDER_REQUIRED/NO_PROVIDER) | | `H5 POST /api/pay/verify`(凭证) | 凭证不经 H5 | 删除;凭证由 SDK Native(PayBridge)直投宿主配置的回执端点 · H5 改为轮询业务查单接口 | | (无)需要订单关联 | — | 注册 createOrder override · 产出 google provider 输入键(orderToken→`profileId`);orderToken 是你 createOrder 自己产的 · 当场持有 · 无需 SDK 回调 | 🔴 **不留兼容包袱**:F038 直接改三态返回 · 不提供 PurchaseResult 旧返回 alias · 不留兼容期。CHANGELOG 标 BREAKING CHANGE。 ## 广告 ### showRewardAd(options) 展示激励视频广告。用户观看完整视频后获得奖励。 ```javascript const result = await SuperSDK.showRewardAd({ adUnitId: 'ca-app-pub-xxx/yyy', // 广告单元 ID adSource: 'ADMOB' // 广告来源: 'ADMOB' | 'APPLOVIN' }); console.log(result.rewarded); // true = 用户获得了奖励 console.log(result.dismissed); // true = 广告已关闭 ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | options.adUnitId | string | 是 | 广告单元 ID | | options.adSource | string | 否 | 广告来源,默认 'ADMOB' | **返回值**: `{ dismissed: boolean, rewarded: boolean }` **错误码**: | code | 说明 | |------|------| | NOT_IMPLEMENTED | 宿主未提供 AdService | | AD_ALREADY_SHOWING | 已有全屏广告正在展示或加载中 | | NO_ACTIVITY | Activity/ViewController 不可用或已销毁 | | LOAD_FAILED | 广告加载失败 | | ERROR | 其他错误 | ### showRewardedInterstitialAd(options) 展示激励插屏广告。调用前 H5/宿主必须先展示奖励内容说明,并提供明确的“跳过/继续”入口,只有用户继续时才调用本方法。 ```javascript // 业务先展示自己的奖励说明/跳过 UI const result = await SuperSDK.showRewardedInterstitialAd({ adUnitId: 'ca-app-pub-xxx/yyy', adSource: 'ADMOB' }); console.log(result.rewarded); console.log(result.dismissed); ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | options.adUnitId | string | 是 | Rewarded Interstitial 广告单元 ID | | options.adSource | string | 否 | 广告来源,默认 'ADMOB' | **返回值**:`{ dismissed: boolean, rewarded: boolean }` 错误码同 `showRewardAd`;旧版原生桥不支持该能力时会立即拒绝 `NOT_SUPPORTED`,不会等待 120 秒超时。 ### showInterstitialAd(options) 展示插屏广告。全屏展示,用户可关闭。 ```javascript const result = await SuperSDK.showInterstitialAd({ adUnitId: 'ca-app-pub-xxx/yyy', adSource: 'ADMOB' }); console.log(result.dismissed); // true = 广告已关闭 ``` 参数和错误码同 `showRewardAd`。 **返回值**: `{ dismissed: boolean, rewarded: false }` ### showBannerAd(options) 展示 Banner 横幅广告。Banner 会持续显示在页面顶部或底部,直到调用 `hideBannerAd` 隐藏。 ```javascript const result = await SuperSDK.showBannerAd({ adUnitId: 'ca-app-pub-xxx/yyy', adSource: 'ADMOB', position: 'bottom' // 'top' | 'bottom',默认 'bottom' }); const adId = result.adId; // 保存 adId 用于后续隐藏 // 隐藏 Banner await SuperSDK.hideBannerAd(adId); ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | options.adUnitId | string | 是 | 广告单元 ID | | options.adSource | string | 否 | 广告来源,默认 'ADMOB' | | options.position | string | 否 | 位置,'top' 或 'bottom',默认 'bottom' | **返回值**: `{ adId: string }` **额外错误码**: | code | 说明 | |------|------| | CONTAINER_UNSUPPORTED | WebView 父容器不支持叠加 Banner | ### hideBannerAd(adId) 隐藏并销毁指定 Banner 广告。 ```javascript await SuperSDK.hideBannerAd(adId); ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | adId | string | 是 | showBannerAd 返回的 adId | --- ### 归因 #### getAttribution(onlyFirst) 获取归因数据,返回包含首次归因和当前归因两部分的数据。 ```javascript // 获取完整归因(首次归因 + 当前归因网络请求) const { first, current } = await SuperSDK.getAttribution(false); console.log('首次归因:', first); console.log('当前归因:', current); // 只获取首次归因(从缓存读取,跳过网络请求,速度更快) const { first } = await SuperSDK.getAttribution(true); ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | onlyFirst | boolean | 是否只返回首次归因。true: 只返回 first 字段;false: 返回 first + current 两个字段。注意:如果没有首次归因缓存,无论该参数值如何都会发起网络请求尝试获取 | **返回值:** ```javascript { first: { // 首次归因(从缓存读取),无则为 null attrFrom: "facebook_ad", // 归因来源 attrCmd: "open_page", // 归因指令 attrParam: "page=home", // 归因参数 recordId: "attr_xxx" // 归因记录 ID }, current: { // 当前归因(网络请求),onlyFirst=true 时为 undefined matched: true, // 是否匹配到归因 attrFrom: "google_ad", // 归因来源 attrCmd: "view_product", // 归因指令 attrParam: "...", // 归因参数 recordId: "..." // 归因记录 ID } } ``` **first 对象:** | 参数 | 类型 | 说明 | |------|------|------| | attrFrom | string | 归因来源(如 facebook_ad, google_ad) | | attrCmd | string | 归因指令(如 open_page) | | attrParam | string | 归因参数 | | recordId | string | 归因记录 ID | **current 对象:** | 参数 | 类型 | 说明 | |------|------|------| | matched | boolean | 是否匹配到归因 | | attrFrom | string | 归因来源 | | attrCmd | string | 归因指令 | | attrParam | string | 归因参数 | | recordId | string | 归因记录 ID | **使用场景:** ```javascript // 场景1:只关心首次归因(如统计用户来源渠道) async function init() { // 如果有缓存直接返回,无缓存会发起网络请求 const { first } = await SuperSDK.getAttribution(true); if (first) { console.log('用户来源:', first.attrFrom); trackUserSource(first.attrFrom); } } // 场景2:需要首次归因和当前归因(如动态广告投放) async function onShareClick() { const { first, current } = await SuperSDK.getAttribution(false); // first: 首次归因(用于统计) // current: 当前归因(用于动态内容) console.log('首次归因:', first); console.log('当前归因:', current); } ``` > **注意:** > - 首次归因(first)是用户安装 App 后首次匹配的归因,会持久化保存 > - 当 `onlyFirst=true` 时,如果没有首次归因缓存,会发起网络请求尝试获取 > - 当 `onlyFirst=false` 时,总是发起网络请求获取 current > - 若 current 匹配成功且无首次归因,会自动保存为首次归因 --- ### Native Block(原生组件嵌入) Native Block 模块提供 H5 与 Native View 的"伪同层渲染"能力,允许在 H5 页面中嵌入由宿主 App 提供的原生视图(如广告、视频播放器等)。 #### nativeBlock.init() 初始化 Native Block 模块,扫描页面中的静态 `` 元素并注册。建议在 `DOMContentLoaded` 后调用。 ```javascript document.addEventListener('DOMContentLoaded', function() { SuperSDK.nativeBlock.init(); }); ``` #### 静态声明方式 在 HTML 中直接声明 `` 元素: ```html ``` **属性说明:** | 属性 | 类型 | 必填 | 说明 | |------|------|------|------| | block-id | string | 是 | 唯一标识 | | block-type | string | 是 | 组件类型(宿主 App 定义,如 "ad-banner") | | block-config | string | 否 | JSON 格式的配置,透传给宿主 App | #### nativeBlock.create(options) 动态创建 Native Block。 ```javascript const block = await SuperSDK.nativeBlock.create({ blockId: 'video-1', blockType: 'video-player', config: { videoUrl: 'https://example.com/video.mp4' }, container: document.getElementById('video-container'), style: { width: '100%', height: '200px' } }); // 销毁该 Block block.destroy(); ``` **参数说明:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | blockId | string | 是 | 唯一标识 | | blockType | string | 是 | 组件类型(宿主 App 定义) | | config | object | 否 | 传递给宿主 App 的配置 | | container | HTMLElement | 否 | 插入到哪个容器,默认 document.body | | style | object | 否 | CSS 样式对象 | **返回值:** | 参数 | 类型 | 说明 | |------|------|------| | blockId | string | Block 的唯一标识 | | element | HTMLElement | 占位的 DOM 元素 | | destroy | function | 销毁该 Block 的方法 | #### nativeBlock.updatePositions() 手动更新所有 Block 的位置。通常不需要手动调用,SDK 会自动监听滚动和窗口大小变化。 ```javascript SuperSDK.nativeBlock.updatePositions(); ``` #### nativeBlock.destroy(blockId) 销毁指定的 Native Block。 ```javascript SuperSDK.nativeBlock.destroy('ad-1'); ``` #### nativeBlock.destroyAll() 销毁所有 Native Block。 ```javascript SuperSDK.nativeBlock.destroyAll(); ``` #### 完整示例 ```html

Native Block 示例

页面内容...

``` > **注意事项:** > 1. Native Block 的内容由宿主 App 提供,SDK 只负责位置同步 > 2. 宿主 App 需要实现 `INativeBlockProvider` 接口并注册 > 3. `blockType` 由宿主 App 定义,需要与宿主约定好支持的类型 > 4. 页面关闭时会自动清理所有 Block,也可手动调用 `destroyAll()` --- ### 响应编码(suenc) `suenc` 是 SuperSDK 统一维护的轻量响应混淆协议:客户端显式请求时,服务端用自定义 Base64 表编码 JSON 响应体,并回 `X-Su-Enc: b64t1`。 > ⚠️ **它只用于降低通用关键字扫描与规则匹配的有效性,不是加密**。不提供机密性、防篡改、防重放,也挡不住已取得客户端代码的攻击者还原编码表。 **SuperSDK 自身发出的请求会自动解码**(settings / reportEvent / 归因 / 短链等),业务方无需介入。下面两个 API 面向**业务方自己发起的 fetch**。 #### getSuEncCapabilities() 查询当前运行时支持的 codec,据此决定是否发送 `suenc=1`。老版本 SDK 上该方法不存在,务必先做存在性判断。 ```javascript const capabilities = SuperSDK.getSuEncCapabilities(); // { supported: true, codecs: ['b64t1'] } const canUseSuEnc = typeof SuperSDK.getSuEncCapabilities === 'function' && capabilities.supported && capabilities.codecs.includes('b64t1'); ``` **返回值**:`{ supported: boolean, codecs: string[] }`(同步返回,冻结对象) #### decodeSuEnc(encodedData, codec) 解码 suenc 响应体,返回原始 UTF-8 JSON 文本。**同步纯函数**,不经 JSBridge 往返。 | 参数 | 类型 | 必填 | 说明 | |------|------|:----:|------| | encodedData | string | 是 | 服务端返回的完整编码响应体 | | codec | string | 是 | `X-Su-Enc` header 的**原始值**,不接受自定义编码表 | ```javascript // 只有在确认支持后才追加 suenc=1 const url = canUseSuEnc ? `${api}?suenc=1` : api; const response = await fetch(url); const codec = response.headers.get('X-Su-Enc'); if (codec) { const decoded = SuperSDK.decodeSuEnc(await response.text(), codec); return JSON.parse(decoded); } // 🔴 若本次请求带了 suenc=1 却收到无 header 的响应,必须判定为失败(防明文降级), // 不得把明文响应直接交给业务 if (canUseSuEnc) throw new Error('SUENC_DOWNGRADE'); return response.json(); ``` **错误码**(均为 `SuperSDKError`,`error.code` 可判别): | 错误码 | 触发条件 | |--------|----------| | `UNSUPPORTED_SUENC_CODEC` | `X-Su-Enc` 指向本端未知的 codec;**不会**尝试其他编码表 | | `SUENC_DECODE_FAILED` | 非法字符、长度非 4 的倍数、padding 位置或数量非法 | | `SUENC_INVALID_UTF8` | 解码结果不是合法 UTF-8 | > **注意事项:** > 1. **`codec` 必须直接用响应 header 的值**,不要硬编码,这样未来新增 `b64t2` 时无需改业务代码 > 2. 解码失败一律视为请求失败,**绝不允许回落读取明文**——那正是该协议要防的降级 > 3. 编码后体积约增加 33%;二进制响应(APK / ZIP / 图片 / 文件流)不参与混淆 > 4. 编码表在 SDK 发布后永不变更;新表一定使用新的 codec key --- ## IAP 推荐架构 > 🔴 **业务方设计 In-App Purchase 时必读** · 本章节给出 SuperSDK + 业务后端的推荐架构(F038 v0.3 哑管道)· 让 IAP 从『客户端主导(脆弱)』演进到『服务端主导(健壮)』。 > **v0.3 核心(哑管道)**:provider = **两个方法** `{ createOrder(payParams)→json, pay(json)→三态 }`。createOrder 产出的 `json` 对 SDK 传输层**完全不透明** —— SDK 只按 `provider` 名路由、把 json 原样透传给 pay,**不解析任何字段**。IAP 的 `pay` 由 SDK 内置 `google_play`(宿主 Apple provider 自实现)原生扣款 · **自读 json**;三方业务两方法都自己写。凭证由 Native **PayBridge 守门**:投回执 + 白名单剥离三态给 H5(凭证不出 Native)。 ### 1. 架构对比:client-driven vs server-driven **❌ 客户端主导(脆弱 · 不推荐生产)** ``` H5 → SDK → 支付平台 → SDK → H5 拿到 purchase 凭证 ↓ H5 POST /verify 给业务后端 ← 唯一通知路径 ↓ 业务后端 校验 + 发货 + ack ``` 致命弱点:用户付款后若客户端崩溃 / 网络故障 / 进程被杀 / 故意不上报 → 业务后端永远不知道用户买了什么 → 用户被扣钱但没发货;且凭证经 H5 业务 JS · 可被恶意 H5 篡改 / 重定向。 **✅ 服务端主导(健壮 · v0.3 哑管道)** ``` H5 → SDK.pay.purchase({provider, payParams}) └─ json = provider.createOrder(payParams) [业务实现 · 可调后端建单 · orderToken 业务当场持有] └─ provider.pay(json) [IAP=SDK 内置原生自读 json · 三方=业务 JS] ↓ provider.pay → PayBridge(内部 NativePayResult 含 credential) ↓ ├──→ PayBridge 白名单构造三态 → H5(finished · 不含凭证) ├──→ PayBridge 投凭证 POST receiptUrl(H5 不经手 · 幂等+重试) │ └──→ 后端独立验签(不信任 SDK claim)+ 幂等发货 └──→ 平台推送 RTDN / ASSN(authoritative 兜底) ↓ H5 用 orderToken 轮询业务查单接口(success/processing/failed/unknown) ``` 服务端 = 真相源 · 凭证 PayBridge 守门直投 + 平台主动推 · 客户端故障不影响发货 · 凭证不出 Native。 ### 2. 哑管道时序(IAP · Android) ```mermaid sequenceDiagram participant H5 participant JS as supersdk.js(哑管道) participant Bridge as PayBridge(原生·守门) participant Prov as GooglePayProvider(内置 pay) participant BE as 业务后端 participant Google as Google Play H5->>JS: pay.purchase({provider:'google_play', payParams}) JS->>JS: json = provider.createOrder(payParams) [override 或 identity · 业务可建单] Note over JS: SDK 不解析 json · 原样透传 JS->>Bridge: appbridge.nativePay('google_play', JSON.stringify(json), cb) Bridge->>Prov: payService.pay('google_play', json, cb) Prov->>Prov: 自读 json.productId(必填)/accountId/profileId Prov->>Google: launchBillingFlow
setObfuscatedAccountId(accountId)
setObfuscatedProfileId(profileId) Google-->>Prov: PurchaseResult Prov-->>Bridge: NativePayResult { status, orderToken?, credential{purchaseToken,...} } alt finished + credential Bridge->>BE: POST receiptUrl(credential · 幂等键 orderToken||purchaseToken) [守门] Bridge-->>H5: 白名单 { status:'finished', orderToken? }(无凭证) else cancelled / error Bridge-->>H5: 白名单三态 { status, phase?, code? } end Google->>BE: RTDN(authoritative 兜底) H5->>BE: 用 orderToken 轮询查单 BE-->>H5: { status: success/processing/failed/unknown } ``` **iOS 同构**:iOS 无内置 ApplePayProvider · 宿主 provider 自读 json 实现 `pay`,把 `credential` 一并返回给 `PayBridge` · **由 PayBridge 投回执**(不是宿主自投)· 双端 receipt 路径一致、均可测。 ### 3. provider 两方法契约(v0.3 哑管道核心) - `pay.purchase` 内部:`json = provider.createOrder(payParams)` → `provider.pay(json)` → 三态(公共面仍仅 `pay.purchase` 一个入口)。 - **createOrder**:provider 实现 · 产出对 SDK **不透明**的 json(业务可在此调后端建单 · orderToken 是业务自己产的 · **当场就持有** · 无需 SDK 回调还给业务)。缺省=identity(json=payParams)。 - **pay**:消费 json · 返回三态(内部含 credential 给 PayBridge)。IAP=SDK 内置原生;三方=业务 JS 自写(自行 checkout+轮询)。 - 🔴 SDK **不读 json 任何字段** · 不碰 productId/accountId/profileId/orderToken · 仅 `JSON.stringify` 透传(JSB 通用管道 `nativePay(provider, jsonString, callbackId)`)。 **JSB 通用管道**:`appbridge.nativePay(provider, jsonString, callbackId)`(取代旧位置参数 purchaseV2/purchaseV3)。一个方法承载所有内置 IAP provider · 参数为不透明 JSON 字符串。 ### 4. google provider 输入键(google_play provider 的文档化输入 · 非 SDK 传输层) 内置 `google_play` provider 的 `pay`(SDK 原生实现)自读 json 的以下键 —— 这是 **google provider 的输入契约**(谁 override 其 createOrder 就照此产),**不是** SDK 传输层契约: | json 键 | 必填 | 内置 pay 映射 | 后端取回 | |---------|------|---------------|---------| | `productId` | **是** | `launchBillingFlow` 商品(缺失 → `phase:'pay'` error) | - | | `productType` | 否 | `'inapp'`(默认)→ `ProductType.INAPP`;`'subs'`→ `ProductType.SUBS` | - | | `basePlanId` | `productType='subs'` 时**是** | 在用户可用 `SubscriptionOfferDetails` 中匹配 base plan | subscriptionsv2 的 base plan | | `offerId` | 否 | 传值时精确匹配优惠;省略时匹配 `offerId == null` 的 base-plan offer | subscriptionsv2 的 offer | | `accountId` | 否 | `setObfuscatedAccountId`(用户标识) | 验签 API `obfuscatedExternalAccountId` | | `profileId` | 否 · 传则须 **UUID** | `setObfuscatedProfileId`(订单标识 · 放 orderToken) | 验签 API `obfuscatedExternalProfileId` | - **字段读取与槽映射是 `provider.pay` 实现内部职责**(在 Native provider 内 · 非 SDK 传输层)· 键为空则不设对应槽。 - **订阅购买前强制重查**:不缓存/复用 `ProductDetails` 或 `offerToken`;每次购买重新查询,再用 `productId + basePlanId + nullable offerId` 精确匹配。无匹配时返回 `OFFER_UNAVAILABLE` 且不拉起 Billing UI;匹配后才把最新 token 写入 `BillingFlowParams.ProductDetailsParams`。 - 🔴 **orderToken(profileId)必须是 UUID 格式 —— 跨端硬约束**:iOS `appAccountToken` 由 StoreKit **强制 UUID**;Android `obfuscatedProfileId` 虽接受更宽字符集(`^[A-Za-z0-9_-]{1,64}$`),但**为跨端一致也要求 UUID**(避免「Android 能跑、iOS 炸」的跨端地雷)。 - 🔴 **内置 `google_play` provider 会校验**:传入的 `profileId`(orderToken)**非 UUID** → 该 provider 直接返回 `{ status:'error', phase:'pay', code:'INVALID_ORDER_TOKEN' }`(provider 校验自己的输入契约 · 不静默放行,以免 Android 通过而 iOS 在 StoreKit 处崩)。 - **iOS** 宿主 Apple provider 自定义其 pay 输入键;典型把订单标识键映射为 `Product.PurchaseOption.appAccountToken = UUID(orderToken)`(StoreKit 单槽 · **强制 UUID** · 非 UUID 会被 StoreKit 拒)· host provider 应**自校验**并按 `INVALID_ORDER_TOKEN`(phase:pay)返回 · 用户身份由后端经订单反查。 - 🔴 **业务 createOrder 产 orderToken 时务必用 UUID**:推荐 `crypto.randomUUID()`(后端创单端点签发亦同 · 见下方示例)。 ### 5. pay 三态语义(白名单 · 回调给 H5) | status | 含义 | 是否含凭证 | H5 处理 | |---|---|---|---| | `finished` | 平台已受理 · **流程正常结束 ≠ 支付成功** | ❌ 不含 | 用 `orderToken` 去业务查单定真相;Google PENDING / ITEM_ALREADY_OWNED 也归此态 | | `cancelled` | 用户取消 | ❌ | 静默 | | `error` | 失败 · 带 `phase`(create/pay)+ `code` | ❌ | create=createOrder 失败未拉起·可重发;pay=平台计费错误(IAP=SDK 映射 / 三方=业务自定) | > 🔴 **闭合白名单**:发给 H5 的对象**只含** `{ status, phase?, code?, message?, orderToken? }`,由 Native `PayBridge` 显式构造(不把 provider 内部结果原样透传)· 凭证(`purchaseToken`/`signature`/`originalJson`)**绝不**到达 H5。`orderToken` 是回带给 H5 的**唯一业务键**(供查单)。 ### 6. 凭证回执直投契约(PayBridge 守门) > 🔴 **凭证全程不经 H5 业务 JS · 也不出 Native 业务回调**:`provider.pay` 把含 credential 的 **NativePayResult** 返回给 `PayBridge`;PayBridge 在 finished 且有 credential 时把凭证 POST 到**宿主 App 初始化时配置**的回执端点(`receiptUrl`)· 再白名单构造三态发 H5。H5 **不可运行时设置** receiptUrl(防恶意 H5 重定向凭证)。 **回执端点配置(host-only · H5 不可设)**: - **Android**:`SDKPayService.Builder.setReceiptEndpoint(absoluteUrl)`(host 构建 PayService 时配置) - **iOS**:`SDKPayService` 初始化 `receiptEndpoint` 参数 **ReceiptPayload schema**(PayBridge 构造): ```json { "purchaseToken": "...", "originalJson": "...", "signature": "...", "productId": "...", "provider": "google_play", "platform": "android", "orderToken": "..." } ``` **投递契约**: - **幂等键 = `orderToken` 存在则用之 · 否则退化 `purchaseToken`**(全局唯一)· 端点须幂等 - timeout 默认 10s · retry 指数退避(默认 ≤3 次) - 带身份头 `X-Aid` / `X-Device-Id` / `X-SDK-Version` - HTTP 2xx 判受理 · **投递失败不阻断三态回调**(`SLogger.e` 记录 · 依赖 RTDN/查单兜底) - 后端须**独立**向 Google/Apple 重新验签(不信任 SDK claim) - **RTDN / ASSN 作权威二次确认与兜底**:PayBridge 回执投递失败/延迟时自愈;后端验签 API 响应取回 `obfuscatedExternalAccountId/ProfileId`(Android)/ `appAccountToken`(iOS) ### 7. 🔴 宿主 provider checklist(iOS · 必读) **SDK 不内置 ApplePayProvider**(沿用 F035/F036 边界)· iOS 支付由宿主实现。v0.3 下宿主 Apple `PayProvider` 必须: - [ ] 实现 `providerName`(自定义名 · 如 `"app_store"`)· H5 须用 `getProviders()` 返回的此名调 `purchase`/`registerProvider`(SDK 无内置 Apple provider 名) - [ ] 实现 `pay(json)`:**自读** json 自定义键(SDK 传输不碰字段)· 拉起 StoreKit - [ ] 🔴 把订单标识键映射为 `Product.PurchaseOption.appAccountToken = UUID(orderToken)`(StoreKit 单槽 · **强制 UUID**) - [ ] 🔴 **自校验 orderToken 是 UUID** —— 非 UUID 会被 StoreKit 拒(`UUID(uuidString:)` 返 nil / 抛错)· host provider 须**先校验**并按 `{ phase:'pay', code:'INVALID_ORDER_TOKEN' }` 返回(与内置 `google_play` 同 code · 跨端一致 · 不放行到 StoreKit 崩) - [ ] 用户标识 iOS **无独立槽** · 不映射(用户身份经 orderToken 后端反查) - [ ] 🔴 把 `credential`(purchaseToken/JWS 等)**返回给 PayBridge**(不自投回执 · 由 PayBridge 守门投递)· success 但 credential 为 nil → 按 error 返回 - [ ] 异常用等价日志打印(对齐 Android `SLogger.e`)· 不吞异常 > 参考:Android 内置 Google provider 名 = `"google_play"`(`GooglePayProvider.getProviderName()`);iOS 宿主 Apple provider 名自定义 · 两端 H5 均以 `getProviders()` 返回为准。 ### 8. 业务后端要做的事(checklist) 业务后端需实现以下端点(本 Feature 仅定义契约 · 不实现该后端): | 端点 | 谁调 | 入参 | 出参 | |------|------|------|------| | 创单端点 | `provider.createOrder`(业务 JS fetch) | payParams | `{ orderToken(🔴 必须 UUID · 双端一致), ...你约定的 json 字段 }` | | 回执端点(receiptUrl) | **PayBridge**(host init 配置) | ReceiptPayload | HTTP 2xx = 受理 | | 查单端点 | H5(orderToken 轮询) | orderToken | `{ status: success/processing/failed/unknown }` | **Android Google Play**: - [ ] 🔴 创单端点签发 **UUID** orderToken(非 UUID → 内置 google_play provider 返回 INVALID_ORDER_TOKEN · 且 iOS 同 orderToken 会被 StoreKit 拒) - [ ] 接 PayBridge 回执端点 · 用 Service Account 调 `androidpublisher.purchases.{products|subscriptions}.get` 独立验签 - [ ] 从响应读 `obfuscatedExternalAccountId`(对应 accountId)+ `obfuscatedExternalProfileId`(对应 orderToken)+ `orderId` - [ ] Play Console 配置 RTDN topic(Cloud Pub/Sub)兜底 · idempotent 发货 + `acknowledge`(**3 天 ack 窗口**) **iOS Apple**: - [ ] 创单端点签发 UUID orderToken - [ ] 接 PayBridge 回执端点 · 用 App Store Server API 调 `/inApps/v1/transactions/{transactionId}` 独立验签 - [ ] 从响应读 `appAccountToken`(对应 orderToken)+ `transactionId` · 经 orderToken→订单反查用户 - [ ] 配置 ASSN V2 endpoint 兜底 · idempotent 发货 · 通知客户端 `Transaction.finish()` 📚 官方文档:[Google Play RTDN](https://developer.android.com/google/play/billing/rtdn-reference) · [Google Play Developer API](https://developers.google.com/android-publisher) · [App Store Server Notifications V2](https://developer.apple.com/documentation/appstoreservernotifications) · [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) ### 9. 常见疑问 **Q: SDK 为什么不解析参数?** A: v0.3 哑管道原则。把 productId/accountId/profileId/orderToken 语义焊进 SDK 传输层会让 SDK 越来越复杂、难以适配新支付方式。改由 provider 实现方(createOrder 产 ↔ pay 消费)自洽 · SDK 只路由 + 透传 JSON。 **Q: orderToken 怎么回到 H5 查单?** A: 它是你 createOrder 自己产的 · **当场就持有**(无需 SDK 回调还给你)。provider.pay 也会把它放进 NativePayResult,PayBridge 白名单透传到三态的 `orderToken` 字段。 **Q: 为什么 iOS 不传 accountId?** A: StoreKit 仅一个槽 `appAccountToken`,承载 orderToken(订单维度)· 用户身份经后端 orderToken→订单反查。 **Q: orderToken 必须 UUID 吗?(跨端地雷)** A: 🔴 是。iOS `appAccountToken` 被 StoreKit **强制 UUID**;Android `obfuscatedProfileId` 虽接受更宽字符集,但若用非 UUID(如 `order_123`)会出现「Android 能跑、iOS 在 StoreKit 处崩」的跨端地雷。因此**统一要求 UUID**:内置 `google_play` 与 iOS host provider 都会校验,非 UUID 返回 `{ phase:'pay', code:'INVALID_ORDER_TOKEN' }`。业务 createOrder 产 orderToken 用 `crypto.randomUUID()`(后端创单端点亦签发 UUID)。 **Q: finished 了为什么还要查单?** A: `finished` 只表示客户端流程结束(含 Google PENDING / ITEM_ALREADY_OWNED)· 真相在后端(验签 + RTDN/ASSN)。客户端发货会被伪造/重放。 **Q: 凭证回执 POST 失败丢单吗?** A: 不会。投递失败不阻断三态;后端 RTDN/ASSN 兜底终会落地;H5 查单从 processing→success。 --- ## 完整示例 ```javascript // 页面初始化 async function init() { // 检查环境 if (!SuperSDK.isInApp()) { console.log('请在 App 内打开'); return; } // 获取环境信息 try { const env = await SuperSDK.getEnv(); console.log('当前 WebView ID:', env.webviewId); console.log('App 版本:', env.versionName); } catch (error) { console.error('获取环境信息失败:', error); } // 监听生命周期 SuperSDK.onForeground((data) => { console.log('页面激活'); refreshData(); }); SuperSDK.onBackground((data) => { console.log('页面进入后台'); pauseVideo(); }); // 监听广播消息 SuperSDK.subscribe('user_logout', () => { location.reload(); }); } // 分享功能 function shareToFriends() { SuperSDK.share('推荐一个好用的 App'); } // 打开详情页 function openDetail(id) { SuperSDK.openPage(`https://example.com/detail/${id}`); } // 完成后关闭页面 function onComplete() { // 通知其他页面刷新 SuperSDK.publish('order_created', { orderId: '12345' }); // 关闭当前页面 SuperSDK.closePage(); } init(); ``` ## 注意事项 1. **环境检测**:始终使用 `isInApp()` 检测运行环境,确保代码兼容性 2. **异步方法**:`getEnv`、`getHostSettings` 等返回 Promise,需要 await 或 .then() 3. **取消监听**:`onForeground`、`onBackground`、`subscribe` 返回取消函数,在组件卸载时调用;`onBackPressed` 使用 `offBackPressed()` 取消 4. **TAB 页面**:`closePage()` 在 TAB 页面中无效果 5. **内部接口**:`onJSBridgeReady` 由 SDK 自动调用,业务代码请勿主动调用 6. **超时处理**:异步方法超时后会 reject 并返回 `TIMEOUT` 错误,各方法超时时间如下: - **5 秒**:`getEnv`、`getAppName`、`getAppIcon`、`getHostSettings`/`ByKey`、`fileExists`、`createDir`、`reportEvent`、`pay.getProviders`、`hideBannerAd` - **10 秒**:`requestShortUrl`、`requestShortUrlWithHtml`、`deleteFile`、`moveFile`、`listDir` - **30 秒**:`getFileMd5`、`readFile`、`writeFile`、`getAttribution`、`pay.restorePurchases`、`showBannerAd` - **60 秒**:`unzipFile`、`zipDir`、`requestLogin`、`showInterstitialAd` - **120 秒**:`showRewardAd`、`showRewardedInterstitialAd` - **190 秒**:`pay.purchase`(Native IAP 路径,含用户在支付界面的操作时间;Web 收银台 inline 路径不设固定超时,按 `pollInterval × maxPolls` 轮询) - **无超时**:`callHostMethod`、`downloadFile`(长时间任务,需业务层自行控制)