import { getRuntimePlatform, isIosDevice, isWechatDevTools } from './env'; import { ensureJssdkReady, getJssdkSignUrl, initWechatJssdk, resetJssdkConfig, } from './jssdk'; import type { WeixinSdkConfig } from './types'; function delay(ms: number): Promise { 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 { 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 { 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 { 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 { 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('请在微信内打开以使用扫码核销'); }