PAYATHON 2026

Reactjs 中设置 Square CreditCard 持卡人姓名和扣款金额

技术老齐

结论

react-square-web-payments-sdk 的 CreditCard 是由 Square 托管的银行卡输入组件。卡号、有效期、CVV 和邮政编码都由 Square 控制,因此无法在组件内加入 Cardholder name,也不能通过受支持的配置单独隐藏邮政编码。

持卡人姓名需要用普通的 React <input> 另行收集。最终扣款金额由后端调用 Square Payments API 时通过 amount_money 指定,前端提交的验证金额和页面显示的金额都不会决定实际扣款金额。

前端应该怎样处理

将持卡人姓名输入框和应付金额放在 <CreditCard> 外部即可:

import { useState } from "react";
import {
  PaymentForm,
  CreditCard,
} from "react-square-web-payments-sdk";

const DISPLAY_AMOUNT = "12.50";
const CURRENCY = "USD";

const Payment = () => {
  const [cardholderName, setCardholderName] = useState("");
  const [errorMessage, setErrorMessage] = useState(null);
  const [submitting, setSubmitting] = useState(false);

  const handleTokenizeResponse = async (tokenResult) => {
    setErrorMessage(null);

    if (!cardholderName.trim()) {
      setErrorMessage("请输入持卡人姓名");
      return;
    }

    if (tokenResult.status !== "OK" || !tokenResult.token) {
      const message =
        tokenResult.errors?.map((error) => error.message).join(", ") ||
        "银行卡信息验证失败";

      setErrorMessage(message);
      return;
    }

    try {
      setSubmitting(true);

      const response = await fetch("/api/pay", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          sourceId: tokenResult.token,
          cardholderName: cardholderName.trim(),
          orderId: "ORDER_123",
        }),
      });

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.message || "支付失败");
      }

      alert("Payment successful!");
    } catch (error) {
      setErrorMessage(error.message);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="container">
      <p>
        应付金额:{CURRENCY} {DISPLAY_AMOUNT}
      </p>

      <label htmlFor="cardholder-name">Cardholder name</label>
      <input
        id="cardholder-name"
        name="cardholderName"
        type="text"
        autoComplete="cc-name"
        value={cardholderName}
        onChange={(event) => setCardholderName(event.target.value)}
        disabled={submitting}
      />

      {errorMessage && <p role="alert">{errorMessage}</p>}

      <PaymentForm
        applicationId="sandbox-Id"
        locationId="locationId"
        cardTokenizeResponseReceived={handleTokenizeResponse}
        createVerificationDetails={() => ({
          amount: DISPLAY_AMOUNT,
          currencyCode: CURRENCY,
          intent: "CHARGE",
        })}
      >
        <CreditCard
          buttonProps={{
            disabled: submitting,
            css: {
              backgroundColor: "#E56B6F",
              fontSize: "14px",
              color: "#fff",
              "&:hover": {
                backgroundColor: "#E56B6F",
              },
            },
          }}
        />
      </PaymentForm>
    </div>
  );
};

export default Payment;

createVerificationDetails 中的 amount 主要用于买家验证,例如需要 3-D Secure 验证的交易,并不是 Payments API 的扣款指令。不同版本的 react-square-web-payments-sdk 对该属性的类型定义可能略有差异,请以项目当前安装版本的类型声明为准。

如果希望支付按钮显示”支付 USD 12.50”,可以先确认当前版本的 buttonProps 是否支持自定义按钮内容。为了减少版本兼容问题,也可以像示例中那样,把应付金额单独显示在银行卡组件上方。

扣款金额必须在后端设置

前端取得的 tokenResult.token 只是一次性支付来源。服务器需要使用该 token 调用 Square 的 CreatePayment 接口,并提交以下字段:

  • source_id
  • idempotency_key
  • amount_money.amount
  • amount_money.currency
  • location_id

下面是一个简化的 Express 示例:

import crypto from "node:crypto";
import express from "express";

const app = express();

app.use(express.json());

app.post("/api/pay", async (req, res) => {
  const { sourceId, orderId, cardholderName } = req.body;

  if (!sourceId || !orderId || !cardholderName) {
    return res.status(400).json({
      message: "缺少必要的支付信息",
    });
  }

  /*
   * 实际项目应根据 orderId 从数据库读取订单,
   * 并在服务器重新计算金额。
   */
  const amountMinor = 1250;
  const currency = "USD";

  try {
    const response = await fetch(
      "https://connect.squareupsandbox.com/v2/payments",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.SQUARE_ACCESS_TOKEN}`,
          "Content-Type": "application/json",
          "Square-Version": process.env.SQUARE_API_VERSION,
        },
        body: JSON.stringify({
          source_id: sourceId,
          idempotency_key: crypto.randomUUID(),
          amount_money: {
            amount: amountMinor,
            currency,
          },
          location_id: process.env.SQUARE_LOCATION_ID,
          reference_id: orderId,
        }),
      }
    );

    const result = await response.json();

    if (!response.ok) {
      return res.status(response.status).json({
        message: "Square 拒绝了支付请求",
        errors: result.errors,
      });
    }

    return res.json({
      paymentId: result.payment?.id,
      status: result.payment?.status,
    });
  } catch {
    return res.status(500).json({
      message: "支付服务暂时不可用",
    });
  }
});

amount_money.amount 使用货币的最小单位,例如 USD 12.50 对应 1250。并非所有货币都有两位小数,换算时应遵循相应货币的规则。

生产环境应使用以下 API 地址:

https://connect.squareup.com/v2/payments

为什么不能直接移除 zipcode

邮政编码是 Square 托管银行卡输入界面的一部分,是否显示还可能取决于商户所在地区和 Square 的风控要求。React 页面无法访问或修改托管 iframe 内的字段。

不要尝试通过 CSS、DOM 操作或覆盖 iframe 来隐藏邮政编码。这些方法通常不起作用,还可能影响银行卡验证、地址校验和 PCI 合规边界。

如果业务确实不能收集邮政编码,应根据当前使用的 Web Payments SDK 版本和商户地区核对 Square 的支持范围,不能假定 SDK 提供了 postalCode: false 配置。

持卡人姓名的用途

单独收集的姓名不一定会随普通支付请求写入银行卡记录。具体可以按业务需求处理:

  • 将姓名保存在自己的订单或付款记录中。
  • 在买家验证所需的 billingContact 中传入结构化姓名。
  • 保存银行卡供后续使用时,在创建 Card on File 的过程中将姓名映射到对应的 cardholder_name 字段。

如果之后要使用 billingContact,建议分别收集 givenName 和 familyName,不要按空格拆分完整姓名,因为不同语言和地区使用的姓名结构并不相同。

当前代码还需要调整的地方

原代码中的 onNonceGenerated 没有被任何组件调用,可以删除。它沿用了旧式的 nonce 命名,而当前回调处理的是 tokenization result,两套流程不应混用。

代码中还要注意:

  • 收到 token 后先检查 tokenResult.status,不要直接使用 token.token。
  • 不要在生产环境日志中输出支付 token 或完整的买家信息。
  • Square access token 只能保存在服务器端,不能写入 React 代码。
  • 浏览器提交的金额可以被修改。后端必须根据 orderId 查询订单并计算实际应付金额。
  • 每次调用 CreatePayment 时都要生成唯一的 idempotency_key,防止网络重试造成重复扣款。
  • 前端显示金额、createVerificationDetails.amount 和后端 amount_money 应使用同一份订单数据,服务器还要在支付前再次校验。
  • applicationId、locationId、access token 和 API 地址必须属于同一个 Sandbox 或 Production 环境。

备注:内容仅供参考。