import { getConfig } from "../config"; import { request } from "../../utils/request"; import { toAny } from "../../utils/toany"; import { getTopWindow } from "./env"; import { getPaymentConfig } from "./config"; const DEFAULT_STORAGE_KEY_PREFIX = "iscan_wechat_openid"; let resolvedOAuthAppId = ""; function getOAuthConfig() { return getPaymentConfig(); } function setResolvedOAuthAppId(appId) { if (appId) { resolvedOAuthAppId = String(appId); } } function getResolvedOAuthAppId() { return resolvedOAuthAppId; } function buildStorageKey(appId) { const cfg = getOAuthConfig(); if (cfg.storageKey) { return cfg.storageKey; } const id = appId || getResolvedOAuthAppId(); if (id) { return `${DEFAULT_STORAGE_KEY_PREFIX}_${id}`; } return DEFAULT_STORAGE_KEY_PREFIX; } function getSessionStorage() { const topWin = getTopWindow(); if (!topWin) { return null; } try { return topWin.sessionStorage; } catch (e) { return null; } } function readCachedOpenid(appId) { const storage = getSessionStorage(); if (!storage) { return ""; } return storage.getItem(buildStorageKey(appId)) || ""; } function writeCachedOpenid(appId, openid) { const storage = getSessionStorage(); if (!storage || !openid) { return; } storage.setItem(buildStorageKey(appId), openid); } function clearCachedOpenid(appId) { const storage = getSessionStorage(); if (!storage) { return; } const key = buildStorageKey(appId); if (key) { storage.removeItem(key); } } function releaseWechatOAuth(appId) { clearCachedOpenid(appId || getResolvedOAuthAppId()); if (!appId || appId === getResolvedOAuthAppId()) { resolvedOAuthAppId = ""; } } function getCodeFromLocation(win) { if (!win || !win.location) { return ""; } try { const params = new URLSearchParams(win.location.search || ""); return params.get("code") || ""; } catch (e) { return ""; } } function stripOAuthQueryFromUrl(win) { if (!win || !win.history || !win.location) { return; } try { const url = new URL(win.location.href); const keys = ["code", "state", "error", "error_description"]; let changed = false; keys.forEach((key) => { if (url.searchParams.has(key)) { url.searchParams.delete(key); changed = true; } }); if (!changed) { return; } const next = url.pathname + url.search + url.hash; win.history.replaceState(win.history.state, "", next); } catch (e) { // ignore } } function getOAuthErrorFromLocation(win) { if (!win || !win.location) { return ""; } try { const params = new URLSearchParams(win.location.search || ""); const error = params.get("error"); if (!error) { return ""; } return params.get("error_description") || error; } catch (e) { return ""; } } function parseOAuthResponse(res) { const body = toAny(res && res.data, {}); if (!body) { throw new Error("wechat oauth response is empty"); } if (body.code !== undefined && body.code !== 0) { throw new Error(body.msg || body.message || "wechat oauth failed"); } const data = body.data != null ? body.data : body; const appId = data && data.appId; const openid = data && data.openid; if (!appId) { throw new Error("wechat oauth appId is empty"); } if (!openid) { throw new Error("wechat oauth openid is empty"); } setResolvedOAuthAppId(appId); return { appId: String(appId), openid: String(openid), }; } function parseAuthorizeUrlResponse(res) { const body = toAny(res && res.data, {}); if (!body) { throw new Error("wechat oauth authorize url response is empty"); } if (body.code !== undefined && body.code !== 0) { throw new Error(body.msg || body.message || "wechat oauth authorize url failed"); } const data = body.data != null ? body.data : body; const appId = data && data.appId; const authorizeUrl = data && (data.authorizeUrl || data.authorize_url); if (!appId) { throw new Error("wechat oauth authorizeUrl appId is empty"); } if (!authorizeUrl) { throw new Error("wechat oauth authorizeUrl is empty"); } setResolvedOAuthAppId(appId); return { appId: String(appId), authorizeUrl: String(authorizeUrl), }; } function buildDefaultRedirectUri(win) { if (!win || !win.location) { return ""; } try { const url = new URL(win.location.href); url.searchParams.delete("code"); url.searchParams.delete("state"); url.searchParams.delete("error"); url.searchParams.delete("error_description"); url.hash = ""; return url.href.split("#")[0]; } catch (e) { return win.location.href.split("#")[0]; } } function resolveRedirectUri(options, win) { const opts = toAny(options, {}); const cfg = getOAuthConfig(); if (opts.redirectUri) { return String(opts.redirectUri).split("#")[0]; } if (cfg.redirectUri) { return String(cfg.redirectUri).split("#")[0]; } return buildDefaultRedirectUri(win); } function exchangeCodeForOpenid(code) { const cfg = getOAuthConfig(); return request({ url: cfg.oauthApiUrl, method: "POST", json: true, data: { code }, }).then(parseOAuthResponse); } function fetchAuthorizeInfo(redirectUri) { const cfg = getOAuthConfig(); if (!redirectUri) { return Promise.reject(new Error("redirectUri is required for wechat oauth authorize url")); } return request({ url: cfg.oauthAuthorizeUrlApiUrl, method: "POST", json: true, data: { redirectUri }, }).then(parseAuthorizeUrlResponse); } function redirectToAuthorize(authorizeUrl) { const topWin = getTopWindow(); if (!topWin) { return; } topWin.location.href = authorizeUrl; } function rejectWechatOAuth(topWin, appId, error) { releaseWechatOAuth(appId); if (topWin) { stripOAuthQueryFromUrl(topWin); } return Promise.reject(error); } /** * 微信支付 JSAPI:先拉取授权信息获取 appId,再查缓存 / 换 code / 跳转授权。 * 需要跳转授权时返回 null;失败时释放 openid 缓存并 reject。 */ export function ensureWechatOpenid(options) { const opts = toAny(options, {}); const topWin = getTopWindow(); if (!topWin) { return Promise.reject(new Error("window is not available")); } const oauthError = getOAuthErrorFromLocation(topWin); if (oauthError) { return rejectWechatOAuth(topWin, getResolvedOAuthAppId(), new Error(`wechat oauth failed: ${oauthError}`)); } const redirectUri = resolveRedirectUri(opts, topWin); const code = getCodeFromLocation(topWin); return fetchAuthorizeInfo(redirectUri).then((authInfo) => { const appId = authInfo.appId; if (!opts.force) { const cached = readCachedOpenid(appId); if (cached) { return cached; } } if (code) { return exchangeCodeForOpenid(code).then((result) => { writeCachedOpenid(result.appId, result.openid); stripOAuthQueryFromUrl(topWin); return result.openid; }).catch((err) => rejectWechatOAuth(topWin, appId, err)); } redirectToAuthorize(authInfo.authorizeUrl); return null; }).catch((err) => rejectWechatOAuth(topWin, getResolvedOAuthAppId(), err)); } export function clearWechatOpenidCache(appId) { clearCachedOpenid(appId || getResolvedOAuthAppId()); } export function getCachedWechatOpenid(appId) { return readCachedOpenid(appId || getResolvedOAuthAppId()); }