Back to Blog

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.

Viprasol Tech Team
13 min read
Updated 2027

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 });
}
React Native - React Native Payments: Stripe SDK, Apple Pay, and Google Pay Integration

🏦 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

ScopeTeamTimelineCost Range
Basic PaymentSheet (card only)1 dev1–2 days$400–800
PaymentSheet + Apple Pay + Google Pay1 dev3–5 days$1,000–2,000
Full payments (subscriptions + saved methods + webhooks)1–2 devs2–3 weeks$5,000–10,000
Marketplace payments (Stripe Connect)2 devs3–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


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.

React NativeStripePaymentsApple PayGoogle PayTypeScriptMobile
Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Building Fintech Solutions?

Payment integrations, trading systems, compliance — we build fintech that passes audits.

Free consultation • No commitment • Response within 24 hours

Viprasol · Trading Software

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.