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.