Adding Stripe payments to your React Native App with Expo.
There are some projects that need to integrate payments of some kind. To approach this with an expo project we are going to use stripe, an online payment processing platform. We are going to build minimal flow within an expo project that covers payments with ApplePay, GooglePay and credit/debit cards.
Stripe has worked along with expo to provide a library to integrate payments with any expo project.
The simplest way of integrating stripe is through a CardField
component, but we would have to implement the ApplePay and GooglePay seperately and that's not what we're looking for. An example of this can be found here. Also, you could implement the default PaymentSheet
flow but this is not flexible enough to maintain a product identity such as a custom buy button or a separated navigation between displaying the cart and buying the products.
So we want to use a unified flow for every payment method and to be able to maintain the product identity. In order to do that, we will confirm the payment with a single unified button and we will select the payment method building a selector and use what they call customFlow.
So overall, we are going to:
1) Select items we want to buy.
2) Ask the server for a paymentSheet that we will later confirm.
3) Select a payment method.
4) Confirm the payment.
Prerequisites
The start point of this guide will be a blank app with managed workflow started with the command expo init
.
Package instalation
Assuming you are using the managed workflow from expo, you just have to run:
yarn add @stripe/stripe-react-native
Or alternatively if you are not using yarn:
expo install @stripe/stripe-react-native
In case you are using the bare workflow additional installation instructions can be found here.
Adding StripeProvider to the App
We'll need to wrap our App
component with the StripeProvider
that the library offers.
Import StripeProvider
//App.js
import { StripeProvider } from '@stripe/stripe-react-native';
And wrap everything you have under your App component with it. For the sake of this tutorial we'll write a Component
which we will modify. App.js
would look like this:
//App.js
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { StripeProvider } from '@stripe/stripe-react-native';
const AppContent = () => {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
)
}
export default function App() {
return (
<StripeProvider>
<AppContent/>
</StripeProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Following the docs it's said that StripeProvider
needs a publishableKey
prop. We don't need to specify it now as we will initialize it later.
ApplePay and GooglePay configuration (optional)
If we want to add support to ApplePay and GooglePay we have to configure the project a bit further.
Important: ApplePay and GooglePay won't work in ExpoGo, they will only work on standalone built apps.
For ApplePay and GooglePay to work you need to have a standalone app using eas build
.
Just add the following configuration to your "plugins"
field in your app.json
or app.config.js
{
"expo": {
...
"plugins": [
[
"@stripe/stripe-react-native",
{
"merchantIdentifier": "yourMerchantIdentifier",
"enableGooglePay": true
}
]
],
}
}
As you can imagin, enableGooglePay
makes GooglePay available within the app. The merchantIdentifier
is related to ApplePay and can be listed here. In case you have more than one just pass them as an array.
If you don't have any, you can register one here.
As description add anything that let you remember it. In identifier it's recommended to write something as:
merchant.com.yourdomainname.yourappname
Also, don't forget to add the merchantIdentifier to the StripeProvider
as a prop:
export default function App() {
return (
<StripeProvider
merchantIdentifier="yourMerchantIdentifier"
>
<AppContent/>
</StripeProvider>
);
}
Building a simple item selector screen
We will build a simple screen that will let us create an order to send the info to the server and create a PaymentIntent
there.
We will use a hardcoded variable inside AppContent
as our product database:
const AppContent = () => {
const products = [{
price: 10,
name: 'Pizza Pepperoni',
id: 'pizza-pepperoni',
}, {
price: 12,
name: 'Pizza 4 Fromaggi',
id: 'pizza-fromaggi'
}, {
price: 8,
name: 'Pizza BBQ',
id: 'pizza-bbq'
}]
// ...
}
Just build a way of rendering and adding these products to a cart. For example:
import { Button } from 'react-native';
const ProductRow = ({ product, cart, setCart }) => {
const modifyCart = (delta) => {
setCart({ ...cart, [product.id]: cart[product.id] + delta })
}
return (
<View style={styles.productRow}>
<View style={{ flexDirection: 'row' }}>
<Text style={{ fontSize: 17, flexGrow: 1 }}>
{product.name} - {product.price}$
</Text>
<Text style={{ fontSize: 17, fontWeight: "700" }}>
{cart[product.id]}
</Text>
</View>
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 8
}}>
<Button
disabled={cart[product.id] <= 0}
title="Remove"
onPress={() => modifyCart(-1)} />
<Button
title="Add"
onPress={() => modifyCart(1)} />
</View>
</View>
)
}
const ProductsScreen = ({ products }) => {
/**
* We will save the state of the cart here
* It will have the inital shape:
* {
* [product.id]: 0
* }
*/
const [cart, setCart] = React.useState(
Object.fromEntries(products.map(p => [p.id, 0]))
);
const handleContinuePress = async () => {
/* We will write it later */
}
return (
<View style={styles.screen}>
{
products.map((p) => {
return <ProductRow
key={p.id}
product={p}
cart={cart}
setCart={setCart} />
})
}
<View style={{marginTop: 16}}>
<Button title="Continue" onPress={handleContinuePress} />
</View>
</View>
)
}
And we add it to our AppContent
component:
const AppContent = () => {
// ...
return (
<View style={styles.container}>
<ProductsScreen products={products} />
</View>
)
}
Finally, add the following to the styles so it doesn't hurt your sight:
const styles = StyleSheet.create({
// ...
screen: {
alignSelf: 'stretch',
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center'
},
productRow: {
paddingVertical: 24,
paddingHorizontal: 8,
borderBottomWidth: 1,
width: '75%',
},
});
Interacting with the backend server
So we've successfully built our ProductScreen
and we have a way of filling a cart. The next step is to send the cart information to our backend server once we press Continue
.
The server has to create a PaymentIntent
with the information we sent and return to us the following fields: {publishableKey, clientSecret, merchantName}
. All these fields are related to the PaymentIntent
that has been created.
Once we have this information, we can navigate to the CheckoutScreen
. In our case we'll use the default react state to do this but you can use react-navigation
and pass this information as route params. You can find more information on how to add react-navigation
to your project here.
const handleContinuePress = async () => {
/* Send the cart to the server */
const URL = 'https://domain.tld/api/create-payment-intent'
const response = await fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application-json'
},
body: JSON.stringify(cart)
})
/* Await the response */
const {
publishableKey,
clientSecret,
merchantName
} = await response.json();
/* Navigate to the CheckoutScreen */
/* You can use navigation.navigate from react-navigation */
navigateToCheckout({
publishableKey,
clientSecret,
merchantName,
cart,
products
});
}
As we didn't add react-navigation
we'll implement the function navigateToCheckout
inside <AppContent />
with a little state management.
const ProductsScreen = ({ products, navigateToCheckout }) => {
//...
}
const AppContent = () => {
//...
const [screenProps, setScreenProps] = React.useState(null);
const navigateToCheckout = (screenProps) => {
setScreenProps(screenProps)
}
const navigateBack = () => {
setScreenProps(null);
}
return (
<View style={styles.container}>
{!screenProps &&
<ProductsScreen
products={products}
navigateToCheckout={navigateToCheckout} />
}
{ !!screenProps &&
<CheckoutScreen
{...screenProps}
navigateBack={navigateBack}/>
}
</View>
)
}
Building the payment method selector
Once in the CheckoutScreen
we have what we need to confirm the payment. Now we'll build a simple Selector
component that will help us choose the payment option we want.
We will start creating the CheckoutScreen component which will show the cart information along with the payment method selector, a button to confirm the payment, and a button to go back:
import { Pressable } from 'react-native';
const CartInfo = ({ products, cart }) => {
return <View>
{
Object.keys(cart).map(productId => {
const product = products.filter(p => p.id === productId)[0];
const quantity = cart[productId];
return (
<View
key={productId}
style={[{ flexDirection: 'row' }, styles.productRow]}>
<Text style={{ flexGrow: 1, fontSize: 17 }}>
{quantity} x {product.name}
</Text>
<Text style={{ fontWeight: "700", fontSize: 17 }}>
{quantity * product.price}$
</Text>
</View>
)
})
}
</View>
}
const MethodSelector = ({ onPress }) => {
// ...
return (
<>
)
}
const CheckoutScreen = ({
products,
navigateBack,
publishableKey,
clientSecret,
merchantName,
cart }) => {
const handleSelectMethod = async () => {
// ...
}
const handleBuyPress = async () => {
// ...
}
return (
<View style={styles.screen}>
<CartInfo cart={cart} products={products} />
<MethodSelector onPress={handleSelectMethod} />
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignSelf: 'stretch',
marginHorizontal: 24,
}}>
<Pressable onPress={navigateBack}>
<Text style={[styles.textButton, styles.boldText]}>
Back
</Text>
</Pressable>
<Pressable style={styles.buyButton} onPress={handleBuyPress}>
<Text
style={[styles.boldText, { color: 'white'}]}>
Buy
</Text>
</Pressable>
</View>
</View>
)
}
Now that we have the skeleton of the screen we can start writing the MethodSelector
component. First of all, we will import all the necessary stuff from stripe and initialize them.
import { initStripe, useStripe } from '@stripe/stripe-react-native';
const CheckoutScreen = (/*...*/) => {
//...
// We will store the selected paymentMethod
const [ paymentMethod, setPaymentMethod ] = React.useState();
// Import some stripe functions
const {
initPaymentSheet,
presentPaymentSheet,
confirmPaymentSheetPayment,
} = useStripe();
// Initialize stripe values upon mounting the screen
React.useEffect( () => {
(async () => {
await initStripe({
publishableKey,
// Only if implementing applePay
// Set the merchantIdentifier to the same
// value in the StripeProvider and
// striple plugin in app.json
merchantIdentifier: 'yourMerchantIdentifier'
});
// Initialize the PaymentSheet with the paymentIntent data,
// we will later present and confirm this
await initializePaymentSheet();
})();
}, []);
const initializePaymentSheet = async () => {
const { error, paymentOption } = await initPaymentSheet({
paymentIntentClientSecret: clientSecret,
customFlow: true,
merchantDisplayName: merchantName,
style: 'alwaysDark', // If darkMode
googlePay: true, // If implementing googlePay
applePay: true, // If implementing applePay
merchantCountryCode: 'ES', // Countrycode of the merchant
testEnv: __DEV__, // Set this flag if it's a test environment
});
if (error) {
console.log(error)
} else {
// Upon initializing if there's a paymentOption
// of choice it will be filled by default
setPaymentMethod(paymentOption);
}
};
//...
}
After we have initialized the necessary values (the StripeProvider and the PaymentSheet) we will implement the selector. To do that we'll use the function presentPaymentSheet
and set the selected method with setPaymentMethod
.
const handleSelectMethod = async () => {
const { error, paymentOption } = await presentPaymentSheet({
confirmPayment: false,
});
if (error) {
alert(`Error code: ${error.code}`, error.message);
}
setPaymentMethod(paymentOption);
}
This is the function we're passing as prop to our MethodSelector
, we will pass it the selected method too and we can finally write its visuals.
First pass the paymentMethod
as a prop to the MethodSelector
.
const CheckoutScreen = (/*...*/) => {
//...
<MethodSelector onPress={handleSelectMethod} paymentMethod={paymentMethod} />
//...
}
And write a minimal selector:
import { Platform } from 'react-native';
const MethodSelector = ({ onPress, paymentMethod }) => {
// ...
return (
<View style={{ marginVertical: 48, width: '75%'}}>
<Text style={{
fontSize: 14,
letterSpacing: 1.5,
color: 'black',
textTransform: 'uppercase'
}}>
Select payment method
</Text>
{/* If there's no paymentMethod selected, show the options */}
{ !paymentMethod &&
<Pressable
onPress={onPress}
style={{
flexDirection: 'row',
paddingVertical: 8,
alignItems: 'center',
}}>
{
Platform.select({
ios: (<Text style={styles.boldText}>
Apple Pay
</Text>),
android: (<Text style={styles.boldText}>
Google Pay
</Text>)
})
}
<View style={[styles.selectButton, {marginLeft: 16} ]}>
<Text style={[styles.boldText, {color: '#007DFF'}]}>Card</Text>
</View>
</Pressable>
}
{/* If there's a paymentMethod selected, show it */}
{ !!paymentMethod &&
<Pressable
onPress={onPress}
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
}}>
{paymentMethod.label.toLowerCase().includes('apple') &&
<Text style={styles.boldText}>
Apple Pay
</Text>
}
{paymentMethod.label.toLowerCase().includes('google') &&
<Text style={styles.boldText}>
Google Pay
</Text>
}
{!paymentMethod.label.toLowerCase().includes('google') &&
!paymentMethod.label.toLowerCase().includes('apple') &&
<View style={[styles.selectButton, { marginRight: 16 }]}>
<Text style={[styles.boldText, { color: '#007DFF' }]}>
{paymentMethod.label}
</Text>
</View>
}
<Text style={[styles.boldText, { color: '#007DFF', flex: 1 }]}>
Change payment method
</Text>
</Pressable>
}
</View>
)}
Finally, as we did before, add these styles so you don't cry too much:
const styles = StyleSheet.create({
// ...
buyButton: {
backgroundColor: '#007DFF',
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 8,
},
textButton: {
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 8,
color: '#007DFF'
},
selectButton: {
borderColor: '#007DFF',
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 8,
borderWidth: 2,
},
boldText: {
fontSize: 17,
fontWeight: '700'
}
});
Confirming the paymentSheet and handling errors
The last thing that's left is to finally confirm the payment. To do that we will write the function handleBuyPress
using the stripe function confirmPaymentSheetPayment
.
const handleBuyPress = async () => {
if (paymentMethod) {
const response = await confirmPaymentSheetPayment();
if (response.error) {
alert(`Error ${response.error.code}`);
console.error(response.error.message);
} else {
alert('Purchase completed!');
}
}
}
And that should be it!
UX touches
There are a lot of things that can be improved in this minimal application. For example, you should show the total of the cart to the user before continuing to the checkout screen and before tapping buy.
Also, you can (and should if you want to upload this to the stores) change the lines that indicates the GooglePay and ApplePay options with the marks of these payment options.
So for example, the lines:
:::javascript
Can be changed to:
:::javascript
And so you can do it with ApplePay. These components are available in the GitHub project of this guide. The files follow: - GooglePayMark - ApplePayMark
You can import them like:
import GooglePayMark from './GooglePayMark';
import ApplePayMark from './ApplePayMark';
You just need to install react-native-svg
yarn add react-native-svg
or
expo install react-native-svg
TL;DR - Logic recap
In case you want to know what is the logic to implement without all the components code here's a quick recap:
1) Configure the project as in the configuration section.
2) Fetch your server creating a payment intent and ask it for the necessary information.
3) Initialize stripe with initStripe
and the paymentSheet
with initPaymentSheet
when mounting the screen. You should have fetched the parameters needed for each function .
4) Show the method selector with presentPaymentSheet
and the parameter confirmPayment: false
.
5) Confirm the payment with confirmPaymentSheetPayment
.
Also, the project of this app can be found in this GitHub project.