3d3588babb
Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.3 KiB
TypeScript
154 lines
5.3 KiB
TypeScript
import type { WechatJssdkConfig } from '@dukang/shared-types';
|
||
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 IOS_ENTRY_URL_KEY = 'dukang_wx_ios_entry_url';
|
||
|
||
let scriptPromise: Promise<void> | null = null;
|
||
let configured = false;
|
||
let configuredUrl: string | null = null;
|
||
|
||
/** 参与 JSSDK 签名的页面 URL(去掉 hash、OAuth 回跳参数) */
|
||
export function normalizeJssdkPageUrl(rawUrl: string): string {
|
||
try {
|
||
const url = new URL(rawUrl);
|
||
url.hash = '';
|
||
url.searchParams.delete('code');
|
||
url.searchParams.delete('state');
|
||
const query = url.searchParams.toString();
|
||
return `${url.origin}${url.pathname}${query ? `?${query}` : ''}`;
|
||
} catch {
|
||
return rawUrl.split('#')[0];
|
||
}
|
||
}
|
||
|
||
/** iOS 微信内 SPA:签名 URL 须用首次进入页面的入口 URL,而非路由跳转后的当前 URL */
|
||
export function captureIosJssdkEntryUrl(): void {
|
||
if (typeof window === 'undefined') return;
|
||
if (!isIosDevice() || !isWechatBrowser() || isWechatDevTools()) return;
|
||
if (sessionStorage.getItem(IOS_ENTRY_URL_KEY)) return;
|
||
sessionStorage.setItem(IOS_ENTRY_URL_KEY, normalizeJssdkPageUrl(window.location.href));
|
||
}
|
||
|
||
/** 获取参与 JSSDK 签名的 URL */
|
||
export function getJssdkSignUrl(rawUrl?: string): string {
|
||
const current = normalizeJssdkPageUrl(rawUrl ?? (typeof window !== 'undefined' ? window.location.href : ''));
|
||
if (typeof window !== 'undefined' && isIosDevice() && isWechatBrowser() && !isWechatDevTools()) {
|
||
const entry = sessionStorage.getItem(IOS_ENTRY_URL_KEY);
|
||
if (entry) return entry;
|
||
}
|
||
return current;
|
||
}
|
||
|
||
function isJssdkDebugEnabled(): boolean {
|
||
if (typeof window === 'undefined') return false;
|
||
try {
|
||
return new URLSearchParams(window.location.search).get('wxdebug') === '1';
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
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;
|
||
url.searchParams.delete('code');
|
||
url.searchParams.delete('state');
|
||
const query = url.searchParams.toString();
|
||
const next = `${url.pathname}${query ? `?${query}` : ''}${url.hash}`;
|
||
window.history.replaceState({}, '', next);
|
||
}
|
||
|
||
function loadScript(): Promise<void> {
|
||
if (typeof document === 'undefined') return Promise.reject(new Error('非浏览器环境'));
|
||
if (window.wx) return Promise.resolve();
|
||
if (scriptPromise) return scriptPromise;
|
||
|
||
scriptPromise = new Promise((resolve, reject) => {
|
||
const existing = document.querySelector<HTMLScriptElement>('script[data-dukang-wx-jssdk]');
|
||
if (existing) {
|
||
existing.addEventListener('load', () => resolve());
|
||
existing.addEventListener('error', () => reject(new Error('微信 JSSDK 加载失败')));
|
||
return;
|
||
}
|
||
const script = document.createElement('script');
|
||
script.src = JSSDK_URL;
|
||
script.async = true;
|
||
script.dataset.dukangWxJssdk = '1';
|
||
script.onload = () => resolve();
|
||
script.onerror = () => reject(new Error('微信 JSSDK 加载失败'));
|
||
document.head.appendChild(script);
|
||
});
|
||
return scriptPromise;
|
||
}
|
||
|
||
async function fetchJssdkConfig(apiBase: string, clientApp: string, url: string, token?: string | null) {
|
||
const headers: Record<string, string> = { 'X-Client-App': clientApp };
|
||
if (token) headers.Authorization = `Bearer ${token}`;
|
||
const qs = new URLSearchParams({ url });
|
||
const res = await fetch(`${apiBase}/common/wechat/jssdk-config?${qs}`, { headers });
|
||
const json = await res.json();
|
||
if (json.code !== 0) throw new Error(json.message || '获取 JSSDK 配置失败');
|
||
return json.data as WechatJssdkConfig;
|
||
}
|
||
|
||
/** 初始化微信 JSSDK(H5 公众号内) */
|
||
export async function initWechatJssdk(options: {
|
||
apiBase: string;
|
||
clientApp: string;
|
||
url?: string;
|
||
getAccessToken?: () => string | null;
|
||
jsApiList?: string[];
|
||
}): Promise<void> {
|
||
captureIosJssdkEntryUrl();
|
||
const { apiBase, clientApp, getAccessToken } = options;
|
||
const pageUrl = options.url ?? getJssdkSignUrl();
|
||
await loadScript();
|
||
if (!window.wx) throw new Error('微信 JSSDK 不可用');
|
||
|
||
const config = await fetchJssdkConfig(apiBase, clientApp, pageUrl, getAccessToken?.());
|
||
const jsApiList = options.jsApiList ?? [...DEFAULT_JS_API_LIST];
|
||
|
||
configured = false;
|
||
configuredUrl = null;
|
||
|
||
await new Promise<void>((resolve, reject) => {
|
||
window.wx!.config({
|
||
...config,
|
||
jsApiList,
|
||
debug: isJssdkDebugEnabled(),
|
||
});
|
||
window.wx!.ready(() => {
|
||
configured = true;
|
||
configuredUrl = pageUrl;
|
||
resolve();
|
||
});
|
||
window.wx!.error((err) => reject(new Error(err.errMsg || 'wx.config 失败')));
|
||
});
|
||
}
|
||
|
||
export function isJssdkReady(): boolean {
|
||
const signUrl = getJssdkSignUrl();
|
||
return configured && !!window.wx && configuredUrl === signUrl;
|
||
}
|
||
|
||
export async function ensureJssdkReady(options: {
|
||
apiBase: string;
|
||
clientApp: string;
|
||
url?: string;
|
||
getAccessToken?: () => string | null;
|
||
jsApiList?: string[];
|
||
}): Promise<void> {
|
||
captureIosJssdkEntryUrl();
|
||
const pageUrl = options.url ?? getJssdkSignUrl();
|
||
if (!isJssdkReady()) {
|
||
await initWechatJssdk({ ...options, url: pageUrl });
|
||
}
|
||
}
|
||
|
||
if (typeof window !== 'undefined') {
|
||
captureIosJssdkEntryUrl();
|
||
}
|