import { isReadyCalled } from "./_core"; import { getConfig } from "./services/config"; import { readWxLikeEnvFromWindow, setParentWxEnvReport, getParentWxEnvReport, } from "./services/embedEnvProbe"; import { hasDistinctParentWindow, resolveUseParentProxy, } from "./services/embedProxy"; import { createUUID } from "./utils/uuid"; import { dispatchEmbedScanResult, acknowledgeEmbedScanConsumed, dispatchEmbedScanError, setEmbedScanHostEnabled, } from "./services/provider/scan"; import { setEmbedScanResultForwarder, setEmbedScanErrorForwarder, } from "./services/embedScanBridge"; import { unlockScanBeep, installScanBeepGestureUnlock } from "./services/web"; import { setEmbedPaymentDialogTarget, clearEmbedPaymentDialogTarget, resolveDelegatedPaymentDialog, handleEmbedPaymentDialogOnChild, } from "./services/payment/embedPaymentDialog"; const EMBED_SOURCE = "IScanEmbed"; const EMBED_V = 1; /** 已向父页发起过 invoke 的嵌入子 frame(用于父页识别结果回传) */ const embedChildSources = new Set(); const EMBED_LISTENER_METHODS = new Set([ "onScanListener", "offScanListener", "onScanErrorListener", "offScanErrorListener", "clear", ]); /** 嵌入 iframe 转发到父页时,须在子页用用户手势解锁提示音 */ const EMBED_SCAN_GESTURE_METHODS = new Set([ "startScan", "scanImage", "scanImageFromFile", ]); function isEmbedMessage(data) { return data && data.source === EMBED_SOURCE && data.v === EMBED_V; } let embedHostInstalled = false; let embedChildInstalled = false; const pendingInvokes = Object.create(null); const pendingCallbackInvokes = Object.create(null); const childCallbackFns = Object.create(null); function cloneArgsReplacingFunctions(val, registry, seen) { if (typeof val === "function") { const id = createUUID(); registry[id] = val; return { __IScanEmbedCb__: id }; } if (val === null || typeof val !== "object") { return val; } if (!seen) { seen = new WeakMap(); } if (seen.has(val)) { throw new Error("[IScan embed]: circular reference in arguments"); } seen.set(val, true); if (Array.isArray(val)) { return val.map((item) => cloneArgsReplacingFunctions(item, registry, seen)); } const out = {}; Object.keys(val).forEach((k) => { out[k] = cloneArgsReplacingFunctions(val[k], registry, seen); }); return out; } function serializeEmbedParams(params) { const serialized = []; const registry = {}; for (let i = 0; i < params.length; i++) { serialized.push(cloneArgsReplacingFunctions(params[i], registry)); } return { serialized, registry }; } function hydrateEmbedParams(params, messageSource, targetOrigin) { function walk(val) { if (val && typeof val === "object" && val.__IScanEmbedCb__) { const cbId = val.__IScanEmbedCb__; return function embedCbProxy() { const args = Array.prototype.slice.call(arguments); const callbackInvokeId = createUUID(); return new Promise((resolve, reject) => { pendingCallbackInvokes[callbackInvokeId] = { resolve, reject }; messageSource.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "callback", cbId, callbackInvokeId, args, }, targetOrigin ); }); }; } if (val === null || typeof val !== "object") { return val; } if (Array.isArray(val)) { return val.map(walk); } const out = {}; Object.keys(val).forEach((k) => { out[k] = walk(val[k]); }); return out; } return params.map(walk); } function broadcastScanErrorToEmbedChildren(error, meta) { if (embedChildSources.size === 0 || error == null || error === "") { return; } embedChildSources.forEach((source) => { try { source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "forwardScanError", error, scanKey: meta && meta.scanKey, }, "*" ); } catch (e) { } }); } function broadcastScanResultToEmbedChildren(result, meta) { if (embedChildSources.size === 0 || result == null || result === "") { return; } embedChildSources.forEach((source) => { try { source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "forwardScanResult", result, scanSource: meta && meta.source, scanKey: meta && meta.scanKey, }, "*" ); } catch (e) { } }); } let embedWxProbeScheduled = false; let embedWxProbeAttempts = 0; const EMBED_WX_PROBE_MAX = 4; function shouldScheduleParentWxProbe() { if (typeof window === "undefined" || !hasDistinctParentWindow()) { return false; } const mode = getConfig("embedProxyMode"); if (mode === false || mode === "local" || mode === "off") { return false; } return true; } /** 向父页询问是否微信环境(子 iframe 跨域时本地 UA 可能不可靠) */ function scheduleEmbedWxEnvProbeIfNeeded() { if (!shouldScheduleParentWxProbe()) { return; } if (embedWxProbeScheduled) { return; } if (embedWxProbeAttempts >= EMBED_WX_PROBE_MAX) { return; } embedWxProbeScheduled = true; embedWxProbeAttempts++; ensureEmbedChildListener(); window.parent.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "probeWxEnv", id: createUUID(), }, "*" ); window.setTimeout(() => { embedWxProbeScheduled = false; if (getParentWxEnvReport() !== null || !shouldScheduleParentWxProbe()) { return; } scheduleEmbedWxEnvProbeIfNeeded(); }, 600); } function embedChildOnMessage(ev) { const data = ev.data; if (!isEmbedMessage(data)) { return; } if (data.kind === "probeWxEnvResult") { setParentWxEnvReport(!!data.wx); return; } if (data.kind === "forwardScanResult") { if (typeof data.result === "string") { const consumed = dispatchEmbedScanResult(data.result, { source: data.scanSource, scanKey: data.scanKey, }); if (consumed && resolveUseParentProxy()) { window.parent.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "scanResultConsumed", result: data.result, }, "*" ); } } return; } if (data.kind === "forwardScanError") { if (typeof data.error === "string") { dispatchEmbedScanError(data.error, { scanKey: data.scanKey, }); } return; } if (data.kind === "paymentDialog") { handleEmbedPaymentDialogOnChild(data, (ok, result, error) => { window.parent.postMessage({ source: EMBED_SOURCE, v: EMBED_V, kind: "paymentDialogResult", id: data.id, ok, result, error, }, "*"); }); return; } if (data.kind === "invokeResult") { const pending = pendingInvokes[data.id]; if (!pending) { return; } delete pendingInvokes[data.id]; if (data.ok) { pending.resolve(data.result); } else { pending.reject(new Error(data.error || "[IScan embed]: invoke failed")); } return; } if (data.kind === "callback") { const fn = childCallbackFns[data.cbId]; const replyCallbackResult = (ok, result, error) => { if (!data.callbackInvokeId || !ev.source) { return; } ev.source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "callbackResult", callbackInvokeId: data.callbackInvokeId, ok, result: ok ? result : undefined, error: ok ? undefined : error, }, ev.origin ); }; if (typeof fn !== "function") { replyCallbackResult(false, null, "[IScan embed]: callback not found"); return; } Promise.resolve(fn.apply(null, data.args || [])) .then((result) => { replyCallbackResult(true, result); }) .catch((err) => { replyCallbackResult(false, null, String((err && err.message) || err)); }); return; } } function ensureEmbedChildListener() { if (embedChildInstalled || typeof window === "undefined") { return; } embedChildInstalled = true; installScanBeepGestureUnlock(); window.addEventListener("message", embedChildOnMessage); } function embedInvoke(methodKey, params) { ensureEmbedChildListener(); scheduleEmbedWxEnvProbeIfNeeded(); const id = createUUID(); const { serialized, registry } = serializeEmbedParams(params); Object.keys(registry).forEach((cbId) => { childCallbackFns[cbId] = registry[cbId]; }); return new Promise((resolve, reject) => { pendingInvokes[id] = { resolve, reject }; window.parent.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "invoke", id, methodKey, params: serialized, }, "*" ); }); } function handleEmbedHostInvoke(lib, ev) { const data = ev.data; const { id, methodKey, params } = data; const shouldDelegatePaymentDialog = methodKey === "requestPayment" || methodKey === "closePaymentDialog"; if (shouldDelegatePaymentDialog && ev.source) { setEmbedPaymentDialogTarget(ev.source); } const hydrated = hydrateEmbedParams(params || [], ev.source, ev.origin); Promise.resolve() .then(() => _exec(lib, methodKey, ...hydrated)) .then((result) => { let out = result; if (out && typeof out.then === "function") { return out.then((r) => { out = r; return out; }); } return out; }) .then((result) => { ev.source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "invokeResult", id, methodKey, ok: true, result, }, ev.origin ); }) .catch((err) => { ev.source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "invokeResult", id, methodKey, ok: false, error: typeof err === "string" ? err : String((err && err.message) || err), }, ev.origin ); }) .then(() => { if (shouldDelegatePaymentDialog) { clearEmbedPaymentDialogTarget(); } }); } /** * 在顶层页面注册 iframe 嵌入代理:子页面(嵌入模式)通过 postMessage 调用同一套 SDK。 * 需在加载 SDK 后调用一次,并传入与 exportSDK 相同的 lib(通常为 ./_core 默认导出)。 */ export function installEmbedHost(lib) { if (embedHostInstalled || typeof window === "undefined") { return; } embedHostInstalled = true; setEmbedScanResultForwarder(broadcastScanResultToEmbedChildren); setEmbedScanErrorForwarder(broadcastScanErrorToEmbedChildren); setEmbedScanHostEnabled(true); window.addEventListener("message", (ev) => { const data = ev.data; if (!isEmbedMessage(data)) { return; } if (data.kind === "probeWxEnv") { if (!ev.source || ev.source === window) { return; } const wx = readWxLikeEnvFromWindow(window); ev.source.postMessage( { source: EMBED_SOURCE, v: EMBED_V, kind: "probeWxEnvResult", id: data.id, wx, }, ev.origin ); return; } if (data.kind === "scanResultConsumed") { if (!ev.source || ev.source === window) { return; } if (typeof data.result === "string") { acknowledgeEmbedScanConsumed(data.result); } return; } if (data.kind === "callbackResult") { const pending = pendingCallbackInvokes[data.callbackInvokeId]; if (!pending) { return; } delete pendingCallbackInvokes[data.callbackInvokeId]; if (data.ok) { pending.resolve(data.result); } else { pending.reject(new Error(data.error || "[IScan embed]: callback failed")); } return; } if (data.kind === "paymentDialogResult") { resolveDelegatedPaymentDialog(data); return; } if (data.kind !== "invoke") { return; } if (!data.id || !data.methodKey) { return; } if (!ev.source || ev.source === window) { return; } embedChildSources.add(ev.source); handleEmbedHostInvoke(lib, ev); }); } function _exec(target, func, ...params) { let instant = target; let funcs = func.split('.'); while (funcs.length > 1) { instant = instant[funcs.shift()]; } if (instant && funcs.length == 1) { if (instant.hasOwnProperty(funcs[0])) { return instant[funcs[0]](...params); } } throw `IScan.${func} not defined`; } function hook(target, method, action) { let fns = method.split('.'); fns.forEach((fn, index) => { if (index == fns.length - 1) { target[fn] = action; } else { if (!target[fn]) { target[fn] = {}; } } target = target[fn]; }); } function forMethods(obj, parent = "") { let keys = {} Object.keys(obj).forEach(key => { let val = obj[key]; if (typeof val === "object") { Object.assign(keys, forMethods(val, `${parent}${key}.`)); } else if (typeof val === "function") { let realKey = `${parent}${key}`; Object.assign(keys, { [realKey]: realKey }); } }) return keys; } function freezeObj(obj) { Object.keys(obj).forEach(key => { let val = obj[key]; if (typeof val === "object") { freezeObj(val) } }) Object.freeze(obj); } /** * 统一调用代理:根据 embedProxyMode + 环境决定走父页面转发或本地 _exec。 */ function createInvokeTransport(lib, method, methodName, initNames) { return function IScanInvokeProxy(...params) { if (resolveUseParentProxy()) { if (EMBED_LISTENER_METHODS.has(methodName)) { if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) { throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}` } return _exec(lib, methodName, ...params); } if (EMBED_SCAN_GESTURE_METHODS.has(method)) { unlockScanBeep(); } return embedInvoke(methodName, params); } if (!isReadyCalled() && initNames && initNames.indexOf(method) < 0) { throw `[IScan]:Can't call the "IScan.${method}" method, because "IScan" not ready, please confirm that "IScan.ready()" has been called. params: ${JSON.stringify(params)}` } return _exec(lib, methodName, ...params); }; } /** * 调试:当前解析到的嵌入转发开关(每次读取最新 config)。 */ export function getEmbedProxyResolved() { return resolveUseParentProxy(); } export { resolveUseParentProxy } from "./services/embedProxy"; export function exportSDK(lib, funcs, ...initNames) { let methods = {}; if (funcs && typeof funcs === 'object') { methods = funcs; } else { methods = forMethods(lib); } const library = {}; Object.keys(methods).forEach(method => { let methodItem = methods[method]; let methodName = methodItem && methodItem.method || method; hook(library, method, createInvokeTransport(lib, method, methodName, initNames)); }); scheduleEmbedWxEnvProbeIfNeeded(); freezeObj(library); return library; }