fix(weixin-sdk): restore iOS JSSDK entry-URL signing for shop scan
Phone re-login + OAuth on iOS left SPA on a different path than the WebView entry URL, so scanQRCode failed until a full reopen. Sign with the document entry URL again and warm up after auth. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,9 @@ import { isIosDevice } from '@dukang/weixin-sdk';
|
||||
/** 扫码前发起 OAuth 时标记,回跳后在首页续扫 */
|
||||
export const SHOP_PENDING_SCAN_KEY = 'shop_pending_scan';
|
||||
|
||||
/** 短信登录后绑定微信等 OAuth 回跳:下次手动扫码加长预热(不自动打开相机) */
|
||||
export const SHOP_SCAN_WARMUP_KEY = 'shop_scan_warmup';
|
||||
|
||||
export function markPendingScanAfterAuth(): void {
|
||||
try {
|
||||
sessionStorage.setItem(SHOP_PENDING_SCAN_KEY, '1');
|
||||
@@ -27,6 +30,25 @@ export function clearPendingScanAfterAuth(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** OAuth(含短信登录后绑微信)回跳后,标记下一次扫码需要加长预热 */
|
||||
export function markScanWarmupAfterAuth(): void {
|
||||
try {
|
||||
sessionStorage.setItem(SHOP_SCAN_WARMUP_KEY, '1');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
export function consumeScanWarmupAfterAuth(): boolean {
|
||||
try {
|
||||
if (sessionStorage.getItem(SHOP_SCAN_WARMUP_KEY) !== '1') return false;
|
||||
sessionStorage.removeItem(SHOP_SCAN_WARMUP_KEY);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** OAuth 回跳后延迟再调 scanQRCode(iOS JSSDK 离线校验更慢) */
|
||||
export function getPostAuthScanDelayMs(): number {
|
||||
return isIosDevice() ? 1200 : 600;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ClientRuntimeConfig, WechatLoginResult } from '@dukang/shared-type
|
||||
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
|
||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
import { markScanWarmupAfterAuth } from './shop-scan-auth';
|
||||
import { request, saveWechatSession, type ShopSessionPayload } from './api';
|
||||
|
||||
export type ShopAccountProfile = {
|
||||
@@ -162,6 +163,7 @@ export async function loginShopWithWechat(): Promise<ShopSessionPayload | null |
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||
}
|
||||
markScanWarmupAfterAuth();
|
||||
const result = await weixinSdk.login();
|
||||
if (result) return handleShopWechatLoginResult(result);
|
||||
}
|
||||
@@ -171,6 +173,8 @@ export async function bindShopWechatAfterSmsLogin(): Promise<void> {
|
||||
const config = await fetchClientConfig();
|
||||
if (!isWxAuthorizeEnabled(config)) return;
|
||||
if (!isWechatEnv()) return;
|
||||
// OAuth 整页回跳后 iOS 需用入场 URL 重配 JSSDK;标记下次扫码加长预热
|
||||
markScanWarmupAfterAuth();
|
||||
await weixinSdk.login();
|
||||
}
|
||||
|
||||
@@ -180,5 +184,6 @@ export async function authorizeShopWechat(): Promise<WechatLoginResult | void> {
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||
}
|
||||
markScanWarmupAfterAuth();
|
||||
return weixinSdk.login();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
|
||||
import { isScanPermissionWarmupError } from '@dukang/weixin-sdk';
|
||||
import { isIosDevice, isScanPermissionWarmupError } from '@dukang/weixin-sdk';
|
||||
|
||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
|
||||
clearPendingScanAfterAuth,
|
||||
|
||||
consumeScanWarmupAfterAuth,
|
||||
|
||||
getPostAuthScanDelayMs,
|
||||
|
||||
markPendingScanAfterAuth,
|
||||
@@ -190,7 +192,8 @@ export default function HomePage() {
|
||||
|
||||
try {
|
||||
|
||||
if (opts?.postAuthWarmup) {
|
||||
// iOS / OAuth 回跳后须重新 wx.config(签名用入场 URL)
|
||||
if (opts?.postAuthWarmup || isIosDevice()) {
|
||||
|
||||
weixinSdk.reset();
|
||||
|
||||
@@ -308,7 +311,9 @@ export default function HomePage() {
|
||||
|
||||
}
|
||||
|
||||
await runScan();
|
||||
const needWarmup = consumeScanWarmupAfterAuth();
|
||||
|
||||
await runScan(needWarmup ? { postAuthWarmup: true } : undefined);
|
||||
|
||||
} catch (e) {
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { WechatJssdkConfig } from '@dukang/shared-types';
|
||||
import { isWechatBrowser, isWechatDevTools } from './env';
|
||||
import { isIosDevice, isWechatBrowser, isWechatDevTools } from './env';
|
||||
import { DEFAULT_JS_API_LIST } from './types';
|
||||
|
||||
const JSSDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||
const SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2';
|
||||
/** 旧版错误地把 SPA 当前 URL 写入 session;清理以免干扰排查 */
|
||||
const LEGACY_SIGN_URL_CACHE_KEY = 'dukang_wx_sign_url_v2';
|
||||
|
||||
let scriptPromise: Promise<void> | null = null;
|
||||
let configured = false;
|
||||
let configuredUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* iOS 微信 WebView:JSSDK 签名校验用的是「本次 document 加载」的入场 URL,
|
||||
* SPA pushState/replaceState 后 location.href 会变,但微信仍按入场 URL 验签。
|
||||
* 使用模块级变量:整页刷新(含 OAuth 回跳)会重置;同页 SPA 路由保持不变。
|
||||
*/
|
||||
let iosEntryUrl: string | null = null;
|
||||
|
||||
/** 清除 JSSDK 配置缓存(路由切换后须重新 wx.config) */
|
||||
export function resetJssdkConfig(): void {
|
||||
configured = false;
|
||||
@@ -42,25 +50,36 @@ function signUrlChanged(prev: string | null, current: string): boolean {
|
||||
return prev !== current;
|
||||
}
|
||||
|
||||
/** 记录最近一次签名 URL;SPA 路由或 ?step= 变化时须重新 wx.config */
|
||||
export function captureIosJssdkEntryUrl(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!isWechatBrowser() || isWechatDevTools()) return;
|
||||
const current = normalizeJssdkPageUrl(window.location.href);
|
||||
const existing = sessionStorage.getItem(SIGN_URL_CACHE_KEY);
|
||||
if (!existing) {
|
||||
sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
|
||||
return;
|
||||
}
|
||||
if (signUrlChanged(existing, current)) {
|
||||
sessionStorage.setItem(SIGN_URL_CACHE_KEY, current);
|
||||
resetJssdkConfig();
|
||||
function clearLegacySignUrlCache(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(LEGACY_SIGN_URL_CACHE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取参与 JSSDK 签名的 URL(始终为当前页完整 URL,含 query) */
|
||||
/**
|
||||
* 捕获 iOS 微信入场 URL(每个 document 生命周期只记一次)。
|
||||
* Android / 非微信环境为 no-op。
|
||||
*/
|
||||
export function captureIosJssdkEntryUrl(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return;
|
||||
clearLegacySignUrlCache();
|
||||
if (iosEntryUrl) return;
|
||||
iosEntryUrl = normalizeJssdkPageUrl(window.location.href);
|
||||
}
|
||||
|
||||
/** 获取参与 JSSDK 签名的 URL;iOS 微信内固定为本次入场 URL */
|
||||
export function getJssdkSignUrl(rawUrl?: string): string {
|
||||
return normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
|
||||
const current = normalizeJssdkPageUrl(
|
||||
rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''),
|
||||
);
|
||||
if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) {
|
||||
captureIosJssdkEntryUrl();
|
||||
if (iosEntryUrl) return iosEntryUrl;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function isJssdkDebugEnabled(): boolean {
|
||||
@@ -76,6 +95,8 @@ export function stripOAuthParamsFromLocation(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
const url = new URL(window.location.href);
|
||||
if (!url.searchParams.has('code') && !url.searchParams.has('state')) return;
|
||||
// iOS:须先锁定入场 URL,再 replaceState;否则签名 URL 与微信内部 URL 不一致
|
||||
captureIosJssdkEntryUrl();
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('state');
|
||||
const query = url.searchParams.toString();
|
||||
@@ -126,7 +147,8 @@ export async function initWechatJssdk(options: {
|
||||
}): Promise<void> {
|
||||
captureIosJssdkEntryUrl();
|
||||
const { apiBase, clientApp, getAccessToken } = options;
|
||||
const pageUrl = options.url ?? getJssdkSignUrl();
|
||||
// iOS 忽略调用方传入的「当前页」URL,强制入场 URL,避免登录后 SPA 到首页签错名
|
||||
const pageUrl = getJssdkSignUrl(options.url);
|
||||
await loadScript();
|
||||
if (!window.wx) throw new Error('微信 JSSDK 不可用');
|
||||
|
||||
@@ -164,7 +186,7 @@ export async function ensureJssdkReady(options: {
|
||||
jsApiList?: string[];
|
||||
}): Promise<void> {
|
||||
captureIosJssdkEntryUrl();
|
||||
const pageUrl = options.url ?? getJssdkSignUrl();
|
||||
const pageUrl = getJssdkSignUrl(options.url);
|
||||
if (configuredUrl && signUrlChanged(configuredUrl, pageUrl)) {
|
||||
resetJssdkConfig();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user