Quick path
Install @stripe/stripe-react-native, create a backend endpoint that returns PaymentIntent client secrets, wrap your app in <StripeProvider>, present PaymentSheet on checkout, handle webhooks server-side. 1–2 hours on a clean project.
Stripe or IAP? The 2-second rule
Apple’s rule is simpler than most blog posts make it. Ask: “Is the thing being sold consumed inside my app?”
- Yes (digital): pro features, unlock levels, subscription tier, in-app coins. Must use IAP.
- No (physical or out-of-app service): products shipped to the user, booking an Uber ride, paying a marketplace seller, food delivery, event tickets, personal training. Use Stripe.
The 30% tax only applies to digital. Use Stripe whenever you legally can.
Prerequisites
- Stripe account with a test mode key and a plan to complete the live activation checklist before launch
- A backend endpoint (Supabase Edge Functions, Expo API Routes, or your own server)
- Expo project using EAS Build — payments do not run in Expo Go
- An Apple Merchant ID if you plan to support Apple Pay
Step 1: install and configure
npx expo install @stripe/stripe-react-native
# app.json plugins:
# "plugins": [
# ["@stripe/stripe-react-native", {
# "merchantIdentifier": "merchant.com.yourapp",
# "enableGooglePay": true
# }]
# ]
# Create a dev build:
eas build --profile development --platform allStep 2: wrap your app in StripeProvider
// app/_layout.tsx (Expo Router):
import { StripeProvider } from '@stripe/stripe-react-native';
import { Slot } from 'expo-router';
export default function RootLayout() {
return (
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PK!}
merchantIdentifier="merchant.com.yourapp"
>
<Slot />
</StripeProvider>
);
}Use the publishable key (pk_test_...). The secret key never appears in the client.
Step 3: backend endpoint to create PaymentIntents
// Supabase Edge Function or Node API:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const { amount, userId } = await req.json();
const customer = await stripe.customers.create({
metadata: { userId },
});
const paymentIntent =
await stripe.paymentIntents.create({
amount, // cents
currency: 'usd',
customer: customer.id,
automatic_payment_methods: { enabled: true },
});
return Response.json({
clientSecret: paymentIntent.client_secret,
customerId: customer.id,
ephemeralKey: (await stripe.ephemeralKeys.create(
{ customer: customer.id },
{ apiVersion: '2024-12-18.acacia' }
)).secret,
});
}Step 4: present PaymentSheet on checkout
import { useStripe } from '@stripe/stripe-react-native';
const { initPaymentSheet, presentPaymentSheet } =
useStripe();
async function onCheckout() {
const res = await fetch('/api/payment-intent', {
method: 'POST',
body: JSON.stringify({ amount: 2999, userId }),
});
const data = await res.json();
await initPaymentSheet({
paymentIntentClientSecret: data.clientSecret,
customerId: data.customerId,
customerEphemeralKeySecret: data.ephemeralKey,
merchantDisplayName: 'Your Brand',
applePay: { merchantCountryCode: 'US' },
googlePay: {
merchantCountryCode: 'US',
testEnv: __DEV__,
},
});
const { error } = await presentPaymentSheet();
if (!error) {
// Payment succeeded — navigate to success screen.
}
}PaymentSheet handles card entry, Apple Pay, Google Pay, saved cards, and 3D Secure. You write the success handler; everything else is Stripe.
Step 5: webhooks for order reliability
Never trust the client to confirm payment. Stripe sends webhooks to a URL you expose; your backend is the source of truth for order status.
payment_intent.succeeded— mark the order paid, trigger fulfillment.payment_intent.payment_failed— notify the customer, allow retry.charge.refunded— update order status.- Verify the Stripe signature on every webhook —
stripe.webhooks.constructEvent.
Testing
- Use
4242 4242 4242 4242as the test card — any future date, any CVC. - Test 3D Secure with
4000 0027 6000 3184. - Apple Pay sandbox requires a real Apple ID signed into a physical device.
- Google Pay in test mode uses Stripe test cards on Android devices signed into Google Pay.
Common gotchas
- Forgetting the merchant identifier in app.json → Apple Pay button never appears.
- Mixing test and live keys between client and backend. Both must match.
- Trusting the client to mark orders paid. Always wait for the webhook.
- Not saving the Stripe Customer ID. Required for saved cards and subscriptions.
- Forgetting to enable Apple Pay in the Xcode capabilities. Simple to miss; breaks silently.
Faster path: scaffold it in the prompt
Apps generated via ShipNative can include the Stripe client-side plumbing if you describe your monetization in the initial prompt — e.g., “Checkout via Stripe PaymentSheet with Apple Pay.” You still wire up the backend endpoints and webhooks yourself, but the RN-side code ships pre-wired and tested.