How to Make Mobile Payment in a React Native App With Stripe and Expo in 15 Minutes

Authors
Jakub Kłobus
Software Engineer
@
Callstack

In this article, I’ll guide you through the process of adding payments to your React Native app. Let's narrow down payment options first.

There are three ways of paying in mobile applications: paid app, in-app payments, and paying in app for services not related to the app itself (e.g. ecommerce). Today we will focus on the third option, the one that gives us the most flexibility. In our case, we will implement paying via a card functionality using Stripe.

But before we dig into coding, let me answer a few questions that probably popped for some of you.

  • Can I use Expo?

Yes, you can! And you will if you decide to join me today.

  • Do I need any complicated setup on my machine?

No, we will be using Expo Snack and Glitch.io. So a browser is the only thing you need.

  • Is any Stripe account needed, how can I test payments?

If you want to properly test your payments implementation, it’s worth having a Stripe account. You can easily create it for free here. For testing, we will be using test card numbers provided by Stripe.

Tools

For simplicity, let’s use Expo Snack to implement and test React Native mobile app and Glitch.io to create a simple backend.

For those who don’t know, Expo Snack is a React Native Playground in your browser. With it, you can play with React Native and test it even on your device without leaving the browser. It is based on Expo.

Glitch is an online playground but for the web with a possibility to create an instantly deployed backend service. No setup is required which means this is the perfect tool for us to create a test server for our payments.

Final examples can be found here:

React Native setup

First, let’s create an empty Expo project. For simplicity we will be using Snack, but if you prefer you can init a new expo project locally using expo init my-app command.

To create a new Snack project just start coding.

Expo has built-in support for the @stripe/stripe-react-native library. This means you can freely use it without ejecting or creating a custom EAS plugin. If these things aren’t familiar to you, don’t worry! You won’t need them for this tutorial.

To use @stripe/stripe-react-native in Snack, you need to add the line below to your dependencies in the package.json file:

"@stripe/stripe-react-native": "0.2.3"

After adding the dependency, let’s replace unnecessary code from App.js with our StripeProvider component:

import * as React from 'react';
import { Text} from 'react-native';
import Constants from 'expo-constants';
import {
StripeProvider,
} from '@stripe/stripe-react-native';
// find your publishable key at https://dashboard.stripe.com/test/apikeys
const STRIPE_PUBLBISHABLE_KEY = “pk_test_YOUR_KEY_HERE”;
export default function App() {
return (
<StripeProvider publishableKey={STRIPE_PUBLBISHABLE_KEY}>
<Text>Card Payments</Text>
</StripeProvider>
);
}

StripeProvider is used to initialize Stripe in your app. It should be used in the root component of your application. The only required prop is publishableKey. You can find this key in your Stripe account settings.

For simplicity, we are passing hardcoded publishable key but if possible it is worth creating an API endpoint that will return it to use with StripeProvider

NOTE: If you’re like me and prefer using TS instead of JS you can just change the file extension in Snack to .tsx.

Now we are ready to go!

Backend setup

Ok, almost ready to go. Remember when I was writing about the backend? So it wasn’t a mistake or a typo. In order to implement payments in our React Native app, we need a server. By using a server weincrease the security of our app, which is quite important when dealing with payments of any kind.

But since this is a “React Native Payment with Stripe” tutorial, we can cheat a little and use pre-built backend. The only thing you have to know about its implementation is that we need:

  • /create-payment-intent endpoint that creates a payment intent. Under the hood it uses stripe-node library to create paymentIntent to start payment flow.
  • Webhooks setup - an endpoint that will be automatically called by Stripe after the payment is completed.

PaymentIntent - Stripe uses this to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process.

To customize the backend to work with your Stripe account first open this project in Glitch.io. It is the working implementation you need. Then just press the “Remix” button, open the .env file, and modify the following key:

From now on you will be using your own cloned Glitch server as your backend. To retrieve the URL of this service press “Preview” and copy the address of the opened page.

Webhooks: For your final backend, you would probably need a webhooks setup. It is a mechanism that allows Stripe to inform you backend that payment was proceeded successfully (or not). For simplicity, we won’t use it in our test app but you can find more details here.

Screen with CardInput

Now we are ready to create a screen with card input so we can collect card data. There is a CardInput component in @stripe/stripe-react-native for that. Some of the props you can pass include: placeholder, autofocus info, or styles to it (the list is longer, see the documentation for more details). We are only going to pass style, postalCodeEnabled, and cardStyle

Below the input, we can render a button that will start our payment process.

The lines below added to App.js file should render an autofocused card input, ready to interact together with a “Pay” button.

// ...your imports
import { View, StyleSheet } from 'react-native';
import {
StripeProvider, CardField
} from '@stripe/stripe-react-native';
import { Button } from 'react-native-paper';
export default function App() {
return (
<StripeProvider
publishableKey="pk_test_51AROWSJX9HHJ5bycpEUP9dK39tXufyuWogSUdeweyZEty3LC7M8yc5d9NlQ96fRCVL0BlAu7Nqt4V7N5xZjJnrkp005fDiTMIr"
>
<View style={styles.container}>
<CardField
postalCodeEnabled={false}
autofocus
style={styles.cardField}
cardStyle={{
textColor: '#1c1c1c',
}}
/>
<Button>Pay</Button>
</View>
</StripeProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 8,
},
cardField: {
height: 50,
},
});
view raw cardinput.js hosted with ❤ by GitHub

You probably already realized that there is no logic to keep our card details available in state. That’s not a mistake! To make the card payment even more secure and PCI compliant, it is up to the CardField component itself, and Stripe’s React Native library, to store card details natively behind the scenes, and share them with the other Stripe functions when needed.

Processing card payment

To process your payments after pressing “Pay” button, we need to follow four easy steps. All of those steps should be done in the onPress callback of our button.

You can write code required in those steps inside your main component function in App.js file. After following all the steps, this is what your code will look like.

Step 1 - create a payment intent

Create a payment intent in the backend and fetch its secret. Simply post to your endpoint, and save the returned payment intent client secret. We will use this secret in step 3.

const fetchPaymentIntentClientSecret = async () => {
const response = await fetch(`${API_URL}/create-payment-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currency: 'usd',
items: [{ id: 'id' }],
}),
});
const { clientSecret } = await response.json();
return clientSecret;
};
const handlePayPress = async () => {
const clientSecret = await fetchPaymentIntentClientSecret();
// step 2
// step 3
// step 4
};

Step 2 - customer data

Gather customer information. In our case let’s just mock it. In a real application you would have to collect these from a user via a form.

const handlePayPress = async () => {
// step 1
const billingDetails = {
email: 'email@stripe.com',
phone: '+48888000888',
addressCity: 'Houston',
addressCountry: 'US',
addressLine1: '1459 Circle Drive',
addressLine2: 'Texas',
addressPostalCode: '77063',
};
// step 3
// step 4
};
// step 4
};

Step 3 - confirm payment

After gathering the required details, we are ready to finalize the payment. This is a step when we are confirming the payment intent created in step 1. Card information is needed in order to call this step but it will be passed automatically from the CardInput component.

To confirm the payment, you can use the useConfirmPayment hook from @stripe/stripe-react-native library. The hook returns an object with the confirmPayment method, and information about the loading state. In order to finalize the payment you need to call it with clientSecret retrieved in step 1 and payment configuration. Since we are paying with a card, the object we pass will contain the field type with the value 'Card' and the field billingDetails which contains the data we collected in step 2.

const { confirmPayment, loading } = useConfirmPayment();
const handlePayPress = async () => {
// step 1
// step 2
const { error, paymentIntent } = await confirmPayment(clientSecret, {
type: 'Card',
billingDetails,
});
// step 4
};

Step 4 - error handling

At this stage, our payment should have completed successfully. But, we cannot forget about error handling. The confirmPayment method used in the previous step returns an object with error and paymentIntent fields. The first one can be used in order to check for any error information (if one occurred). The second one is needed to check details about our succeeded payment intent.

const handlePayPress = async () => {
// step 1
// step 2
// step 3
if (error) {
// Handle error
} else if (paymentIntent) {
// Handle success
}
};

Connecting and testing everything

That’s it. Our card payments implementation is complete. The last thing to do is test it. To do that, you can open the app you created and proceed with payment using one of the test cards provided by Stripe.

You can even test error scenarios just by using different card numbers.

Summary

React Native Expo, around 70 lines of code, one additional dependency, one simple backend, and one free Stripe account - this is everything you need to implement working card payments in a mobile application. Easy, right?What’s great is that after implementing this one simple payment method it is not that hard to add more. Setting up future payments, 3D Secure, drop-in UI Payment Element with support for >10 different payment methods, Google Pay, Apple Pay, and many more features are waiting for you to use from @stripe/stripe-react-native. Probably the best place to start will be the official Stripe documentation.

‍

Need a hand with growing your mobile app? Contact us, and let’s discuss how we can use our React Native development skills to your advantage.

Insights

Learn more about Expo

cover

Mar 26Live Stream

Expo UI with SwiftUI and Jetpack Compose in practice

Exploring Expo UI with SwiftUI and Jetpack Compose, including setup challenges, architecture tradeoffs, and real-world developer insights.

cover

Dec 9Podcast

From Hobby Tool to Default: Expo's Journey and the New App Awards

Jon Samp (Expo) discusses the framework's evolution since becoming the React Native default, the death of the "eject" mentality, and the launch of the Expo App Awards.

cover

Aug 5Webinar

How to Gradually Migrate From React Native Community CLI to Expo

Learn how to migrate your React Native Community CLI app to Expo incrementally, with minimal risk. A recording of a webinar hosted by Callstack & Expo.

cover

Jan 2Podcast

Universal React Native Apps with DOM & React Server Components

Tune in to discussion with Evan Bacon, Expo Router's creator, about how DOM and React Server Components make creating React Native universal apps a breeze.

cover

Oct 11Podcast

Improving Developer Experience With Expo

In this episode, we dive into Expo's potential to reshape the future of React Native development and enhance the developer experience. Our guests are Jon Samp, Head of Product at Expo, and Cedric van Putten, a Software Engineer at Expo. Tune in to learn how Expo can help you build React Native apps faster and more efficiently.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.