feat:更新176
This commit is contained in:
@@ -94,9 +94,35 @@ export default {
|
||||
scrollTop: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
guideShow: {
|
||||
handler(val) {
|
||||
// #ifdef MP
|
||||
this.$nextTick(() => {
|
||||
if (val) {
|
||||
uni.hideTabBar({ animation: false, fail: () => {} });
|
||||
} else {
|
||||
uni.showTabBar({ animation: false, fail: () => {} });
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.checkGuideShow();
|
||||
},
|
||||
beforeDestroy() {
|
||||
// #ifdef MP
|
||||
uni.showTabBar({ animation: false, fail: () => {} });
|
||||
// #endif
|
||||
},
|
||||
unmounted() {
|
||||
// #ifdef MP
|
||||
uni.showTabBar({ animation: false, fail: () => {} });
|
||||
// #endif
|
||||
},
|
||||
methods: {
|
||||
onScroll(e) {
|
||||
this.scrollTop = e.detail.scrollTop;
|
||||
@@ -112,12 +138,18 @@ export default {
|
||||
if (!hasSeen) {
|
||||
this.guideStep = 0;
|
||||
this.guideShow = true;
|
||||
// #ifdef MP
|
||||
uni.hideTabBar({ animation: false, fail: () => {} });
|
||||
// #endif
|
||||
this.resetScroll();
|
||||
}
|
||||
},
|
||||
closeGuide() {
|
||||
this.guideShow = false;
|
||||
uni.setStorageSync("has_seen_home_guide", true);
|
||||
// #ifdef MP
|
||||
uni.showTabBar({ animation: false, fail: () => {} });
|
||||
// #endif
|
||||
this.$emit("close");
|
||||
},
|
||||
nextGuideStep() {
|
||||
@@ -131,6 +163,9 @@ export default {
|
||||
open() {
|
||||
this.guideStep = 0;
|
||||
this.guideShow = true;
|
||||
// #ifdef MP
|
||||
uni.hideTabBar({ animation: false, fail: () => {} });
|
||||
// #endif
|
||||
this.resetScroll();
|
||||
},
|
||||
},
|
||||
|
||||
+4
-1
@@ -191,7 +191,10 @@
|
||||
"uglifyFileName" : true,
|
||||
"es6" : true,
|
||||
"postcss" : true,
|
||||
"sourcemap" : false
|
||||
"sourcemap" : false,
|
||||
"ignoreDevUnusedFiles" : true,
|
||||
"minifyWXML" : true,
|
||||
"minifyWXSS" : true
|
||||
},
|
||||
"usingComponents" : true,
|
||||
"optimization" : {
|
||||
|
||||
@@ -675,12 +675,6 @@
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "default" // 默认样式,会显示返回图标
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "douyin_pay_test/douyin_pay_test",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+10
-1
@@ -274,7 +274,7 @@
|
||||
</up-overlay>
|
||||
|
||||
<HomePopup :show="homeShow" :dataObj="curData" @jumpShop="onJumpShop" @close="homeShow = false"></HomePopup>
|
||||
<GuidePopup />
|
||||
<GuidePopup ref="guidePopupRef" />
|
||||
<qiaobao-assistant page-key="pages/home/home" title="商城首页" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -376,6 +376,14 @@ export default {
|
||||
this.isScrolled = top > 10;
|
||||
},
|
||||
onShow() {
|
||||
// #ifdef MP
|
||||
const hasSeenGuide = uni.getStorageSync("has_seen_home_guide");
|
||||
if (hasSeenGuide) {
|
||||
uni.showTabBar({ animation: false, fail: () => {} });
|
||||
} else {
|
||||
uni.hideTabBar({ animation: false, fail: () => {} });
|
||||
}
|
||||
// #endif
|
||||
this.refreshKefuUnread();
|
||||
startKefuRealtime();
|
||||
// 获取设备是安卓还是ios
|
||||
@@ -421,6 +429,7 @@ export default {
|
||||
this.initMPHeader();
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 分享给朋友
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,24 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$HBuilderXHome,
|
||||
[Parameter(Mandatory = $true)][string]$SdkJar
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Compile the generated UTS and the original native bridge against real SDKs.
|
||||
# First export App resources using HBuilderX. SdkJar is classes.jar extracted
|
||||
# from the official dy-pay-sdk-tob 1.1.0.9 AAR, not a mock or reflection shim.
|
||||
$projectDir = Split-Path -Parent $PSScriptRoot
|
||||
$runtimeDir = Join-Path $HBuilderXHome 'plugins/uniapp-runextension'
|
||||
$compilerDir = Join-Path $runtimeDir 'kotlinc'
|
||||
$generated = Join-Path $projectDir 'unpackage/resources/uni_modules/tb-douyin-pay/utssdk/app-android/src/index.kt'
|
||||
$native = Join-Path $projectDir 'uni_modules/tb-douyin-pay/utssdk/app-android/TbDouyinPayNative.kt'
|
||||
foreach ($required in @($SdkJar, $generated, $native, (Join-Path $compilerDir 'lib/kotlin-compiler.jar'))) {
|
||||
if (!(Test-Path -LiteralPath $required)) { throw "Missing dependency or source: $required" }
|
||||
}
|
||||
$taskClasspath = ((Get-ChildItem -LiteralPath (Join-Path $runtimeDir 'lib') -Filter '*.jar').FullName + @((Resolve-Path -LiteralPath $SdkJar).Path)) -join ';'
|
||||
$resultDir = New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) ('tb-douyin-kotlin-' + [guid]::NewGuid().ToString('N')))
|
||||
$resultJar = Join-Path $resultDir.FullName 'tb-douyin-pay.jar'
|
||||
& java -cp (Join-Path $compilerDir 'lib/*') org.jetbrains.kotlin.cli.jvm.K2JVMCompiler `
|
||||
-kotlin-home $compilerDir $generated $native -classpath $taskClasspath -d $resultJar
|
||||
if ($LASTEXITCODE -ne 0) { throw "Kotlin compilation failed: $LASTEXITCODE" }
|
||||
Write-Output "Kotlin compilation passed: $resultJar"
|
||||
Write-Output 'This check does not build/sign an APK or execute a payment.'
|
||||
@@ -1,630 +0,0 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
function preprocess(source, app) {
|
||||
const stack = [true];
|
||||
return source.split('\n').filter(line => {
|
||||
const directive = line.match(/\/\/\s*#(ifdef|ifndef) (APP-PLUS|H5)/);
|
||||
if (directive) {
|
||||
const enabled = directive[2] === 'APP-PLUS' ? app === true : app === 'h5';
|
||||
stack.push(stack.at(-1) && (directive[1] === 'ifdef' ? enabled : !enabled)); return false;
|
||||
}
|
||||
if (/\/\/\s*#endif/.test(line)) { stack.pop(); return false; }
|
||||
return stack.at(-1);
|
||||
}).join('\n');
|
||||
}
|
||||
function loadModule(file, context, app = true) {
|
||||
const source = preprocess(fs.readFileSync(path.join(root, file), 'utf8'), app)
|
||||
.replace(/^import [^\r\n]*;[^\S\r\n]*(?:\/\/[^\r\n]*)?\r?$/gm, '')
|
||||
.replace(/export (async function|function)/g, '$1')
|
||||
.replace(/export const /g, 'const ')
|
||||
.replace(/export default request;/g, '');
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source, context, { filename: file });
|
||||
return context;
|
||||
}
|
||||
function uniMock() {
|
||||
const storage = new Map([['token', 'test-token'], ['user', { userId: 7 }]]);
|
||||
const messages = [];
|
||||
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?.(); },
|
||||
redirectTo: options => { navigations.push(options.url); options.success?.(); } };
|
||||
}
|
||||
const payment = { orderNum: 'order-1', douyin: { appId: 'app', mchId: 'mch', prepayId: 'prepay',
|
||||
packageValue: 'Sign=DYPay', nonceStr: 'nonce', timeStamp: '1780000000', sign: 'sig' } };
|
||||
async function payContext(overrides = {}, app = true) {
|
||||
const adapter = await import('../utils/douyin-pay-adapter.mjs');
|
||||
const context = { uni: uniMock(), BASE_URL: 'test-api', TOKEN_NAME: 'token', USER_DATA: 'user',
|
||||
...adapter, canOpenDypay: () => true, initDypay: () => true, resetDypay: () => {},
|
||||
openDypay: (_, cb) => cb({ resultCode: '0' }),
|
||||
request: async options => options.url === '/payment/douyin/resume'
|
||||
? ({ bizcode: 100, data: { state: 'paid', orderNum: options.data.orderNum } })
|
||||
: ({ bizcode: 100, data: { state: 2 } }), ...overrides };
|
||||
loadModule('utils/payment-navigation.js', context, app);
|
||||
return loadModule('utils/douyin-pay.js', context, app);
|
||||
}
|
||||
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} }; } });
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(queries, 1);
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
test('SDK success plus network failure remains recoverable, never success', async () => {
|
||||
const ctx = await payContext({ request: async () => { throw new Error('offline'); } });
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-1');
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'), false);
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
});
|
||||
test('cancel still queries authoritative server state', async () => {
|
||||
const ctx = await payContext({ openDypay: (_, cb) => cb({ resultCode:'1' }) });
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
test('old backend parameters never invoke native SDK', async () => {
|
||||
let calls = 0;
|
||||
const ctx = await payContext({ openDypay: () => calls++ });
|
||||
await ctx.appDypayFun({ orderNum:'old', douyin:{appId:'app'} });
|
||||
assert.equal(calls, 0);
|
||||
assert.match(ctx.uni.messages.at(-1), /缺少支付参数/);
|
||||
});
|
||||
test('new backend orderInfo (object or JSON) reaches native SDK with exact signed fields', async () => {
|
||||
const { toSdkPayInfo } = await import('../utils/douyin-pay-adapter.mjs');
|
||||
const expected = toSdkPayInfo(payment);
|
||||
for (const orderInfo of [expected, JSON.stringify(expected)]) {
|
||||
let received;
|
||||
const ctx = await payContext({ openDypay: (options, cb) => { received = options.payInfo; cb({resultCode:'0'}); } });
|
||||
const result = await ctx.appDypayFun({orderNum:'order-1', douyin:{type:'app', orderInfo}});
|
||||
assert.deepEqual(received, expected);
|
||||
assert.equal(result.state, 'paid');
|
||||
}
|
||||
});
|
||||
test('legacy douyinPayFun calls native bridge, never uni payment provider or URL fallback', async () => {
|
||||
let nativeCalls = 0;
|
||||
const ctx = await payContext({ openDypay: (_, cb) => { nativeCalls++; cb({resultCode:'0'}); } });
|
||||
ctx.uni.requestPayment = () => assert.fail('Must not use built-in payment provider for Douyin');
|
||||
ctx.plus = { runtime: { openURL: () => assert.fail('Must not synthesize cashier URLs') } };
|
||||
loadModule('utils/payUtils.js', ctx);
|
||||
const result = await ctx.douyinPayFun(payment.douyin, 1, payment.orderNum);
|
||||
assert.equal(nativeCalls, 1);
|
||||
assert.equal(result.state, 'paid');
|
||||
});
|
||||
test('invalid JSON and Scheme-only payload do not call SDK or create pending state', async () => {
|
||||
for (const douyin of [{orderInfo:'{'}, {orderStr:'snssdk1128://open'}, {type:'h5', prepayId:'prepay'}]) {
|
||||
const ctx = await payContext({openDypay: () => assert.fail('Invalid data reached SDK')});
|
||||
const result = await ctx.appDypayFun({orderNum:'invalid', douyin});
|
||||
assert.equal(result.state, 'error');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
}
|
||||
});
|
||||
test('legacy retry of settled order queries state without opening payment again', async () => {
|
||||
const ctx = await payContext({openDypay: () => assert.fail('Retry opened payment again')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
const result = await ctx.appDypayFun(payment);
|
||||
assert.equal(result.state, 'paid');
|
||||
});
|
||||
test('duplicate invocation does not open SDK twice', async () => {
|
||||
let done;
|
||||
let calls = 0;
|
||||
const ctx = await payContext({ openDypay: (_, cb) => { calls++; done = cb; } });
|
||||
const first = ctx.appDypayFun(payment);
|
||||
await ctx.appDypayFun(payment);
|
||||
done({resultCode:'0'});
|
||||
await first;
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
test('account switch suppresses old account result and retains its pending record', async () => {
|
||||
const ctx = await payContext();
|
||||
ctx.request = async () => { ctx.uni.storage.set('user', {userId:8}); return {bizcode:100,data:{state:2}}; };
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'), false);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-1');
|
||||
});
|
||||
test('cold start queries saved order; unavailable app does not create order', async () => {
|
||||
const ctx = await payContext({ canOpenDypay: () => false });
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', 'pending');
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
test('non-App excludes Douyin and never accesses native SDK', async () => {
|
||||
const ctx = await payContext({}, false);
|
||||
assert.equal(ctx.douyinPayTypes('1,2,3'), '1,2,3');
|
||||
assert.equal(ctx.filterDouyinPayWays([{type:'douyin'},{type:'wechat'}]).length, 1);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
});
|
||||
|
||||
test('H5 displays configured Douyin but blocks payment before order creation', async () => {
|
||||
const ctx = await payContext({ canOpenDypay: () => { throw new Error('Native SDK must not run'); } }, 'h5');
|
||||
assert.equal(ctx.douyinPayTypes('1,2,3'), '1,2,3,6');
|
||||
assert.equal(ctx.douyinPayTypes('1,6'), '1,6');
|
||||
assert.equal(ctx.filterDouyinPayWays([{type:'douyin'},{type:'wechat'}]).length, 2);
|
||||
assert.equal(ctx.filterDouyinPayWays([]).length, 0);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
assert.match(ctx.uni.messages.at(-1), /仅支持/);
|
||||
});
|
||||
test('request terminates on HTTP500, business500, bad JSON, auth failure and offline', async () => {
|
||||
for (const response of [{statusCode:500,data:{}}, {statusCode:200,data:{status:500}},
|
||||
{statusCode:200,data:'<html>'}, {statusCode:200,data:{bizcode:201,errcode:999}}, null]) {
|
||||
const uni = uniMock();
|
||||
uni.request = options => response ? options.success(response) : options.fail(new Error('offline'));
|
||||
const ctx = loadModule('utils/request.js', { uni, TOKEN_NAME:'token', USER_DATA:'user',
|
||||
getStorageFun: () => '', BASE_URL:'test-api', Authorization:'test-only', console:{log(){}}, setTimeout, clearTimeout });
|
||||
await assert.rejects(ctx.request({url:'/payment/queryResult'}));
|
||||
}
|
||||
});
|
||||
|
||||
test('killed App only resumes the original transaction when the current response selects that same order', async () => {
|
||||
const ctx = await payContext();
|
||||
// This is exactly the legacy persisted format: only an order number survives process death.
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.queryUntilSettled = async () => ({state:'pending'});
|
||||
await ctx.resumeDouyinPay();
|
||||
let opened = 0;
|
||||
const urls = [];
|
||||
ctx.request = async options => {
|
||||
urls.push(options.url);
|
||||
assert.equal(options.data.orderNum, payment.orderNum);
|
||||
return {bizcode:100, data:{state:'unpaid', orderNum:payment.orderNum, payment}};
|
||||
};
|
||||
ctx.openDypay = (options, cb) => { opened++; assert.equal(options.payInfo.prepayid, 'prepay'); cb({resultCode:'0'}); };
|
||||
ctx.queryUntilSettled = async () => ({state:'paid'});
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.equal(opened, 0);
|
||||
assert.deepEqual(urls, []);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(opened, 1);
|
||||
assert.deepEqual(urls, ['/payment/douyin/resume']);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
|
||||
test('cold start never opens the SDK or asks for payment and preserves pending state', async () => {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Unexpected SDK call'),
|
||||
queryUntilSettled: async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.uni.showModal = () => assert.fail('onShow must not ask for payment');
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
});
|
||||
|
||||
test('retry of the SAME order remains blocked on unknown, processing, network error or missing backend', async () => {
|
||||
for (const mode of ['pending','NEW_STATE','offline','missing']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Unknown result must not open SDK'),
|
||||
request: async()=> {
|
||||
if (mode === 'offline') throw new Error('offline');
|
||||
if (mode === 'missing') return {bizcode:404};
|
||||
return {bizcode:100,data:{state:mode,orderNum:payment.orderNum}};
|
||||
}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
|
||||
test('terminal server result clears saved order without payment and allows the next action', async () => {
|
||||
for (const state of ['paid','closed']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Terminal order reached SDK'),
|
||||
request: async options => options.url === '/payment/douyin/resume'
|
||||
? ({bizcode:100,data:{state,orderNum:payment.orderNum}})
|
||||
: ({bizcode:100,data:{state:2}})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations, [state === 'paid' ? successPage : failurePage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('a different current order does not resume or close the cached old transaction', async () => {
|
||||
let resets=0, opened;
|
||||
const ctx=await payContext({resetDypay:()=>resets++, request:async options => {
|
||||
assert.equal(options.url, '/payment/queryResult');
|
||||
assert.equal(options.data.orderNum, 'fresh-order');
|
||||
return {bizcode:100,data:{state:2}};
|
||||
},
|
||||
openDypay:(options,cb)=>{opened=options.payInfo.prepayid; cb({resultCode:'0'});}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','expired-order');
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
assert.deepEqual(ctx.uni.navigations,[]);
|
||||
assert.equal(resets,0);
|
||||
const fresh={...payment,orderNum:'fresh-order',douyin:{...payment.douyin,prepayId:'fresh-prepay'}};
|
||||
await ctx.appDypayFun(fresh);
|
||||
assert.equal(resets,1);
|
||||
assert.equal(opened,'fresh-prepay');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('switching accounts during same-order reconciliation cannot create a new payment', async () => {
|
||||
const ctx=await payContext();
|
||||
ctx.request=async()=>{
|
||||
ctx.uni.storage.set('user',{userId:8});
|
||||
return {bizcode:100,data:{state:'closed',orderNum:'expired-order'}};
|
||||
};
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','expired-order');
|
||||
await ctx.appDypayFun({...payment,orderNum:'expired-order'});
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'expired-order');
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
});
|
||||
|
||||
test('unconfirmed server state never resets the native lock and explains why', async () => {
|
||||
const ctx=await payContext({resetDypay:()=>assert.fail('Unconfirmed native reset'),
|
||||
request:async()=>({bizcode:100,data:{state:'pending',reason:'QUERY_FAILED',orderNum:payment.orderNum}})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.match(ctx.uni.messages.at(-1),/查单失败/);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
});
|
||||
|
||||
test('unpaid server confirmation resets a missing native callback before reinitializing SDK', async () => {
|
||||
const sequence=[];
|
||||
const ctx=await payContext({resetDypay:()=>sequence.push('reset'),initDypay:()=>{sequence.push('init');return true;},
|
||||
request:async()=>({bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}}),
|
||||
queryUntilSettled:async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(sequence,['reset','init']);
|
||||
});
|
||||
|
||||
test('SDK rejection stays recoverable but no longer hides the native failure', async () => {
|
||||
const ctx=await payContext({openDypay:(_,cb)=>cb({resultCode:'103'}),queryUntilSettled:async()=>({state:'pending'})});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.match(ctx.uni.messages.at(-1),/原生支付仍在处理中/);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'),false);
|
||||
});
|
||||
|
||||
test('account switch during resume response or native reset cannot invoke SDK', async () => {
|
||||
for (const phase of ['response','reset']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Old account reached SDK')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
ctx.request = async()=> {
|
||||
if (phase === 'response') ctx.uni.storage.set('user',{userId:8});
|
||||
return {bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}};
|
||||
};
|
||||
if (phase === 'reset') ctx.resetDypay = () => ctx.uni.storage.set('user',{userId:8});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
|
||||
test('concurrent clicks and onShow during continuation open only one native payment', async () => {
|
||||
let done;
|
||||
let calls = 0;
|
||||
const ctx = await payContext({request: async()=>({bizcode:100,data:{state:'unpaid',orderNum:payment.orderNum,payment}}),
|
||||
queryUntilSettled: async()=>({state:'paid'}),
|
||||
openDypay:(_,cb)=>{ calls++; done = cb; }});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
const first = ctx.appDypayFun(payment);
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), false);
|
||||
await ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
done({resultCode:'0'});
|
||||
await first;
|
||||
assert.equal(calls, 1);
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
});
|
||||
|
||||
test('returning from Douyin with a missing callback completes via server query and navigates once', async () => {
|
||||
let lateCallback;
|
||||
const ctx = await payContext({openDypay:(_,cb)=>{ lateCallback = cb; }});
|
||||
const paying = ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal((await paying).state, 'paid');
|
||||
lateCallback({resultCode:'0'});
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
});
|
||||
|
||||
test('pending or failed foreground query never unlocks or navigates before the native callback', async () => {
|
||||
for (const mode of ['pending', 'offline']) {
|
||||
let done, resets = 0;
|
||||
const ctx = await payContext({openDypay:(_,cb)=>{ done=cb; }, resetDypay:()=>resets++,
|
||||
queryUntilSettled:async()=>{ if (mode === 'offline') throw new Error('offline'); return {state:'pending'}; }});
|
||||
const paying = ctx.appDypayFun(payment);
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(resets, 0);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
ctx.queryUntilSettled = async()=>({state:'paid'});
|
||||
done({resultCode:'0'});
|
||||
await paying;
|
||||
assert.deepEqual(ctx.uni.navigations, [successPage]);
|
||||
}
|
||||
});
|
||||
|
||||
test('native callback racing with foreground query still navigates only once', async () => {
|
||||
let done, finishQuery;
|
||||
const ctx=await payContext({openDypay:(_,cb)=>{done=cb;}});
|
||||
const paying=ctx.appDypayFun(payment);
|
||||
ctx.queryUntilSettled=()=>new Promise(resolve=>{finishQuery=resolve;});
|
||||
const foreground=ctx.resumeDouyinPay();
|
||||
ctx.queryUntilSettled=async()=>({state:'paid'});
|
||||
done({resultCode:'0'});
|
||||
await paying;
|
||||
finishQuery({state:'paid'});
|
||||
await foreground;
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage]);
|
||||
});
|
||||
|
||||
test('full page stack falls back to redirect; navigation failure remains retryable without another payment', async () => {
|
||||
const ctx=await payContext();
|
||||
ctx.uni.navigateTo=options=>options.fail();
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage]);
|
||||
const failed=await payContext();
|
||||
failed.uni.navigateTo=options=>options.fail();
|
||||
failed.uni.redirectTo=options=>options.fail();
|
||||
await failed.appDypayFun(payment);
|
||||
assert.equal(failed.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
failed.uni.redirectTo=options=>{ failed.uni.navigations.push(options.url); options.success(); };
|
||||
failed.openDypay=()=>assert.fail('Already paid order must not open SDK again');
|
||||
await failed.appDypayFun(payment);
|
||||
assert.deepEqual(failed.uni.navigations,[successPage]);
|
||||
assert.equal(failed.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('already visible success page is not pushed again', async () => {
|
||||
const ctx=await payContext({getCurrentPages:()=>[{route:successPage.slice(1)}]});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations,[]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('WeChat and Alipay retain the same shared success page', async () => {
|
||||
const ctx=await payContext({feedback:async()=>({bizcode:100}),console:{log(){},error(){}}});
|
||||
loadModule('utils/payUtils.js',ctx);
|
||||
ctx.uni.requestPayment=options=>options.success({});
|
||||
ctx.appWxpayFun({type:'app'},1,'wechat-order');
|
||||
ctx.zfbPayFun({type:'app',orderStr:'test'},2,'alipay-order');
|
||||
assert.deepEqual(ctx.uni.navigations,[successPage,successPage]);
|
||||
});
|
||||
|
||||
test('resume refuses mismatched order, bad signed parameters and native initialization failure', async () => {
|
||||
for (const mode of ['outer-order','inner-order','signature','init']) {
|
||||
const ctx = await payContext({openDypay:()=>assert.fail('Invalid resume reached SDK')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', payment.orderNum);
|
||||
const resumePayment = JSON.parse(JSON.stringify(payment));
|
||||
if (mode === 'inner-order') resumePayment.orderNum = 'different';
|
||||
if (mode === 'signature') delete resumePayment.douyin.sign;
|
||||
if (mode === 'init') ctx.initDypay = ()=>false;
|
||||
ctx.request = async()=>({bizcode:100,data:{state:'unpaid',
|
||||
orderNum:mode === 'outer-order' ? 'different' : payment.orderNum,payment:resumePayment}});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), payment.orderNum);
|
||||
}
|
||||
});
|
||||
|
||||
test('native initialization failure before first payment does not leave a permanent lock', async () => {
|
||||
const ctx = await payContext({initDypay:()=>false,openDypay:()=>assert.fail('Invalid SDK init')});
|
||||
assert.equal((await ctx.appDypayFun(payment)).state, 'error');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'), false);
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
});
|
||||
|
||||
test('cancel and native failure use the same failure page without losing the unresolved transaction', async () => {
|
||||
for (const code of ['1','2','100','103','-1']) {
|
||||
const ctx=await payContext({openDypay:(_,cb)=>cb({resultCode:code}),queryUntilSettled:async()=>({state:'pending'})});
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage]);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
assert.equal(ctx.uni.messages.includes('支付成功'),false);
|
||||
}
|
||||
});
|
||||
|
||||
test('killing the App during SDK payment restores into the failure page rather than checkout or a modal', async () => {
|
||||
const before=await payContext({openDypay:()=>{ /* Process dies before callback. */ }});
|
||||
before.appDypayFun(payment);
|
||||
assert.equal(before.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
const restarted=await payContext({openDypay:()=>assert.fail('Restart must never launch payment'),
|
||||
queryUntilSettled:async()=>({state:'pending'})});
|
||||
restarted.uni.storage.set('tb-douyin:test-api:7',before.uni.storage.get('tb-douyin:test-api:7'));
|
||||
await restarted.resumeDouyinPay();
|
||||
await restarted.resumeDouyinPay();
|
||||
assert.deepEqual(restarted.uni.navigations,[failurePage]);
|
||||
assert.equal(restarted.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
restarted.queryUntilSettled=async()=>({state:'paid'});
|
||||
await restarted.resumeDouyinPay();
|
||||
assert.deepEqual(restarted.uni.navigations,[failurePage,successPage]);
|
||||
assert.equal(restarted.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('restarting after a closed payment shows failure and clears only the confirmed terminal record', async () => {
|
||||
const ctx=await payContext({queryUntilSettled:async()=>({state:'failed'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('offline restart exits checkout but retains the order for later authoritative reconciliation', async () => {
|
||||
const ctx=await payContext({queryUntilSettled:async()=>{throw new Error('offline');}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
await ctx.resumeDouyinPay();
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage]);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
assert.match(ctx.uni.messages.at(-1),/待确认/);
|
||||
});
|
||||
|
||||
test('failure navigation fallback and retry never forget a terminal order before presenting the result', async () => {
|
||||
const ctx=await payContext({queryUntilSettled:async()=>({state:'failed'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7',payment.orderNum);
|
||||
ctx.uni.navigateTo=options=>options.fail();
|
||||
ctx.uni.redirectTo=options=>options.fail();
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),payment.orderNum);
|
||||
ctx.uni.redirectTo=options=>{ctx.uni.navigations.push(options.url);options.success();};
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage]);
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:7'),false);
|
||||
});
|
||||
|
||||
test('WeChat and Alipay failures retain the same shared failure page', async () => {
|
||||
const ctx=await payContext({console:{log(){},error(){}}});
|
||||
loadModule('utils/payUtils.js',ctx);
|
||||
ctx.uni.requestPayment=options=>options.fail({});
|
||||
ctx.appWxpayFun({type:'app'},1,'wx');
|
||||
ctx.zfbPayFun({type:'app'},2,'ali');
|
||||
assert.deepEqual(ctx.uni.navigations,[failurePage,failurePage]);
|
||||
});
|
||||
|
||||
test('cancel 200 yuan then buy 0.2 yuan uses the current checkout response, never the cached 200 yuan order', async () => {
|
||||
const oldOrder = {...payment, orderNum:'order-200', douyin:{...payment.douyin, prepayId:'prepay-200', sign:'signature-200'}};
|
||||
const current = {...payment, orderNum:'order-0.2', douyin:{...payment.douyin, prepayId:'prepay-0.2', sign:'signature-0.2'}};
|
||||
const opened = [], queried = [], requested = [];
|
||||
let lateOldCallback;
|
||||
const ctx = await payContext({
|
||||
request: async options => {
|
||||
requested.push(options.url);
|
||||
assert.equal(options.url, '/cart/settlementOrder');
|
||||
assert.equal(options.data.goodsId, 'product-0.2');
|
||||
return {bizcode:100, data:current};
|
||||
},
|
||||
queryUntilSettled: async (_, orderNum) => { queried.push(orderNum); return {state:orderNum === 'order-200' ? 'pending' : 'paid'}; },
|
||||
openDypay: (options, cb) => {
|
||||
opened.push(options.payInfo);
|
||||
if (options.payInfo.prepayid === 'prepay-200') { lateOldCallback = cb; cb({resultCode:'1'}); }
|
||||
else cb({resultCode:'0'});
|
||||
}
|
||||
});
|
||||
await ctx.appDypayFun(oldOrder);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'order-200');
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage]);
|
||||
// Same sequence as checkout: preflight -> current business API -> SDK.
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.equal(opened.length, 1);
|
||||
loadModule('api/cart.js', ctx);
|
||||
const response = await ctx.settlementOrder({goodsId:'product-0.2',payway:'douyin'});
|
||||
await ctx.appDypayFun(response.data);
|
||||
lateOldCallback({resultCode:'0'});
|
||||
assert.deepEqual(requested, ['/cart/settlementOrder']);
|
||||
assert.deepEqual(opened.map(v=>v.prepayid), ['prepay-200','prepay-0.2']);
|
||||
assert.equal(opened[1].sign, 'signature-0.2');
|
||||
assert.deepEqual(queried, ['order-200','order-0.2']);
|
||||
assert.deepEqual(ctx.uni.navigations, [failurePage,successPage]);
|
||||
});
|
||||
|
||||
test('new purchase, recharge and existing-order payment all ignore an unrelated cached order', async () => {
|
||||
for (const [entry, file, method, url] of [
|
||||
['purchase','api/cart.js','settlementOrder','/cart/settlementOrder'],
|
||||
['recharge','api/finance.js','recharge','/finance/recharge'],
|
||||
['existing-order','api/order.js','payment','/order/payment']
|
||||
]) {
|
||||
const current={...payment,orderNum:entry,douyin:{...payment.douyin,prepayId:entry}};
|
||||
const ctx = await payContext({request:async options=>{
|
||||
assert.equal(options.url,url);
|
||||
assert.equal(options.data.payway,'douyin');
|
||||
return {bizcode:100,data:current};
|
||||
},
|
||||
queryUntilSettled:async(_, orderNum)=>{assert.equal(orderNum, entry);return {state:'pending'};}});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7', 'old-200');
|
||||
let received;
|
||||
ctx.openDypay = (options, cb)=>{received=options.payInfo;cb({resultCode:'1'});};
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'), true);
|
||||
assert.deepEqual(ctx.uni.navigations, []);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), 'old-200');
|
||||
loadModule(file,ctx);
|
||||
const response=await ctx[method]({payway:'douyin'});
|
||||
await ctx.appDypayFun(response.data);
|
||||
assert.equal(received.prepayid, entry);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'), entry);
|
||||
}
|
||||
});
|
||||
|
||||
test('legacy entry also pays the supplied order rather than a cached order', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Unrelated resume'), queryUntilSettled:async()=>({state:'paid'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
loadModule('utils/payUtils.js',ctx);
|
||||
let opened;
|
||||
ctx.openDypay=(options,cb)=>{opened=options.payInfo.prepayid;cb({resultCode:'0'});};
|
||||
await ctx.douyinPayFun({...payment.douyin,prepayId:'current-0.2'},2,'current-order');
|
||||
assert.equal(opened,'current-0.2');
|
||||
});
|
||||
|
||||
test('bad current parameters never fall back to an unrelated old order or reset its native session', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Old-order fallback'),
|
||||
resetDypay:()=>assert.fail('Invalid response must not reset native'),openDypay:()=>assert.fail('Invalid payment')});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
assert.equal((await ctx.appDypayFun({orderNum:'new',douyin:{appId:'app'}})).state,'error');
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'old-200');
|
||||
});
|
||||
|
||||
test('restart can reconcile the last attempt but a later new checkout cannot reopen it', async () => {
|
||||
const ctx=await payContext({request:()=>assert.fail('Old resume'),queryUntilSettled:async()=>({state:'pending'})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
await ctx.resumeDouyinPay();
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
let opened;
|
||||
ctx.openDypay=(options,cb)=>{opened=options.payInfo.prepayid;cb({resultCode:'1'});};
|
||||
await ctx.appDypayFun({...payment,orderNum:'new-0.2',douyin:{...payment.douyin,prepayId:'new-0.2'}});
|
||||
assert.equal(opened,'new-0.2');
|
||||
});
|
||||
|
||||
test('an actually running SDK payment still blocks a second order until its callback completes', async () => {
|
||||
let callback, calls=0;
|
||||
const ctx=await payContext({openDypay:(_,cb)=>{calls++;callback=cb;},queryUntilSettled:async()=>({state:'pending'})});
|
||||
const first=ctx.appDypayFun(payment);
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),false);
|
||||
await ctx.appDypayFun({...payment,orderNum:'different'});
|
||||
assert.equal(calls,1);
|
||||
callback({resultCode:'1'});
|
||||
await first;
|
||||
assert.equal(await ctx.prepareDouyinPay('douyin'),true);
|
||||
});
|
||||
|
||||
test('native reset for a different order cannot pay after an account switch', async () => {
|
||||
const ctx=await payContext({openDypay:()=>assert.fail('Wrong account'),
|
||||
resetDypay:()=>ctx.uni.storage.set('user',{userId:8})});
|
||||
ctx.uni.storage.set('tb-douyin:test-api:7','old-200');
|
||||
await ctx.appDypayFun(payment);
|
||||
assert.equal(ctx.uni.storage.get('tb-douyin:test-api:7'),'old-200');
|
||||
assert.equal(ctx.uni.storage.has('tb-douyin:test-api:8'),false);
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
// Exercises the actual shared UTS logic as JS, not a duplicate implementation.
|
||||
// This does not replace Kotlin/Swift native compilation or device tests.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const hx = process.env.HBUILDERX_HOME;
|
||||
assert.ok(hx, 'Set HBUILDERX_HOME to the HBuilderX installation directory');
|
||||
const ts = require(path.join(hx, 'plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js'));
|
||||
const source = fs.readFileSync(path.join(__dirname, '../uni_modules/tb-douyin-pay/utssdk/common.uts'), 'utf8');
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS },
|
||||
}).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;
|
||||
const exports = {};
|
||||
const ctx = { exports, JSON: { stringify, parseObject(raw) {
|
||||
const value = JSON.parse(raw);
|
||||
return value == null ? null : { getString: key => typeof value[key] === 'string' ? value[key] : null };
|
||||
} }, setTimeout(fn) { timers.set(++timerId, fn); return timerId; },
|
||||
clearTimeout(id) { timers.delete(id); } };
|
||||
vm.runInNewContext(compiled, ctx);
|
||||
assert.equal(exports.configure('app'), true);
|
||||
return { ...exports, timers };
|
||||
}
|
||||
|
||||
test('shared bridge keeps signed fields unchanged and handles default/explicit loading', () => {
|
||||
for (const showLoading of [undefined, null, true, false]) {
|
||||
const bridge = load();
|
||||
const results = [];
|
||||
bridge.invoke({ payInfo: info, showLoading }, result => results.push(result), (payload, loading, done) => {
|
||||
assert.deepEqual(JSON.parse(payload), info);
|
||||
assert.equal(loading, showLoading ?? true);
|
||||
done('{"resultCode":"0","errorMsg":""}');
|
||||
});
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].resultCode, '0');
|
||||
assert.equal(results[0].needsQuery, true);
|
||||
assert.equal(bridge.timers.size, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('missing/null/empty payment fields never reach native code or take a lock', () => {
|
||||
const invalid = [null, undefined, {}, { ...info, appid: 'wrong' },
|
||||
{ ...info, package: 'wrong' }, { ...info, timestamp: '1780000000000' },
|
||||
{ ...info, noncestr: 'n'.repeat(33) }];
|
||||
for (const key of Object.keys(info)) {
|
||||
for (const value of [null, undefined, '', ' ']) invalid.push({ ...info, [key]: value });
|
||||
}
|
||||
for (const payInfo of invalid) {
|
||||
const bridge = load();
|
||||
const results = [];
|
||||
bridge.invoke({ payInfo }, r => results.push(r), () => assert.fail('Invalid native call'));
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].resultCode, '-1');
|
||||
assert.equal(bridge.timers.size, 0);
|
||||
assert.equal(bridge.configure('app'), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('null or failed serialization returns an error before the lock and native call', () => {
|
||||
for (const stringify of [() => null, () => undefined, () => '', () => { throw Error('serialize'); }]) {
|
||||
const bridge = load(stringify);
|
||||
bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '-1'), () => assert.fail('Invalid payload'));
|
||||
assert.equal(bridge.timers.size, 0);
|
||||
assert.equal(bridge.configure('app'), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('reentrant calls, duplicate callbacks and late callbacks after reset are isolated', () => {
|
||||
const bridge = load();
|
||||
const results = [];
|
||||
let oldDone;
|
||||
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { oldDone = done; });
|
||||
bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '103'), () => assert.fail('Duplicate pay'));
|
||||
assert.equal(bridge.configure('other'), false);
|
||||
bridge.resetPending();
|
||||
let newDone;
|
||||
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { newDone = done; });
|
||||
oldDone('{"resultCode":"0"}');
|
||||
assert.equal(results.length, 0);
|
||||
newDone('{"resultCode":"1"}');
|
||||
newDone('{"resultCode":"0"}');
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].resultCode, '1');
|
||||
for (const timer of bridge.timers.values()) timer();
|
||||
assert.equal(results.length, 1);
|
||||
});
|
||||
|
||||
test('timeout, malformed result and native exception remain query-required and unlock', () => {
|
||||
for (const mode of ['timeout', 'malformed', 'throw']) {
|
||||
const bridge = load();
|
||||
const results = [];
|
||||
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => {
|
||||
if (mode === 'throw') throw Error('native');
|
||||
if (mode === 'malformed') done('{');
|
||||
});
|
||||
if (mode === 'timeout') for (const timer of bridge.timers.values()) timer();
|
||||
assert.equal(results.length, 1);
|
||||
assert.notEqual(results[0].resultCode, '0');
|
||||
assert.equal(results[0].needsQuery, true);
|
||||
assert.equal(bridge.configure('app'), true);
|
||||
}
|
||||
});
|
||||
+6
-6
@@ -6,14 +6,14 @@ if (process.env.NODE_ENV === 'development') {
|
||||
}
|
||||
// #endif
|
||||
|
||||
// export const BASE_URL = "https://api.tbmall.xin"; //生产
|
||||
// export const MILD_BASE_URL = "https://agent.tbmall.xin/"; //中台生产
|
||||
// export const kefuBaseUrl = "https://admin.tbmall.xin/api";
|
||||
export const BASE_URL = "https://api.tbmall.xin"; //生产
|
||||
export const MILD_BASE_URL = "https://agent.tbmall.xin/"; //中台生产
|
||||
export const kefuBaseUrl = "https://admin.tbmall.xin/api";
|
||||
|
||||
|
||||
export const BASE_URL = baseUrl; // 本地测试
|
||||
export const MILD_BASE_URL = "https://agent.o.tbmall.xin/"; //中台测试
|
||||
export const kefuBaseUrl = "https://admin.o.tbmall.xin/api";
|
||||
// export const BASE_URL = baseUrl; // 本地测试
|
||||
// export const MILD_BASE_URL = "https://agent.o.tbmall.xin/"; //中台测试
|
||||
// export const kefuBaseUrl = "https://admin.o.tbmall.xin/api";
|
||||
|
||||
|
||||
// https://h.o.tbmall.xin/#/
|
||||
|
||||
@@ -27,9 +27,6 @@ async function query(orderNum) {
|
||||
}
|
||||
|
||||
export function douyinPayTypes(types) {
|
||||
// #ifdef H5
|
||||
return types.split(',').includes('6') ? types : `${types},6`;
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
return types.split(',').includes('6') ? types : `${types},6`;
|
||||
// #endif
|
||||
@@ -38,9 +35,6 @@ export function douyinPayTypes(types) {
|
||||
// #endif
|
||||
}
|
||||
export function filterDouyinPayWays(ways) {
|
||||
// #ifdef H5
|
||||
return ways || [];
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
return ways || [];
|
||||
// #endif
|
||||
|
||||
+3
-3
@@ -7,11 +7,11 @@ export default defineConfig({
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: process.env.NODE_ENV === 'production',
|
||||
drop_debugger: process.env.NODE_ENV === 'production',
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
assetsInlineLimit: 4096,
|
||||
assetsInlineLimit: 2048,
|
||||
cssCodeSplit: true,
|
||||
chunkSizeWarningLimit: 2000,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user