diff --git a/docs/douyin-ios-return-20260911.md b/docs/douyin-ios-return-20260911.md new file mode 100644 index 0000000..f0b7973 --- /dev/null +++ b/docs/douyin-ios-return-20260911.md @@ -0,0 +1,26 @@ +# iOS 抖音支付回跳修复(2026-09-11) + +基于 `charles/frontend-app` 的 `dev_codex` / `355ad11`。用户已确认在实际 iPhone 上打开 `trustbridgeapp://` 可以唤起商城。 + +## 发现与修改 + +该基线已经在 manifest.json 和 info.plist 注册了 trustbridgeapp,但支付初始化又默认传入 `https://app.tbmall.xin/uni-universallinks/__UNI__4910728/`。项目所带 DypaySDK 的 DypayAPI.h 明确说明:有 Universal Link 时优先使用它,降级使用 Scheme。 + +2026-09-11 10:48—10:49(北京时间)外网 GET 检查发现,该域名的 `/.well-known/apple-app-site-association` 和 `/apple-app-site-association` 都返回 HTTP 200、Content-Type text/html,正文是官网首页,不是 Apple 关联 JSON。默认回跳 URL 本身同样返回官网首页。缺少有效关联文件的链接不能作为已经验证可唤起 App 的 Universal Link;本次没有取得手机日志,不能把网络检查等同于具体真机失败路径的证明。 + +- iOS 支付初始化固定使用宿主已注册且用户实测有效的 `trustbridgeapp`,不带 `://`。支付接口里的回跳 Scheme 和 Universal Link 不覆盖该值;支付单号、金额、签名参数保持原值。 +- 移除抖音支付入口对 Universal Link 的默认传入。Android 保留原有 Scheme 选择,Android 原生初始化仍只使用 appId。 +- iOS 桥接收到空 Universal Link 时,调用官方两参数 `register(withAppID:callbackScheme:)`。插件的显式非空 Universal Link 调用仍保留三参数 API,便于其他已验证宿主使用。 +- 插件升级至 0.2.2。已有 UTSiOSHookProxy 的 URL/Universal Link 回调接收、App.onShow 查单及服务端确认后跳转成功页继续沿用。不以 URL 回跳本身判断支付到账。 + +本次不修改微信、支付宝的 Universal Link 配置、后端、SQL 或业务接口,也不部署 AASA 文件。 + +## 验证与发布 + +`node --test tests/douyin-integration.test.cjs tests/douyin-uts.test.cjs`(设置 HBUILDERX_HOME)53 项通过。新增测试验证 iOS 遇到错误 Scheme、带 `://` 的 Scheme 和两种 Universal Link 字段时仍注册正确宿主 Scheme,且签名参数不变、成功仍以服务端查单为准;Android 原有选择行为保留。 + +UTS 测试将实际共享 UTS 转译为 JS 运行,不是原生 Swift 编译。当前 Windows 无 Xcode,完整 IPA 编译和真机回跳需要发布端验证。 + +HBuilderX 5.24 `cli publish --platform APP --type appResource` 于 10:51:59 编译、导出成功,只有既有 CSS Autoprefixer 注释警告。资源导出成功不代表完整 IPA 原生构建通过。 + +发布必须重新云打包并安装完整 IPA 或自定义基座;只有 WGT 无法更新本次 Swift 原生修改。安装后验收支付成功、取消、手动返回 App、杀进程恢复,以及确认到账后只跳转一次支付成功页。若仍不能从抖音返回,请保留该包的构建版本和真机 SDK 日志继续定位,不通过直接标记支付成功绕过查单。 diff --git a/tests/douyin-integration.test.cjs b/tests/douyin-integration.test.cjs index 0a7504b..5872919 100644 --- a/tests/douyin-integration.test.cjs +++ b/tests/douyin-integration.test.cjs @@ -33,6 +33,7 @@ function uniMock() { const navigations = []; return { storage, messages, navigations, getStorageSync: k => storage.get(k), setStorageSync: (k,v) => storage.set(k,v), removeStorageSync: k => storage.delete(k), showToast: v => messages.push(v.title), $emit() {}, + getSystemInfoSync: () => ({ platform: 'android' }), showModal: () => assert.fail('Douyin must not add payment confirmation modals'), showLoading() {}, hideLoading() {}, navigateTo: options => { navigations.push(options.url); options.success?.(); }, @@ -53,6 +54,34 @@ async function payContext(overrides = {}, app = true) { } const successPage = '/pages/order_package/order_submit_ok/order_submit_ok'; const failurePage = '/pages/order_package/order_submit_error/order_submit_error'; + +test('iOS always registers the installed host Scheme without a Universal Link', async () => { + for (const overrides of [{}, { callbackScheme: 'wrong-app' }, { callbackScheme: 'trustbridgeapp://' }, + { universalLink: 'https://app.tbmall.xin/uni-universallinks/__UNI__4910728/' }, + { UniversalLinks: 'https://example.com/return/' }]) { + let registration; + let signedInfo; + const ctx = await payContext({ + initDypay: options => { registration = options; return true; }, + openDypay: (options, cb) => { signedInfo = options.payInfo; cb({ resultCode: '0' }); } + }); + ctx.uni.getSystemInfoSync = () => ({ platform: 'ios' }); + const current = { ...payment, douyin: { ...payment.douyin, ...overrides } }; + const result = await ctx.appDypayFun(current); + assert.deepEqual({ ...registration }, { appId: 'app', callbackScheme: 'trustbridgeapp' }); + assert.deepEqual(signedInfo, ctx.toSdkPayInfo(current)); + assert.equal(result.state, 'paid'); + assert.deepEqual(ctx.uni.navigations, [successPage]); + } +}); + +test('Android keeps its existing callback Scheme selection', async () => { + let registration; + const ctx = await payContext({ initDypay: options => { registration = options; return true; } }); + await ctx.appDypayFun({ ...payment, douyin: { ...payment.douyin, callbackScheme: 'android-host' } }); + assert.equal(registration.callbackScheme, 'android-host'); + assert.equal(registration.appId, 'app'); +}); test('SDK success is confirmed with server before showing success', async () => { let queries = 0; const ctx = await payContext({ request: async () => { queries++; return { bizcode:100, data:{state:2} }; } }); diff --git a/uni_modules/tb-douyin-pay/package.json b/uni_modules/tb-douyin-pay/package.json index f83d5b0..e6149db 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.1", + "version": "0.2.2", "description": "独立实现的抖音支付官方 SDK UTS 桥接", "engines": { "HBuilderX": ">=4.36.0" }, "uni_modules": { diff --git a/uni_modules/tb-douyin-pay/utssdk/app-ios/TbDouyinPayNative.swift b/uni_modules/tb-douyin-pay/utssdk/app-ios/TbDouyinPayNative.swift index 85c82d0..48c1e13 100644 --- a/uni_modules/tb-douyin-pay/utssdk/app-ios/TbDouyinPayNative.swift +++ b/uni_modules/tb-douyin-pay/utssdk/app-ios/TbDouyinPayNative.swift @@ -7,7 +7,13 @@ public class TbDouyinPayNative { private static var generation = UUID() public static func initialize(_ appId: String, _ scheme: String, _ link: String) { - DypayAPI.register(withAppID: appId, universalLink: link, callbackScheme: scheme) + if link.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + // Use the SDK's Scheme-only API instead of registering an empty + // Universal Link with the higher-priority Universal Link API. + DypayAPI.register(withAppID: appId, callbackScheme: scheme) + } else { + DypayAPI.register(withAppID: appId, universalLink: link, callbackScheme: scheme) + } } public static func available() -> Bool { DypayAPI.canOpenDypay() } diff --git a/utils/douyin-pay.js b/utils/douyin-pay.js index 0303399..9085bc5 100644 --- a/utils/douyin-pay.js +++ b/utils/douyin-pay.js @@ -119,9 +119,12 @@ async function executePayment(payment, key) { if (key !== accountKey()) return { state: 'pending' }; const payInfo = toSdkPayInfo(payment); if (!canOpenDypay()) throw new Error('请先安装或升级抖音客户端'); - const callbackScheme = payment.douyin?.callbackScheme || 'trustbridgeapp'; - const universalLink = payment.douyin?.universalLink || payment.douyin?.UniversalLinks || 'https://app.tbmall.xin/uni-universallinks/__UNI__4910728/'; - if (!initDypay({ appId: payInfo.appid, callbackScheme, universalLink })) { + // iOS must return to this installed host's registered Scheme. The previous + // default Universal Link serves HTML instead of an Apple association file; + // Dypay prioritizes that link over the working Scheme. Do not register it. + const isIOS = uni.getSystemInfoSync().platform === 'ios'; + const callbackScheme = isIOS ? 'trustbridgeapp' : (payment.douyin?.callbackScheme || 'trustbridgeapp'); + if (!initDypay({ appId: payInfo.appid, callbackScheme })) { throw new Error('支付初始化失败,请稍后重试'); } // Persist before entering native code so a killed process remains recoverable.