feat: switch mini program recharge to virtual payment
This commit is contained in:
@@ -1,90 +1,3 @@
|
||||
function parsePayParams(rawValue) {
|
||||
try {
|
||||
const params = JSON.parse(decodeURIComponent(String(rawValue || '')));
|
||||
if (!params || typeof params !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return params;
|
||||
} catch (error) {
|
||||
console.error('[wechat-pay] parse params failed', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const { createWechatPayPage } = require('./index.shared');
|
||||
|
||||
function requestPayment(payParams) {
|
||||
return new Promise((resolve) => {
|
||||
wx.requestPayment({
|
||||
timeStamp: String(payParams.timeStamp || ''),
|
||||
nonceStr: String(payParams.nonceStr || ''),
|
||||
package: String(payParams.package || ''),
|
||||
signType: payParams.signType || 'RSA',
|
||||
paySign: String(payParams.paySign || ''),
|
||||
success() {
|
||||
resolve('success');
|
||||
},
|
||||
fail(error) {
|
||||
const errMsg = error && error.errMsg ? error.errMsg : '';
|
||||
resolve(/cancel/i.test(errMsg) ? 'cancel' : 'fail');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
|
||||
function appendPayResult(url, requestId, status) {
|
||||
const value = `${requestId}:${status}`;
|
||||
const hashIndex = String(url || '').indexOf('#');
|
||||
const baseUrl =
|
||||
hashIndex >= 0 ? String(url).slice(0, hashIndex) : String(url || '');
|
||||
const rawHash = hashIndex >= 0 ? String(url).slice(hashIndex + 1) : '';
|
||||
const nextHash = rawHash
|
||||
.split('&')
|
||||
.filter((part) => part && !part.startsWith('wx_pay_result='))
|
||||
.concat(`wx_pay_result=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
return `${baseUrl}#${nextHash}`;
|
||||
}
|
||||
|
||||
function notifyPreviousWebView(requestId, status) {
|
||||
const result = `${requestId}:${status}`;
|
||||
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,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
title: '正在拉起支付',
|
||||
errorMessage: '',
|
||||
},
|
||||
|
||||
async onLoad(query) {
|
||||
const requestId = String(query.requestId || '');
|
||||
const payParams = parsePayParams(query.payParams);
|
||||
if (!requestId || !payParams) {
|
||||
this.setData({
|
||||
title: '支付失败',
|
||||
errorMessage: '缺少支付参数。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await requestPayment(payParams);
|
||||
notifyPreviousWebView(requestId, status);
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
handleBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
});
|
||||
Page(createWechatPayPage());
|
||||
|
||||
184
miniprogram/pages/wechat-pay/index.shared.js
Normal file
184
miniprogram/pages/wechat-pay/index.shared.js
Normal file
@@ -0,0 +1,184 @@
|
||||
/* global wx, getCurrentPages */
|
||||
|
||||
function parsePayParams(rawValue) {
|
||||
try {
|
||||
const params = JSON.parse(decodeURIComponent(String(rawValue || '')));
|
||||
if (!params || typeof params !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return params;
|
||||
} catch (error) {
|
||||
console.error('[wechat-pay] parse params failed', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isVirtualPaymentParams(payParams) {
|
||||
return (
|
||||
typeof payParams.mode === 'string' &&
|
||||
typeof payParams.signData === 'string' &&
|
||||
typeof payParams.paySig === 'string' &&
|
||||
typeof payParams.signature === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function safeCompareVersion(left, right) {
|
||||
const leftParts = String(left || '')
|
||||
.split('.')
|
||||
.map((part) => Number(part) || 0);
|
||||
const rightParts = String(right || '')
|
||||
.split('.')
|
||||
.map((part) => Number(part) || 0);
|
||||
const length = Math.max(leftParts.length, rightParts.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const leftValue = leftParts[index] || 0;
|
||||
const rightValue = rightParts[index] || 0;
|
||||
if (leftValue > rightValue) {
|
||||
return 1;
|
||||
}
|
||||
if (leftValue < rightValue) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function canUseVirtualPayment() {
|
||||
if (typeof wx === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
if (typeof wx.canIUse === 'function' && wx.canIUse('requestVirtualPayment')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const version =
|
||||
typeof wx.getSystemInfoSync === 'function'
|
||||
? wx.getSystemInfoSync()?.SDKVersion || ''
|
||||
: '';
|
||||
return safeCompareVersion(version, '2.19.2') >= 0;
|
||||
}
|
||||
|
||||
function resolvePayStatus(error) {
|
||||
const errMsg = error && error.errMsg ? error.errMsg : '';
|
||||
const errCode = Number(error && error.errCode);
|
||||
return errCode === -2 || /cancel/i.test(errMsg) ? 'cancel' : 'fail';
|
||||
}
|
||||
|
||||
function requestOrdinaryPayment(payParams) {
|
||||
return new Promise((resolve) => {
|
||||
wx.requestPayment({
|
||||
timeStamp: String(payParams.timeStamp || ''),
|
||||
nonceStr: String(payParams.nonceStr || ''),
|
||||
package: String(payParams.package || ''),
|
||||
signType: payParams.signType || 'RSA',
|
||||
paySign: String(payParams.paySign || ''),
|
||||
success() {
|
||||
resolve('success');
|
||||
},
|
||||
fail(error) {
|
||||
resolve(resolvePayStatus(error));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function requestVirtualPayment(payParams) {
|
||||
return new Promise((resolve) => {
|
||||
if (!canUseVirtualPayment() || typeof wx.requestVirtualPayment !== 'function') {
|
||||
resolve('fail');
|
||||
return;
|
||||
}
|
||||
wx.requestVirtualPayment({
|
||||
mode: String(payParams.mode || ''),
|
||||
signData: String(payParams.signData || ''),
|
||||
paySig: String(payParams.paySig || ''),
|
||||
signature: String(payParams.signature || ''),
|
||||
success() {
|
||||
resolve('success');
|
||||
},
|
||||
fail(error) {
|
||||
resolve(resolvePayStatus(error));
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function requestWechatPayment(payParams) {
|
||||
if (isVirtualPaymentParams(payParams)) {
|
||||
return requestVirtualPayment(payParams);
|
||||
}
|
||||
return requestOrdinaryPayment(payParams);
|
||||
}
|
||||
|
||||
const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result';
|
||||
|
||||
function appendPayResult(url, requestId, status) {
|
||||
const value = `${requestId}:${status}`;
|
||||
const hashIndex = String(url || '').indexOf('#');
|
||||
const baseUrl =
|
||||
hashIndex >= 0 ? String(url).slice(0, hashIndex) : String(url || '');
|
||||
const rawHash = hashIndex >= 0 ? String(url).slice(hashIndex + 1) : '';
|
||||
const nextHash = rawHash
|
||||
.split('&')
|
||||
.filter((part) => part && !part.startsWith('wx_pay_result='))
|
||||
.concat(`wx_pay_result=${encodeURIComponent(value)}`)
|
||||
.join('&');
|
||||
return `${baseUrl}#${nextHash}`;
|
||||
}
|
||||
|
||||
function notifyPreviousWebView(requestId, status) {
|
||||
const result = `${requestId}:${status}`;
|
||||
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,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createWechatPayPage(pageContext) {
|
||||
return {
|
||||
data: {
|
||||
title: '正在拉起支付',
|
||||
errorMessage: '',
|
||||
},
|
||||
|
||||
async onLoad(query) {
|
||||
const requestId = String(query.requestId || '');
|
||||
const payParams = parsePayParams(query.payParams);
|
||||
if (!requestId || !payParams) {
|
||||
const page = pageContext ?? this;
|
||||
page.setData({
|
||||
title: '支付失败',
|
||||
errorMessage: '缺少支付参数。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
notifyPreviousWebView(requestId, status);
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
handleBack() {
|
||||
wx.navigateBack();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
canUseVirtualPayment,
|
||||
PAY_RESULT_STORAGE_KEY,
|
||||
appendPayResult,
|
||||
createWechatPayPage,
|
||||
parsePayParams,
|
||||
safeCompareVersion,
|
||||
requestWechatPayment,
|
||||
requestVirtualPayment,
|
||||
};
|
||||
159
miniprogram/pages/wechat-pay/index.test.js
Normal file
159
miniprogram/pages/wechat-pay/index.test.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import wechatPayBridge from './index.shared.js';
|
||||
|
||||
const {
|
||||
appendPayResult,
|
||||
createWechatPayPage,
|
||||
parsePayParams,
|
||||
requestWechatPayment,
|
||||
} = wechatPayBridge;
|
||||
|
||||
describe('wechat-pay mini program payment bridge', () => {
|
||||
beforeEach(() => {
|
||||
globalThis.wx = {
|
||||
requestPayment: vi.fn(),
|
||||
requestVirtualPayment: vi.fn(),
|
||||
setStorageSync: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => []);
|
||||
});
|
||||
|
||||
test('routes virtual payloads to wx.requestVirtualPayment', async () => {
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
const payParams = {
|
||||
mode: 'short_series_coin',
|
||||
signData:
|
||||
'{"offerId":"offer-1","buyQuantity":1,"env":0,"currencyType":"CNY","outTradeNo":"order-virtual-1","attach":"mud_points_60"}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
};
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(globalThis.wx.requestVirtualPayment).toHaveBeenCalledWith({
|
||||
mode: 'short_series_coin',
|
||||
signData: payParams.signData,
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
success: expect.any(Function),
|
||||
fail: expect.any(Function),
|
||||
});
|
||||
expect(globalThis.wx.requestPayment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('routes goods virtual payloads to wx.requestVirtualPayment', async () => {
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
const payParams = {
|
||||
mode: 'short_series_goods',
|
||||
signData:
|
||||
'{"offerId":"offer-1","buyQuantity":1,"env":0,"currencyType":"CNY","productId":"member_month","goodsPrice":2800,"outTradeNo":"order-goods-1","attach":"member_month"}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
};
|
||||
|
||||
const status = await requestWechatPayment(payParams);
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(globalThis.wx.requestVirtualPayment).toHaveBeenCalledWith({
|
||||
mode: 'short_series_goods',
|
||||
signData: payParams.signData,
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
success: expect.any(Function),
|
||||
fail: expect.any(Function),
|
||||
});
|
||||
expect(globalThis.wx.requestPayment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('keeps ordinary requestPayment payloads on wx.requestPayment', async () => {
|
||||
globalThis.wx.requestPayment.mockImplementationOnce((options) => {
|
||||
options.success?.();
|
||||
});
|
||||
|
||||
const status = await requestWechatPayment({
|
||||
timeStamp: '1777110165',
|
||||
nonceStr: 'nonce',
|
||||
package: 'prepay_id=wx-prepay',
|
||||
signType: 'RSA',
|
||||
paySign: 'signature',
|
||||
});
|
||||
|
||||
expect(status).toBe('success');
|
||||
expect(globalThis.wx.requestPayment).toHaveBeenCalledWith({
|
||||
timeStamp: '1777110165',
|
||||
nonceStr: 'nonce',
|
||||
package: 'prepay_id=wx-prepay',
|
||||
signType: 'RSA',
|
||||
paySign: 'signature',
|
||||
success: expect.any(Function),
|
||||
fail: expect.any(Function),
|
||||
});
|
||||
expect(globalThis.wx.requestVirtualPayment).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('maps virtual payment cancel errCode to cancel result', async () => {
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.fail?.({ errCode: -2, errMsg: 'requestVirtualPayment:fail cancel' });
|
||||
});
|
||||
|
||||
await expect(
|
||||
requestWechatPayment({
|
||||
mode: 'short_series_coin',
|
||||
signData: '{}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
}),
|
||||
).resolves.toBe('cancel');
|
||||
});
|
||||
|
||||
test('page notifies previous web-view after virtual payment', async () => {
|
||||
const previousPage = {
|
||||
data: { webViewUrl: 'https://web.test/#tab=profile' },
|
||||
setData: vi.fn(),
|
||||
};
|
||||
globalThis.getCurrentPages = vi.fn(() => [{}, previousPage]);
|
||||
globalThis.wx.requestVirtualPayment.mockImplementationOnce((options) => {
|
||||
options.success?.({ errMsg: 'requestVirtualPayment:ok' });
|
||||
});
|
||||
const page = createWechatPayPage({
|
||||
setData: vi.fn(),
|
||||
});
|
||||
|
||||
await page.onLoad({
|
||||
requestId: 'request-1',
|
||||
payParams: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
mode: 'short_series_coin',
|
||||
signData: '{}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
|
||||
'genarrative:wechat-pay-result',
|
||||
'request-1:success',
|
||||
);
|
||||
expect(previousPage.setData).toHaveBeenCalledWith({
|
||||
webViewUrl: 'https://web.test/#tab=profile&wx_pay_result=request-1%3Asuccess',
|
||||
});
|
||||
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('parsePayParams and appendPayResult keep existing behavior', () => {
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user