fix wechat mini program virtual payment flow
This commit is contained in:
@@ -15,6 +15,7 @@ const CLIENT_INSTANCE_STORAGE_KEY = 'genarrative:mini-program-client-instance-id
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
const AUTH_RESULT_STORAGE_KEY = 'genarrative:mini-program-auth-result';
|
||||
const AUTH_ACTION_LOGIN = 'login';
|
||||
const PAY_RESULT_RECHECK_DELAY_MS = 120;
|
||||
|
||||
function isConfiguredEntryUrl(value) {
|
||||
const trimmed = String(value || '').trim();
|
||||
@@ -45,6 +46,7 @@ function appendQuery(url, query) {
|
||||
}
|
||||
|
||||
function appendHashParams(url, params) {
|
||||
const nextKeys = new Set(Object.keys(params).filter((key) => params[key]));
|
||||
const pairs = Object.keys(params)
|
||||
.filter((key) => params[key])
|
||||
.map(
|
||||
@@ -58,8 +60,18 @@ function appendHashParams(url, params) {
|
||||
const hashIndex = url.indexOf('#');
|
||||
const baseUrl = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
||||
const rawHash = hashIndex >= 0 ? url.slice(hashIndex + 1) : '';
|
||||
const separator = rawHash ? '&' : '';
|
||||
return `${baseUrl}#${rawHash}${separator}${pairs.join('&')}`;
|
||||
const keptHashParts = rawHash.split('&').filter((part) => {
|
||||
if (!part) {
|
||||
return false;
|
||||
}
|
||||
const [rawKey = ''] = part.split('=');
|
||||
try {
|
||||
return !nextKeys.has(decodeURIComponent(rawKey));
|
||||
} catch (_error) {
|
||||
return !nextKeys.has(rawKey);
|
||||
}
|
||||
});
|
||||
return `${baseUrl}#${keptHashParts.concat(pairs).join('&')}`;
|
||||
}
|
||||
|
||||
function parseBooleanQueryFlag(value) {
|
||||
@@ -259,6 +271,18 @@ async function resolveAuthResult() {
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshMiniProgramSessionSilently() {
|
||||
if (!isConfiguredApiBaseUrl(API_BASE_URL)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await resolveAuthResult();
|
||||
} catch (error) {
|
||||
console.warn('[web-view] silent mini program login refresh failed', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
authResult: null,
|
||||
@@ -285,13 +309,14 @@ Page({
|
||||
const forcedPhoneBinding = parseBooleanQueryFlag(query.phoneBindingRequired);
|
||||
const returnToPreviousPage = shouldReturnToPreviousPage(query);
|
||||
if (!shouldStartAuthFromQuery(query) && !forcedPhoneBinding) {
|
||||
const authResult = await refreshMiniProgramSessionSilently();
|
||||
this.setData({
|
||||
authResult: null,
|
||||
authResult,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
phoneBindingRequired: false,
|
||||
returnToPreviousPage: false,
|
||||
webViewUrl: resolveWebViewUrl(null),
|
||||
webViewUrl: resolveWebViewUrl(authResult),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -362,6 +387,13 @@ Page({
|
||||
});
|
||||
}
|
||||
|
||||
this.consumePayResult();
|
||||
setTimeout(() => {
|
||||
this.consumePayResult();
|
||||
}, PAY_RESULT_RECHECK_DELAY_MS);
|
||||
},
|
||||
|
||||
consumePayResult() {
|
||||
const result = wx.getStorageSync(PAY_RESULT_STORAGE_KEY);
|
||||
if (result && this.data.webViewUrl) {
|
||||
wx.removeStorageSync(PAY_RESULT_STORAGE_KEY);
|
||||
|
||||
@@ -64,6 +64,23 @@ function resolvePayStatus(error) {
|
||||
return errCode === -2 || /cancel/i.test(errMsg) ? 'cancel' : 'fail';
|
||||
}
|
||||
|
||||
function normalizePayError(error) {
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify({
|
||||
errCode: error.errCode,
|
||||
errMsg: error.errMsg,
|
||||
});
|
||||
} catch (_error) {
|
||||
return String(error.errMsg || error);
|
||||
}
|
||||
}
|
||||
|
||||
function requestOrdinaryPayment(payParams) {
|
||||
return new Promise((resolve) => {
|
||||
wx.requestPayment({
|
||||
@@ -73,10 +90,13 @@ function requestOrdinaryPayment(payParams) {
|
||||
signType: payParams.signType || 'RSA',
|
||||
paySign: String(payParams.paySign || ''),
|
||||
success() {
|
||||
resolve('success');
|
||||
resolve({ status: 'success', errorMessage: '' });
|
||||
},
|
||||
fail(error) {
|
||||
resolve(resolvePayStatus(error));
|
||||
resolve({
|
||||
status: resolvePayStatus(error),
|
||||
errorMessage: normalizePayError(error),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -85,7 +105,10 @@ function requestOrdinaryPayment(payParams) {
|
||||
function requestVirtualPayment(payParams) {
|
||||
return new Promise((resolve) => {
|
||||
if (!canUseVirtualPayment() || typeof wx.requestVirtualPayment !== 'function') {
|
||||
resolve('fail');
|
||||
resolve({
|
||||
status: 'fail',
|
||||
errorMessage: '当前微信基础库不支持 requestVirtualPayment',
|
||||
});
|
||||
return;
|
||||
}
|
||||
wx.requestVirtualPayment({
|
||||
@@ -94,10 +117,13 @@ function requestVirtualPayment(payParams) {
|
||||
paySig: String(payParams.paySig || ''),
|
||||
signature: String(payParams.signature || ''),
|
||||
success() {
|
||||
resolve('success');
|
||||
resolve({ status: 'success', errorMessage: '' });
|
||||
},
|
||||
fail(error) {
|
||||
resolve(resolvePayStatus(error));
|
||||
resolve({
|
||||
status: resolvePayStatus(error),
|
||||
errorMessage: normalizePayError(error),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -112,8 +138,7 @@ function requestWechatPayment(payParams) {
|
||||
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
|
||||
function appendPayResult(url, requestId, status) {
|
||||
const value = `${requestId}:${status}`;
|
||||
function appendPayResult(url, result) {
|
||||
const hashIndex = String(url || '').indexOf('#');
|
||||
const baseUrl =
|
||||
hashIndex >= 0 ? String(url).slice(0, hashIndex) : String(url || '');
|
||||
@@ -121,23 +146,27 @@ function appendPayResult(url, requestId, status) {
|
||||
const nextHash = rawHash
|
||||
.split('&')
|
||||
.filter((part) => part && !part.startsWith('wx_pay_result='))
|
||||
.concat(`wx_pay_result=${encodeURIComponent(value)}`)
|
||||
.concat(`wx_pay_result=${encodeURIComponent(result)}`)
|
||||
.join('&');
|
||||
return `${baseUrl}#${nextHash}`;
|
||||
}
|
||||
|
||||
function notifyPreviousWebView(requestId, status) {
|
||||
const result = `${requestId}:${status}`;
|
||||
function buildPayResultValue(requestId, orderId, payResult) {
|
||||
const segments = [requestId, payResult.status, orderId || ''];
|
||||
if (payResult.errorMessage) {
|
||||
segments.push(encodeURIComponent(payResult.errorMessage));
|
||||
}
|
||||
return segments.join(':');
|
||||
}
|
||||
|
||||
function notifyPreviousWebView(requestId, orderId, payResult) {
|
||||
const result = buildPayResultValue(requestId, orderId, payResult);
|
||||
wx.setStorageSync(PAY_RESULT_STORAGE_KEY, result);
|
||||
const pages = getCurrentPages();
|
||||
const previousPage = pages.length >= 2 ? pages[pages.length - 2] : null;
|
||||
if (previousPage && typeof previousPage.setData === 'function') {
|
||||
previousPage.setData({
|
||||
webViewUrl: appendPayResult(
|
||||
previousPage.data.webViewUrl,
|
||||
requestId,
|
||||
status,
|
||||
),
|
||||
webViewUrl: appendPayResult(previousPage.data.webViewUrl, result),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -151,6 +180,7 @@ function createWechatPayPage(pageContext) {
|
||||
|
||||
async onLoad(query) {
|
||||
const requestId = String(query.requestId || '');
|
||||
const orderId = String(query.orderId || '');
|
||||
const payParams = parsePayParams(query.payParams);
|
||||
if (!requestId || !payParams) {
|
||||
const page = pageContext ?? this;
|
||||
@@ -161,8 +191,8 @@ function createWechatPayPage(pageContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
notifyPreviousWebView(requestId, status);
|
||||
const payResult = await requestWechatPayment(payParams);
|
||||
notifyPreviousWebView(requestId, orderId, payResult);
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
@@ -176,7 +206,9 @@ module.exports = {
|
||||
canUseVirtualPayment,
|
||||
PAY_RESULT_STORAGE_KEY,
|
||||
appendPayResult,
|
||||
buildPayResultValue,
|
||||
createWechatPayPage,
|
||||
normalizePayError,
|
||||
parsePayParams,
|
||||
safeCompareVersion,
|
||||
requestWechatPayment,
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
globalThis.wx = {
|
||||
requestPayment: vi.fn(),
|
||||
requestVirtualPayment: vi.fn(),
|
||||
getSystemInfoSync: vi.fn(() => ({ SDKVersion: '2.32.0' })),
|
||||
setStorageSync: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
};
|
||||
@@ -32,9 +33,9 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
signature: 'user-sig',
|
||||
};
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
const result = await requestWechatPayment(payParams);
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(result).toEqual({ status: 'success', errorMessage: '' });
|
||||
expect(globalThis.wx.requestVirtualPayment).toHaveBeenCalledWith({
|
||||
mode: 'short_series_coin',
|
||||
signData: payParams.signData,
|
||||
@@ -58,9 +59,9 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
signature: 'user-sig',
|
||||
};
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
const result = await requestWechatPayment(payParams);
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(result).toEqual({ status: 'success', errorMessage: '' });
|
||||
expect(globalThis.wx.requestVirtualPayment).toHaveBeenCalledWith({
|
||||
mode: 'short_series_goods',
|
||||
signData: payParams.signData,
|
||||
@@ -77,7 +78,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
options.success?.();
|
||||
});
|
||||
|
||||
const status = await requestWechatPayment({
|
||||
const result = await requestWechatPayment({
|
||||
timeStamp: '1777110165',
|
||||
nonceStr: 'nonce',
|
||||
package: 'prepay_id=wx-prepay',
|
||||
@@ -85,7 +86,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
paySign: 'signature',
|
||||
});
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(result).toEqual({ status: 'success', errorMessage: '' });
|
||||
expect(globalThis.wx.requestPayment).toHaveBeenCalledWith({
|
||||
timeStamp: '1777110165',
|
||||
nonceStr: 'nonce',
|
||||
@@ -110,7 +111,13 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
}),
|
||||
).resolves.toBe('cancel');
|
||||
).resolves.toEqual({
|
||||
status: 'cancel',
|
||||
errorMessage: JSON.stringify({
|
||||
errCode: -2,
|
||||
errMsg: 'requestVirtualPayment:fail cancel',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test('page notifies previous web-view after virtual payment', async () => {
|
||||
@@ -118,7 +125,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
data: { webViewUrl: 'https://web.test/#tab=profile' },
|
||||
setData: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => [{}, previousPage]);
|
||||
globalThis.getCurrentPages = vi.fn(() => [previousPage, {}]);
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
@@ -128,6 +135,7 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
|
||||
await page.onLoad({
|
||||
requestId: 'request-1',
|
||||
orderId: 'order-1',
|
||||
payParams: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
mode: 'short_series_coin',
|
||||
@@ -140,10 +148,10 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
|
||||
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
|
||||
'genarrative:wechat-pay-result',
|
||||
'request-1:success',
|
||||
'request-1:success:order-1',
|
||||
);
|
||||
expect(previousPage.setData).toHaveBeenCalledWith({
|
||||
webViewUrl: 'https://web.test/#tab=profile&wx_pay_result=request-1%3Asuccess',
|
||||
webViewUrl: 'https://web.test/#tab=profile&wx_pay_result=request-1%3Asuccess%3Aorder-1',
|
||||
});
|
||||
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
|
||||
});
|
||||
@@ -152,8 +160,8 @@ describe('wechat-pay mini program payment bridge', () => {
|
||||
expect(parsePayParams(encodeURIComponent('{"paySign":"sig"}'))).toEqual({
|
||||
paySign: 'sig',
|
||||
});
|
||||
expect(appendPayResult('https://web.test/#old=1', 'req', 'fail')).toBe(
|
||||
'https://web.test/#old=1&wx_pay_result=req%3Afail',
|
||||
expect(appendPayResult('https://web.test/#old=1', 'req:fail:order-1')).toBe(
|
||||
'https://web.test/#old=1&wx_pay_result=req%3Afail%3Aorder-1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user