170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
import { getRuntimePlatform, isIosDevice, isWechatDevTools } from './env';
|
|
import {
|
|
ensureJssdkReady,
|
|
getJssdkSignUrl,
|
|
initWechatJssdk,
|
|
resetJssdkConfig,
|
|
} from './jssdk';
|
|
import type { WeixinSdkConfig } from './types';
|
|
|
|
function delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
}
|
|
|
|
/** 将微信 scanQRCode fail 的 errMsg 转为用户可读文案(避免误判为系统相机权限) */
|
|
export function formatScanFailMessage(errMsg: string): string {
|
|
const msg = errMsg.trim() || '扫码失败';
|
|
if (/cancel/i.test(msg)) return '';
|
|
|
|
// iOS / 授权回调后常见:JSSDK 权限离线校验尚未完成,并非系统相机权限
|
|
if (/offline verifying|permission value is offline/i.test(msg)) {
|
|
return '微信权限验证中,请稍候再试或刷新页面后重新扫码';
|
|
}
|
|
|
|
if (/invalid signature|config:fail|signature/i.test(msg)) {
|
|
return '微信 JSSDK 签名校验失败,请刷新页面后重试';
|
|
}
|
|
|
|
// 系统级相机权限(iPhone 设置 → 隐私 → 相机 → 微信)
|
|
if (/system.*camera|NotAllowedError|camera.*not.*allowed|无法访问相机|无相机权限/i.test(msg)) {
|
|
return '相机权限未开启,请在 iPhone「设置 → 隐私与安全性 → 相机」中允许微信使用摄像头';
|
|
}
|
|
|
|
// JSSDK 接口授权失败(含 permission,但不等于相机权限)
|
|
if (/permission|auth|denied|授权|拒绝/i.test(msg)) {
|
|
return `微信扫码接口未就绪(${msg}),请刷新页面后重试`;
|
|
}
|
|
|
|
return msg;
|
|
}
|
|
|
|
function isScanPermissionWarmupError(msg: string): boolean {
|
|
return /offline verifying|permission value is offline|权限验证中|接口未就绪|invalid signature|config:fail|signature/i.test(
|
|
msg,
|
|
);
|
|
}
|
|
|
|
function checkJsApi(apiName: string): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
if (!window.wx?.checkJsApi) {
|
|
resolve(false);
|
|
return;
|
|
}
|
|
window.wx.checkJsApi({
|
|
jsApiList: [apiName],
|
|
success: (res) => resolve(!!res.checkResult?.[apiName]),
|
|
fail: () => resolve(false),
|
|
});
|
|
});
|
|
}
|
|
|
|
async function ensureScanJssdk(config: WeixinSdkConfig): Promise<void> {
|
|
const jssdkOptions = {
|
|
apiBase: config.apiBase ?? '/api/v1',
|
|
clientApp: config.clientApp,
|
|
getAccessToken: config.getAccessToken,
|
|
jsApiList: ['scanQRCode', 'checkJsApi'],
|
|
};
|
|
|
|
await ensureJssdkReady(jssdkOptions);
|
|
|
|
let ready = await checkJsApi('scanQRCode');
|
|
if (!ready) {
|
|
resetJssdkConfig();
|
|
await initWechatJssdk({ ...jssdkOptions, url: getJssdkSignUrl() });
|
|
ready = await checkJsApi('scanQRCode');
|
|
}
|
|
if (!ready) {
|
|
throw new Error('微信扫码接口未授权,请刷新页面后重试');
|
|
}
|
|
}
|
|
|
|
function invokeScanQrCode(): Promise<string | null> {
|
|
return new Promise((resolve, reject) => {
|
|
window.wx!.scanQRCode!({
|
|
needResult: 1,
|
|
scanType: ['qrCode', 'barCode'],
|
|
success: (res) => resolve(res.resultStr || null),
|
|
fail: (res) => {
|
|
const msg = res.errMsg || '扫码失败';
|
|
if (/cancel/i.test(msg)) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
const formatted = formatScanFailMessage(msg);
|
|
reject(new Error(formatted || msg));
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
export type ScanQrCodeOptions = {
|
|
/** OAuth / 首次绑定后权限离线校验更慢,加长预热 */
|
|
postAuthWarmup?: boolean;
|
|
};
|
|
|
|
/** 调起扫码(返回二维码/条码内容,取消返回 null) */
|
|
export async function scanQrCode(
|
|
config: WeixinSdkConfig,
|
|
options: ScanQrCodeOptions = {},
|
|
): Promise<string | null> {
|
|
const platform = getRuntimePlatform();
|
|
|
|
if (platform === 'mini' && window.wx?.scanCode) {
|
|
return new Promise((resolve) => {
|
|
window.wx!.scanCode!({
|
|
onlyFromCamera: true,
|
|
scanType: ['qrCode', 'barCode'],
|
|
success: (res) => resolve(res.result || null),
|
|
fail: () => resolve(null),
|
|
});
|
|
});
|
|
}
|
|
|
|
if (platform === 'wechat-h5') {
|
|
const jssdkOptions = {
|
|
apiBase: config.apiBase ?? '/api/v1',
|
|
clientApp: config.clientApp,
|
|
getAccessToken: config.getAccessToken,
|
|
jsApiList: ['scanQRCode', 'checkJsApi'],
|
|
};
|
|
|
|
await ensureScanJssdk(config);
|
|
if (!window.wx?.scanQRCode) {
|
|
throw new Error('当前微信版本不支持扫码,请升级微信后重试');
|
|
}
|
|
|
|
// wx.ready ≠ 权限离线校验完成;授权回跳后更明显
|
|
const warmupMs = options.postAuthWarmup
|
|
? 1200
|
|
: isIosDevice() && !isWechatDevTools()
|
|
? 800
|
|
: 300;
|
|
await delay(warmupMs);
|
|
|
|
const maxAttempts = options.postAuthWarmup ? 3 : 2;
|
|
let lastError: Error | null = null;
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
if (attempt > 0) {
|
|
if (lastError && isScanPermissionWarmupError(lastError.message)) {
|
|
resetJssdkConfig();
|
|
await initWechatJssdk({ ...jssdkOptions, url: getJssdkSignUrl() });
|
|
await checkJsApi('scanQRCode');
|
|
}
|
|
await delay(700 + attempt * 500);
|
|
}
|
|
try {
|
|
return await invokeScanQrCode();
|
|
} catch (e) {
|
|
lastError = e instanceof Error ? e : new Error('扫码失败');
|
|
const retryable = isScanPermissionWarmupError(lastError.message);
|
|
if (attempt < maxAttempts - 1 && retryable) continue;
|
|
throw lastError;
|
|
}
|
|
}
|
|
throw lastError ?? new Error('扫码失败');
|
|
}
|
|
|
|
throw new Error('请在微信内打开以使用扫码核销');
|
|
}
|