PAYATHON 2026

Reactjs 集成 Square CreditCard payment 的字段与金额设置

支付小周

结论

CreditCard 中的卡号、有效期、CVV 和邮编由 Square Web Payments SDK 托管,不能像普通的 React 输入框那样随意增删。

  • Cardholder name 需要单独添加在 CreditCard 外部,并由业务代码管理。
  • 不建议用 CSS 隐藏邮编。邮编是否可以关闭,取决于 Square SDK、商户所在国家和当前使用的 SDK 版本。如果该版本没有提供公开的关闭选项,就应保留这个字段。
  • 扣款金额不在 <CreditCard> 上设置。前端可以显示金额,但后端调用 Square Payments API 时,必须通过 amountMoney 指定实际扣款金额。
  • 后端不能直接相信前端提交的金额。生产环境中,前端应提交订单 ID,再由后端重新计算并校验金额。

在前端添加持卡人姓名和金额

可以把 Cardholder name 作为普通的 React 输入框放在 CreditCard 上方:

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

const Payment = ({ orderId, amount = "10.00", currency = "USD" }) => {
  const [cardholderName, setCardholderName] = useState("");
  const [errorMessage, setErrorMessage] = useState(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleTokenReceived = async (token, verifiedBuyer) => {
    if (token.status !== "OK" || !token.token) {
      const message =
        token.errors?.map((error) => error.message).join(", ") ||
        "Card tokenization failed";

      setErrorMessage(message);
      return;
    }

    if (!cardholderName.trim()) {
      setErrorMessage("Please enter the cardholder name");
      return;
    }

    try {
      setIsSubmitting(true);
      setErrorMessage(null);

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

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.message || "Failed to process payment");
      }

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

  return (
    <div className="container">
      <p>
        Amount: {currency} {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={isSubmitting}
        required
      />

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

      <PaymentForm
        applicationId="sandbox-application-id"
        locationId="location-id"
        cardTokenizeResponseReceived={handleTokenReceived}
        createVerificationDetails={() => ({
          amount,
          currencyCode: currency,
          intent: "CHARGE",
          billingContact: {
            givenName: cardholderName.trim(),
          },
        })}
      >
        <CreditCard
          buttonProps={{
            disabled: isSubmitting,
            css: {
              backgroundColor: "#E56B6F",
              fontSize: "14px",
              color: "#fff",
              "&:hover": {
                backgroundColor: "#E56B6F",
              },
            },
          }}
        />
      </PaymentForm>
    </div>
  );
};

export default Payment;

createVerificationDetails 中的金额用于买家验证,例如 3-D Secure。这里的金额和币种必须与后端创建支付时使用的值一致,但这个配置本身不会完成扣款。

如果当前安装的版本不支持 createVerificationDetails,请按照该版本的类型定义或文档调整,不能自行添加一个不存在的属性并传给 CreditCard。

在后端设置扣款金额

Square 卡片组件只负责安全地生成一次性支付令牌,也就是 sourceId。真正的扣款由后端调用 Create Payment API 完成。

下面是后端逻辑的示意代码。具体的导入方式可能随 Square Node SDK 版本变化:

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

    if (!sourceId || !orderId) {
      return res.status(400).json({
        message: "Missing sourceId or orderId",
      });
    }

    // 必须从数据库读取订单并在服务端计算金额。
    const order = await getOrderFromDatabase(orderId);

    if (!order || order.status === "paid") {
      return res.status(400).json({
        message: "Invalid or already paid order",
      });
    }

    const paymentRequest = {
      sourceId,
      idempotencyKey: crypto.randomUUID(),
      locationId: process.env.SQUARE_LOCATION_ID,
      amountMoney: {
        amount: order.totalInMinorUnits,
        currency: order.currency,
      },
      note: `Cardholder: ${cardholderName}`,
    };

    if (verificationToken) {
      paymentRequest.verificationToken = verificationToken;
    }

    const payment = await squareClient.payments.create(paymentRequest);

    await markOrderAsPaid(orderId, payment);

    res.json({
      paymentId: payment.id,
      status: payment.status,
    });
  } catch (error) {
    res.status(500).json({
      message: "Payment could not be completed",
    });
  }
});

amountMoney.amount 必须是以货币最小单位表示的整数。例如:

amountMoney: {
  amount: 1050,
  currency: "USD",
}

这段代码表示 USD 10.50,而不是 USD 1050.00。部分 Square Node SDK 版本可能要求写成 BigInt(1050),具体应以项目当前 SDK 的类型定义为准。

邮编字段

邮编是 Square 托管的安全卡片表单字段,通常用于地址验证和风险控制。不要这样处理:

/* 不建议这样处理 */
.postal-code-field {
  display: none;
}

Square 的输入框通常放在受控 iframe 中,外部 CSS 不一定能够访问。强行隐藏邮编还可能影响验证、风控或支付成功率。

如果当前版本的 react-square-web-payments-sdk 明确提供了关闭 postal code 的公开配置,可以按照对应版本的文档设置。如果没有公开配置,就无法可靠地从 <CreditCard> 中移除邮编。即使业务不需要保存这个字段,也可以让 Square 收集并用于支付验证,无须写入自己的数据库。

其他需要调整的地方

原代码中的 onNonceGenerated 没有传给任何组件,所以不会执行。可以直接删除,也可以把相关错误处理并入 cardTokenizeResponseReceived。

另外还要注意:

  • applicationId 和 locationId 必须属于同一个 Square 环境,不能混用 Sandbox 和 Production。
  • 后端需要保存并检查订单状态,防止重复扣款。
  • 每次创建支付时都要使用唯一的 idempotencyKey。
  • 前端只提交 orderId 和支付令牌。最终金额、币种、折扣和税费均由后端确认。
  • 不要记录完整 token、卡号、CVV 或其他敏感支付信息。
  • Cardholder name 是业务自行采集的字段,不会自动成为 Square 卡片组件内部的认证字段。如果要把姓名传入买家验证流程,应分别采集 givenName 和 familyName,不能假设所有姓名都可以按空格准确拆分。

备注:内容仅供参考。