fix
This commit is contained in:
54
dist/index.d.ts
vendored
54
dist/index.d.ts
vendored
@@ -228,16 +228,54 @@ interface ScanConfigOptions {
|
|||||||
* Native 支付二维码弹层面板 class
|
* Native 支付二维码弹层面板 class
|
||||||
*/
|
*/
|
||||||
paymentNativeQrPanelClass?: string,
|
paymentNativeQrPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* PC 支付宝表单是否使用弹层 + iframe,默认 true;false 时整页跳转
|
||||||
|
*/
|
||||||
|
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 自动处理)
|
* 微信支付 OAuth 配置(paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
|
||||||
*/
|
*/
|
||||||
initWechatPayment?: {
|
initWechatPayment?: {
|
||||||
/**
|
/**
|
||||||
* 用 code 换 openid,对应 api.biz.wechat.oauth
|
* 用 code 换 openid,对应 api.biz.wechat.oauth;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauth(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
*/
|
*/
|
||||||
oauthApiUrl?: string,
|
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,
|
oauthAuthorizeUrlApiUrl?: string,
|
||||||
/**
|
/**
|
||||||
@@ -404,6 +442,8 @@ interface PaymentPrepareParams {
|
|||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
/** 支付渠道 */
|
/** 支付渠道 */
|
||||||
paymentType: PaymentType;
|
paymentType: PaymentType;
|
||||||
|
/** 支付金额(元),由 requestPayment 传入 */
|
||||||
|
amount?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -428,10 +468,12 @@ interface PaymentInvokeResult {
|
|||||||
oauthFailed?: boolean;
|
oauthFailed?: boolean;
|
||||||
/** OAuth 或调起失败时的错误信息 */
|
/** OAuth 或调起失败时的错误信息 */
|
||||||
error?: string;
|
error?: string;
|
||||||
/** H5 / 支付宝是否已跳转 */
|
/** H5 / 移动端支付宝是否已跳转整页 */
|
||||||
redirected?: boolean;
|
redirected?: boolean;
|
||||||
/** Native 场景是否展示了二维码弹层 */
|
/** Native 场景是否展示了二维码弹层 */
|
||||||
qrShown?: boolean;
|
qrShown?: boolean;
|
||||||
|
/** PC 支付宝是否展示了表单弹层 */
|
||||||
|
dialogShown?: boolean;
|
||||||
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
||||||
cashierResult?: PaymentCashierResult;
|
cashierResult?: PaymentCashierResult;
|
||||||
}
|
}
|
||||||
@@ -440,6 +482,10 @@ interface PaymentInvokeResult {
|
|||||||
* 支付请求选项
|
* 支付请求选项
|
||||||
*/
|
*/
|
||||||
interface RequestPaymentOptions {
|
interface RequestPaymentOptions {
|
||||||
|
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
|
||||||
|
amount?: number | string;
|
||||||
|
/** 货币符号,默认 CNY(¥) */
|
||||||
|
currency?: string;
|
||||||
/** 支付完成回跳地址,未传时使用当前页 URL */
|
/** 支付完成回跳地址,未传时使用当前页 URL */
|
||||||
returnUrl?: string;
|
returnUrl?: string;
|
||||||
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
||||||
@@ -524,7 +570,7 @@ interface IScan {
|
|||||||
options?: RequestPaymentOptions
|
options?: RequestPaymentOptions
|
||||||
): Promise<PaymentInvokeResult>;
|
): Promise<PaymentInvokeResult>;
|
||||||
/**
|
/**
|
||||||
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层)
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)
|
||||||
*/
|
*/
|
||||||
closePaymentDialog(): void;
|
closePaymentDialog(): void;
|
||||||
|
|
||||||
|
|||||||
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -21,6 +21,12 @@ import {
|
|||||||
setEmbedScanErrorForwarder,
|
setEmbedScanErrorForwarder,
|
||||||
} from "./services/embedScanBridge";
|
} from "./services/embedScanBridge";
|
||||||
import { unlockScanBeep, installScanBeepGestureUnlock } from "./services/web";
|
import { unlockScanBeep, installScanBeepGestureUnlock } from "./services/web";
|
||||||
|
import {
|
||||||
|
setEmbedPaymentDialogTarget,
|
||||||
|
clearEmbedPaymentDialogTarget,
|
||||||
|
resolveDelegatedPaymentDialog,
|
||||||
|
handleEmbedPaymentDialogOnChild,
|
||||||
|
} from "./services/payment/embedPaymentDialog";
|
||||||
|
|
||||||
const EMBED_SOURCE = "IScanEmbed";
|
const EMBED_SOURCE = "IScanEmbed";
|
||||||
const EMBED_V = 1;
|
const EMBED_V = 1;
|
||||||
@@ -253,6 +259,20 @@ function embedChildOnMessage(ev) {
|
|||||||
}
|
}
|
||||||
return;
|
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") {
|
if (data.kind === "invokeResult") {
|
||||||
const pending = pendingInvokes[data.id];
|
const pending = pendingInvokes[data.id];
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
@@ -336,6 +356,10 @@ function embedInvoke(methodKey, params) {
|
|||||||
function handleEmbedHostInvoke(lib, ev) {
|
function handleEmbedHostInvoke(lib, ev) {
|
||||||
const data = ev.data;
|
const data = ev.data;
|
||||||
const { id, methodKey, params } = 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);
|
const hydrated = hydrateEmbedParams(params || [], ev.source, ev.origin);
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => _exec(lib, methodKey, ...hydrated))
|
.then(() => _exec(lib, methodKey, ...hydrated))
|
||||||
@@ -376,6 +400,11 @@ function handleEmbedHostInvoke(lib, ev) {
|
|||||||
},
|
},
|
||||||
ev.origin
|
ev.origin
|
||||||
);
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (shouldDelegatePaymentDialog) {
|
||||||
|
clearEmbedPaymentDialogTarget();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,6 +464,10 @@ export function installEmbedHost(lib) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.kind === "paymentDialogResult") {
|
||||||
|
resolveDelegatedPaymentDialog(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.kind !== "invoke") {
|
if (data.kind !== "invoke") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
118
src/services/payment/alipayFormView.js
Normal file
118
src/services/payment/alipayFormView.js
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,11 +1,58 @@
|
|||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
import { toAny } from "../../utils/toany";
|
import { toAny } from "../../utils/toany";
|
||||||
|
|
||||||
export function getPaymentConfig() {
|
const DEFAULT_OAUTH_ACTION = "api.biz.wechat.oauth";
|
||||||
return toAny(getConfig("initWechatPayment"), {});
|
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() {
|
function deriveApiUrlFromJssdk(jssdkApiUrl, action) {
|
||||||
const cfg = getPaymentConfig();
|
if (!jssdkApiUrl) {
|
||||||
return !!(cfg.oauthApiUrl && cfg.oauthAuthorizeUrlApiUrl);
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/services/payment/embedPaymentDialog.js
Normal file
92
src/services/payment/embedPaymentDialog.js
Normal 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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,8 +2,17 @@ import { toAny } from "../../utils/toany";
|
|||||||
import { getConfig } from "../config";
|
import { getConfig } from "../config";
|
||||||
import { getTopWindow, detectPayScene } from "./env";
|
import { getTopWindow, detectPayScene } from "./env";
|
||||||
import { showPaymentNativeQrCode, closePaymentNativeQrCode } from "./nativeQrView";
|
import { showPaymentNativeQrCode, closePaymentNativeQrCode } from "./nativeQrView";
|
||||||
import { ensureWechatOpenid } from "./oauth";
|
import { showAlipayPaymentForm, closeAlipayPaymentForm } from "./alipayFormView";
|
||||||
import { isWechatOauthConfigReady } from "./config";
|
import {
|
||||||
|
hasEmbedPaymentDialogTarget,
|
||||||
|
delegatePaymentDialog,
|
||||||
|
registerPaymentDialogChildRunner,
|
||||||
|
clearLastPaymentEmbedSource,
|
||||||
|
} from "./embedPaymentDialog";
|
||||||
|
import { runPaymentDialogChildAction } from "./paymentDialogChild";
|
||||||
|
|
||||||
|
registerPaymentDialogChildRunner(runPaymentDialogChildAction);
|
||||||
|
import { resolvePaymentAmount } from "./paymentAmount";
|
||||||
|
|
||||||
function buildInvokeResult(payType, extra) {
|
function buildInvokeResult(payType, extra) {
|
||||||
return Object.assign({ payType, invoked: true }, extra || {});
|
return Object.assign({ payType, invoked: true }, extra || {});
|
||||||
@@ -49,6 +58,7 @@ function buildPaymentPrepareParams(paymentType, openid, options) {
|
|||||||
clientIp: "",
|
clientIp: "",
|
||||||
returnUrl: opts.returnUrl || buildDefaultReturnUrl(),
|
returnUrl: opts.returnUrl || buildDefaultReturnUrl(),
|
||||||
paymentType,
|
paymentType,
|
||||||
|
amount: opts.amount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +96,6 @@ function ensureOAuthBeforePayment(paymentType, oauthOptions) {
|
|||||||
if (!shouldEnsureWechatOAuth(paymentType)) {
|
if (!shouldEnsureWechatOAuth(paymentType)) {
|
||||||
return Promise.resolve("");
|
return Promise.resolve("");
|
||||||
}
|
}
|
||||||
if (!isWechatOauthConfigReady()) {
|
|
||||||
return Promise.reject(new Error("initWechatPayment.oauthApiUrl and oauthAuthorizeUrlApiUrl are required for wechat jsapi pay"));
|
|
||||||
}
|
|
||||||
return ensureWechatOpenid(oauthOptions);
|
return ensureWechatOpenid(oauthOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +135,19 @@ function normalizePaymentFormData(paymentFormData, paymentType) {
|
|||||||
return { type: String(payType).toLowerCase(), data: formData };
|
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();
|
const topWin = getTopWindow();
|
||||||
if (!topWin || !topWin.document) {
|
if (!topWin || !topWin.document) {
|
||||||
return Promise.reject(new Error("top window document is not available"));
|
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 }));
|
return Promise.resolve(buildInvokeResult("h5", { redirected: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function execWechatNativePay(formData) {
|
function execWechatNativePay(formData, amount, currency) {
|
||||||
const codeUrl = formData.codeUrl;
|
const codeUrl = formData.codeUrl;
|
||||||
if (!codeUrl) {
|
if (!codeUrl) {
|
||||||
return Promise.reject(new Error("paymentFormData.codeUrl is required"));
|
return Promise.reject(new Error("paymentFormData.codeUrl is required"));
|
||||||
@@ -161,7 +180,7 @@ function execWechatNativePay(formData) {
|
|||||||
|
|
||||||
const showBuiltinQr = getConfig("paymentNativeQrEnabled") !== false;
|
const showBuiltinQr = getConfig("paymentNativeQrEnabled") !== false;
|
||||||
const showPromise = showBuiltinQr
|
const showPromise = showBuiltinQr
|
||||||
? showPaymentNativeQrCode(codeUrl)
|
? showPaymentNativeQrCode(codeUrl, amount, currency)
|
||||||
: Promise.resolve({ shown: false });
|
: Promise.resolve({ shown: false });
|
||||||
|
|
||||||
return showPromise.then((qrResult) => {
|
return showPromise.then((qrResult) => {
|
||||||
@@ -223,7 +242,7 @@ function execWechatJsapiPay(formData) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function invokePaymentFormData(paymentFormData, paymentType) {
|
function invokePaymentFormData(paymentFormData, paymentType, amount, currency) {
|
||||||
let parsed;
|
let parsed;
|
||||||
try {
|
try {
|
||||||
parsed = normalizePaymentFormData(paymentFormData, paymentType);
|
parsed = normalizePaymentFormData(paymentFormData, paymentType);
|
||||||
@@ -233,23 +252,33 @@ function invokePaymentFormData(paymentFormData, paymentType) {
|
|||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case "alipay":
|
case "alipay":
|
||||||
return execAlipayPay(parsed.data);
|
return execAlipayPay(parsed.data, amount, currency);
|
||||||
case "jsapi":
|
case "jsapi":
|
||||||
return execWechatJsapiPay(parsed.data);
|
return execWechatJsapiPay(parsed.data);
|
||||||
case "h5":
|
case "h5":
|
||||||
return execWechatH5Pay(parsed.data);
|
return execWechatH5Pay(parsed.data);
|
||||||
case "native":
|
case "native":
|
||||||
return execWechatNativePay(parsed.data);
|
return execWechatNativePay(parsed.data, amount, currency);
|
||||||
default:
|
default:
|
||||||
return Promise.reject(new Error(`unknown pay type: ${parsed.type}`));
|
return Promise.reject(new Error(`unknown pay type: ${parsed.type}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层)。
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)。
|
||||||
*/
|
*/
|
||||||
export function closePaymentDialog() {
|
export function closePaymentDialog() {
|
||||||
|
if (hasEmbedPaymentDialogTarget()) {
|
||||||
|
const delegated = delegatePaymentDialog("closePaymentDialog", {});
|
||||||
|
if (delegated) {
|
||||||
|
return delegated.then((result) => {
|
||||||
|
clearLastPaymentEmbedSource();
|
||||||
|
return result;
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
closePaymentNativeQrCode();
|
closePaymentNativeQrCode();
|
||||||
|
closeAlipayPaymentForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -289,7 +318,9 @@ export function requestPayment(paymentType, fetchPaymentFormData, options) {
|
|||||||
`paymentFormData is empty (payScene=${prepareParams.payScene}, check business payment api response)`
|
`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)
|
(err) => buildPaymentFailureResult(err)
|
||||||
|
|||||||
@@ -1,34 +1,11 @@
|
|||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import { getConfig } from "../config";
|
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 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>";
|
const dialogHost = createPaymentDialogHost(OVERLAY_ID);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQrSize() {
|
function getQrSize() {
|
||||||
const size = getConfig("paymentNativeQrSize");
|
const size = getConfig("paymentNativeQrSize");
|
||||||
@@ -50,17 +27,11 @@ function renderQrToCanvas(canvas, codeUrl) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭 PC Native 支付二维码弹层
|
|
||||||
*/
|
|
||||||
export function closePaymentNativeQrCode() {
|
export function closePaymentNativeQrCode() {
|
||||||
removeOverlay();
|
dialogHost.removeOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function showPaymentNativeQrCodeLocal(codeUrl, amount, currency) {
|
||||||
* 在顶层页面展示微信 Native 扫码支付二维码
|
|
||||||
*/
|
|
||||||
export function showPaymentNativeQrCode(codeUrl) {
|
|
||||||
if (!codeUrl) {
|
if (!codeUrl) {
|
||||||
return Promise.reject(new Error("codeUrl is required"));
|
return Promise.reject(new Error("codeUrl is required"));
|
||||||
}
|
}
|
||||||
@@ -68,86 +39,62 @@ export function showPaymentNativeQrCode(codeUrl) {
|
|||||||
return Promise.resolve({ shown: false, codeUrl });
|
return Promise.resolve({ shown: false, codeUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = getTopDocument();
|
let canvas;
|
||||||
if (!doc || !doc.body) {
|
try {
|
||||||
return Promise.reject(new Error("top window document is not available"));
|
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(() => ({
|
return renderQrToCanvas(canvas, codeUrl).then(() => ({
|
||||||
shown: true,
|
shown: true,
|
||||||
codeUrl,
|
codeUrl,
|
||||||
})).catch((err) => {
|
})).catch((err) => {
|
||||||
removeOverlay();
|
dialogHost.removeOverlay();
|
||||||
throw err;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,12 +211,8 @@ function resolveRedirectUri(options, win) {
|
|||||||
|
|
||||||
function exchangeCodeForOpenid(code) {
|
function exchangeCodeForOpenid(code) {
|
||||||
const cfg = getOAuthConfig();
|
const cfg = getOAuthConfig();
|
||||||
const apiUrl = cfg.oauthApiUrl;
|
|
||||||
if (!apiUrl) {
|
|
||||||
return Promise.reject(new Error("initWechatPayment.oauthApiUrl is required"));
|
|
||||||
}
|
|
||||||
return request({
|
return request({
|
||||||
url: apiUrl,
|
url: cfg.oauthApiUrl,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
json: true,
|
json: true,
|
||||||
data: { code },
|
data: { code },
|
||||||
@@ -225,15 +221,11 @@ function exchangeCodeForOpenid(code) {
|
|||||||
|
|
||||||
function fetchAuthorizeInfo(redirectUri) {
|
function fetchAuthorizeInfo(redirectUri) {
|
||||||
const cfg = getOAuthConfig();
|
const cfg = getOAuthConfig();
|
||||||
const apiUrl = cfg.oauthAuthorizeUrlApiUrl;
|
|
||||||
if (!apiUrl) {
|
|
||||||
return Promise.reject(new Error("initWechatPayment.oauthAuthorizeUrlApiUrl is required"));
|
|
||||||
}
|
|
||||||
if (!redirectUri) {
|
if (!redirectUri) {
|
||||||
return Promise.reject(new Error("redirectUri is required for wechat oauth authorize url"));
|
return Promise.reject(new Error("redirectUri is required for wechat oauth authorize url"));
|
||||||
}
|
}
|
||||||
return request({
|
return request({
|
||||||
url: apiUrl,
|
url: cfg.oauthAuthorizeUrlApiUrl,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
json: true,
|
json: true,
|
||||||
data: { redirectUri },
|
data: { redirectUri },
|
||||||
|
|||||||
67
src/services/payment/paymentAmount.js
Normal file
67
src/services/payment/paymentAmount.js
Normal 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);
|
||||||
|
}
|
||||||
18
src/services/payment/paymentDialogChild.js
Normal file
18
src/services/payment/paymentDialogChild.js
Normal 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}`));
|
||||||
|
}
|
||||||
95
src/services/payment/paymentDialogHost.js
Normal file
95
src/services/payment/paymentDialogHost.js
Normal 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
54
types/index.d.ts
vendored
@@ -228,16 +228,54 @@ interface ScanConfigOptions {
|
|||||||
* Native 支付二维码弹层面板 class
|
* Native 支付二维码弹层面板 class
|
||||||
*/
|
*/
|
||||||
paymentNativeQrPanelClass?: string,
|
paymentNativeQrPanelClass?: string,
|
||||||
|
/**
|
||||||
|
* PC 支付宝表单是否使用弹层 + iframe,默认 true;false 时整页跳转
|
||||||
|
*/
|
||||||
|
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 自动处理)
|
* 微信支付 OAuth 配置(paymentType 为 wechat 且微信内 JSAPI 时由 requestPayment 自动处理)
|
||||||
*/
|
*/
|
||||||
initWechatPayment?: {
|
initWechatPayment?: {
|
||||||
/**
|
/**
|
||||||
* 用 code 换 openid,对应 api.biz.wechat.oauth
|
* 用 code 换 openid,对应 api.biz.wechat.oauth;
|
||||||
|
* 未配置时默认 /api?action=api.biz.wechat.oauth(若已配置 initWechatJssdk.apiUrl 则沿用其域名与路径)
|
||||||
*/
|
*/
|
||||||
oauthApiUrl?: string,
|
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,
|
oauthAuthorizeUrlApiUrl?: string,
|
||||||
/**
|
/**
|
||||||
@@ -404,6 +442,8 @@ interface PaymentPrepareParams {
|
|||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
/** 支付渠道 */
|
/** 支付渠道 */
|
||||||
paymentType: PaymentType;
|
paymentType: PaymentType;
|
||||||
|
/** 支付金额(元),由 requestPayment 传入 */
|
||||||
|
amount?: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -428,10 +468,12 @@ interface PaymentInvokeResult {
|
|||||||
oauthFailed?: boolean;
|
oauthFailed?: boolean;
|
||||||
/** OAuth 或调起失败时的错误信息 */
|
/** OAuth 或调起失败时的错误信息 */
|
||||||
error?: string;
|
error?: string;
|
||||||
/** H5 / 支付宝是否已跳转 */
|
/** H5 / 移动端支付宝是否已跳转整页 */
|
||||||
redirected?: boolean;
|
redirected?: boolean;
|
||||||
/** Native 场景是否展示了二维码弹层 */
|
/** Native 场景是否展示了二维码弹层 */
|
||||||
qrShown?: boolean;
|
qrShown?: boolean;
|
||||||
|
/** PC 支付宝是否展示了表单弹层 */
|
||||||
|
dialogShown?: boolean;
|
||||||
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
/** 微信 JSAPI 收银台交互结果(非支付到账结果) */
|
||||||
cashierResult?: PaymentCashierResult;
|
cashierResult?: PaymentCashierResult;
|
||||||
}
|
}
|
||||||
@@ -440,6 +482,10 @@ interface PaymentInvokeResult {
|
|||||||
* 支付请求选项
|
* 支付请求选项
|
||||||
*/
|
*/
|
||||||
interface RequestPaymentOptions {
|
interface RequestPaymentOptions {
|
||||||
|
/** 支付金额(元),展示在支付弹窗;也可由业务返回 amount / totalAmount / payAmount */
|
||||||
|
amount?: number | string;
|
||||||
|
/** 货币符号,默认 CNY(¥) */
|
||||||
|
currency?: string;
|
||||||
/** 支付完成回跳地址,未传时使用当前页 URL */
|
/** 支付完成回跳地址,未传时使用当前页 URL */
|
||||||
returnUrl?: string;
|
returnUrl?: string;
|
||||||
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
/** OAuth 授权回跳完整 URL(redirectUri),覆盖 initWechatPayment.redirectUri */
|
||||||
@@ -524,7 +570,7 @@ interface IScan {
|
|||||||
options?: RequestPaymentOptions
|
options?: RequestPaymentOptions
|
||||||
): Promise<PaymentInvokeResult>;
|
): Promise<PaymentInvokeResult>;
|
||||||
/**
|
/**
|
||||||
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层)
|
* 关闭 SDK 管理的支付相关 UI(如 PC Native 二维码弹层、支付宝表单弹层)
|
||||||
*/
|
*/
|
||||||
closePaymentDialog(): void;
|
closePaymentDialog(): void;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user