Introduction

Mobile wallets have transformed payment expectations. Users who have set up Apple Pay or Google Pay expect to pay with a single tap or Face ID authentication. Asking them to type card numbers feels archaic.

For merchants, mobile wallets offer higher conversion rates and reduced fraud. The tokenization approach means you never handle actual card numbers, simplifying PCI compliance.

This guide covers implementing Apple Pay and Google Pay using Stripe as the payment processor. Stripe provides a unified API that works across both platforms, simplifying development significantly. We will walk through merchant setup, React Native integration, and handling common scenarios like subscriptions and refunds.

Understanding Mobile Payment Flows

tanding Mobile Payment Flows Infographic](/images/mobile-payment-integration-apple-pay-google-pay-understanding-mobile-payment-flows.webp)

Before implementing, understand how mobile payments work:

Token-Based Payments

When a user adds a card to Apple Pay or Google Pay, the wallet creates a device-specific token. This token can only be used by that device and is useless if intercepted.

User's Card → Wallet (Apple/Google) → Device Token

Your App → Payment Processor → Network Token → Card Network → Bank

Payment Flow

  1. User initiates payment in your app
  2. Your app requests a payment token from the wallet
  3. User authenticates (Face ID, fingerprint, PIN)
  4. Wallet returns encrypted payment token
  5. Your app sends token to your server
  6. Server sends token to payment processor (Stripe)
  7. Processor decrypts and processes payment
  8. Result returned to app

Your server never sees the actual card number. You receive a confirmation that payment succeeded or failed.

Me

Merchant Setup Infographic rchant Setup

Apple Pay Setup

1. Apple Developer Account Configuration

In the Apple Developer portal:

  • Navigate to Certificates, Identifiers & Profiles
  • Under Identifiers, select Merchant IDs
  • Create a new Merchant ID (e.g., merchant.com.yourcompany.yourapp)

2. Create Payment Processing Certificate

Apple Pay requires a certificate to encrypt payment data:

  • In Merchant IDs, select your merchant ID
  • Under Apple Pay Payment Processing Certificate, click Create Certificate
  • Upload a Certificate Signing Request (CSR)

For Stripe, you skip this step since Stripe handles certificate management.

3. Enable Apple Pay in Xcode

In your Xcode project:

  • Select your target
  • Go to Signing & Capabilities
  • Click + Capability and add Apple Pay
  • Enable your Merchant ID

4. Stripe Dashboard Configuration

In Stripe Dashboard:

  • Navigate to Settings > Payment Methods > Apple Pay
  • Register your iOS app’s merchant ID
  • Stripe generates and manages the certificate automatically

Google Pay Setup

1. Google Pay API Console

Google Pay requires less configuration:

  • No merchant ID registration needed for testing
  • For production, register your app in the Google Pay Business Console

2. Stripe Dashboard Configuration

In Stripe Dashboard:

  • Navigate to Settings > Payment Methods > Google Pay
  • Enable Google Pay
  • No additional configuration needed for basic integration

React Native Im

![React Native

React Native Implementation Infographic

React Native Implementation Infographic Implementation Infographic](/images/mobile-payment-integration-apple-pay-google-pay-react-native-implementation.webp) plementation

Dependencies

npm install @stripe/stripe-react-native
cd ios && pod install

iOS Configuration

Add to ios/YourApp/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>yourapp</string>
    </array>
  </dict>
</array>

Android Configuration

Add to android/app/src/main/AndroidManifest.xml:

<application>
  <meta-data
    android:name="com.google.android.gms.wallet.api.enabled"
    android:value="true" />
</application>

Stripe Provider Setup

// App.tsx
import { StripeProvider } from '@stripe/stripe-react-native';

function App() {
  return (
    <StripeProvider
      publishableKey="pk_live_..."
      merchantIdentifier="merchant.com.yourcompany.yourapp"
      urlScheme="yourapp"
    >
      <NavigationContainer>
        <AppNavigator />
      </NavigationContainer>
    </StripeProvider>
  );
}

Check Platform Payment Availability

// hooks/usePaymentMethods.ts
import { useEffect, useState } from 'react';
import {
  isPlatformPaySupported,
  PlatformPay
} from '@stripe/stripe-react-native';
import { Platform } from 'react-native';

interface PaymentMethods {
  applePay: boolean;
  googlePay: boolean;
  loading: boolean;
}

export function usePaymentMethods(): PaymentMethods {
  const [applePay, setApplePay] = useState(false);
  const [googlePay, setGooglePay] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function checkAvailability() {
      try {
        if (Platform.OS === 'ios') {
          const isApplePaySupported = await isPlatformPaySupported();
          setApplePay(isApplePaySupported);
        } else {
          const isGooglePaySupported = await isPlatformPaySupported({
            googlePay: {
              testEnv: __DEV__,
              merchantCountryCode: 'AU',
              billingAddressConfig: {
                isRequired: true,
                format: 'FULL',
                isPhoneNumberRequired: false
              }
            }
          });
          setGooglePay(isGooglePaySupported);
        }
      } catch (error) {
        console.error('Error checking payment availability:', error);
      } finally {
        setLoading(false);
      }
    }

    checkAvailability();
  }, []);

  return { applePay, googlePay, loading };
}

Creating Payment Intents

Your server creates payment intents:

// server/routes/payments.ts
import Stripe from 'stripe';
import { Router } from 'express';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const router = Router();

router.post('/create-payment-intent', async (req, res) => {
  const { amount, currency = 'aud', customerId } = req.body;

  try {
    const paymentIntent = await stripe.paymentIntents.create({
      amount: Math.round(amount * 100), // Convert to cents
      currency,
      customer: customerId,
      automatic_payment_methods: {
        enabled: true
      },
      metadata: {
        orderId: req.body.orderId
      }
    });

    res.json({
      clientSecret: paymentIntent.client_secret,
      paymentIntentId: paymentIntent.id
    });
  } catch (error) {
    console.error('Error creating payment intent:', error);
    res.status(500).json({ error: 'Failed to create payment intent' });
  }
});

export default router;

Implementing Apple Pay Checkout

// screens/CheckoutScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  Alert,
  StyleSheet,
  ActivityIndicator
} from 'react-native';
import {
  usePlatformPay,
  PlatformPayButton,
  PlatformPay
} from '@stripe/stripe-react-native';
import { usePaymentMethods } from '../hooks/usePaymentMethods';

interface CheckoutProps {
  amount: number;
  currency: string;
  onSuccess: (paymentIntentId: string) => void;
  onError: (error: string) => void;
}

function CheckoutScreen({ amount, currency, onSuccess, onError }: CheckoutProps) {
  const [loading, setLoading] = useState(false);
  const { applePay, googlePay } = usePaymentMethods();
  const { confirmPlatformPayPayment } = usePlatformPay();

  const handlePlatformPay = async () => {
    setLoading(true);

    try {
      // 1. Create payment intent on server
      const response = await fetch('/api/create-payment-intent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ amount, currency })
      });

      const { clientSecret, paymentIntentId } = await response.json();

      // 2. Configure payment request
      const { error } = await confirmPlatformPayPayment(clientSecret, {
        applePay: {
          cartItems: [
            {
              label: 'Total',
              amount: amount.toFixed(2),
              paymentType: PlatformPay.PaymentType.Immediate
            }
          ],
          merchantCountryCode: 'AU',
          currencyCode: currency.toUpperCase(),
          requiredShippingAddressFields: [
            PlatformPay.ContactField.EmailAddress,
            PlatformPay.ContactField.Name
          ],
          requiredBillingContactFields: [
            PlatformPay.ContactField.PostalAddress
          ]
        },
        googlePay: {
          merchantCountryCode: 'AU',
          currencyCode: currency.toUpperCase(),
          testEnv: __DEV__,
          merchantName: 'Your Company',
          billingAddressConfig: {
            isRequired: true,
            format: 'FULL'
          }
        }
      });

      if (error) {
        onError(error.message);
        return;
      }

      onSuccess(paymentIntentId);
    } catch (error) {
      onError('Payment failed. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  const isPlatformPayAvailable = applePay || googlePay;

  return (
    <View style={styles.container}>
      <View style={styles.summaryContainer}>
        <Text style={styles.summaryTitle}>Order Summary</Text>
        <View style={styles.summaryRow}>
          <Text>Total</Text>
          <Text style={styles.amount}>
            {new Intl.NumberFormat('en-AU', {
              style: 'currency',
              currency
            }).format(amount)}
          </Text>
        </View>
      </View>

      {loading ? (
        <ActivityIndicator size="large" color="#007AFF" />
      ) : (
        <>
          {isPlatformPayAvailable && (
            <PlatformPayButton
              type={PlatformPay.ButtonType.Buy}
              appearance={PlatformPay.ButtonStyle.Black}
              onPress={handlePlatformPay}
              style={styles.platformPayButton}
            />
          )}

          <TouchableOpacity
            style={styles.cardButton}
            onPress={() => {/* Navigate to card payment screen */}}
          >
            <Text style={styles.cardButtonText}>Pay with Card</Text>
          </TouchableOpacity>
        </>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20
  },
  summaryContainer: {
    backgroundColor: '#f5f5f5',
    padding: 16,
    borderRadius: 12,
    marginBottom: 24
  },
  summaryTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 12
  },
  summaryRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  amount: {
    fontSize: 24,
    fontWeight: '700'
  },
  platformPayButton: {
    height: 50,
    marginBottom: 16
  },
  cardButton: {
    height: 50,
    backgroundColor: '#333',
    borderRadius: 8,
    justifyContent: 'center',
    alignItems: 'center'
  },
  cardButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600'
  }
});

export default CheckoutScreen;

Handling Subscriptions

For recurring payments with mobile wallets:

// server/routes/subscriptions.ts
router.post('/create-subscription', async (req, res) => {
  const { customerId, priceId } = req.body;

  try {
    // Create setup intent for recurring payments
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: 'default_incomplete',
      payment_settings: {
        save_default_payment_method: 'on_subscription'
      },
      expand: ['latest_invoice.payment_intent']
    });

    const invoice = subscription.latest_invoice as Stripe.Invoice;
    const paymentIntent = invoice.payment_intent as Stripe.PaymentIntent;

    res.json({
      subscriptionId: subscription.id,
      clientSecret: paymentIntent.client_secret
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to create subscription' });
  }
});
// screens/SubscriptionScreen.tsx
function SubscriptionScreen({ priceId, onSuccess }) {
  const { confirmPlatformPayPayment } = usePlatformPay();

  const handleSubscribe = async () => {
    const response = await fetch('/api/create-subscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        customerId: user.stripeCustomerId,
        priceId
      })
    });

    const { clientSecret, subscriptionId } = await response.json();

    const { error } = await confirmPlatformPayPayment(clientSecret, {
      applePay: {
        cartItems: [
          {
            label: 'Premium Subscription',
            amount: '9.99',
            paymentType: PlatformPay.PaymentType.Recurring,
            intervalUnit: PlatformPay.IntervalUnit.Month,
            intervalCount: 1
          }
        ],
        merchantCountryCode: 'AU',
        currencyCode: 'AUD'
      },
      googlePay: {
        merchantCountryCode: 'AU',
        currencyCode: 'AUD',
        merchantName: 'Your Company'
      }
    });

    if (!error) {
      onSuccess(subscriptionId);
    }
  };

  return (
    <PlatformPayButton
      type={PlatformPay.ButtonType.Subscribe}
      onPress={handleSubscribe}
    />
  );
}

Webhook Handling

Handle payment events server-side:

// server/routes/webhooks.ts
import { Router } from 'express';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const router = Router();

router.post(
  '/stripe-webhook',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const sig = req.headers['stripe-signature']!;

    let event: Stripe.Event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET!
      );
    } catch (err) {
      console.error('Webhook signature verification failed');
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    switch (event.type) {
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object as Stripe.PaymentIntent;
        await handleSuccessfulPayment(paymentIntent);
        break;

      case 'payment_intent.payment_failed':
        const failedPayment = event.data.object as Stripe.PaymentIntent;
        await handleFailedPayment(failedPayment);
        break;

      case 'customer.subscription.created':
        const subscription = event.data.object as Stripe.Subscription;
        await handleNewSubscription(subscription);
        break;

      case 'customer.subscription.deleted':
        const canceledSub = event.data.object as Stripe.Subscription;
        await handleCanceledSubscription(canceledSub);
        break;

      case 'invoice.payment_failed':
        const invoice = event.data.object as Stripe.Invoice;
        await handleFailedInvoice(invoice);
        break;

      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    res.json({ received: true });
  }
);

async function handleSuccessfulPayment(paymentIntent: Stripe.PaymentIntent) {
  const orderId = paymentIntent.metadata.orderId;

  // Update order status
  await db.orders.update({
    where: { id: orderId },
    data: {
      status: 'paid',
      paidAt: new Date(),
      stripePaymentIntentId: paymentIntent.id
    }
  });

  // Send confirmation email
  await sendOrderConfirmationEmail(orderId);

  // Send push notification
  await sendPushNotification(paymentIntent.customer as string, {
    title: 'Payment Successful',
    body: 'Your order has been confirmed'
  });
}

export default router;

Error Handling

Handle common payment errors gracefully:

// utils/paymentErrors.ts
import { StripeError } from '@stripe/stripe-react-native';

export function getPaymentErrorMessage(error: StripeError<any>): string {
  switch (error.code) {
    case 'Canceled':
      return 'Payment was canceled';
    case 'Failed':
      return 'Payment failed. Please try again.';
    case 'authentication_required':
      return 'Additional authentication required';
    case 'card_declined':
      return 'Your card was declined';
    case 'expired_card':
      return 'Your card has expired';
    case 'insufficient_funds':
      return 'Insufficient funds';
    case 'processing_error':
      return 'Processing error. Please try again.';
    default:
      return 'An error occurred. Please try again.';
  }
}
// components/PaymentError.tsx
function PaymentError({ error, onRetry, onUseCard }) {
  const message = getPaymentErrorMessage(error);

  return (
    <View style={styles.container}>
      <Icon name="alert-circle" size={48} color="#FF3B30" />
      <Text style={styles.message}>{message}</Text>

      <TouchableOpacity style={styles.retryButton} onPress={onRetry}>
        <Text style={styles.retryButtonText}>Try Again</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.cardButton} onPress={onUseCard}>
        <Text style={styles.cardButtonText}>Pay with Card Instead</Text>
      </TouchableOpacity>
    </View>
  );
}

Testing

Test Cards

Use Stripe’s test cards:

ScenarioCard Number
Success4242 4242 4242 4242
Requires authentication4000 0027 6000 3184
Declined4000 0000 0000 0002
Insufficient funds4000 0000 0000 9995

Testing Apple Pay

Apple Pay cannot be tested in the iOS Simulator. Use:

  • Physical device with test cards in Apple Wallet
  • Stripe’s test mode with test Merchant ID

Testing Google Pay

Google Pay works in the Android Emulator with test mode:

const { confirmPlatformPayPayment } = usePlatformPay();

await confirmPlatformPayPayment(clientSecret, {
  googlePay: {
    testEnv: true, // Enable test environment
    merchantCountryCode: 'AU',
    currencyCode: 'AUD'
  }
});

Integration Tests

// tests/payments.test.ts
describe('Payment Integration', () => {
  it('creates payment intent successfully', async () => {
    const response = await request(app)
      .post('/api/create-payment-intent')
      .send({
        amount: 99.99,
        currency: 'aud'
      });

    expect(response.status).toBe(200);
    expect(response.body.clientSecret).toBeDefined();
    expect(response.body.clientSecret).toMatch(/^pi_.*_secret_/);
  });

  it('handles webhook for successful payment', async () => {
    const paymentIntent = {
      id: 'pi_test_123',
      status: 'succeeded',
      metadata: { orderId: 'order_123' }
    };

    const response = await request(app)
      .post('/api/stripe-webhook')
      .set('stripe-signature', generateTestSignature(paymentIntent))
      .send(JSON.stringify({
        type: 'payment_intent.succeeded',
        data: { object: paymentIntent }
      }));

    expect(response.status).toBe(200);

    // Verify order was updated
    const order = await db.orders.findUnique({
      where: { id: 'order_123' }
    });
    expect(order.status).toBe('paid');
  });
});

Security Considerations

PCI Compliance

Using Stripe with Apple Pay and Google Pay keeps you largely out of PCI scope:

  • You never handle card numbers
  • Tokens are device-specific and one-time use
  • All sensitive data is encrypted end-to-end

However, you still must:

  • Use HTTPS for all API communication
  • Secure your Stripe API keys
  • Implement proper authentication
  • Log payment events for auditing

Fraud Prevention

Stripe provides built-in fraud protection. Enable additional measures:

// Enhanced fraud protection
const paymentIntent = await stripe.paymentIntents.create({
  amount: 9999,
  currency: 'aud',
  metadata: {
    orderId: order.id,
    customerIp: req.ip,
    userAgent: req.headers['user-agent']
  },
  // Enable Radar for fraud detection
  statement_descriptor: 'YOURCOMPANY',
  statement_descriptor_suffix: order.id.slice(-8)
});

Rate Limiting

Protect your payment endpoints:

import rateLimit from 'express-rate-limit';

const paymentLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // 10 payment attempts per window
  message: 'Too many payment attempts. Please try again later.'
});

router.post('/create-payment-intent', paymentLimiter, async (req, res) => {
  // ...
});

Regional Considerations

Australia-Specific Requirements

For Australian merchants:

  • ABN required for business accounts
  • GST must be included in displayed prices
  • ACCC consumer protection laws apply to refunds
// Display GST-inclusive prices
function formatAustralianPrice(amount: number): string {
  return new Intl.NumberFormat('en-AU', {
    style: 'currency',
    currency: 'AUD'
  }).format(amount);
}

// Calculate GST component
function calculateGST(totalAmount: number): number {
  return totalAmount / 11; // GST is 1/11th of GST-inclusive price
}

Multi-Currency Support

const paymentIntent = await stripe.paymentIntents.create({
  amount: Math.round(convertedAmount * 100),
  currency: customerCurrency,
  // Stripe handles conversion at current rates
});

Conclusion

Apple Pay and Google Pay integration dramatically improves checkout conversion. Users complete payments in seconds instead of minutes, and the tokenized approach reduces fraud while simplifying your compliance requirements.

Stripe’s unified SDK makes implementing both platforms straightforward. The same React Native code handles Apple Pay on iOS and Google Pay on Android. Server-side, a single PaymentIntent API works for all payment methods.

Start with one-time payments to understand the flow. Add subscription support once the basics work. Build in proper error handling and testing from the start - payment bugs are expensive in both money and trust.

The initial setup requires attention to detail, especially around certificates and merchant configuration. But once configured, mobile payments become the simplest and most reliable payment method in your app.


Implementing mobile payments for your app? Our team has integrated payment systems handling millions in transactions. Contact us to discuss your requirements.