Updating to Stripe SCA for subscriptions
David Adler
Oct 7, 2019
On 14th September 2019 the new regulatory requirements for online payments came into effect in Europe as part of the second Payment Services Directive (PSD2). The new rules, called Strong Customer Authentication (SCA), aim to reduce fraud and make online payments more secure.
For payments to be successfully processed Strong Customer Authentication (SCA) requires the use of two independent sources of validation out of the three below (commonly known as ‘two-factor authentication’).
- Something the customer knows that can be entered or given, normally in the form of a password or pin
- Something the customer has, in the form of a token
- Something the customer is i.e. a fingerprint or face recognition
Exemptions to SCA
Not all payments will require additional two factor authentication. The following scenarios will most likely be exempt:
- Low-risk transactions
- Payments below €30
- Fixed-amount subscriptions
- Off session or merchant-initiated transactions
For recurring subscriptions and off-session transactions SCA will be required on the initial setup of the card being added to Stripe, with then subsequent payments not requiring additional action.
UK Banks' Position
The Financial Conduit Authority (UK regulator) announced an 18-month phase-in period of SCA requirements for online card payments. As a result, we don't expect banks to fully require SCA for online payments from UK cards until March 2021.
How to implement Stripe SCA using ReactJS and Node.
Aim
- A step by step guide for integrating Stripe in an SCA compliant way for subscriptions.
- Stripe React Elements GitHub repository also has some demo code
which we made into a working example here. - Live demo on glitch.com with working frontend and backend code you can fork it here, shown below.
Overview
Prerequisites
- Upgrade stripe
- Upgrade stripejs (server)
- Upgrade stripe-react-elements
- Upgrade stripejs (web)
- Create subscriptions in dashboard
Integrate Stripe
- Setup the intent
- Authenticate with 3D secure
- Attach payment method to customer
- Start subscription
Prerequisites
- Upgrade to latest version of stripe in the dashboard.
2019-09-09
or later.
- Use the latest version of
stripe
nodejs library.
$ npm uninstall stripe -S ; npm install stripe -S
# or if you use yarn
yarn remove stripe ; yarn add stripe
- Use latest version of
react-stripe-elements
.
npm uninstall react-stripe-elements -S ; npm install react-stripe-elements -S
# or if you use yarn
yarn remove react-stripe-elements ; yarn add react-stripe-elements
- Use latest version of client side stripe. If you are already using stripe make sure to update it. If you are starting fresh, we'll include it later in the tutorial, so don't worry about including it now!
<script src="https://js.stripe.com/v3/"></script>
- In the dashboard, create a subscription plan or do it programmatically.
If you have a subscription it should look a bit like this in the dashboard. Jot down theID
shown in the image for later.
Activate Stripe SCA
It requires these four steps
- Setup the intent
- Authenticate with 3D secure
- Attach payment method to customer
- Start subscription
1. Setup the intent
1a. Setup the intent (react)
Let's create a reusable CardElement which will always use SCA.
class _SCACardElement extends React.Component {
constructor(props) {
super(props)
this.state = {
clientSecret: null,
error: null,
}
// We expose this so that when the form is submitted we can
// do 3D Secure from the parent
props.getHandleCardSetupRef(this.handleCardSetup)
}
async componentDidMount() {
try {
let clientSecret = await api.createSetupIntent({
payment_method_types: ['card'],
})
this.setState({ clientSecret, disabled: false })
} catch (err) {
this.setState({ error: err.message })
}
}
handleCardSetup = async () =>
this.props.stripe.handleCardSetup(this.state.clientSecret)
render() {
const { error } = this.state
return (
<React.Fragment>
<CardElement
hidePostalCode
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
onReady={handleReady}
{...createOptions(this.props.fontSize)}
/>
{error && (
<div className="error" key="SetupIntentError">
Setup Intent Error: {error}
</div>
)}
</React.Fragment>
)
}
}
_SCACardElement.propTypes = {
getHandleCardSetupRef: PropTypes.func.isRequired,
}
const Card = injectStripe(_SCACardElement)
1b. Setup the intent (server)
app.post('/setup_intents', async (req, res) => {
try {
const { options } = req.body
const intent = await stripe.setupIntents.create(options)
res.json(intent)
} catch (err) {
console.error('SetupIntent err', err)
res.status(500).json(errToJSON(err))
}
})
2. Authenticate with 3D secure
On submit of the form we call handleCardSetup
to trigger the SCA modal. A successful handleCardSetup
will get us a paymentMethodId
.
class _CardForm extends React.Component {
state = {
error: null,
disabled: true,
succeeded: false,
processing: false,
message: null,
}
handleSubmit = async (ev) => {
ev.preventDefault()
this.setState({ disabled: true, processing: true })
const paymentMethodId = await this.handleCardSetup()
// Do something with the paymentMethodId ...
}
handleCardSetup = async () => {
const payload = await this.handleCardSetupRef()
if (payload.error) {
console.log('Handle Card Setup error', payload.error)
return this.setState({
error: `Handle Card Setup failed: ${payload.error.message}`,
disabled: false,
})
}
console.log('Setup intent', payload.setupIntent)
this.setState({
message: `Setup succeeded! SetupIntent is in status: ${payload.setupIntent.status}`,
error: null,
})
return payload.setupIntent.payment_method
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Use 4000002500003155 to test 3D secure.{' '}
<a href="https://stripe.com/docs/testing#testing">More cards</a>.
</label>
<Card
getHandleCardSetupRef={(ref) => (this.handleCardSetupRef = ref)}
/>
{this.state.error && (
<div key="error" className="error">
{this.state.error}
</div>
)}
{this.state.message && (
<div key="message" className="message">
{this.state.message}
</div>
)}
{!this.state.succeeded && (
<button disabled={this.state.disabled}>
{this.state.processing ? 'Processing…' : 'Start trial'}
</button>
)}
</form>
)
}
}
3. Attach payment method to customer
Now that we have our paymentMethodId
we can attach it to the user and start the subscription.
class _CardForm extends React.Component {
state = {
error: null,
disabled: true,
succeeded: false,
processing: false,
message: null,
}
handleSubmit = async (ev) => {
ev.preventDefault()
this.setState({ disabled: true, processing: true })
const paymentMethodId = await this.handleCardSetup()
if (paymentMethodId) return this.attachPaymentMethod(paymentMethodId)
}
handleCardSetup = async () => {
const payload = await this.handleCardSetupRef()
if (payload.error) {
console.log('Handle Card Setup error', payload.error)
return this.setState({
error: `Handle Card Setup failed: ${payload.error.message}`,
disabled: false,
})
}
console.log('Setup intent', payload.setupIntent)
this.setState({
message: `Setup succeeded! SetupIntent is in status: ${payload.setupIntent.status}`,
error: null,
})
return payload.setupIntent.payment_method
}
attachPaymentMethod = async (paymentMethod) => {
// Email hard coded for demo!
const attached = await api.attachPaymentMethod({
email: 'david@crowdform.studio',
paymentMethod,
})
console.log('Attached', attached)
this.setState({
succeeded: true,
error: null,
message: `Subscription started!`,
})
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Use 4000002500003155 to test 3D secure.{' '}
<a href="https://stripe.com/docs/testing#testing">More cards</a>.
</label>
<Card
getHandleCardSetupRef={(ref) => (this.handleCardSetupRef = ref)}
/>
{this.state.error && (
<div key="error" className="error">
{this.state.error}
</div>
)}
{this.state.message && (
<div key="message" className="message">
{this.state.message}
</div>
)}
{!this.state.succeeded && (
<button disabled={this.state.disabled}>
{this.state.processing ? 'Processing…' : 'Start trial'}
</button>
)}
</form>
)
}
}
4. Start subscription
We can use this payment method to create the subscription after we have attached it to the user.
app.post('/attach_payment_method', async (req, res) => {
try {
const { email, paymentMethod: paymentMethodId } = req.body.options
// Step 1
// Get or create the stripe customer
let customerId = database.loadCustomerId(email)
if (!customerId) {
const customer = await stripe.customers.create({ email })
console.log('Created new customer', customer)
customerId = customer.id
// In production you should store the newly created customerId
// on the user model in your DB
database.storeCustomerId(email, customerId)
}
// Step 2
// Attach the payment method
const paymentMethod = await stripe.paymentMethods.attach(paymentMethodId, {
customer: customerId,
})
console.log('Attached Payment Method', paymentMethod)
// Step 3
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: customerId,
tax_percent: 0,
...SUBSCRIPTION_PLAN,
})
console.log('Created subscription', subscription)
res.json({ subscription, paymentMethod })
} catch (err) {
console.error('SetupIntent err', err)
res.status(500).json(errToJSON(err))
}
})
David Adler
David is a Senior Full Stack Developer at Crowdform. In his spare time you’ll find him practicing Capoeira or working on his latest startup idea. Say hello at @da_adler on twitter or mailto:david@crowdform.studio.