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...

相关推荐
腾讯TNTWeb前端团队6 分钟前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰3 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy4 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom5 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom5 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom5 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom5 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom5 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试