PAYATHON 2026

移动端 SecurionPay Checkout popup 被浏览器阻止

支付小周

结论

JavaScript 无法绕过移动端浏览器的弹窗拦截机制,也无法在用户没有操作页面时强制打开 SecurionPay Checkout popup。

支付窗口需要由点击、触摸等明确的用户操作直接触发。在页面加载、定时器回调或异步请求回调中打开窗口,浏览器通常会将其视为非用户触发并予以拦截。用户不必手动允许所有弹窗,保留“点击后支付”的交互即可。

出现 Cannot read property 'focus' of null,通常是因为弹窗创建失败并返回了 null,但后续代码仍调用了窗口的 focus() 方法。

为什么页面加载时会被阻止

移动端浏览器通常只允许在短暂的用户激活状态内创建新窗口,例如:

  • click
  • touchend
  • 其他可信的用户输入事件

下面的调用发生在页面初始化阶段,没有用户操作,很容易被浏览器拦截:

$(function () {
    SecurionpayCheckout.open({
        checkoutRequest: 'xyz=',
        name: 'SecurionPay',
        description: 'Checkout example'
    });
});

改用 setTimeout()、Promise 或模拟点击,也无法可靠地绕过这项限制:

setTimeout(function () {
    SecurionpayCheckout.open(/* ... */);
}, 1000);

浏览器判断的是事件是否来自真实用户,而不是代码有没有触发某个 click 事件。

推荐实现

在页面上保留一个明确的支付按钮,并在用户点击时立即调用 SecurionpayCheckout.open():

$(function () {
    SecurionpayCheckout.key = 'key';

    SecurionpayCheckout.success = function (result) {
        // Send the payment result to your server for verification
        console.log(result);
    };

    SecurionpayCheckout.error = function (errorMessage) {
        console.error(errorMessage);
    };

    $('#payment-button').on('click', function () {
        SecurionpayCheckout.open({
            checkoutRequest: 'xyz=',
            name: 'SecurionPay',
            description: 'Checkout example'
        });
    });
});
<button id="payment-button" type="button">
    Payment button
</button>

按钮文案可以使用“继续支付”或“打开安全支付页面”,让用户知道点击后会发生什么。

避免在异步回调后打开窗口

即使操作最初由用户点击触发,如果代码先执行耗时的异步操作,再打开 Checkout,浏览器提供的用户激活状态也可能已经失效:

$('#payment-button').on('click', function () {
    fetch('/create-checkout-request')
        .then(function (response) {
            return response.json();
        })
        .then(function (data) {
            // 此时用户激活状态可能已经失效
            SecurionpayCheckout.open({
                checkoutRequest: data.checkoutRequest,
                name: 'SecurionPay',
                description: 'Checkout example'
            });
        });
});

更稳妥的做法是提前从服务器获取 checkoutRequest,准备完成后启用支付按钮。用户点击按钮时,直接在点击处理函数中打开 Checkout:

let checkoutRequest = null;

fetch('/create-checkout-request')
    .then(function (response) {
        return response.json();
    })
    .then(function (data) {
        checkoutRequest = data.checkoutRequest;
        $('#payment-button').prop('disabled', false);
    })
    .catch(function () {
        $('#payment-status').text('暂时无法初始化支付,请稍后重试。');
    });

$('#payment-button').on('click', function () {
    if (!checkoutRequest) {
        return;
    }

    SecurionpayCheckout.open({
        checkoutRequest: checkoutRequest,
        name: 'SecurionPay',
        description: 'Checkout example'
    });
});
<button id="payment-button" type="button" disabled>
    Payment button
</button>

<p id="payment-status"></p>

关于 focus 错误

浏览器阻止 window.open() 时,返回值通常是 null:

const popup = window.open('https://example.com');

if (popup) {
    popup.focus();
} else {
    console.warn('Popup was blocked');
}

如果 focus() 由 SecurionPay Checkout 内部调用,业务代码可能无法直接加上这个判断。解决问题的办法仍是让 SecurionpayCheckout.open() 在真实的用户操作中同步执行,不要放在页面加载流程或异步回调中。

可选方案

如果业务要求用户进入页面后立即开始支付流程,可以考虑在当前标签页中跳转,而不是打开弹窗:

window.location.assign(paymentUrl);

这种方式能否用于当前的 SecurionPay 集成,要看其接口是否提供可直接跳转的托管支付地址。仅修改现有 SecurionpayCheckout.open() 的参数无法实现,需要根据实际接入版本支持的 Checkout 流程进行确认。

无论使用弹窗还是页面跳转,都应将支付成功结果发送到服务端验证,不能只根据前端 success 回调认定订单已经支付。

备注:内容仅供参考。