Stripe is a payment SaaS (Software As A Service) that has gained popularity due to its ease of use for developers and its turnkey handling of PCI compliance. It provides not only a simple way to implement payments on web and mobile, but also fraud detection mechanisms. We are going to see how to implement Stripe credit card payments from the ground up in React Native using the tipsi-stripe library.
In this article we will see:
- How to setup your front-end and back-end to make a simple card payment
- Group your payment with the
customer
object - Use Stripe's two-step payment flow to separately authorize and process payments
Credit card data is very sensitive information so you won't be handling credit card numbers! Instead, you are going to use Stripe objects:
-
A charge is a transaction record for an amount to be debited from a given credit card.
-
A token is essentially a representation of your credit card with its information encoded by Stripe using your publishable key. It can only be used once and will be disabled after payment is done.
-
A customer is the holder of one or multiple means of payment e.g. sources.
-
A source is another representation of your card that can generate recurring payments. It can only be generated by your secret key or directly in the Stripe dashboard.
- An account created on www.stripe.com. In the
Developer
tab, you will see two API keys: a publishable one for your frontend and a secret one for your back-end.
You can share your publishable key with confidence as it can only create card tokens. On the other hand, the secret key will handle sensitive operations such as payment, refunds and so on. Store it securely in your server and avoid if possible committing it in versioned source code.
-
A React Native mobile application.
-
A back-end server. We will use Node in this example but bear in mind that Stripe supports a variety of widely used languages such as Python, PHP, Java and .NET.
In the directory of your server's code where your package.json
is located, install the node package provided by Stripe:
yarn add stripe
The idea is to expose a POST route that will handle payment on the server side. Let's declare a payment route in our Express server that takes two arguments, amount
and tokenId
:
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const stripe = require('stripe')(YOUR_STRIPE_SECRET_KEY);
app.post('/api/doPayment/', (req, res) => {
return stripe.charges
.create({
amount: req.body.amount, // Unit: cents
currency: 'eur',
source: req.body.tokenId,
description: 'Test payment',
})
.then(result => res.status(200).json(result));
});
app.listen(5000);
The amount to charge is specified in cents for payments in decimal currencies, e.g. you can divide a euro in 100 cents. Replace YOUR_STRIPE_SECRET_KEY
with the secret key you can view on https://dashboard.stripe.com/account/apikeys. As mentioned before, a better practice is to import the secret key from an untracked file.
Go to https://stripe.com/docs to generate a Stripe token with your publishable key, and then try out your route with Postman. You can also generate a token using cURL in your terminal:
$ curl -silent https://api.stripe.com/v1/tokens \ -u YOUR_STRIPE_PUBLIC_KEY: \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ -d card[exp_year]=2019 \ -d card[cvc]=123 | grep tok_
Be sure to replace YOUR_STRIPE_PUBLIC_KEY by the publishable key from your Stripe dashboard.
There are two options at this point:
- Use the iOS / Android SDKs provided by Stripe
- Use a 3rd party library for React Native:
tipsi-stripe
There are pros and cons to each path, mainly revolving around the fact that the React Native library from Tipsi has not been publicly approved by Stripe. For maximum compliance in production environments, we recommend using Stripe's fully vetted SDKs as we did in https://github.com/bamlab/react-native-stripe. In the rest of this tutorial we are going to use the tipsi-stripe
library.
Follow the installation and linking procedure on https://tipsi.github.io/tipsi-stripe/docs/installation.html, and then create a basic payment page payment.js
:
import React, { Component } from 'react';
import { View, Button } from 'react-native';
import stripe from 'tipsi-stripe';
stripe.setOptions({
publishableKey: 'YOUR_STRIPE_PUBLIC_KEY',
});
export default class Payment extends Component {
requestPayment = () => {
return stripe
.paymentRequestWithCardForm()
.then(stripeTokenInfo => {
console.warn('Token created', { stripeTokenInfo });
})
.catch(error => {
console.warn('Payment failed', { error });
});
};
render() {
return (
<View style={styles.container}>
<Button
title="Make a payment"
onPress={this.requestPayment}
disabled={this.state.isPaymentPending}
/>
</View>
);
}
}
const styles = {
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
};
You can now generate tokens with your own publishable key using test cards (cf. https://stripe.com/docs/testing) and see them in your console. You will be the only one able to create a charge from this token with your secret key.
We will now connect our front-end and our back-end. You can group all API calls in a separate api.js
file in the React Native app code:
import axios from 'axios';
export const doPayment = (amount, tokenId, accessToken) => {
const body = {
amount: amount,
tokenId: tokenId,
};
const headers = {
'Content-Type': 'application/json',
};
return axios
.post('http://localhost:5000/api/doPayment', body, { headers })
.then(({ data }) => {
return data;
})
.catch(error => {
return Promise.reject('Error in making payment', error);
});
};
I used here the axios library that directly rejects the fetch promise if it has a 40X status. Install it with yarn add axios
and change the requestPayment
callback in payment.js
:
import { doPayment } from 'path/to/api.js';
requestPayment = () => {
this.setState({ isPaymentPending: true });
return stripe
.paymentRequestWithCardForm()
.then(stripeTokenInfo => {
return doPayment(100, stripeTokenInfo.tokenId);
})
.then(() => {
console.warn('Payment succeeded!');
})
.catch(error => {
console.warn('Payment failed', { error });
})
.finally(() => {
this.setState({ isPaymentPending: false });
});
};
You should now see your first 1€ payment on the Stripe dashboard
If you repeat the above example a certain number of times, you will see in your payment history a long list of unrelated charges. If payment is the last step of an authenticated process (as it should be!), this means that you won't be able to associate customers to the payments they have made.
The next step is to charge a specific Stripe customer
with the payment. We can create a customer with a token using customers.create({ email: [email protected], source: TOKEN_ID, })
. Note that the token will be unusable after that i.e. you won't be able to create a charge with it. Instead, modify your doPayment
route in the back-end:
app.post('/api/doPayment/', (req, res) => {
return stripe.customers.create({
email: '[email protected]',
source: req.body.tokenId
})
.then(customer => {
stripe.charges.create({
amount: req.body.amount, // Unit: cents
currency: 'eur',
customer: customer.id
source: customer.default_source.id,
description: 'Test payment',
})
})
.then(result => res.status(200).json(result))
});
The customer object returned from the first Stripe call has a default_source
object which is in this case a card object on which the payment will be charged. Now that we are not using a token, we also need to pass the customer's Stripe ID as a reference for the payment to be successful.
You should now see on your Stripe dashboard new payments connected to a customer whose email is [email protected]
Try this a couple of times then head to the Customers
section of your Stripe dashboard. You will notice that there are as many customers created as the number of calls to customers.create
you made. There is no uniqueness constraint on the email value passed. A solution is to create a Stripe customer the first time a user initiates a payment, store its customer ID locally in a database and retrieve this when you initiate another payment with the same user.
Because most payment solutions are implement in a scenario where a user is authenticated, a good practice to recover the Stripe customer ID is to pass an access token to the request that is sent to the user and recover the user from this access token. Your doPayment
route should look like this in the case of a recurring payment:
app.post('/api/doPayment/', (req, res) => {
let databaseUser = null
return getDbUser(req.accessToken) // Some method to get a user from the database
.then(dbUser => {
databaseUser = dbUser
return stripe.customers
.createSource(databaseUser.stripeCustomerId, { source: req.body.tokenId })
}) // This Stripe service returns a source object
.then(newSource => {
return stripe.customers
.update(databaseUser.stripeCustomerId, { default_source: newSource.id })
}) // This Stripe service returns a customer object
.then(stripeCustomer => {
return stripe.charges.create({
amount: req.body.amount, // Unit: cents
currency: 'eur',
customer: stripeCustomer.id
source: stripeCustomer.default_source.id,
description: 'Test payment',
})
})
.then(result => res.status(200).json(result))
});
To cover both cases, you can factor the logic retrieving a Stripe customer in a separate function:
findOrCreateStripeCustomer = (dbUser, tokenId) => {
if(!!dbUser.stripeCustomerId) {
return return stripe.customers
.createSource(dbUser.stripeCustomerId, { source: tokenId })
}) // This Stripe service returns a source object
.then(newSource => {
return stripe.customers
.update(dbUser.stripeCustomerId, { default_source: newSource.id })
})
} else { // First payment
return stripe.customers.create({
email: dbUser.email,
source: tokenId
})
}
}
The doPayment
route becomes:
app.post('/api/doPayment/', (req, res) => {
return getDbUser(req.accessToken) // Some method to get a user from the database
.then(dbUser => {
findOrCreateStripeCustomer(dbUser, req.body.tokenId)
}) // This Stripe service returns a customer object
.then(stripeCustomer => {
updateDbUser(stripeCustomer.id) // Save your Stripe customer ID for the next time
return stripe.charges.create({
amount: req.body.amount, // Unit: cents
currency: 'eur',
customer: stripeCustomer.id
source: stripeCustomer.default_source.id,
description: 'Test payment',
})
})
.then(result => res.status(200).json(result))
});
The implementation of a customer database is outside the scope of this article, but you can easily use PostgreSQL + Sequelize and try out payments with the findOrCreateStripeCustomer
logic
Now all payments linked to one customer are grouped in the Stripe dashboard
We went through how to make payments in a bipolar setting, where your frontend requests the payment with a token, and your back-end processes this payment. But what happens when your app/server system is coupled to another system, for instance a third-party booking database? We came across this situation with one of our clients who expressed the following needs:
- I want to record a new booking in my database before you record it in the application's back-end.
- I want to record in my database only bookings for which payment is successful i.e. the funds are available and the transaction is authorized by the bank.
- If there is an error in posting the booking to my database, it is crucial that the customer is not billed anything and that we don't have to handle refunds.
Point 1 was simple enough to implement, but the other two seemed to conflict one another. What we needed was a way to authorize the payment, put the funds on hold, and effectively perform the transaction after posting to the 3rd party database. Fortunately enough, this kind of payment flow is natively supported in Stripe with uncaptured charges.
To this end, we need to the capture
parameter to the stripe.charges.create
call we have been using in the doPayment
route. Let us define a new method:
authorizePayment = (amount, stripeCustomer) => {
return stripe.charges.create({ // Return a charge object
amount: req.body.amount, // Unit: cents
currency: 'eur',
capture: false,
customer: stripeCustomer.id
source: stripeCustomer.default_source.id,
description: 'Test payment',
})
}
By setting the capture
parameter to false
instead of its default value true
, this call will tell Stripe to authorize the payment without processing it. We can afterwards perform any necessary external checks and capture or release (refund) the charge:
doExternalCheck = (stripeCharge, dbUser) => {
// A mock for an API call
return Promise.resolve('OK');
// return Promise.reject(Error('Failed'))
};
app.post('/api/doPayment/', (req, res) => {
return getDbUser(req.accessToken) // Some method to get a user from the database
.then(dbUser => {
findOrCreateStripeCustomer(dbUser, req.body.tokenId);
}) // This Stripe service returns a customer object
.then(stripeCustomer => {
updateDbUser(stripeCustomer.id); // Save your Stripe customer ID for the next time
return authorizePayment(amount, stripeCustomer);
})
.then(stripeCharge => {
return doExternalCheck(stripeCharge, dbUser);
})
.then(() => {
return stripe.charges.capture(stripeCharge.id);
})
.catch(error => {
return stripe.refunds.create({ charge: stripeChargeId }).then(() => {
return Promise.reject(error);
});
})
.then(result => res.status(200).json(result))
.catch(error => res.status(403).json(error));
});
By commenting the relevant line in
doExternalCheck
and making a payment, you should see captured or released charges on your Stripe dashboard.
A few notes concerning the two-step payment flow:
-
Uncaptured charges expire after 7 days after their creation and are then fully refunded. If you don't want to fully release an uncaptured charge, you should partially capture it and the remaining amount will be refunded.
-
Capturing a uncaptured charge is always successful as long as the charge has not expired and that the amount to capture is valid. The release operation is instantaneous while a refund might take 3~5 days to take place.
-
Stripe charges a fee for capturing a charge and refunding captured charges, while releasing an uncaptured charge is free. This is another advantage to the delayed capture payment flow: you are only charged for the money you effectively receive!
-
You can pass to
stripe.charges.capture
an optional parameter which is less than or equal to the charge's amount. In case of equality, the charge will be fully captured. Otherwise, the remaining amount will be automatically refunded: this means you can only capture a charge once.
Stripe provides an efficient and simple solution to implement and manage your payment layer for your mobile application. For a reasonable fee you get PCI compliance out of the box and many tools aside from payments (Billing for subscriptions, Radar for fraud prevention...) and features a neat two-step payment flow that can be exploited for other applications such as managing deposits. A more complete example is available on https://github.com/yassinecc/Dollarz, feel free to check out the code and share your feedback!