stripe是一个海外的第三方支付平台,可以支持信用卡,支付宝微信,苹果谷歌,银行,PayPal等支付方式,类似于国内的支付宝。 由于海外信用卡支付使用比较多,所以分享的是使用stripe的自定义表单元素paymentElement来搭建前端页面, 方便用户输入卡号信息等完成绑定信用卡支付。
stripe支付表单分类
stripe支付有好几种实现方式,checkout,element,link等, checkout和link类似, 都是跳转到stripe的页面完成支付,这里讲下checkout和element两种
自定义elements
使用stripe提供的表单组件,在自己的网站中自由地搭建支付页面,定制化程度高,我们可以按照stripe提供的api去修改表单样式,甚至可以自己选择需要哪些输入框,比如信用卡输入框、日期选择器和邮政编码输入框等。
checkout
跳转新页面 到stripe的网站,用户在Stripe管理的支付页面上完成支付,开发周期短,前端只需要做跳转即可,无需关心样式。
上图中看到的页面布局都是stripe控制的,需要什么内容可以给stripe传参,在stripe的官网后台也能做一些配置修改,关于checkout简单配置可以看我的另一篇文章。 stripe的checkout支付
elements的分类
elements方式在web端又主要分为两类
paymentElement
是stripe官网推荐的一种方式, 兼容性更好更安全,是以iframe的形式嵌入到网页里的一整套表单,包含了处理整个支付过程所需的所有字段,如信用卡号、过期日期、CVC码等。可以通过Elements Appearance API来修改定制外观。
- 优点是提供一套集成表单,可以节省组装表单的精力,使用api控制内容元素,安全性好
- 缺点是因为是iframe嵌入的, 所以不支持直接修改表单内部元素的样式,提供的Elements Appearance API有局限性,导致可以修改的样式不多
cardElement
此处cardElement是作为一个统称,意指除paymentElement外的单一组件,可以理解cardElement是paymentElement一个子集,包含以下这些
- CardElement:用于输入信用卡信息的组件,包括信用卡号、过期日期和 CVC 码。
- CardNumberElement:用于单独输入信用卡号的组件。
- CardExpiryElement:用于单独输入信用卡过期日期的组件。
- CardCvcElement:用于单独输入信用卡 CVC 码的组件。
- PostalCodeElement:用于输入邮政编码的组件,可用于验证信用卡支付的额外安全性。
- 优点是会更加轻量级,可以根据需要自由组合,单一组件学习曲线小
- 缺点是安全性较低,扩展性低(仅支持信用卡),集成难度高
paymentElement代码实现
实际开发中我使用了paymentElement,这是复刻的包含完整支付的一个demo, github仓库地址贴出来~包括从商品选择到支付表单和支付提交, 并且密钥也有,直接拉取启动就能支付。
启动方式
-
拉取代码
git clone https://github.com/yeeVincent/stripe-paymentElement.git
-
分别进入client和server文件夹目录
npm i
,client是前端,server是后端 -
分别进入client和server文件夹目录
npm run start
推荐前后端使用不同终端分开启动,后端服务没有热更新
密钥修改
在server目录下的.env中包含了stripe测试环境的公钥和私钥,想要自建商品可以替换成自己的,也可以不修改直接用。实际开发中,私钥是放在后端服务器中,公钥是前端通过接口获取。
商品选择
刚进入页面会看到商品选择的列表,选择商品后跳转路由传递price_id,这是与后端协商的商品唯一id,不用传递给stripe,后端拿到后用对应的商品信息创建付款意图, 也就是stripe.paymentIntents,并回传客户端付款密钥,这时一个购买意图就创建好了
js
// 前端GoodList.js页面
useEffect(() => {
fetch("/create-payment-intent", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ price_id }),
}).then(async (result) => {
const { clientSecret } = await result.json();
setClientSecret(clientSecret);
});
}, [price_id]);
js
// 后台server页面
const goodsMap = {
price_1NyET4KuMZn11UP4dw7XsSmb: {
currency: "usd",
amount: 200,
},
price_1NyESlKuMZn11UP4CrF59jB8: {
currency: "usd",
amount: 500,
},
price_1NyESUKuMZn11UP4UjfL0rga: {
currency: "usd",
amount: 1000,
},
};
app.post("/create-payment-intent", async (req, res) => {
try {
const paymentIntent = await stripe.paymentIntents.create(
goodsMap[req.body.price_id]
);
// Send publishable key and PaymentIntent details to client
res.send({
clientSecret: paymentIntent.client_secret,
});
} catch (e) {
return res.status(400).send({
error: {
message: e.message,
},
});
}
});
支付表单
拿到客户端密钥后,创建支付表单,将客户端密钥传递进去, 表单属性option用来设置一些初始化信息
其中可以设置3种mode模式,payment,subscription和setup,分别对应直接支付,订阅和试用期(未来付款)。
注意这里货币种类currency及金额amount由后端生成意图时提供,前端这里填写是无效的,但是stripe要求指定mode后,此项必须为必填项,可随便填写。(这里也可以不指定mode,只填写clientSecret也可以,后面会讲到。)
appearance可以参照stripe的api定制修改样式, 能改的内容不多, 基本上是字体大小,颜色, 按钮间距等。
paymentMethodTypes可以限制付款方式,这里限制只能为银行卡。
js
// 前端payment.js
const appearance = {
theme: "stripe", // 设置主题为stripe
variables: {
fontSizeBase: "14px",
spacingUnit: "4px",
},
rules: {
".TabIcon": {},
},
};
const options = {
mode: "payment", // 付款模式,分为payment和subscription, setup三种
amount: 1099, // 付款金额,单位为分
currency: "usd", // 付款货币
appearance, // 自定义样式
locale: "en", // 语言
paymentMethodTypes: ["card"], // 支持的支付方式
};
return (
<>
<h1>React Stripe and the Payment Element</h1>
{clientSecret && stripePromise && (
<Elements
stripe={stripePromise}
options={options}>
<CheckoutForm clientSecret={clientSecret} />
</Elements>
)}
</>
);
支付提交
来到支付提交环节,付款账号可以填写4242 4242 4242 4242,这是一个支付结果必然成功的测试银行卡号, 过期时间往后填写,CVC码3位数字随便填写, 然后点击提交,付款成功!用作测试的付款必然失败的账号也有,可以看stripe的银行卡测试卡号。
在提交的逻辑中,elements.submit是提交表单校验,通过后如果是直接支付则为stripe.confirmPayment,如果是试用期则需要调用stripe.confirmSetup来完成支付,如果支付完成不想跳转可以加入redirect: 'if_required', 后面还有stripe错误报错,可以根据不同错误类型来进行定制化需求。
js
// 前端checkoutForm.js页面
const handleSubmit = async (e) => {
e.preventDefault();
if (!stripe || !elements) return;
const { error: submitError } = await elements.submit();
if (submitError) return;
const { error } = await stripe.confirmPayment({
elements,
clientSecret: clientSecret,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: `${window.location.origin}/completion`,
},
// redirect: 'if_required',
});
if (error.type === "card_error" || error.type === "validation_error") {
setMessage(error.message);
} else {
setMessage("An unexpected error occured.");
}
};
在这个demo中我们点击完商品后就立刻创建了付款意图,拿到了客户端密钥clientSecret,但实际开发中,用户点击完商品可能并不想要直接付款,因此创建意图这一步其实可以放在用户填写完支付表单后,也就是在点击付款确认时再创建,这样可以避免因用户随意点击而产生的付款意图。
因此上面的代码可以调整成如下,把前面fetch请求获取clientSecret添加到提交环节。
js
// 前端checkoutForm.js页面
const handleSubmit = async (e) => {
e.preventDefault()
if (!stripe || !elements) return
const { error: submitError } = await elements.submit()
if (submitError) return handleError(submitError)
// get clientSecret
const resultInfo = await fetch("/create-payment-intent", {
method: "POST",
body: JSON.stringify({ price_id }),
})
const { _clientSecret } = resultInfo
const { error } = await stripe.confirmPayment({
elements,
clientSecret: _clientSecret,
confirmParams: {
return_url: `${window.location.origin}/completion`,
},
redirect: 'if_required',
})
}
另一个paymentElementOptions也可以控制支付表单的些许样式,设置表单默认值,以及隐私协议是否展示等等, 但邮箱的显示会根据国家地区来自动判断,无法通过此选项来控制。
js
const paymentElementOptions = {
layout: {
type: "accordion",
defaultCollapsed: false,
radios: true,
spacedAccordionItems: true,
},
defaultValues: {},
terms: {
bancontact: "never",
card: "never",
ideal: "never",
sepaDebit: "never",
sofort: "never",
auBecsDebit: "never",
usBankAccount: "never",
},
};
总结
从代码实现上来看,stripe官方推荐使用的paymentElement表单组件有较高的集成度,需要接入的api较少并且通俗易懂,最主要的是官方文档还讲解的很详细!为开发者去做支付省轻了很多,真心很赞!
最后欢迎大家在评论区留言互动,如果代码中有错误也有劳各位大佬指正!
码字不易,望多点赞~
文档引用
本篇文章中的代码基于此仓库修改~