React Native Payments: Stripe SDK, Apple Pay, and Google Pay Integration
Integrate Stripe payments in React Native with Apple Pay and Google Pay. Covers @stripe/stripe-react-native setup, PaymentSheet, card forms, wallet payments, and subscription billing on mobile.
Payment integration in React Native is notoriously tricky โ native payment sheet UX, Apple Pay entitlements, Google Pay configuration, PCI compliance, and subscription management across two platforms. Stripe's @stripe/stripe-react-native library wraps all of this behind a clean API that handles the native UI and security requirements.
This guide covers the complete React Native payment stack: PaymentSheet for one-time charges, Apple Pay and Google Pay, and Stripe subscription setup.
Installation
npx expo install @stripe/stripe-react-native
npx pod-install
For bare React Native:
npm install @stripe/stripe-react-native
cd ios && pod install
iOS Configuration
Add capabilities in Xcode: Signing & Capabilities โ + Capability โ Apple Pay.
<!-- ios/YourApp/AppDelegate.mm -->
<!-- No changes needed โ Stripe SDK handles Apple Pay automatically -->
<!-- ios/YourApp/Info.plist -->
<!-- No special keys needed for basic card entry -->
<!-- Apple Pay requires merchant ID configuration in Xcode -->
Android Configuration
<!-- android/app/src/main/AndroidManifest.xml -->
<application>
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
</application>
App Root Setup
// app/_layout.tsx
import { StripeProvider } from "@stripe/stripe-react-native";
export default function RootLayout() {
return (
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
merchantIdentifier="merchant.com.yourapp" // For Apple Pay
urlScheme="yourapp" // For 3DS redirects (deep linking)
>
{/* navigation */}
</StripeProvider>
);
}
One-Time Payment with PaymentSheet
PaymentSheet is Stripe's prebuilt native UI โ handles card entry, Apple Pay, Google Pay, and saved payment methods automatically.
Server: Create PaymentIntent
// app/api/payments/payment-intent/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { stripe } from "@/lib/stripe";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
const PaymentIntentSchema = z.object({
amount: z.number().int().positive(),
currency: z.string().length(3).default("usd"),
description: z.string().optional(),
orderId: z.string().optional(),
});
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await req.json();
const parsed = PaymentIntentSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.issues[0].message },
{ status: 400 }
);
}
// Ensure customer exists in Stripe
let customerId = session.user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: session.user.email!,
name: session.user.name ?? undefined,
metadata: { userId: session.user.id },
});
customerId = customer.id;
await prisma.user.update({
where: { id: session.user.id },
data: { stripeCustomerId: customerId },
});
}
// Create Ephemeral Key (lets mobile SDK manage saved payment methods)
const ephemeralKey = await stripe.ephemeralKeys.create(
{ customer: customerId },
{ apiVersion: "2024-06-20" }
);
// Create PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount: parsed.data.amount,
currency: parsed.data.currency,
customer: customerId,
setup_future_usage: "off_session", // Save card for future use
automatic_payment_methods: { enabled: true },
description: parsed.data.description,
metadata: {
userId: session.user.id,
orderId: parsed.data.orderId ?? "",
},
});
return NextResponse.json({
paymentIntent: paymentIntent.client_secret,
ephemeralKey: ephemeralKey.secret,
customer: customerId,
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
});
}
Client: PaymentSheet Component
// components/payments/checkout-button.tsx
import { useState, useCallback } from "react";
import { Alert, Pressable, Text, StyleSheet, ActivityIndicator } from "react-native";
import {
useStripe,
PaymentSheetError,
} from "@stripe/stripe-react-native";
interface CheckoutButtonProps {
amount: number; // in cents
currency?: string;
description?: string;
orderId?: string;
onSuccess: (paymentIntentId?: string) => void;
onError?: (error: string) => void;
label?: string;
}
export function CheckoutButton({
amount,
currency = "usd",
description,
orderId,
onSuccess,
onError,
label = "Pay Now",
}: CheckoutButtonProps) {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
const [loading, setLoading] = useState(false);
const handlePayment = useCallback(async () => {
setLoading(true);
try {
// 1. Fetch PaymentIntent from server
const response = await fetch("/api/payments/payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount, currency, description, orderId }),
});
if (!response.ok) {
throw new Error("Failed to create payment intent");
}
const { paymentIntent, ephemeralKey, customer } = await response.json();
// 2. Initialize PaymentSheet with native Stripe UI
const { error: initError } = await initPaymentSheet({
merchantDisplayName: "Acme Corp",
customerId: customer,
customerEphemeralKeySecret: ephemeralKey,
paymentIntentClientSecret: paymentIntent,
allowsDelayedPaymentMethods: false,
defaultBillingDetails: {
// Pre-fill if known
},
appearance: {
// Match your app's design
colors: {
primary: "#3b82f6",
background: "#ffffff",
componentBackground: "#f9fafb",
componentBorder: "#e5e7eb",
componentDivider: "#f3f4f6",
primaryText: "#111827",
secondaryText: "#6b7280",
componentText: "#111827",
placeholderText: "#9ca3af",
icon: "#6b7280",
error: "#ef4444",
},
shapes: {
borderRadius: 8,
borderWidth: 1,
},
primaryButton: {
colors: {
background: "#3b82f6",
text: "#ffffff",
},
shapes: {
borderRadius: 8,
},
},
},
applePay: {
merchantCountryCode: "US",
},
googlePay: {
merchantCountryCode: "US",
testEnv: process.env.NODE_ENV !== "production",
},
});
if (initError) {
throw new Error(initError.message);
}
// 3. Present native PaymentSheet UI
const { error: presentError } = await presentPaymentSheet();
if (presentError) {
if (presentError.code === PaymentSheetError.Canceled) {
// User cancelled โ not an error
return;
}
throw new Error(presentError.message);
}
// 4. Success!
onSuccess();
} catch (err) {
const message = err instanceof Error ? err.message : "Payment failed";
onError?.(message);
Alert.alert("Payment Failed", message);
} finally {
setLoading(false);
}
}, [amount, currency, description, orderId, onSuccess, onError, initPaymentSheet, presentPaymentSheet]);
return (
<Pressable
onPress={handlePayment}
disabled={loading}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
loading && styles.buttonDisabled,
]}
>
{loading ? (
<ActivityIndicator color="white" size="small" />
) : (
<Text style={styles.buttonText}>
{label} ยท {formatAmount(amount, currency)}
</Text>
)}
</Pressable>
);
}
function formatAmount(amountCents: number, currency: string): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
}).format(amountCents / 100);
}
const styles = StyleSheet.create({
button: {
backgroundColor: "#3b82f6",
borderRadius: 8,
paddingVertical: 14,
paddingHorizontal: 24,
alignItems: "center",
justifyContent: "center",
minHeight: 50,
},
buttonPressed: { opacity: 0.9, transform: [{ scale: 0.98 }] },
buttonDisabled: { opacity: 0.6 },
buttonText: { color: "white", fontSize: 16, fontWeight: "700" },
});
๐ณ Fintech That Passes Compliance โ Not Just Demos
Payment integrations, KYC/AML flows, trading APIs, and regulatory compliance โ we build fintech that survives real audits, not just product demos.
- PCI DSS, PSD2, FCA, GDPR-aware architecture
- Stripe, Plaid, Rapyd, OpenBanking integrations
- Real-time transaction monitoring and fraud flags
- UK/EU/US compliance requirements mapped from day one
Subscription Setup
// app/api/payments/setup-intent/route.ts
// For saving a payment method without charging (subscription setup)
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { stripe } from "@/lib/stripe";
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user?.stripeCustomerId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const setupIntent = await stripe.setupIntents.create({
customer: session.user.stripeCustomerId,
payment_method_types: ["card"],
usage: "off_session",
});
const ephemeralKey = await stripe.ephemeralKeys.create(
{ customer: session.user.stripeCustomerId },
{ apiVersion: "2024-06-20" }
);
return NextResponse.json({
setupIntent: setupIntent.client_secret,
ephemeralKey: ephemeralKey.secret,
customer: session.user.stripeCustomerId,
});
}
// components/payments/payment-method-setup.tsx
import { useStripe } from "@stripe/stripe-react-native";
import { useState } from "react";
import { Pressable, Text, StyleSheet, Alert } from "react-native";
interface PaymentMethodSetupProps {
onSuccess: () => void;
}
export function PaymentMethodSetup({ onSuccess }: PaymentMethodSetupProps) {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
const [loading, setLoading] = useState(false);
const handleSetup = async () => {
setLoading(true);
try {
const res = await fetch("/api/payments/setup-intent", {
method: "POST",
});
const { setupIntent, ephemeralKey, customer } = await res.json();
const { error: initError } = await initPaymentSheet({
merchantDisplayName: "Acme Corp",
customerId: customer,
customerEphemeralKeySecret: ephemeralKey,
setupIntentClientSecret: setupIntent,
});
if (initError) throw new Error(initError.message);
const { error: presentError } = await presentPaymentSheet();
if (presentError) throw new Error(presentError.message);
onSuccess();
} catch (err) {
Alert.alert("Error", err instanceof Error ? err.message : "Setup failed");
} finally {
setLoading(false);
}
};
return (
<Pressable onPress={handleSetup} disabled={loading} style={styles.button}>
<Text style={styles.text}>
{loading ? "Setting up..." : "Add Payment Method"}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: "#111827",
borderRadius: 8,
padding: 14,
alignItems: "center",
},
text: { color: "white", fontWeight: "600", fontSize: 15 },
});
Webhook: Confirm Payment Server-Side
Never trust the client for payment confirmation โ always verify via webhook:
// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";
import { prisma } from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import type Stripe from "stripe";
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "payment_intent.succeeded": {
const pi = event.data.object as Stripe.PaymentIntent;
const orderId = pi.metadata.orderId;
if (orderId) {
await prisma.order.update({
where: { id: orderId },
data: {
paymentStatus: "paid",
paymentIntentId: pi.id,
paidAt: new Date(),
},
});
// Trigger fulfillment
await triggerOrderFulfillment(orderId);
}
break;
}
case "payment_intent.payment_failed": {
const pi = event.data.object as Stripe.PaymentIntent;
const orderId = pi.metadata.orderId;
if (orderId) {
await prisma.order.update({
where: { id: orderId },
data: { paymentStatus: "failed" },
});
}
break;
}
case "customer.subscription.created":
case "customer.subscription.updated": {
const sub = event.data.object as Stripe.Subscription;
await syncSubscription(sub);
break;
}
}
return NextResponse.json({ received: true });
}
๐ฆ Trading Systems, Payment Rails, and Financial APIs
From algorithmic trading platforms to neobank backends โ Viprasol has built the full spectrum of fintech. Senior engineers, no junior handoffs, verified track record.
- MT4/MT5 EA development for prop firms and hedge funds
- Custom payment gateway and wallet systems
- Regulatory reporting automation (MiFID, EMIR)
- Free fintech architecture consultation
Saved Payment Methods Display
// components/payments/saved-cards.tsx
import { useEffect, useState } from "react";
import { View, Text, FlatList, Pressable, StyleSheet } from "react-native";
import { CreditCard, Trash2 } from "lucide-react-native";
interface PaymentMethod {
id: string;
brand: string;
last4: string;
expMonth: number;
expYear: number;
isDefault: boolean;
}
export function SavedCards() {
const [methods, setMethods] = useState<PaymentMethod[]>([]);
useEffect(() => {
fetch("/api/payments/methods")
.then((r) => r.json())
.then((d) => setMethods(d.paymentMethods));
}, []);
const BRAND_ICONS: Record<string, string> = {
visa: "๐ณ",
mastercard: "๐ณ",
amex: "๐ณ",
discover: "๐ณ",
};
return (
<View>
<Text style={styles.heading}>Payment Methods</Text>
<FlatList
data={methods}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.icon}>
{BRAND_ICONS[item.brand.toLowerCase()] ?? "๐ณ"}
</Text>
<View style={styles.info}>
<Text style={styles.brand}>
{item.brand.charAt(0).toUpperCase() + item.brand.slice(1)} โขโขโขโข
{item.last4}
</Text>
<Text style={styles.expiry}>
Expires {item.expMonth}/{item.expYear}
</Text>
</View>
{item.isDefault && (
<View style={styles.defaultBadge}>
<Text style={styles.defaultText}>Default</Text>
</View>
)}
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
heading: { fontSize: 16, fontWeight: "600", color: "#111827", marginBottom: 12 },
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "white",
borderRadius: 10,
padding: 14,
marginBottom: 8,
borderWidth: 1,
borderColor: "#e5e7eb",
},
icon: { fontSize: 20, marginRight: 12 },
info: { flex: 1 },
brand: { fontSize: 15, fontWeight: "500", color: "#111827" },
expiry: { fontSize: 13, color: "#6b7280", marginTop: 2 },
defaultBadge: {
backgroundColor: "#dbeafe",
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 3,
},
defaultText: { fontSize: 11, fontWeight: "600", color: "#1d4ed8" },
});
Cost and Timeline Estimates
| Scope | Team | Timeline | Cost Range |
|---|---|---|---|
| Basic PaymentSheet (card only) | 1 dev | 1โ2 days | $400โ800 |
| PaymentSheet + Apple Pay + Google Pay | 1 dev | 3โ5 days | $1,000โ2,000 |
| Full payments (subscriptions + saved methods + webhooks) | 1โ2 devs | 2โ3 weeks | $5,000โ10,000 |
| Marketplace payments (Stripe Connect) | 2 devs | 3โ5 weeks | $10,000โ22,000 |
Apple Pay / Google Pay requirements:
- Apple Pay: Requires Apple Developer paid account, merchant ID, and valid HTTPS domain. No additional Stripe fees.
- Google Pay: Available on all Android devices with Google Pay app. Production requires Google Pay API approval.
See Also
- Stripe Webhook Handling with Idempotency
- Stripe Connect Platform Payments
- SaaS Team Billing and Per-Seat Pricing
- SaaS Dunning Management and Failed Payment Recovery
- React Native Gesture Handler
Working With Viprasol
Mobile payment integration has more moving parts than web โ Apple Pay merchant certificates, Google Pay approval, 3DS redirects with deep linking, and PCI compliance on two platforms. Our team has shipped production payment flows in React Native apps for e-commerce, SaaS subscription, and marketplace platforms.
What we deliver:
- @stripe/stripe-react-native setup with Apple Pay and Google Pay
- PaymentSheet and SetupSheet integration for one-time and subscription flows
- Webhook server for authoritative payment confirmation
- Saved payment method management UI
- Apple Pay merchant certificate and Google Pay production approval guidance
Talk to our team about your mobile payment integration โ
Or explore our fintech and trading software services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Building Fintech Solutions?
Payment integrations, trading systems, compliance โ we build fintech that passes audits.
Free consultation โข No commitment โข Response within 24 hours
Building fintech or trading infrastructure?
Viprasol delivers custom trading software โ MT4/MT5 EAs, TradingView indicators, backtesting frameworks, and real-time execution systems. Trusted by traders and prop firms worldwide.