I've wired up Stripe in two production React Native apps — FanGenie and SplitMart — both now live on the App Store. This guide covers the path that actually works in 2025, not the outdated tutorials that still use the old Elements API or manual card field handling.
Setup: The Right Packages
Use @stripe/stripe-react-native. Don't use the older stripe-react-native-sdk or roll your own WebView approach. The official package gives you native 3DS, Apple Pay, and Google Pay out of the box.
npx expo install @stripe/stripe-react-nativeWrap your app root with the StripeProvider:
import { StripeProvider } from '@stripe/stripe-react-native';
export default function App() {
return (
<StripeProvider publishableKey={process.env.EXPO_PUBLIC_STRIPE_KEY!}>
<RootNavigator />
</StripeProvider>
);
}PaymentSheet: The Only Flow You Need
Forget building a custom card form. Stripe's PaymentSheet handles card entry, Apple Pay, Google Pay, saved cards, and 3DS in one pre-built UI. PCI compliance is Stripe's problem, not yours. Here's the complete flow:
import { useStripe } from '@stripe/stripe-react-native';
export function CheckoutButton({ amount }: { amount: number }) {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
async function handlePayment() {
// 1. Create PaymentIntent on your backend
const { paymentIntent, ephemeralKey, customer } = await fetch(
'/api/create-payment-intent',
{ method: 'POST', body: JSON.stringify({ amount }) }
).then(r => r.json());
// 2. Init the sheet
const { error: initError } = await initPaymentSheet({
merchantDisplayName: 'Your App',
customerId: customer,
customerEphemeralKeySecret: ephemeralKey,
paymentIntentClientSecret: paymentIntent,
allowsDelayedPaymentMethods: false,
});
if (initError) return;
// 3. Present — handles everything including 3DS
const { error } = await presentPaymentSheet();
if (!error) {
// Payment confirmed — update your UI
}
}
return <Button onPress={handlePayment} title="Pay Now" />;
}The Backend: Node.js PaymentIntent
Your backend needs to create the PaymentIntent and return the three secrets. Never do this client-side — your secret key would be exposed.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
app.post('/api/create-payment-intent', async (req, res) => {
const { amount } = req.body;
const customer = await stripe.customers.create();
const ephemeralKey = await stripe.ephemeralKeys.create(
{ customer: customer.id },
{ apiVersion: '2024-06-20' }
);
const paymentIntent = await stripe.paymentIntents.create({
amount, // in cents
currency: 'usd',
customer: customer.id,
automatic_payment_methods: { enabled: true },
});
res.json({
paymentIntent: paymentIntent.client_secret,
ephemeralKey: ephemeralKey.secret,
customer: customer.id,
});
});Webhooks: The Part Everyone Skips
Don't rely on the client to confirm payment success. The app can close, crash, or lose network after the PaymentSheet resolves. Stripe webhooks are your source of truth. Listen for payment_intent.succeeded and update your database there.
Critical: Always verify payment server-side via webhooks before granting access to paid features. Client-side confirmation can be spoofed or interrupted.
- payment_intent.succeeded → unlock premium features
- customer.subscription.created → handle recurring billing
- invoice.payment_failed → notify user to update card
- charge.dispute.created → flag for manual review
Apple Pay & Google Pay
Apple Pay is enabled automatically if you pass the merchantDisplayName to initPaymentSheet and add the Apple Pay entitlement. For Expo, add the Stripe plugin to your app.config.ts and run a new build — no native code changes needed.
RevenueCat: When Stripe Isn't the Right Choice
If you're building subscriptions, consider RevenueCat instead of Stripe. RevenueCat handles the App Store and Play Store in-app purchase APIs (which Apple requires for digital goods sold through iOS apps). It also unifies analytics, paywalls, and receipt validation across platforms. For FanGenie's subscription tier, I used RevenueCat and saved two weeks of native billing code.
Common Mistakes
- 1.Using test keys in production (yes, this happens — use environment variables)
- 2.Not handling 3DS — some European cards require it, your app will silently fail without it
- 3.Confirming payment on the client only — always verify with webhooks
- 4.Forgetting to add STRIPE_WEBHOOK_SECRET to production environment
- 5.Not setting currency correctly — amount is always in the smallest currency unit (cents)