From b6e45cd70722b805c9fbb32a0c2c8e82ca1d962a Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 11 Sep 2026 13:58:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8iOS=E6=8A=96=E9=9F=B3?= =?UTF-8?q?=E6=94=AF=E4=BB=98SDK=E5=9B=9E=E8=B7=B3URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/douyin-ios-return-20260911.md | 19 ++++++++ tests/douyin-uts.test.cjs | 44 +++++++++++++++++++ uni_modules/tb-douyin-pay/package.json | 2 +- .../tb-douyin-pay/utssdk/app-ios/index.uts | 15 ++++++- .../tb-douyin-pay/utssdk/interface.uts | 1 + 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/docs/douyin-ios-return-20260911.md b/docs/douyin-ios-return-20260911.md index f0b7973..cedaa91 100644 --- a/docs/douyin-ios-return-20260911.md +++ b/docs/douyin-ios-return-20260911.md @@ -1,5 +1,24 @@ # iOS 抖音支付回跳修复(2026-09-11) +## 二次修复:SDK 要求完整回跳 URL(插件 0.2.3) + +用户确认重新打包安装了 f568fa0 的完整 IPA,支付后仍停在抖音,商城未打开。前次仅修复 Universal Link 选择,遗漏了 SDK 参数格式,不能视为真机回跳问题已经解决。 + +本次读取[官方 OpenSDK 集成指南原文](https://pay.douyinpay.com/wiki/639fd757f4a57b0226bb2066/639fd78317c2f3021d238378.md)和[官方 App 调起支付文档](https://pay.douyinpay.com/wiki/639fd48f17c2f3021d237f61/639fd5e470f838021f2961e5.md)。注册示例明确将 `://dypay` 传给 callbackScheme,不能直接传系统注册的裸 Scheme。 + +两个配置的值不同: + +- manifest.json 的 ios.urltypes、Info.plist 的 CFBundleURLSchemes:仍为 `trustbridgeapp`。 +- iOS 原生 DypayAPI 注册的 callbackScheme:必须为 `trustbridgeapp://dypay`,appId 仍为服务端返回的签名 appid。 + +app-ios/index.uts 在调用 Swift 前将裸 Scheme 或带 `://` 的根地址补齐为完整回跳 URL;已经完整的 URL 不重复拼接,非法格式在修改共享配置及调用原生前拒绝。JS 入口仍传宿主 Scheme,Android 不变。导出资源确认 TbDouyinPayHook 已生成且记录在 hooksClass 中,不额外添加重复 Hook。保留前次移除默认无效 Universal Link 的修复。 + +56 项支付回归通过,新增测试运行实际 iOS index.uts 转译代码,捕获送入原生桥接的参数,验证裸 Scheme、根地址、完整 URL、非法地址和回调转发。原生 SDK 使用模拟边界;尚无完整 IPA 编译或真机成功回跳的验证结果。必须重新打包安装包含插件 0.2.3 的完整 IPA/自定义基座,WGT 无法更新本次 UTS 原生代码。 + +HBuilderX 5.24 于 13:58:18 App 资源编译、导出成功,仅既有 CSS 警告;已检查生成的 Swift,确认 callbackURL 拼接、格式验证、原生 initialize 参数及 hooksClass 均生成。此检查不等同 Xcode 编译或真机支付。 + +## 前次修复记录(插件 0.2.2,未解决完整 URL 格式) + 基于 `charles/frontend-app` 的 `dev_codex` / `355ad11`。用户已确认在实际 iPhone 上打开 `trustbridgeapp://` 可以唤起商城。 ## 发现与修改 diff --git a/tests/douyin-uts.test.cjs b/tests/douyin-uts.test.cjs index 1c810a9..d67952d 100644 --- a/tests/douyin-uts.test.cjs +++ b/tests/douyin-uts.test.cjs @@ -14,6 +14,50 @@ const compiled = ts.transpileModule(source, { }).outputText; const info = { appid: 'app', partnerid: 'merchant', prepayid: 'prepay', package: 'Sign=DYPay', noncestr: 'nonce', timestamp: '1780000000', sign: 'signed+value/==' }; +function loadIOS(configure = () => true) { + const iosSource = fs.readFileSync(path.join(__dirname, '../uni_modules/tb-douyin-pay/utssdk/app-ios/index.uts'), 'utf8') + .replace(/^import .*$/gm, ''); + const iosJS = ts.transpileModule(iosSource, { + compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS }, + }).outputText; + const registrations = []; + const native = { initialize: (...args) => registrations.push(args), processURL: () => false }; + const ctx = { exports: {}, configure, TbDouyinPayNative: native }; + vm.runInNewContext(iosJS, ctx); + return { ...ctx.exports, registrations, native }; +} + +test('actual iOS bridge passes a complete callback URL to the native SDK boundary', () => { + for (const scheme of ['trustbridgeapp', ' trustbridgeapp ', 'trustbridgeapp://', 'trustbridgeapp://dypay']) { + const bridge = loadIOS(); + assert.equal(bridge.initDypay({ appId: 'signed-app-id', callbackScheme: scheme }), true); + assert.deepEqual(bridge.registrations, [['signed-app-id', 'trustbridgeapp://dypay', '']]); + } + const bridge = loadIOS(); + bridge.initDypay({ appId: 'signed-app-id', callbackScheme: 'trustbridgeapp', universalLink: 'https://example.com/pay/' }); + assert.deepEqual(bridge.registrations, [['signed-app-id', 'trustbridgeapp://dypay', 'https://example.com/pay/']]); +}); + +test('iOS rejects malformed return URLs and failed configuration before native registration', () => { + for (const scheme of ['', ' ', 'invalid scheme', 'trustbridgeapp://other', 'trustbridgeapp://dypay?unexpected=1']) { + const bridge = loadIOS(() => assert.fail('Invalid URL must not change shared configuration')); + assert.equal(bridge.initDypay({ appId: 'app', callbackScheme: scheme }), false); + assert.equal(bridge.registrations.length, 0); + } + const bridge = loadIOS(() => false); + assert.equal(bridge.initDypay({ appId: 'app', callbackScheme: 'trustbridgeapp' }), false); + assert.equal(bridge.registrations.length, 0); +}); + +test('iOS return hook forwards the original callback URL and preserves SDK handled result', () => { + const bridge = loadIOS(); + const url = { absoluteString: 'trustbridgeapp://dypay?resultCode=0' }; + for (const handled of [false, true]) { + bridge.native.processURL = received => { assert.equal(received, url); return handled; }; + assert.equal(new bridge.TbDouyinPayHook().applicationOpenURLOptions(null, url, null), handled); + } +}); + function load(stringify = JSON.stringify) { const timers = new Map(); let timerId = 0; diff --git a/uni_modules/tb-douyin-pay/package.json b/uni_modules/tb-douyin-pay/package.json index e6149db..9a9f368 100644 --- a/uni_modules/tb-douyin-pay/package.json +++ b/uni_modules/tb-douyin-pay/package.json @@ -1,7 +1,7 @@ { "id": "tb-douyin-pay", "displayName": "TB 抖音支付", - "version": "0.2.2", + "version": "0.2.3", "description": "独立实现的抖音支付官方 SDK UTS 桥接", "engines": { "HBuilderX": ">=4.36.0" }, "uni_modules": { diff --git a/uni_modules/tb-douyin-pay/utssdk/app-ios/index.uts b/uni_modules/tb-douyin-pay/utssdk/app-ios/index.uts index 80ac619..4d86d05 100644 --- a/uni_modules/tb-douyin-pay/utssdk/app-ios/index.uts +++ b/uni_modules/tb-douyin-pay/utssdk/app-ios/index.uts @@ -9,8 +9,19 @@ export function resetDypay(): void { } export function initDypay(options: InitOptions): boolean { - if (options.callbackScheme.trim().length == 0 || !configure(options.appId)) return false - TbDouyinPayNative.initialize(options.appId, options.callbackScheme, options.universalLink ?? '') + let callbackURL = options.callbackScheme.trim() + if (callbackURL.length == 0) return false + // CFBundleURLSchemes contains a bare scheme, but Dypay's callbackScheme API + // requires a complete URL. Official registration example: ://dypay. + // https://pay.douyinpay.com/wiki/639fd757f4a57b0226bb2066/639fd78317c2f3021d238378 + if (callbackURL.indexOf('://') < 0) { + callbackURL = callbackURL + '://dypay' + } else if (callbackURL.endsWith('://')) { + callbackURL = callbackURL + 'dypay' + } + if (!new RegExp('^[A-Za-z][A-Za-z0-9+.-]*://dypay$').test(callbackURL)) return false + if (!configure(options.appId)) return false + TbDouyinPayNative.initialize(options.appId, callbackURL, options.universalLink ?? '') return true } diff --git a/uni_modules/tb-douyin-pay/utssdk/interface.uts b/uni_modules/tb-douyin-pay/utssdk/interface.uts index 4c80146..eea5ecc 100644 --- a/uni_modules/tb-douyin-pay/utssdk/interface.uts +++ b/uni_modules/tb-douyin-pay/utssdk/interface.uts @@ -1,5 +1,6 @@ export type InitOptions = { appId: string, + // Host's registered scheme; iOS converts it to ://dypay for the SDK. callbackScheme: string, universalLink?: string | null }