Snap Finance SDK 支付按钮在 Next 12 中无法渲染
结论
问题通常不在 checkout.button(),而是 SDK 还没有加载完成。
next/script 会在客户端异步加载脚本。用户点击按钮时,window.snapuk 可能尚未创建。原代码中的判断:
if (win.snapuk) {
// ...
}
会直接跳过后续代码,所以页面既不会报错,也不会渲染支付按钮。
可以监听 <Script> 的 onLoad 或 onReady,等 SDK 可用后再调用 window.snapuk.init() 和 window.snapuk.checkout.button()。还要确认代码在浏览器端执行,并且 #snap-uk-checkout 已挂载到 DOM。
推荐实现
import React, { useCallback, useState } from 'react';
import Script from 'next/script';
declare global {
interface Window {
snapuk?: {
init: (clientId: string) => void;
checkout: {
button: (options: {
transaction: unknown;
merchantId: string;
}) => void;
};
};
}
}
export default function TestPage() {
const [sdkReady, setSdkReady] = useState(false);
const clientId = 'cfB9gZpXGM0HkhwVMtfyaD0m';
// 使用符合 Snap Finance 文档要求的真实 transaction 数据。
const transaction = {
// ...
};
const renderButton = useCallback(() => {
if (!sdkReady || typeof window === 'undefined') {
console.warn('Snap Finance SDK is not ready');
return;
}
const snapuk = window.snapuk;
if (!snapuk) {
console.error('window.snapuk is unavailable');
return;
}
const container = document.getElementById('snap-uk-checkout');
if (!container) {
console.error('Snap Finance checkout container was not found');
return;
}
snapuk.init(clientId);
snapuk.checkout.button({
transaction,
merchantId: clientId,
});
}, [sdkReady, transaction]);
return (
<div>
<Script
id="snap-finance-sdk"
src="https://sandbox-sdk.snapfinance.co.uk/v1/snapuk.min.js"
strategy="afterInteractive"
onLoad={() => {
console.log('Snap Finance SDK loaded');
setSdkReady(true);
}}
onError={(error) => {
console.error('Failed to load Snap Finance SDK', error);
}}
/>
<h1>Hello</h1>
<div id="snap-uk-checkout" />
<button type="button" disabled={!sdkReady} onClick={renderButton}>
{sdkReady ? 'Pay Now' : 'Loading payment SDK...'}
</button>
</div>
);
}
如果当前使用的 Next 12 小版本支持 onReady,可以优先使用:
<Script
id="snap-finance-sdk"
src="https://sandbox-sdk.snapfinance.co.uk/v1/snapuk.min.js"
strategy="afterInteractive"
onReady={() => setSdkReady(true)}
onError={(error) => {
console.error('Failed to load Snap Finance SDK', error);
}}
/>
在客户端路由场景中,onReady 比 onLoad 更合适。即使脚本已经加载过,页面组件重新挂载时,它仍能通知组件 SDK 已经就绪。
如果所用的 Next 12 版本不支持这种行为,可以继续使用 onLoad,同时在组件挂载时检查 window.snapuk:
React.useEffect(() => {
if (typeof window !== 'undefined' && window.snapuk) {
setSdkReady(true);
}
}, []);
自动渲染支付按钮
如果不需要用户先点击自定义按钮,可以在 SDK 加载完成后直接渲染支付按钮:
const initializeCheckout = useCallback(() => {
if (typeof window === 'undefined' || !window.snapuk) {
return;
}
window.snapuk.init(clientId);
window.snapuk.checkout.button({
transaction,
merchantId: clientId,
});
}, [transaction]);
return (
<>
<Script
id="snap-finance-sdk"
src="https://sandbox-sdk.snapfinance.co.uk/v1/snapuk.min.js"
strategy="afterInteractive"
onLoad={initializeCheckout}
onError={(error) => {
console.error('Failed to load Snap Finance SDK', error);
}}
/>
<div id="snap-uk-checkout" />
</>
);
onLoad 执行时,承载按钮的 <div> 必须已经存在。如果 SDK 回调触发时容器还没有挂载,可以先记录 SDK 的就绪状态,再通过 useEffect 调用:
React.useEffect(() => {
if (!sdkReady || !window.snapuk) {
return;
}
window.snapuk.init(clientId);
window.snapuk.checkout.button({
transaction,
merchantId: clientId,
});
}, [sdkReady, transaction]);
为什么普通 HTML 可以工作
普通 HTML 示例通常按照以下顺序执行:
- 浏览器加载 SDK。
- SDK 创建
window.snapuk。 - 页面调用
snapuk.init()。 - SDK 查找
#snap-uk-checkout并插入按钮。
Next.js 页面还涉及服务端渲染、React 挂载和异步脚本加载。组件已经显示,不代表外部 SDK 也加载完成,因此不能把”页面已经渲染”等同于”window.snapuk 已经可用”。
事件处理函数中使用 window 不会引发服务端渲染问题,因为点击事件只会在浏览器端发生。需要确认的是,用户点击时 SDK 是否已经成功加载。
还需要检查的事项
- 不要用固定的
setTimeout()猜测 SDK 何时加载完成,应该监听onLoad或onReady。 transaction必须符合 Snap Finance SDK 的要求。即使 SDK 已经加载,参数无效也可能导致按钮无法生成。merchantId是否应该与clientId使用相同的值,需要以当前 Snap Finance 环境配置和文档为准,不能只根据示例判断。- Sandbox SDK、Sandbox 凭据和 Sandbox 交易数据需要配套使用。
- 在浏览器的 Network 面板中确认
snapuk.min.js是否成功返回。广告拦截器、内容安全策略或网络限制都可能阻止脚本加载。 - 避免重复调用
checkout.button()。如果 SDK 每次调用都会向容器追加内容,应禁用重复点击,或者按照 SDK 文档提供的方式,在重新渲染前清理旧实例。 useCallback(..., [])会捕获首次渲染时的transaction。如果交易数据会发生变化,必须将其加入依赖数组,否则 SDK 可能收到过期数据。
备注:内容仅供参考。
