PAYATHON 2026

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 示例通常按照以下顺序执行:

  1. 浏览器加载 SDK。
  2. SDK 创建 window.snapuk。
  3. 页面调用 snapuk.init()。
  4. 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 可能收到过期数据。

备注:内容仅供参考。