在这个Stripe & JS教程中,我将展示如何使用Stripe支付集成、React和Express创建一个简单的网络商店。我们将熟悉Stripe仪表板和Stripe的基本功能,如收费、客户、订单、优惠券等。此外,你还会了解到webhooks和受限API密钥的用法。
如果你读了这篇文章,你会在15分钟内熟悉Stripe的集成,这样你就可以跳过埋头苦读官方文档的过程(因为我们已经为你做了这些了!)。
介绍一下我的Stripe经验和写这个教程的原因。在RisingStack,我们一直在与一个来自美国医疗保健领域的客户合作,他们聘请我们创建一个大规模的网络商店,用来销售他们的产品。在创建这个基于Stripe的平台的过程中,我们花了很多时间来研究文档并弄清楚整合的问题。不是因为它很难,而是有一定量的Stripe相关知识需要内化。
我们将在本教程中一起建立一个示例应用程序--这样你就可以学习如何从头开始创建一个Stripe网络商店了该示例应用程序的前端可以在github.com/RisingStack…而它的后端可以在github.com/RisingStack…
我将在下面的文章中使用这些 repo 的代码样本。
Stripe支付集成的基础知识
首先,Stripe的承诺是什么?它基本上是一个支付提供商:您设置您的账户,将其集成到您的应用程序中,然后让钱生钱。很简单吧?好吧,让您的财务人员根据他们提供的计划来决定它是否是一个好的供应商。
如果你在这里,你可能对整合的技术问题更感兴趣,所以我会深入研究这一部分。 为了向你展示如何使用Stripe,我们将用它建立一个简单的演示应用程序。
在我们开始编码之前,我们需要创建一个Stripe账户。不要担心,在这个阶段不需要信用卡。你只需要在尝试激活你的账户时提供一个支付方式。
直接进入Stripe仪表板,点击注册按钮。电子邮件、姓名、密码......像往常一样。**砰!**你有一个仪表板。您可以创建、管理和跟踪订单、支付流程、客户......所以基本上您想知道的关于您的商店的一切都在这里。
如果你想创建一个新的优惠券或产品,你只需要点击几个按钮或输入一个简单的curl命令到你的终端,就像Stripe API文件描述的那样。当然,你可以将Stripe集成到你的产品中,这样你的管理员就可以从你的用户界面上进行设置,然后使用Stripe.js将其集成并暴露给你的客户。
仪表板上的另一个重要菜单是开发人员部分,我们将在这里添加我们的第一个webhook并创建我们的限制性API密钥。当我们在下面实现我们的演示商店时,我们会更加熟悉仪表板和API。
在React中用Charges创建一个网络商店
让我们创建一个有两种产品的React网络商店:香蕉和黄瓜。无论如何,你还想在网络商店里买什么,对吗?
- 我们可以使用Create React App来开始。
- 我们将在HTTP请求中使用Axios
- 和query-string-object来将对象转换为Stripe请求的查询字符串。
- 我们还将需要React Stripe Elements,它是Stripe.js和Stripe Elements的React包装器。它增加了安全的信用卡输入,并将卡的数据发送至Stripe API进行标记化。
接受我的建议:你不应该将原始的信用卡信息发送到你自己的API,而是让Stripe为你处理信用卡安全问题。
你将能够使用你从Stripe得到的令牌来识别用户提供的卡。
npx create-react-app webshop
cd webshop
npm install --save react-stripe-elements
npm install --save axios
npm install --save query-string-object
完成准备工作后,我们必须在我们的应用程序中包含Stripe.js。只需在你的index.html
的头部添加<script src="https://js.stripe.com/v3/"></script>
。
现在我们准备开始编码了。
首先,我们必须从react-stripe-elements
中添加一个<StripeProvider/>
到我们的根React App组件。
这将使我们能够访问Stripe对象。在道具中,我们应该传递一个公共访问密钥(apiKey
),它可以在仪表板的开发者部分的API密钥菜单下找到可发布的密钥。
// App.js
import React from 'react'
import {StripeProvider, Elements} from 'react-stripe-elements'
import Shop from './Shop'
const App = () => {
return (
<StripeProvider apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxx">
<Elements>
<Shop/>
</Elements>
</StripeProvider>
)
}
export default App
<Shop/>
是我们商店表单的Stripe实现,你可以从import Shop from './Shop'
。我们稍后会详细说明。
正如你所看到的,<Shop/>
被包裹在从react-stripe-elements
中导入的<Elements>
中,因此你可以在你的组件中使用injectStripe
。为了阐明这一点,让我们看一下我们在Shop.js
中的实现。
// Shop.js
import React, { Component } from 'react'
import { CardElement } from 'react-stripe-elements'
import PropTypes from 'prop-types'
import axios from 'axios'
import qs from 'query-string-object'
const prices = {
banana: 150,
cucumber: 100
}
class Shop extends Component {
constructor(props) {
super(props)
this.state = {
fetching: false,
cart: {
banana: 0,
cucumber: 0
}
}
this.handleCartChange = this.handleCartChange.bind(this)
this.handleCartReset = this.handleCartReset.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleCartChange(evt) {
evt.preventDefault()
const cart = this.state.cart
cart[evt.target.name]+= parseInt(evt.target.value)
this.setState({cart})
}
handleCartReset(evt) {
evt.preventDefault()
this.setState({cart:{banana: 0, cucumber: 0}})
}
handleSubmit(evt) {
// TODO
}
render () {
const cart = this.state.cart
const fetching = this.state.fetching
return (
<form onSubmit={this.handleSubmit} style={{width: '550px', margin: '20px', padding: '10px', border: '2px solid lightseagreen', borderRadius: '10px'}}>
<div>
Banana {(prices.banana / 100).toLocaleString('en-US', {style: 'currency', currency: 'usd'})}:
<div>
<button name="banana" value={1} onClick={this.handleCartChange}>+</button>
<button name="banana" value={-1} onClick={this.handleCartChange} disabled={cart.banana <= 0}>-</button>
{cart.banana}
</div>
</div>
<div>
Cucumber {(prices.cucumber / 100).toLocaleString('en-US', {style: 'currency', currency: 'usd'})}:
<div>
<button name="cucumber" value={1} onClick={this.handleCartChange}>+</button>
<button name="cucumber" value={-1} onClick={this.handleCartChange} disabled={cart.cucumber <= 0}>-</button>
{cart.cucumber}
</div>
</div>
<button onClick={this.handleCartReset}>Reset Cart</button>
<div style={{width: '450px', margin: '10px', padding: '5px', border: '2px solid green', borderRadius: '10px'}}>
<CardElement style={{base: {fontSize: '18px'}}}/>
</div>
{!fetching
? <button type="submit" disabled={cart.banana === 0 && cart.cucumber === 0}>Purchase</button>
: 'Purchasing...'
}
Price:{((cart.banana * prices.banana + cart.cucumber * prices.cucumber) / 100).toLocaleString('en-US', {style: 'currency', currency: 'usd'})}
</form>
)
}
}
Shop.propTypes = {
stripe: PropTypes.shape({
createToken: PropTypes.func.isRequired
}).isRequired
}
如果你看一下,Shop
是一个简单的React表单,有可购买的元素:Banana
和Cucumber
,并且每个元素都有一个数量增加/减少的按钮。点击这些按钮将改变它们在this.state.cart
中各自的数量。
下面有一个submit
按钮,而购物车的当前总价则打印在表格的最底部。价格将期望以美分为单位的价格,所以我们将它们存储为美分,但当然,我们希望将它们以美元显示给用户。我们希望它们被显示到小数点后第二位,例如2.5美元而不是2.5美元。为了实现这一点,我们可以使用内置的toLocaleString()
函数来格式化价格。
现在是Stripe的具体部分:我们需要添加一个表单元素,以便用户可以输入他们的卡的详细信息。为了实现这一点,我们只需要从react-stripe-elements
中添加<CardElment/>
,就可以了。我还添加了一些不费吹灰之力的内联css,以使这个商店至少在某种程度上赏心悦目。
我们还需要使用injectStripe
Higher-Order-Component,以便将Stripe对象作为道具传递给<Shop/>
组件,这样我们就可以在handleSubmit
中调用Stripe的createToken()
函数来标记用户的卡,这样他们就可以被收费了。
// Shop.js
import { injectStripe } from 'react-stripe-elements'
export default injectStripe(Shop)
一旦我们从Stripe收到了标记化的卡,我们就可以向它收费了。
现在让我们保持简单,通过向https://api.stripe.com/v1/charges
发送一个POST请求,指定支付source
(这是令牌ID),收费amount
(收费)和currency
,如Stripe API中所述。
我们需要在头中发送API密钥以获得授权。我们可以在开发者菜单的仪表板上创建一个受限制的API密钥。将收费的权限设置为 "读和写",如下面的截图所示。
不要忘记:。你千万不要在客户端使用你的瑞士军队的秘密密钥!
让我们来看看它的运行情况。
// Shop.js
// ...
const stripeAuthHeader = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer rk_test_xxxxxxxxxxxxxxxxxxxxxxxx`
}
class Shop extends Component {
// ...
handleSubmit(evt) {
evt.preventDefault()
this.setState({fetching: true})
const cart = this.state.cart
this.props.stripe.createToken().then(({token}) => {
const price = cart.banana * prices.banana + cart.cucumber * prices.cucumber
axios.post(`https://api.stripe.com/v1/charges`,
qs.stringify({
source: token.id,
amount: price,
currency: 'usd'
}),
{ headers: stripeAuthHeader })
.then((resp) => {
this.setState({fetching: false})
alert(`Thank you for your purchase! You card has been charged with: ${(resp.data.amount / 100).toLocaleString('en-US', {style: 'currency', currency: 'usd'})}`)
})
.catch(error => {
this.setState({fetching: false})
console.log(error)
})
}).catch(error => {
this.setState({fetching: false})
console.log(error)
})
}
// ...
}
为测试目的,你可以使用Stripe提供的这些国际卡。
看起来不错,我们已经可以从卡上创建代币,并向他们收费,但我们应该如何知道谁买了什么,我们应该把包裹送到哪里?
这就是产品和订单的作用。
用Stripe下订单
实现一个简单的收费方法是一个好的开始,但我们需要更进一步来创建订单。为此,我们必须建立一个服务器并公开一个API,处理这些订单并接受来自Stripe的webhooks,以便在他们得到付款后进行处理。
我们将使用Express来处理我们的API的路由。你可以在下面找到其他几个节点包的列表,以便开始使用。让我们创建一个新的根文件夹并开始吧。
npm install express stripe body-parser cors helmet
这个骨架是一个简单的表达式你好,世界使用CORS,这样当我们试图到达我们的PI服务器时,浏览器就不会惊慌失措,Helmet会自动为我们设置一堆安全头信息。
// index.js
const express = require('express')
const helmet = require('helmet')
const cors = require('cors')
const app = express()
const port = 3001
app.use(helmet())
app.use(cors({
origin: [/http:\/\/localhost:\d+$/],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}))
app.get('/api/', (req, res) => res.send({ version: '1.0' }))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
为了访问Stripe,需要Stripe.js并使用你的秘密密钥直接调用它(你可以在仪表盘->开发人员->Api密钥中找到它),我们将使用stripe.orders.create()
,用于传递客户调用我们的服务器下订单时收到的数据。
订单将不会被自动支付。为了向客户收费,我们可以直接使用一个Source
,如Card Token ID,或者我们可以创建一个**Stripe客户**。
创建Stripe客户的额外好处是,我们可以跟踪多次收费,或为他们创建循环收费,还可以指示Stripe存储运输数据和其他必要的信息以完成订单。
即使你的应用程序已经处理了用户,你可能也想从卡片令牌和运输数据中创建客户。这样你就可以给这些客户附加永久或季节性的折扣,允许他们随时点击购物并在你的用户界面上列出他们的订单。
现在让我们保持简单,一旦订单成功创建,就使用卡片令牌作为我们的Source
调用stripe.orders.pay()
。
在现实世界中,你可能希望通过在不同的端点上公开订单创建和支付,所以如果支付失败,客户可以稍后再试,而不必重新创建订单。然而,我们仍然有很多东西要讲,所以我们不要把事情搞得太复杂。
// index.js
const stripe = require('stripe')('sk_test_xxxxxxxxxxxxxxxxxxxxxx')
app.post('/api/shop/order', async (req, res) => {
const order = req.body.order
const source = req.body.source
try {
const stripeOrder = await stripe.orders.create(order)
console.log(`Order created: ${stripeOrder.id}`)
await stripe.orders.pay(stripeOrder.id, {source})
} catch (err) {
// Handle stripe errors here: No such coupon, sku, ect
console.log(`Order error: ${err}`)
return res.sendStatus(404)
}
return res.sendStatus(200)
})
现在我们能够在后端处理订单,但我们也需要在用户界面上实现这一点。
首先,让我们把<Shop/>
的状态实现为Stripe API所期望的对象。
你可以在这里找到一个订单请求应该是什么样的。我们需要一个带有line1, city, state, country, postal_code
字段的address
对象,一个name
,一个email
和一个coupon
字段,以使我们的客户为寻找优惠券做好准备。
// Shop.js
class Shop extends Component {
constructor(props) {
super(props)
this.state = {
fetching: false,
cart: {
banana: 0,
cucumber: 0
},
coupon: '',
email: '',
name: '',
address : {
line1: '',
city: '',
state: '',
country: '',
postal_code: ''
}
}
this.handleCartChange = this.handleCartChange.bind(this)
this.handleCartReset = this.handleCartReset.bind(this)
this.handleAddressChange = this.handleAddressChange.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(evt) {
evt.preventDefault()
this.setState({[evt.target.name]: evt.target.value})
}
handleAddressChange(evt) {
evt.preventDefault()
const address = this.state.address
address[evt.target.name] = evt.target.value
this.setState({address})
}
// ...
}
现在我们准备创建输入字段。当然,当输入字段为空时,我们应该禁用提交按钮。就像通常的交易一样。
// Shop.js
render () {
const state = this.state
const fetching = state.fetching
const cart = state.cart
const address = state.address
const submittable = (cart.banana !== 0 || cart.cucumber !== 0) && state.email && state.name && address.line1 && address.city && address.state && address.country && address.postal_code
return (
// ...
<div>Name: <input type="text" name="name" onChange={this.handleChange}/></div>
<div>Email: <input type="text" name="email" onChange={this.handleChange}/></div>
<div>Address Line: <input type="text" name="line1" onChange={this.handleAddressChange}/></div>
<div>City: <input type="text" name="city" onChange={this.handleAddressChange}/></div>
<div>State: <input type="text" name="state" onChange={this.handleAddressChange}/></div>
<div>Country: <input type="text" name="country" onChange={this.handleAddressChange}/></div>
<div>Postal Code: <input type="text" name="postal_code" onChange={this.handleAddressChange}/></div>
<div>Coupon Code: <input type="text" name="coupon" onChange={this.handleChange}/></div>
{!fetching
? <button type="submit" disabled={!submittable}>Purchase</button>
: 'Purchasing...'}
// ...
我们还必须定义可购买的项目。
这些项目将由Stripe的库存管理单元来识别,这也可以在仪表板上创建。
首先,我们必须创建产品*(在仪表板->订单->产品中的香蕉和黄瓜*),然后给它们分配一个SKU(点击创建的产品,在库存组中添加SKU)。一个SKU指定产品,包括其属性--尺寸、颜色、数量和价格--,所以一个产品可以有多个SKU。
.
在我们创建了产品并为其分配了SKU后,我们将其添加到网店,这样我们就可以解析订单了。
// Shop.js
const skus = {
banana: 1,
cucumber: 2
}
我们已经准备好在提交时向我们的快递API发送订单。从现在开始,我们不必再计算订单的总价了。Stripe可以根据SKU、数量和优惠券为我们进行汇总。
// Shop.js
handleSubmit(evt) {
evt.preventDefault()
this.setState({fetching: true})
const state = this.state
const cart = state.cart
this.props.stripe.createToken({name: state.name}).then(({token}) => {
// Create order
const order = {
currency: 'usd',
items: Object.keys(cart).filter((name) => cart[name] > 0 ? true : false).map(name => {
return {
type: 'sku',
parent: skus[name],
quantity: cart[name]
}
}),
email: state.email,
shipping: {
name: state.name,
address: state.address
}
}
// Add coupon if given
if (state.coupon) {
order.coupon = state.coupon
}
// Send order
axios.post(`http://localhost:3001/api/shop/order`, {order, source: token.id})
.then(() => {
this.setState({fetching: false})
alert(`Thank you for your purchase!`)
})
.catch(error => {
this.setState({fetching: false})
console.log(error)
})
}).catch(error => {
this.setState({fetching: false})
console.log(error)
})
}
让我们为测试目的创建一个优惠券。这也可以在仪表板上完成。你可以在计费菜单下的优惠券标签上找到这个选项。
根据持续时间,有多种类型的优惠券,但只有类型为Once的优惠券可以用于订单。其余的优惠券可以附加到Stripe客户。
您还可以为您创建的优惠券指定很多参数,例如可以使用多少次,是基于金额还是基于百分比,以及优惠券何时到期。现在我们需要一个只能使用一次的优惠券,并提供一定数量的减价。
很好!现在我们有了我们的产品,我们可以创建订单,我们也可以要求Stripe为我们从客户的卡上收费。但是我们仍然没有准备好运送产品,因为我们现在还不知道收费是否成功。为了获得这些信息,我们需要设置Webhooks,这样Stripe就可以让我们知道钱何时到账。
设置Stripe Webhooks来验证付款
正如我们前面所讨论的,我们不是在分配卡片,而是在分配源码给客户。这背后的原因是Stripe能够使用多种支付方式,其中一些可能需要几天时间来验证。
我们需要设置一个端点,让Stripe在事件发生时(如成功付款)可以调用。当一个事件不是由我们通过调用API发起,而是直接来自Stripe时,Webhooks也很有用。
想象一下,你有一个订阅服务,而你不想每个月都向客户收费。在这种情况下,您可以设置一个webhook,当循环付款成功或失败时,您会得到通知。
在这个例子中,我们只想在订单被支付时得到通知。当它发生时,Stripe可以通过调用我们API上的一个端点来通知我们,该端点的HTTP请求中包含请求体中的支付数据。目前,我们没有一个静态IP,但我们需要一种方法将我们的本地API暴露在公共互联网上。我们可以使用Ngrok来实现这一点。只要下载并运行./ngrok http 3001
命令,就可以得到一个指向我们的localhost:3001
的ngrok网址。
我们还必须在Stripe仪表板上设置我们的webhook。进入 "开发人员"->"Webhooks",点击 "添加端点",然后输入ngrok网址和要调用的端点,例如:http://92832de0.ngrok.io/api/shop/order/process
。然后在Filter event下选择Select types to send并搜索order.payment_succeeded。
请求正文中发送的数据是加密的,只能通过使用头中发送的签名和webhook秘密来解密,webhook秘密可以在webhook仪表板上找到。
这也意味着我们不能简单地使用bodyParser
来解析主体,所以我们需要给bodyParser
添加一个例外,这样当URL以/api/shop/order/process
开始时,它就会被绕过。我们需要使用由Stripe JavaScript SDK提供的stripe.webhooks.constructEvent()
函数来代替,为我们解密信息。
// index.js
const bodyParser = require('body-parser')
app.use(bodyParser.json({
verify: (req, res, buf) => {
if (req.originalUrl.startsWith('/api/shop/order/process')) {
req.rawBody = buf.toString()
}
}
}))
app.use(bodyParser.urlencoded({
extended: false
}))
app.post('/api/shop/order/process', async (req, res) => {
const sig = req.headers['stripe-signature']
try {
const event = await stripe.webhooks.constructEvent(req.rawBody, sig, 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
console.log(`Processing Order : ${event.data.object.id}`)
// Process payed order here
} catch (err) {
return res.sendStatus(500)
}
return res.sendStatus(200)
})
在订单成功支付后,我们可以将其解析发送至其他API,如Salesforce或Stamps,将东西打包并准备发送出去。
Stripe JS教程的结束语
我编写本指南的目的是通过使用JavaScript和Stripe创建网店的过程为您提供帮助。我希望您能从我们的经验中学习,并在您将来决定实施类似的系统时使用本指南。
The postStripe & JS: Payments Integration Tutorialappeared first onRisingStack Engineering.