This commit is contained in:
iqudoo
2026-06-17 16:36:12 +08:00
parent c2476b9f85
commit 378c7e4ba5
13 changed files with 678 additions and 146 deletions

54
dist/index.d.ts vendored
View File

@@ -228,16 +228,54 @@ interface ScanConfigOptions {
* Native 支付二维码弹层面板 class
*/
paymentNativeQrPanelClass?: string,
/**
* PC 支付宝表单是否使用弹层 + iframe默认 truefalse 时整页跳转
*/
paymentAlipayFormEnabled?: boolean,
/**
* 支付宝表单弹层标题
*/
paymentAlipayFormTitle?: string,
/**
* 支付宝表单弹层说明文案
*/
paymentAlipayFormMessage?: string,
/**
* 支付宝表单 iframe 高度(像素),默认 520
*/
paymentAlipayFormIframeHeight?: number,
/**
* 支付宝表单弹层遮罩样式(未配置时回退 paymentNativeQrOverlayStyle
*/
paymentAlipayFormOverlayStyle?: string,
/**
* 支付宝表单弹层面板样式(未配置时回退 paymentNativeQrPanelStyle
*/
paymentAlipayFormPanelStyle?: string,
/**
* 支付宝表单弹层关闭按钮样式(未配置时回退 paymentNativeQrCloseButtonStyle
*/
paymentAlipayFormCloseButtonStyle?: string,
/**
* 支付宝表单弹层遮罩 class未配置时回退 paymentNativeQrOverlayClass
*/
paymentAlipayFormOverlayClass?: string,
/**
* 支付宝表单弹层面板 class未配置时回退 paymentNativeQrPanelClass
*/
paymentAlipayFormPanelClass?: string,
/**
* 微信支付 OAuth 配置paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
*/
initWechatPayment?: {
/**
* 用 code 换 openid对应 api.biz.wechat.oauth
* 用 code 换 openid对应 api.biz.wechat.oauth
* 未配置时默认 /api?action=api.biz.wechat.oauth若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
*/
oauthApiUrl?: string,
/**
* 获取 OAuth 授权信息(含 appId、authorizeUrl对应 api.biz.wechat.oauthAuthorizeUrl
* 获取 OAuth 授权信息(含 appId、authorizeUrl对应 api.biz.wechat.oauthAuthorizeUrl
* 未配置时默认 /api?action=api.biz.wechat.oauthAuthorizeUrl若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
*/
oauthAuthorizeUrlApiUrl?: string,
/**
@@ -404,6 +442,8 @@ interface PaymentPrepareParams {
returnUrl: string;
/** 支付渠道 */
paymentType: PaymentType;
/** 支付金额(元),由 requestPayment 传入 */
amount?: number | string;
}
/**
@@ -428,10 +468,12 @@ interface PaymentInvokeResult {
oauthFailed?: boolean;
/** OAuth 或调起失败时的错误信息 */
error?: string;
/** H5 / 支付宝是否已跳转 */
/** H5 / 移动端支付宝是否已跳转整页 */
redirected?: boolean;
/** Native 场景是否展示了二维码弹层 */
qrShown?: boolean;
/** PC 支付宝是否展示了表单弹层 */
dialogShown?: boolean;
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
cashierResult?: PaymentCashierResult;
}
@@ -440,6 +482,10 @@ interface PaymentInvokeResult {
* 支付请求选项
*/
interface RequestPaymentOptions {
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
amount?: number | string;
/** 货币符号,默认 CNY¥ */
currency?: string;
/** 支付完成回跳地址,未传时使用当前页 URL */
returnUrl?: string;
/** OAuth 授权回跳完整 URLredirectUri覆盖 initWechatPayment.redirectUri */
@@ -524,7 +570,7 @@ interface IScan {
options?: RequestPaymentOptions
): Promise<PaymentInvokeResult>;
/**
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层)
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层、支付宝表单弹层
*/
closePaymentDialog(): void;

2
dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -21,6 +21,12 @@ import {
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;
@@ -253,6 +259,20 @@ function embedChildOnMessage(ev) {
}
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) {
@@ -336,6 +356,10 @@ function embedInvoke(methodKey, params) {
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))
@@ -376,6 +400,11 @@ function handleEmbedHostInvoke(lib, ev) {
},
ev.origin
);
})
.then(() => {
if (shouldDelegatePaymentDialog) {
clearEmbedPaymentDialogTarget();
}
});
}
@@ -435,6 +464,10 @@ export function installEmbedHost(lib) {
}
return;
}
if (data.kind === "paymentDialogResult") {
resolveDelegatedPaymentDialog(data);
return;
}
if (data.kind !== "invoke") {
return;
}

View File

@@ -0,0 +1,118 @@
import { getConfig } from "../config";
import { createPaymentDialogHost } from "./paymentDialogHost";
import { appendPaymentAmount } from "./paymentAmount";
import { hasEmbedPaymentDialogTarget, delegatePaymentDialog } from "./embedPaymentDialog";
const OVERLAY_ID = "__iscan_payment_alipay_form__";
const dialogHost = createPaymentDialogHost(OVERLAY_ID);
function getStyleConfig(alipayKey, nativeKey, fallback) {
const value = getConfig(alipayKey);
if (value) {
return value;
}
const nativeValue = getConfig(nativeKey);
if (nativeValue) {
return nativeValue;
}
return fallback;
}
function getIframeHeight() {
const height = getConfig("paymentAlipayFormIframeHeight");
if (typeof height === "number" && height > 0) {
return height;
}
return 520;
}
function writeHtmlToIframe(iframe, html) {
const iframeWin = iframe.contentWindow;
const iframeDoc = iframe.contentDocument || (iframeWin && iframeWin.document);
if (!iframeDoc) {
throw new Error("alipay payment iframe document is not available");
}
iframeDoc.open();
iframeDoc.write(html);
iframeDoc.close();
}
export function closeAlipayPaymentForm() {
dialogHost.removeOverlay();
}
export function showAlipayPaymentFormLocal(html, amount, currency) {
if (!html) {
return Promise.reject(new Error("alipay payment form html is required"));
}
if (getConfig("paymentAlipayFormEnabled") === false) {
return Promise.resolve({ shown: false });
}
let paymentIframe;
try {
dialogHost.mountOverlay({
overlayClass: getConfig("paymentAlipayFormOverlayClass") || getConfig("paymentNativeQrOverlayClass"),
panelClass: getConfig("paymentAlipayFormPanelClass") || getConfig("paymentNativeQrPanelClass"),
overlayStyle: getStyleConfig(
"paymentAlipayFormOverlayStyle",
"paymentNativeQrOverlayStyle",
undefined
),
panelStyle: getStyleConfig(
"paymentAlipayFormPanelStyle",
"paymentNativeQrPanelStyle",
"position:relative;width:100%;max-width:480px;background:#fff;border-radius:12px;padding:24px 20px 20px;box-shadow:0 8px 28px rgba(0,0,0,0.22);"
),
closeButtonStyle: getStyleConfig(
"paymentAlipayFormCloseButtonStyle",
"paymentNativeQrCloseButtonStyle",
undefined
),
fillPanel(panel) {
const doc = panel.ownerDocument;
const title = doc.createElement("h3");
title.textContent = getConfig("paymentAlipayFormTitle") || "支付宝支付";
title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#222;line-height:1.35;text-align:center;";
const message = doc.createElement("p");
message.textContent = getConfig("paymentAlipayFormMessage") || "请在下方页面完成支付";
message.style.cssText = "margin:0 0 12px;font-size:14px;color:#666;line-height:1.5;text-align:center;";
const iframeWrap = doc.createElement("div");
iframeWrap.style.cssText = "width:100%;overflow:hidden;border:1px solid #eee;border-radius:8px;background:#fff;";
paymentIframe = doc.createElement("iframe");
paymentIframe.setAttribute("title", "支付宝支付");
paymentIframe.style.cssText = `display:block;width:100%;height:${getIframeHeight()}px;border:0;background:#fff;`;
iframeWrap.appendChild(paymentIframe);
panel.appendChild(title);
appendPaymentAmount(panel, doc, amount, currency);
panel.appendChild(message);
panel.appendChild(iframeWrap);
},
});
} catch (err) {
return Promise.reject(err);
}
try {
writeHtmlToIframe(paymentIframe, html);
return Promise.resolve({ shown: true });
} catch (err) {
dialogHost.removeOverlay();
return Promise.reject(err);
}
}
export function showAlipayPaymentForm(html, amount, currency) {
if (hasEmbedPaymentDialogTarget()) {
const delegated = delegatePaymentDialog("showAlipayPaymentForm", { html, amount, currency });
if (delegated) {
return delegated;
}
}
return showAlipayPaymentFormLocal(html, amount, currency);
}

View File

@@ -1,11 +1,58 @@
import { getConfig } from "../config";
import { toAny } from "../../utils/toany";
export function getPaymentConfig() {
return toAny(getConfig("initWechatPayment"), {});
const DEFAULT_OAUTH_ACTION = "api.biz.wechat.oauth";
const DEFAULT_OAUTH_AUTHORIZE_URL_ACTION = "api.biz.wechat.oauthAuthorizeUrl";
const DEFAULT_API_PATH = "/api";
function buildApiUrlByAction(action) {
if (typeof window === "undefined" || !window.location) {
return `${DEFAULT_API_PATH}?action=${action}`;
}
try {
const url = new URL(DEFAULT_API_PATH, window.location.origin);
url.searchParams.set("action", action);
return url.href;
} catch (e) {
return `${DEFAULT_API_PATH}?action=${action}`;
}
}
export function isWechatOauthConfigReady() {
const cfg = getPaymentConfig();
return !!(cfg.oauthApiUrl && cfg.oauthAuthorizeUrlApiUrl);
function deriveApiUrlFromJssdk(jssdkApiUrl, action) {
if (!jssdkApiUrl) {
return "";
}
try {
const url = new URL(String(jssdkApiUrl), typeof window !== "undefined" ? window.location.origin : undefined);
url.searchParams.set("action", action);
return url.href;
} catch (e) {
return "";
}
}
function resolveOauthApiUrl(cfg, jssdkApiUrl) {
if (cfg.oauthApiUrl) {
return String(cfg.oauthApiUrl);
}
return deriveApiUrlFromJssdk(jssdkApiUrl, DEFAULT_OAUTH_ACTION) || buildApiUrlByAction(DEFAULT_OAUTH_ACTION);
}
function resolveOauthAuthorizeUrlApiUrl(cfg, jssdkApiUrl) {
if (cfg.oauthAuthorizeUrlApiUrl) {
return String(cfg.oauthAuthorizeUrlApiUrl);
}
return deriveApiUrlFromJssdk(jssdkApiUrl, DEFAULT_OAUTH_AUTHORIZE_URL_ACTION)
|| buildApiUrlByAction(DEFAULT_OAUTH_AUTHORIZE_URL_ACTION);
}
export function getPaymentConfig() {
const cfg = toAny(getConfig("initWechatPayment"), {});
const jssdkApiUrl = toAny(getConfig("initWechatJssdk"), {}).apiUrl;
return {
oauthApiUrl: resolveOauthApiUrl(cfg, jssdkApiUrl),
oauthAuthorizeUrlApiUrl: resolveOauthAuthorizeUrlApiUrl(cfg, jssdkApiUrl),
storageKey: cfg.storageKey,
redirectUri: cfg.redirectUri,
};
}

View File

@@ -0,0 +1,92 @@
import { createUUID } from "../../utils/uuid";
const EMBED_SOURCE = "IScanEmbed";
const EMBED_V = 1;
const DELEGATE_TIMEOUT_MS = 15000;
let embedDialogTarget = null;
let lastPaymentEmbedSource = null;
const pendingDialogOps = Object.create(null);
let childActionRunner = null;
export function setEmbedPaymentDialogTarget(source) {
embedDialogTarget = source || null;
if (source) {
lastPaymentEmbedSource = source;
}
}
export function clearEmbedPaymentDialogTarget() {
embedDialogTarget = null;
}
export function clearLastPaymentEmbedSource() {
lastPaymentEmbedSource = null;
}
function getPaymentDialogDelegateTarget() {
return embedDialogTarget || lastPaymentEmbedSource;
}
export function hasEmbedPaymentDialogTarget() {
return !!getPaymentDialogDelegateTarget();
}
export function registerPaymentDialogChildRunner(fn) {
childActionRunner = typeof fn === "function" ? fn : null;
}
export function delegatePaymentDialog(action, payload) {
const target = getPaymentDialogDelegateTarget();
if (!target) {
return null;
}
const id = createUUID();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
delete pendingDialogOps[id];
reject(new Error("payment dialog delegate timeout"));
}, DELEGATE_TIMEOUT_MS);
pendingDialogOps[id] = { resolve, reject, timer };
try {
target.postMessage({
source: EMBED_SOURCE,
v: EMBED_V,
kind: "paymentDialog",
id,
action,
payload: payload || {},
}, "*");
} catch (err) {
clearTimeout(timer);
delete pendingDialogOps[id];
reject(err);
}
});
}
export function resolveDelegatedPaymentDialog(data) {
const pending = pendingDialogOps[data.id];
if (!pending) {
return;
}
clearTimeout(pending.timer);
delete pendingDialogOps[data.id];
if (data.ok) {
pending.resolve(data.result);
return;
}
pending.reject(new Error(data.error || "payment dialog delegate failed"));
}
export function handleEmbedPaymentDialogOnChild(data, reply) {
if (typeof childActionRunner !== "function") {
reply(false, null, "payment dialog child runner is not ready");
return;
}
Promise.resolve(childActionRunner(data.action, data.payload || {}))
.then((result) => reply(true, result))
.catch((err) => {
reply(false, null, err && err.message ? err.message : String(err || "payment dialog failed"));
});
}

View File

@@ -2,8 +2,17 @@ import { toAny } from "../../utils/toany";
import { getConfig } from "../config";
import { getTopWindow, detectPayScene } from "./env";
import { showPaymentNativeQrCode, closePaymentNativeQrCode } from "./nativeQrView";
import { ensureWechatOpenid } from "./oauth";
import { isWechatOauthConfigReady } from "./config";
import { showAlipayPaymentForm, closeAlipayPaymentForm } from "./alipayFormView";
import {
hasEmbedPaymentDialogTarget,
delegatePaymentDialog,
registerPaymentDialogChildRunner,
clearLastPaymentEmbedSource,
} from "./embedPaymentDialog";
import { runPaymentDialogChildAction } from "./paymentDialogChild";
registerPaymentDialogChildRunner(runPaymentDialogChildAction);
import { resolvePaymentAmount } from "./paymentAmount";
function buildInvokeResult(payType, extra) {
return Object.assign({ payType, invoked: true }, extra || {});
@@ -49,6 +58,7 @@ function buildPaymentPrepareParams(paymentType, openid, options) {
clientIp: "",
returnUrl: opts.returnUrl || buildDefaultReturnUrl(),
paymentType,
amount: opts.amount,
};
}
@@ -86,9 +96,6 @@ function ensureOAuthBeforePayment(paymentType, oauthOptions) {
if (!shouldEnsureWechatOAuth(paymentType)) {
return Promise.resolve("");
}
if (!isWechatOauthConfigReady()) {
return Promise.reject(new Error("initWechatPayment.oauthApiUrl and oauthAuthorizeUrlApiUrl are required for wechat jsapi pay"));
}
return ensureWechatOpenid(oauthOptions);
}
@@ -128,7 +135,19 @@ function normalizePaymentFormData(paymentFormData, paymentType) {
return { type: String(payType).toLowerCase(), data: formData };
}
function execAlipayPay(html) {
function isPcBrowser() {
return detectPayScene() === "native";
}
function execAlipayPay(html, amount, currency) {
if (isPcBrowser() && getConfig("paymentAlipayFormEnabled") !== false) {
return showAlipayPaymentForm(html, amount, currency).then((result) => {
return buildInvokeResult("alipay", {
dialogShown: !!(result && result.shown),
});
});
}
const topWin = getTopWindow();
if (!topWin || !topWin.document) {
return Promise.reject(new Error("top window document is not available"));
@@ -153,7 +172,7 @@ function execWechatH5Pay(formData) {
return Promise.resolve(buildInvokeResult("h5", { redirected: true }));
}
function execWechatNativePay(formData) {
function execWechatNativePay(formData, amount, currency) {
const codeUrl = formData.codeUrl;
if (!codeUrl) {
return Promise.reject(new Error("paymentFormData.codeUrl is required"));
@@ -161,7 +180,7 @@ function execWechatNativePay(formData) {
const showBuiltinQr = getConfig("paymentNativeQrEnabled") !== false;
const showPromise = showBuiltinQr
? showPaymentNativeQrCode(codeUrl)
? showPaymentNativeQrCode(codeUrl, amount, currency)
: Promise.resolve({ shown: false });
return showPromise.then((qrResult) => {
@@ -223,7 +242,7 @@ function execWechatJsapiPay(formData) {
});
}
function invokePaymentFormData(paymentFormData, paymentType) {
function invokePaymentFormData(paymentFormData, paymentType, amount, currency) {
let parsed;
try {
parsed = normalizePaymentFormData(paymentFormData, paymentType);
@@ -233,23 +252,33 @@ function invokePaymentFormData(paymentFormData, paymentType) {
switch (parsed.type) {
case "alipay":
return execAlipayPay(parsed.data);
return execAlipayPay(parsed.data, amount, currency);
case "jsapi":
return execWechatJsapiPay(parsed.data);
case "h5":
return execWechatH5Pay(parsed.data);
case "native":
return execWechatNativePay(parsed.data);
return execWechatNativePay(parsed.data, amount, currency);
default:
return Promise.reject(new Error(`unknown pay type: ${parsed.type}`));
}
}
/**
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层)。
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层、支付宝表单弹层)。
*/
export function closePaymentDialog() {
if (hasEmbedPaymentDialogTarget()) {
const delegated = delegatePaymentDialog("closePaymentDialog", {});
if (delegated) {
return delegated.then((result) => {
clearLastPaymentEmbedSource();
return result;
}).catch(() => {});
}
}
closePaymentNativeQrCode();
closeAlipayPaymentForm();
}
/**
@@ -289,7 +318,9 @@ export function requestPayment(paymentType, fetchPaymentFormData, options) {
`paymentFormData is empty (payScene=${prepareParams.payScene}, check business payment api response)`
));
}
return invokePaymentFormData(paymentFormData, normalizedPaymentType);
const amount = resolvePaymentAmount(opts, rawResult, paymentFormData);
const currency = opts.currency;
return invokePaymentFormData(paymentFormData, normalizedPaymentType, amount, currency);
});
},
(err) => buildPaymentFailureResult(err)

View File

@@ -1,34 +1,11 @@
import QRCode from "qrcode";
import { getConfig } from "../config";
import { getTopWindow } from "./env";
import { createPaymentDialogHost } from "./paymentDialogHost";
import { appendPaymentAmount } from "./paymentAmount";
import { hasEmbedPaymentDialogTarget, delegatePaymentDialog } from "./embedPaymentDialog";
const OVERLAY_ID = "__iscan_payment_native_qr__";
const CLOSE_ICON_SVG = "<svg viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M566.97558594 521.09667969L856.8828125 231.18945312c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355468l-1.58203125-1.58203125c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0L512 466.51660156 222.09277344 176.21386719c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0l-1.58203125 1.58203125c-15.02929688 14.63378906-15.02929688 38.75976563 0 53.39355469l289.90722656 289.90722656L167.1171875 811.00390625c-14.63378906 14.63378906-14.63378906 38.75976563 0 53.39355469l1.58203125 1.58203125c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0L512 576.07226563 801.90722656 865.97949219c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0l1.58203125-1.58203125c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355469L566.97558594 521.09667969z\" fill=\"currentColor\"/></svg>";
let activeCloseHandler = null;
function getTopDocument() {
const topWin = getTopWindow();
if (!topWin || !topWin.document) {
return null;
}
return topWin.document;
}
function removeOverlay() {
const doc = getTopDocument();
if (!doc) {
return;
}
const overlay = doc.getElementById(OVERLAY_ID);
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
if (activeCloseHandler && doc.removeEventListener) {
doc.removeEventListener("keydown", activeCloseHandler);
}
activeCloseHandler = null;
}
const dialogHost = createPaymentDialogHost(OVERLAY_ID);
function getQrSize() {
const size = getConfig("paymentNativeQrSize");
@@ -50,17 +27,11 @@ function renderQrToCanvas(canvas, codeUrl) {
});
}
/**
* 关闭 PC Native 支付二维码弹层
*/
export function closePaymentNativeQrCode() {
removeOverlay();
dialogHost.removeOverlay();
}
/**
* 在顶层页面展示微信 Native 扫码支付二维码
*/
export function showPaymentNativeQrCode(codeUrl) {
export function showPaymentNativeQrCodeLocal(codeUrl, amount, currency) {
if (!codeUrl) {
return Promise.reject(new Error("codeUrl is required"));
}
@@ -68,86 +39,62 @@ export function showPaymentNativeQrCode(codeUrl) {
return Promise.resolve({ shown: false, codeUrl });
}
const doc = getTopDocument();
if (!doc || !doc.body) {
return Promise.reject(new Error("top window document is not available"));
let canvas;
try {
dialogHost.mountOverlay({
overlayClass: getConfig("paymentNativeQrOverlayClass"),
panelClass: getConfig("paymentNativeQrPanelClass"),
overlayStyle: getConfig("paymentNativeQrOverlayStyle") || undefined,
panelStyle: getConfig("paymentNativeQrPanelStyle")
|| "position:relative;width:100%;max-width:320px;background:#fff;border-radius:12px;padding:24px 20px 20px;box-shadow:0 8px 28px rgba(0,0,0,0.22);text-align:center;",
closeButtonStyle: getConfig("paymentNativeQrCloseButtonStyle") || undefined,
fillPanel(panel) {
const doc = panel.ownerDocument;
const title = doc.createElement("h3");
title.textContent = getConfig("paymentNativeQrTitle") || "微信扫码支付";
title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#222;line-height:1.35;";
const message = doc.createElement("p");
message.textContent = getConfig("paymentNativeQrMessage") || "请使用微信扫一扫,扫描二维码完成支付";
message.style.cssText = "margin:0 0 16px;font-size:14px;color:#666;line-height:1.5;";
const qrWrap = doc.createElement("div");
qrWrap.style.cssText = "display:flex;align-items:center;justify-content:center;padding:8px;background:#fff;border:1px solid #eee;border-radius:8px;";
canvas = doc.createElement("canvas");
qrWrap.appendChild(canvas);
const hint = doc.createElement("p");
hint.textContent = getConfig("paymentNativeQrHint") || "支付完成后请稍候,系统将自动确认结果";
hint.style.cssText = "margin:14px 0 0;font-size:12px;color:#999;line-height:1.45;";
panel.appendChild(title);
appendPaymentAmount(panel, doc, amount, currency);
panel.appendChild(message);
panel.appendChild(qrWrap);
panel.appendChild(hint);
},
});
} catch (err) {
return Promise.reject(err);
}
removeOverlay();
const overlayClass = getConfig("paymentNativeQrOverlayClass");
const panelClass = getConfig("paymentNativeQrPanelClass");
const overlay = doc.createElement("div");
overlay.id = OVERLAY_ID;
overlay.style.cssText = getConfig("paymentNativeQrOverlayStyle")
|| "position:fixed;left:0;top:0;right:0;bottom:0;z-index:100002;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;";
if (overlayClass) {
overlay.className = overlayClass;
}
const panel = doc.createElement("div");
panel.style.cssText = getConfig("paymentNativeQrPanelStyle")
|| "position:relative;width:100%;max-width:320px;background:#fff;border-radius:12px;padding:24px 20px 20px;box-shadow:0 8px 28px rgba(0,0,0,0.22);text-align:center;";
if (panelClass) {
panel.className = panelClass;
}
const closeBtn = doc.createElement("button");
closeBtn.type = "button";
closeBtn.setAttribute("aria-label", "关闭");
closeBtn.style.cssText = getConfig("paymentNativeQrCloseButtonStyle")
|| "position:absolute;top:10px;right:10px;width:32px;height:32px;padding:0;border:0;border-radius:50%;background:rgba(0,0,0,0.06);color:#666;display:flex;align-items:center;justify-content:center;cursor:pointer;";
closeBtn.innerHTML = CLOSE_ICON_SVG;
closeBtn.onclick = (event) => {
event.preventDefault && event.preventDefault();
removeOverlay();
};
const title = doc.createElement("h3");
title.textContent = getConfig("paymentNativeQrTitle") || "微信扫码支付";
title.style.cssText = "margin:0 0 8px;font-size:18px;font-weight:600;color:#222;line-height:1.35;";
const message = doc.createElement("p");
message.textContent = getConfig("paymentNativeQrMessage") || "请使用微信扫一扫,扫描二维码完成支付";
message.style.cssText = "margin:0 0 16px;font-size:14px;color:#666;line-height:1.5;";
const qrWrap = doc.createElement("div");
qrWrap.style.cssText = "display:flex;align-items:center;justify-content:center;padding:8px;background:#fff;border:1px solid #eee;border-radius:8px;";
const canvas = doc.createElement("canvas");
qrWrap.appendChild(canvas);
const hint = doc.createElement("p");
hint.textContent = getConfig("paymentNativeQrHint") || "支付完成后请稍候,系统将自动确认结果";
hint.style.cssText = "margin:14px 0 0;font-size:12px;color:#999;line-height:1.45;";
panel.appendChild(closeBtn);
panel.appendChild(title);
panel.appendChild(message);
panel.appendChild(qrWrap);
panel.appendChild(hint);
overlay.appendChild(panel);
doc.body.appendChild(overlay);
activeCloseHandler = (event) => {
if (event && event.key === "Escape") {
removeOverlay();
}
};
doc.addEventListener("keydown", activeCloseHandler);
overlay.onclick = (event) => {
if (event.target === overlay) {
removeOverlay();
}
};
return renderQrToCanvas(canvas, codeUrl).then(() => ({
shown: true,
codeUrl,
})).catch((err) => {
removeOverlay();
dialogHost.removeOverlay();
throw err;
});
}
export function showPaymentNativeQrCode(codeUrl, amount, currency) {
if (hasEmbedPaymentDialogTarget()) {
const delegated = delegatePaymentDialog("showPaymentNativeQr", { codeUrl, amount, currency });
if (delegated) {
return delegated;
}
}
return showPaymentNativeQrCodeLocal(codeUrl, amount, currency);
}

View File

@@ -211,12 +211,8 @@ function resolveRedirectUri(options, win) {
function exchangeCodeForOpenid(code) {
const cfg = getOAuthConfig();
const apiUrl = cfg.oauthApiUrl;
if (!apiUrl) {
return Promise.reject(new Error("initWechatPayment.oauthApiUrl is required"));
}
return request({
url: apiUrl,
url: cfg.oauthApiUrl,
method: "POST",
json: true,
data: { code },
@@ -225,15 +221,11 @@ function exchangeCodeForOpenid(code) {
function fetchAuthorizeInfo(redirectUri) {
const cfg = getOAuthConfig();
const apiUrl = cfg.oauthAuthorizeUrlApiUrl;
if (!apiUrl) {
return Promise.reject(new Error("initWechatPayment.oauthAuthorizeUrlApiUrl is required"));
}
if (!redirectUri) {
return Promise.reject(new Error("redirectUri is required for wechat oauth authorize url"));
}
return request({
url: apiUrl,
url: cfg.oauthAuthorizeUrlApiUrl,
method: "POST",
json: true,
data: { redirectUri },

View File

@@ -0,0 +1,67 @@
import { toAny } from "../../utils/toany";
export function formatPaymentAmountDisplay(amount, currency) {
if (amount == null || amount === "") {
return "";
}
if (typeof amount === "string" && /[¥¥$]/.test(amount)) {
return amount.trim();
}
const num = Number(amount);
if (isNaN(num)) {
return String(amount);
}
const symbol = !currency || currency === "CNY" ? "¥" : String(currency);
return `${symbol}${num.toFixed(2)}`;
}
export function resolvePaymentAmount(options, rawResult, paymentFormData) {
const opts = toAny(options, {});
if (opts.amount != null && opts.amount !== "") {
return opts.amount;
}
const raw = toAny(rawResult, {});
if (raw.amount != null && raw.amount !== "") {
return raw.amount;
}
const data = raw.data != null ? toAny(raw.data, {}) : {};
if (data.amount != null && data.amount !== "") {
return data.amount;
}
let formData = paymentFormData;
if (typeof formData === "string") {
try {
formData = JSON.parse(formData);
} catch (e) {
formData = null;
}
}
if (formData && typeof formData === "object") {
const form = toAny(formData, {});
if (form.amount != null && form.amount !== "") {
return form.amount;
}
if (form.totalAmount != null && form.totalAmount !== "") {
return form.totalAmount;
}
if (form.payAmount != null && form.payAmount !== "") {
return form.payAmount;
}
}
return "";
}
export function appendPaymentAmount(panel, doc, amount, currency) {
const text = formatPaymentAmountDisplay(amount, currency);
if (!text) {
return;
}
const amountEl = doc.createElement("p");
amountEl.textContent = text;
amountEl.style.cssText = "margin:0 0 12px;font-size:28px;font-weight:700;color:#e64340;line-height:1.2;text-align:center;";
panel.appendChild(amountEl);
}

View File

@@ -0,0 +1,18 @@
import { showPaymentNativeQrCodeLocal, closePaymentNativeQrCode } from "./nativeQrView";
import { showAlipayPaymentFormLocal, closeAlipayPaymentForm } from "./alipayFormView";
export function runPaymentDialogChildAction(action, payload) {
const data = payload || {};
if (action === "showPaymentNativeQr") {
return showPaymentNativeQrCodeLocal(data.codeUrl, data.amount, data.currency);
}
if (action === "showAlipayPaymentForm") {
return showAlipayPaymentFormLocal(data.html, data.amount, data.currency);
}
if (action === "closePaymentDialog") {
closePaymentNativeQrCode();
closeAlipayPaymentForm();
return Promise.resolve({});
}
return Promise.reject(new Error(`unknown payment dialog action: ${action}`));
}

View File

@@ -0,0 +1,95 @@
export const PAYMENT_CLOSE_ICON_SVG = "<svg viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M566.97558594 521.09667969L856.8828125 231.18945312c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355468l-1.58203125-1.58203125c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0L512 466.51660156 222.09277344 176.21386719c-14.63378906-14.63378906-38.75976563-14.63378906-53.39355469 0l-1.58203125 1.58203125c-15.02929688 14.63378906-15.02929688 38.75976563 0 53.39355469l289.90722656 289.90722656L167.1171875 811.00390625c-14.63378906 14.63378906-14.63378906 38.75976563 0 53.39355469l1.58203125 1.58203125c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0L512 576.07226563 801.90722656 865.97949219c14.63378906 14.63378906 38.75976563 14.63378906 53.39355469 0l1.58203125-1.58203125c14.63378906-14.63378906 14.63378906-38.75976563 0-53.39355469L566.97558594 521.09667969z\" fill=\"currentColor\"/></svg>";
const DEFAULT_OVERLAY_STYLE = "position:fixed;left:0;top:0;right:0;bottom:0;z-index:100002;background:rgba(0,0,0,0.55);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;";
const DEFAULT_CLOSE_BUTTON_STYLE = "position:absolute;top:10px;right:10px;width:32px;height:32px;padding:0;border:0;border-radius:50%;background:rgba(0,0,0,0.06);color:#666;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;";
const overlayCloseHandlers = Object.create(null);
function getLocalDocument() {
if (typeof window === "undefined" || !window.document) {
return null;
}
return window.document;
}
export function createPaymentDialogHost(overlayId) {
function getDocument() {
return getLocalDocument();
}
function removeOverlay() {
const doc = getDocument();
if (!doc) {
return;
}
const overlay = doc.getElementById(overlayId);
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
const closeHandler = overlayCloseHandlers[overlayId];
if (closeHandler && doc.removeEventListener) {
doc.removeEventListener("keydown", closeHandler);
}
overlayCloseHandlers[overlayId] = null;
}
function mountOverlay(options) {
const doc = getDocument();
if (!doc || !doc.body) {
throw new Error("payment dialog document is not available");
}
removeOverlay();
const overlay = doc.createElement("div");
overlay.id = overlayId;
overlay.style.cssText = options.overlayStyle || DEFAULT_OVERLAY_STYLE;
if (options.overlayClass) {
overlay.className = options.overlayClass;
}
const panel = doc.createElement("div");
panel.style.cssText = options.panelStyle || "";
if (options.panelClass) {
panel.className = options.panelClass;
}
const closeBtn = doc.createElement("button");
closeBtn.type = "button";
closeBtn.setAttribute("aria-label", "关闭");
closeBtn.style.cssText = options.closeButtonStyle || DEFAULT_CLOSE_BUTTON_STYLE;
closeBtn.innerHTML = PAYMENT_CLOSE_ICON_SVG;
closeBtn.onclick = (event) => {
event.preventDefault && event.preventDefault();
removeOverlay();
};
panel.appendChild(closeBtn);
if (typeof options.fillPanel === "function") {
options.fillPanel(panel, closeBtn);
}
overlay.appendChild(panel);
doc.body.appendChild(overlay);
const closeHandler = (event) => {
if (event && event.key === "Escape") {
removeOverlay();
}
};
overlayCloseHandlers[overlayId] = closeHandler;
doc.addEventListener("keydown", closeHandler);
overlay.onclick = (event) => {
if (event.target === overlay) {
removeOverlay();
}
};
return { doc, overlay, panel, closeBtn };
}
return {
removeOverlay,
mountOverlay,
};
}

54
types/index.d.ts vendored
View File

@@ -228,16 +228,54 @@ interface ScanConfigOptions {
* Native 支付二维码弹层面板 class
*/
paymentNativeQrPanelClass?: string,
/**
* PC 支付宝表单是否使用弹层 + iframe默认 truefalse 时整页跳转
*/
paymentAlipayFormEnabled?: boolean,
/**
* 支付宝表单弹层标题
*/
paymentAlipayFormTitle?: string,
/**
* 支付宝表单弹层说明文案
*/
paymentAlipayFormMessage?: string,
/**
* 支付宝表单 iframe 高度(像素),默认 520
*/
paymentAlipayFormIframeHeight?: number,
/**
* 支付宝表单弹层遮罩样式(未配置时回退 paymentNativeQrOverlayStyle
*/
paymentAlipayFormOverlayStyle?: string,
/**
* 支付宝表单弹层面板样式(未配置时回退 paymentNativeQrPanelStyle
*/
paymentAlipayFormPanelStyle?: string,
/**
* 支付宝表单弹层关闭按钮样式(未配置时回退 paymentNativeQrCloseButtonStyle
*/
paymentAlipayFormCloseButtonStyle?: string,
/**
* 支付宝表单弹层遮罩 class未配置时回退 paymentNativeQrOverlayClass
*/
paymentAlipayFormOverlayClass?: string,
/**
* 支付宝表单弹层面板 class未配置时回退 paymentNativeQrPanelClass
*/
paymentAlipayFormPanelClass?: string,
/**
* 微信支付 OAuth 配置paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
*/
initWechatPayment?: {
/**
* 用 code 换 openid对应 api.biz.wechat.oauth
* 用 code 换 openid对应 api.biz.wechat.oauth
* 未配置时默认 /api?action=api.biz.wechat.oauth若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
*/
oauthApiUrl?: string,
/**
* 获取 OAuth 授权信息(含 appId、authorizeUrl对应 api.biz.wechat.oauthAuthorizeUrl
* 获取 OAuth 授权信息(含 appId、authorizeUrl对应 api.biz.wechat.oauthAuthorizeUrl
* 未配置时默认 /api?action=api.biz.wechat.oauthAuthorizeUrl若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
*/
oauthAuthorizeUrlApiUrl?: string,
/**
@@ -404,6 +442,8 @@ interface PaymentPrepareParams {
returnUrl: string;
/** 支付渠道 */
paymentType: PaymentType;
/** 支付金额(元),由 requestPayment 传入 */
amount?: number | string;
}
/**
@@ -428,10 +468,12 @@ interface PaymentInvokeResult {
oauthFailed?: boolean;
/** OAuth 或调起失败时的错误信息 */
error?: string;
/** H5 / 支付宝是否已跳转 */
/** H5 / 移动端支付宝是否已跳转整页 */
redirected?: boolean;
/** Native 场景是否展示了二维码弹层 */
qrShown?: boolean;
/** PC 支付宝是否展示了表单弹层 */
dialogShown?: boolean;
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
cashierResult?: PaymentCashierResult;
}
@@ -440,6 +482,10 @@ interface PaymentInvokeResult {
* 支付请求选项
*/
interface RequestPaymentOptions {
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
amount?: number | string;
/** 货币符号,默认 CNY¥ */
currency?: string;
/** 支付完成回跳地址,未传时使用当前页 URL */
returnUrl?: string;
/** OAuth 授权回跳完整 URLredirectUri覆盖 initWechatPayment.redirectUri */
@@ -524,7 +570,7 @@ interface IScan {
options?: RequestPaymentOptions
): Promise<PaymentInvokeResult>;
/**
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层)
* 关闭 SDK 管理的支付相关 UI如 PC Native 二维码弹层、支付宝表单弹层
*/
closePaymentDialog(): void;