PAYATHON 2026

Square API 集成 Cash App 时 ontokenization 提前触发

排障赵师傅

结论

ontokenization 表示 Square Web Payments SDK 已生成可用于付款的 sourceId。tokenResult.status === 'OK' 仅表示令牌生成成功,不能说明付款已经完成,也不能证明用户点击了 Cash App 支付页面中的 OK。

前端无法可靠监听 Cash App 内部按钮。付款是否成功,应以服务端调用 Payments API 后返回的支付状态为准,并通过 webhook 确认最终结果。收到 token 后不要直接向用户发放 bucks。

当前流程运行在移动应用的 WebView 中。Cash App Pay 会涉及浏览器跳转、deep link 和返回后的页面恢复,WebView 对这些生命周期的处理可能与普通浏览器不同。如果该 WebView 不属于 Square SDK 当前明确支持的环境,可能出现事件提前触发、重复触发或返回行为异常。建议优先使用系统浏览器或 Square 明确支持的原生集成方式。

提前收到 ontokenization 的原因

付款过程分为两个阶段:

  1. Square.js 完成 tokenization,生成一次性的付款来源 token。
  2. 服务端使用该 token 调用 Payments API 创建付款。

ontokenization 对应第一个阶段,并不表示付款已经完成,也不是 Cash App 内部 OK 按钮的点击回调。Cash App 跳转返回、WebView 恢复页面或 SDK 恢复之前的支付会话,都可能让页面收到 tokenization 结果。

下面的判断只表示已经取得可提交给后端的 token:

tokenResult.status === 'OK'

是否扣款成功,需要检查服务端返回的 Square Payment 对象。只有 Payment 达到业务要求的状态,例如即时付款场景中的 COMPLETED,才能发放商品。可接受哪些状态,还要根据是否启用延迟捕获等配置来确定。

如果在受支持的浏览器环境中,用户没有授权却仍然创建并完成了付款,问题就不只是事件触发时机。需要保留浏览器和系统版本、SDK 加载地址、Payment ID、请求时间及 Square 请求标识,并提交给 Square 支持团队核查。

调整前端处理逻辑

当前代码中,这一行位于事件处理函数的开头:

await paymentFail(detail);

无论 tokenization 成功还是失败,代码都会先调用失败接口。虽然这不会影响 Square 是否付款,却会造成业务订单状态和日志混乱,应将它移到失败分支。

还需要避免 WebView 页面恢复或事件重复触发导致多次提交。前端可以增加提交锁,后端则使用幂等键处理重复请求:

let paymentSubmitting = false;

cashAppPay.addEventListener('ontokenization', async ({ detail }) => {
  const tokenResult = detail.tokenResult;

  if (tokenResult.status !== 'OK') {
    await paymentFail(detail);

    let errorMessage =
      `Tokenization failed with status: ${tokenResult.status}`;

    if (tokenResult.errors) {
      errorMessage +=
        ` and errors: ${JSON.stringify(tokenResult.errors)}`;
    }

    redirectToPaymentResults(
      'FAILURE',
      encodeURIComponent(errorMessage)
    );
    return;
  }

  if (paymentSubmitting) {
    return;
  }

  paymentSubmitting = true;

  try {
    const paymentResults = await createPayment(
      tokenResult.token,
      tokenResult
    );

    /*
     * 不要只判断 HTTP 200 或自定义的 SUCCESS。
     * 后端必须先检查 Square Payment 的真实状态。
     */
    if (
      paymentResults.status === 'SUCCESS' &&
      paymentResults.paymentStatus === 'COMPLETED'
    ) {
      redirectToPaymentResults('SUCCESS', '');
      return;
    }

    const message =
      paymentResults.message || 'Payment was not completed';

    redirectToPaymentResults(
      'FAILURE',
      encodeURIComponent(message)
    );
  } catch (error) {
    redirectToPaymentResults(
      'FAILURE',
      encodeURIComponent(error.message)
    );
  } finally {
    paymentSubmitting = false;
  }
});

提交锁只能减少同一页面内的重复请求。页面刷新、应用返回和网络重试仍可能再次发起请求,所以服务端必须负责真正的防重。

服务端检查付款结果

PHP 接口收到 sourceId 后,需要执行以下操作:

  1. 根据 temp_order_id 查询服务端订单,不采用前端传来的金额和支付结果。
  2. 为订单生成并持久化幂等键。同一订单重试时继续使用这个幂等键。
  3. 使用 token 调用 Square Payments API。
  4. 检查返回的 Payment 状态。
  5. Payment 状态满足业务要求后,才能将订单标记为已支付并发放 bucks。
  6. 通过 Square webhook 再次核对最终状态。

服务端可以统一返回类似的结构:

{
  "status": "SUCCESS",
  "paymentStatus": "COMPLETED",
  "paymentId": "..."
}

付款被取消、拒绝或尚未完成时,应返回明确的状态,例如:

{
  "status": "ERROR",
  "paymentStatus": "CANCELED",
  "message": "Payment was not completed"
}

PHP SDK 的类名和调用签名会随版本变化,应按照项目实际安装的版本构造 CreatePaymentRequest。判断付款结果时,始终以 Square 返回的 Payment 状态为准,不能将 tokenization 成功视为付款成功。

WebView 场景的处理建议

先用同一设备上的 Safari 或 Chrome 测试完全相同的页面:

  • 如果外部浏览器运行正常,只有 WebView 会提前或重复触发,问题通常出在 WebView 的 deep link、页面恢复或兼容性处理上。
  • 如果外部浏览器也能稳定复现,需要检查 Square.js 是否来自正确的环境、应用 ID 和 location ID 是否匹配,以及 redirectURL 能否正确恢复本次付款。
  • 不要根据 WebView 拦截到的 URL 或页面前后台状态推断用户已经确认付款,因为这些信号无法证明扣款成功。
  • referenceId 和业务订单号需要对应真实订单,不能让所有付款长期共用同一个固定值。
  • token 通常具有一次性和时效性,不要保存后反复提交。
  • webhook 也需要做幂等处理,防止重复发放 bucks。

如果需要在扣款前增加业务确认页,可以让用户先在应用内确认订单,再启动 Cash App Pay。在业务允许且 Square 当前接口支持的情况下,也可以采用先授权、后完成付款的流程。无论采用哪种方式,都不能把 WebView 返回事件当作 Cash App 内部按钮的点击回调。

备注:内容仅供参考。