stripe支付自定义elements实现方式

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一个子集,包含以下这些

  1. CardElement:用于输入信用卡信息的组件,包括信用卡号、过期日期和 CVC 码。
  2. CardNumberElement:用于单独输入信用卡号的组件。
  3. CardExpiryElement:用于单独输入信用卡过期日期的组件。
  4. CardCvcElement:用于单独输入信用卡 CVC 码的组件。
  5. PostalCodeElement:用于输入邮政编码的组件,可用于验证信用卡支付的额外安全性。
  • 优点是会更加轻量级,可以根据需要自由组合,单一组件学习曲线小
  • 缺点是安全性较低,扩展性低(仅支持信用卡),集成难度高

paymentElement代码实现

实际开发中我使用了paymentElement,这是复刻的包含完整支付的一个demo, github仓库地址贴出来~包括从商品选择到支付表单和支付提交, 并且密钥也有,直接拉取启动就能支付。

启动方式

  1. 拉取代码 git clone https://github.com/yeeVincent/stripe-paymentElement.git

  2. 分别进入client和server文件夹目录npm i,client是前端,server是后端

  3. 分别进入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较少并且通俗易懂,最主要的是官方文档还讲解的很详细!为开发者去做支付省轻了很多,真心很赞!

最后欢迎大家在评论区留言互动,如果代码中有错误也有劳各位大佬指正!

码字不易,望多点赞~

文档引用

本篇文章中的代码基于此仓库修改~

github.com/matthewling...

相关推荐
前端百草阁1 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜2 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4042 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish3 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple3 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five4 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序4 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫5415 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普6 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省6 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript