rafael Published Nov. 11, 2021 · 29 min read

Adding Stripe payments to your React Native App with Expo.

A decorative hero image for this page.

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 Google Pay

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.

Ready to bring your vision to life?

Get in touch