Back to Blog
๐Ÿ’ฐFintech

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
March 20, 2027
13 min read

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

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.

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 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.

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.