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_ididempotency_keyamount_money.amountamount_money.currencylocation_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 环境。
备注:内容仅供参考。
