Back to Blog

Mobile Payment Integration: Apple Pay, Google Pay, and Stripe Mobile SDK in 2026

Integrate Apple Pay, Google Pay, and Stripe Mobile SDK in React Native. In-app purchases with RevenueCat, subscription management, payment sheet implementation, and 3DS handling.

Viprasol Tech Team
August 3, 2026
13 min read

Mobile Payment Integration: Apple Pay, Google Pay, and Stripe Mobile SDK in 2026

Payments on mobile are fundamentally different from web. You can't just render a Stripe Elements form in a WebView and call it done. Native payment sheets (Apple Pay, Google Pay) dramatically increase conversion โ€” often 30โ€“50% over card entry forms โ€” because users don't have to enter 16 digits on a small keyboard. For subscriptions, RevenueCat handles App Store and Play Store billing so you don't have to implement it yourself.

This post covers the full mobile payment stack in React Native: Stripe's @stripe/stripe-react-native for card and wallet payments, and RevenueCat for in-app subscriptions.


Payment Architecture: Native vs. WebView

ApproachConversionMaintenanceApple Pay / Google PayApp Store Rules
Native Stripe SDKโœ… BestMediumโœ… YesCompliant
WebView โ†’ Stripe.jsโš ๏ธ LowerLowโŒ Noโš ๏ธ Gray area
RevenueCat (subscriptions)โœ… BestLowโœ… Yes (via StoreKit/Billing)โœ… Required for IAP
Custom payment sheetโš ๏ธ VariableHighโœ… PossibleCompliant

Important: For in-app subscriptions and digital goods on iOS, App Store rules require using Apple's StoreKit (in-app purchase) โ€” you cannot use Stripe. For physical goods, services, and B2B SaaS, Stripe is fine.


Stripe React Native: Setup

npx expo install @stripe/stripe-react-native
# iOS: cd ios && pod install
// App.tsx โ€” wrap app with StripeProvider
import { StripeProvider } from '@stripe/stripe-react-native';

export default function App() {
  return (
    <StripeProvider
      publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
      merchantIdentifier="merchant.com.myapp"  // Required for Apple Pay
      urlScheme="myapp"                          // For 3DS redirect back
    >
      <NavigationContainer>
        <RootNavigator />
      </NavigationContainer>
    </StripeProvider>
  );
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Payment Sheet: One-Time Payments

The Payment Sheet is Stripe's pre-built UI โ€” handles card entry, Apple Pay, Google Pay, and 3DS in a single bottom sheet:

// src/api/payments.ts โ€” create PaymentIntent server-side
export async function createPaymentIntent(
  amount: number,  // cents
  currency: string,
  customerId: string,
): Promise<{ clientSecret: string; ephemeralKey: string; customer: string }> {
  const response = await fetch(`${API_BASE}/payments/intent`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${await getAuthToken()}`,
    },
    body: JSON.stringify({ amount, currency, customerId }),
  });

  if (!response.ok) {
    throw new Error('Failed to create payment intent');
  }

  return response.json();
}
// src/screens/CheckoutScreen.tsx
import { useStripe } from '@stripe/stripe-react-native';
import { useCallback, useState } from 'react';

export function CheckoutScreen({ order }: { order: Order }) {
  const { initPaymentSheet, presentPaymentSheet } = useStripe();
  const [loading, setLoading] = useState(false);

  const handleCheckout = useCallback(async () => {
    setLoading(true);

    try {
      // 1. Create PaymentIntent on your server
      const { clientSecret, ephemeralKey, customer } = await createPaymentIntent(
        order.totalCents,
        order.currency,
        order.customerId,
      );

      // 2. Initialize Payment Sheet
      const { error: initError } = await initPaymentSheet({
        merchantDisplayName: 'MyApp Inc.',
        customerId: customer,
        customerEphemeralKeySecret: ephemeralKey,
        paymentIntentClientSecret: clientSecret,

        // Enable Apple Pay / Google Pay
        applePay: {
          merchantCountryCode: 'US',
        },
        googlePay: {
          merchantCountryCode: 'US',
          testEnv: __DEV__,
        },

        // Show billing address collection
        billingDetailsCollectionConfiguration: {
          name: 'automatic',
          email: 'automatic',
        },

        // Allow saving payment method for future use
        setupFutureUsage: 'off_session',

        // Appearance customization
        appearance: {
          colors: {
            primary: '#2563EB',
            background: '#FFFFFF',
            componentBackground: '#F9FAFB',
            primaryText: '#111827',
            secondaryText: '#6B7280',
          },
          shapes: {
            borderRadius: 12,
            borderWidth: 1,
          },
        },
      });

      if (initError) {
        Alert.alert('Error', initError.message);
        return;
      }

      // 3. Present Payment Sheet
      const { error: presentError } = await presentPaymentSheet();

      if (presentError) {
        if (presentError.code !== 'Canceled') {
          Alert.alert('Payment failed', presentError.message);
        }
        return;
      }

      // 4. Payment succeeded โ€” verify on server and fulfill order
      await fulfillOrder(order.id);
      navigation.replace('OrderConfirmation', { orderId: order.id });

    } catch (err) {
      Alert.alert('Error', 'Something went wrong. Please try again.');
    } finally {
      setLoading(false);
    }
  }, [order]);

  return (
    <View style={styles.container}>
      <OrderSummary order={order} />

      <Pressable
        style={[styles.button, loading && styles.buttonDisabled]}
        onPress={handleCheckout}
        disabled={loading}
      >
        {loading ? (
          <ActivityIndicator color="#fff" />
        ) : (
          <Text style={styles.buttonText}>
            Pay ${(order.totalCents / 100).toFixed(2)}
          </Text>
        )}
      </Pressable>
    </View>
  );
}

Apple Pay: Native Integration

For a fully native Apple Pay button (without the Payment Sheet wrapper):

// src/components/ApplePayButton.tsx
import { ApplePayButton, useApplePay } from '@stripe/stripe-react-native';

export function NativeApplePayButton({ order }: { order: Order }) {
  const { isApplePaySupported, presentApplePay, confirmApplePayPayment } = useApplePay();

  const handleApplePay = async () => {
    if (!isApplePaySupported) return;

    const { error } = await presentApplePay({
      cartItems: [
        ...order.items.map((item) => ({
          label: item.name,
          amount: (item.price * item.quantity / 100).toFixed(2),
          type: 'final' as const,
        })),
        {
          label: 'Tax',
          amount: (order.taxCents / 100).toFixed(2),
          type: 'final',
        },
        {
          label: 'MyApp Inc.',   // Must match merchant name
          amount: (order.totalCents / 100).toFixed(2),
          type: 'final',
        },
      ],
      country: 'US',
      currency: order.currency.toUpperCase(),
      shippingMethods: [],
      requiredBillingContactFields: ['emailAddress'],
    });

    if (error) {
      Alert.alert('Apple Pay Error', error.message);
      return;
    }

    // Get client secret from server and confirm
    const { clientSecret } = await createPaymentIntent(
      order.totalCents, order.currency, order.customerId,
    );

    const { error: confirmError } = await confirmApplePayPayment(clientSecret);

    if (!confirmError) {
      await fulfillOrder(order.id);
      navigation.replace('OrderConfirmation', { orderId: order.id });
    }
  };

  if (!isApplePaySupported) return null;

  return (
    <ApplePayButton
      onPress={handleApplePay}
      type="buy"
      buttonStyle="black"
      borderRadius={8}
      style={{ width: '100%', height: 50 }}
    />
  );
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

RevenueCat: In-App Subscriptions

For digital subscriptions sold through the App Store / Play Store, use RevenueCat โ€” it abstracts StoreKit 2 (iOS) and Google Play Billing Library 5 (Android) behind a unified API:

npx expo install react-native-purchases
// src/lib/revenuecat.ts
import Purchases, { LOG_LEVEL, CustomerInfo } from 'react-native-purchases';
import { Platform } from 'react-native';

export async function initRevenueCat(userId: string): Promise<void> {
  const apiKey = Platform.select({
    ios:     process.env.EXPO_PUBLIC_RC_IOS_KEY!,
    android: process.env.EXPO_PUBLIC_RC_ANDROID_KEY!,
  })!;

  await Purchases.configure({ apiKey, appUserID: userId });

  if (__DEV__) {
    Purchases.setLogLevel(LOG_LEVEL.DEBUG);
  }
}

export interface SubscriptionOffering {
  identifier: string;
  monthly?: { price: number; priceString: string; productId: string };
  annual?: { price: number; priceString: string; productId: string };
}

export async function getOfferings(): Promise<SubscriptionOffering[]> {
  const offerings = await Purchases.getOfferings();

  return Object.values(offerings.all).map((offering) => ({
    identifier: offering.identifier,
    monthly: offering.monthly ? {
      price: offering.monthly.product.price,
      priceString: offering.monthly.product.priceString,
      productId: offering.monthly.product.identifier,
    } : undefined,
    annual: offering.annual ? {
      price: offering.annual.product.price,
      priceString: offering.annual.product.priceString,
      productId: offering.annual.product.identifier,
    } : undefined,
  }));
}

export async function purchaseSubscription(
  packageIdentifier: string,
  offeringIdentifier: string,
): Promise<CustomerInfo> {
  const offerings = await Purchases.getOfferings();
  const offering = offerings.all[offeringIdentifier];
  const pkg = offering.availablePackages.find(
    (p) => p.identifier === packageIdentifier,
  );

  if (!pkg) throw new Error('Package not found');

  const { customerInfo } = await Purchases.purchasePackage(pkg);
  return customerInfo;
}

export async function restorePurchases(): Promise<CustomerInfo> {
  return Purchases.restorePurchases();
}

export async function hasActiveSubscription(entitlement: string): Promise<boolean> {
  const info = await Purchases.getCustomerInfo();
  return info.entitlements.active[entitlement] !== undefined;
}
// src/screens/SubscriptionScreen.tsx
import { useEffect, useState } from 'react';
import { getOfferings, purchaseSubscription, SubscriptionOffering } from '@/lib/revenuecat';

export function SubscriptionScreen() {
  const [offerings, setOfferings] = useState<SubscriptionOffering[]>([]);
  const [selected, setSelected] = useState<'monthly' | 'annual'>('annual');
  const [purchasing, setPurchasing] = useState(false);

  useEffect(() => {
    getOfferings().then(setOfferings);
  }, []);

  const handlePurchase = async () => {
    const offering = offerings[0];
    if (!offering) return;

    const pkg = selected === 'annual' ? offering.annual : offering.monthly;
    if (!pkg) return;

    setPurchasing(true);
    try {
      await purchaseSubscription(
        selected === 'annual' ? '$rc_annual' : '$rc_monthly',
        offering.identifier,
      );
      // Update app state โ€” user is now subscribed
      navigation.replace('Dashboard');
    } catch (err: any) {
      if (!err.userCancelled) {
        Alert.alert('Purchase Failed', err.message);
      }
    } finally {
      setPurchasing(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Choose your plan</Text>

      {/* Plan toggle */}
      <View style={styles.toggle}>
        {(['monthly', 'annual'] as const).map((plan) => (
          <Pressable
            key={plan}
            style={[styles.toggleOption, selected === plan && styles.toggleSelected]}
            onPress={() => setSelected(plan)}
          >
            <Text style={selected === plan ? styles.toggleTextSelected : styles.toggleText}>
              {plan === 'monthly' ? 'Monthly' : 'Annual (save 40%)'}
            </Text>
          </Pressable>
        ))}
      </View>

      {/* Price display */}
      {offerings[0] && (
        <View style={styles.priceBox}>
          <Text style={styles.price}>
            {selected === 'annual'
              ? offerings[0].annual?.priceString
              : offerings[0].monthly?.priceString}
          </Text>
          <Text style={styles.priceSubtext}>
            {selected === 'annual' ? 'per year' : 'per month'}
          </Text>
        </View>
      )}

      <Pressable
        style={[styles.ctaButton, purchasing && styles.ctaDisabled]}
        onPress={handlePurchase}
        disabled={purchasing}
      >
        {purchasing ? (
          <ActivityIndicator color="#fff" />
        ) : (
          <Text style={styles.ctaText}>Start Free Trial</Text>
        )}
      </Pressable>

      <Text style={styles.legal}>
        Free 7-day trial. Cancel anytime. Payment charged through{' '}
        {Platform.OS === 'ios' ? 'App Store' : 'Google Play'}.
      </Text>
    </View>
  );
}

Server-Side: Handling RevenueCat Webhooks

// src/api/webhooks/revenuecat.ts
export async function revenueCatWebhookHandler(
  req: FastifyRequest,
  reply: FastifyReply,
): Promise<void> {
  const event = req.body as RevenueCatWebhookEvent;

  // Verify webhook secret
  const secret = req.headers['authorization'];
  if (secret !== `Bearer ${process.env.REVENUECAT_WEBHOOK_SECRET}`) {
    return reply.status(401).send();
  }

  reply.status(200).send(); // Respond immediately

  const userId = event.app_user_id;
  const entitlement = 'premium';

  switch (event.type) {
    case 'INITIAL_PURCHASE':
    case 'RENEWAL':
      await db.query(
        `UPDATE users SET subscription_status = 'active', subscription_expires_at = $1
         WHERE id = $2`,
        [new Date(event.expiration_at_ms), userId],
      );
      break;

    case 'CANCELLATION':
    case 'EXPIRATION':
      await db.query(
        `UPDATE users SET subscription_status = 'cancelled' WHERE id = $1`,
        [userId],
      );
      break;

    case 'BILLING_ISSUE':
      await sendEmail({ to: event.subscriber_attributes?.email, template: 'billing_issue' });
      break;
  }
}

Payment Security Checklist

## Mobile Payment Security

### Client-side
- [ ] Never log payment tokens or card data
- [ ] Use Stripe SDK โ€” never handle raw card data
- [ ] Certificate pinning for production API calls
- [ ] Store tokens in Keychain (iOS) / Keystore (Android), not AsyncStorage

### Server-side
- [ ] Verify PaymentIntent on server before fulfilling order (don't trust client)
- [ ] Idempotency keys on all Stripe API calls
- [ ] Webhook signature verification (Stripe-Signature header)
- [ ] RevenueCat webhook secret verification

### App Store compliance
- [ ] Digital goods sold through App Store use StoreKit (IAP)
- [ ] Physical goods can use Stripe
- [ ] Subscription management links to App Store manage subscriptions

Working With Viprasol

Our mobile team integrates Stripe and RevenueCat into React Native apps โ€” from Apple Pay payment sheets to subscription management and webhook processing.

What we deliver:

  • Stripe Payment Sheet integration with Apple Pay and Google Pay
  • RevenueCat setup for iOS/Android in-app subscriptions
  • Server-side PaymentIntent creation and webhook handlers
  • App Store and Play Store subscription compliance review
  • 3DS handling and failed payment recovery flows

โ†’ Discuss your mobile payments integration โ†’ Mobile development services


See Also

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

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.