300 lines
7.3 KiB
JavaScript
300 lines
7.3 KiB
JavaScript
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());
|
||
}
|