DEV Community

Cover image for How I Integrated Apple Pay in React Native — After Every Package Failed Me
Barath E
Barath E

Posted on

How I Integrated Apple Pay in React Native — After Every Package Failed Me

I was building a payment flow for a production mobile app — one that needed to support Apple Pay. Simple enough on paper. But what followed was a week of broken packages, version conflicts, and silent failures that led me to one conclusion: sometimes, you just have to write the native code yourself.

This is the story of how I got Apple Pay working in React Native using a Swift native module — and how you can do the same.


The Problem: Third-Party Packages Were Not Reliable

My first instinct, like most React Native developers, was to find a package. I tried several options available in the ecosystem. Some didn't work at all. Some gave partial functionality but broke on specific iOS versions. Others had instability issues tied to their own version mismatches with React Native's evolving architecture.

I won't name every package I tried — the ecosystem changes fast and a package that fails today might be maintained tomorrow. The point is: none of them gave me the stability I needed for a production app with real users.

At that point, I made a decision: write a Swift native module and bridge it to React Native myself.

It turned out to be the right call.


The Solution: Swift Native Module + React Native Bridge

React Native allows you to write native iOS code (Swift or Objective-C) and expose it to JavaScript through the Native Modules API. This is exactly what we need here.

Here's the full approach, step by step.


Step 1: Set Up the Swift File in Xcode

Open your React Native project's iOS folder in Xcode. Create a new Swift file — for example, ApplePayModule.swift.

When Xcode asks if you want to create a bridging header, say Yes. This bridging header allows Swift to communicate with Objective-C, which is what React Native's native module system uses under the hood.

// ApplePayModule.swift
import Foundation
import PassKit

@objc(ApplePayModule)
class ApplePayModule: NSObject {

  @objc
  func canMakePayments(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
    let canPay = PKPaymentAuthorizationViewController.canMakePayments()
    resolve(canPay)
  }

  @objc
  func initiatePayment(
    _ amount: String,
    currency: String,
    label: String,
    resolver resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: @escaping RCTPromiseRejectBlock
  ) {
    guard let decimalAmount = Decimal(string: amount) else {
      reject("INVALID_AMOUNT", "Amount is not valid", nil)
      return
    }

    let paymentItem = PKPaymentSummaryItem(
      label: label,
      amount: NSDecimalNumber(decimal: decimalAmount)
    )

    let request = PKPaymentRequest()
    request.merchantIdentifier = "merchant.com.yourapp.payments" // Replace with your merchant ID
    request.supportedNetworks = [.visa, .masterCard, .amex]
    request.merchantCapabilities = .capability3DS
    request.countryCode = "US"
    request.currencyCode = currency
    request.paymentSummaryItems = [paymentItem]

    DispatchQueue.main.async {
      guard let controller = PKPaymentAuthorizationViewController(paymentRequest: request) else {
        reject("APPLE_PAY_UNAVAILABLE", "Could not create Apple Pay controller", nil)
        return
      }
      controller.delegate = self
      self.resolve = resolve
      self.reject = reject

      if let topVC = UIApplication.shared.windows.first?.rootViewController {
        topVC.present(controller, animated: true, completion: nil)
      }
    }
  }

  private var resolve: RCTPromiseResolveBlock?
  private var reject: RCTPromiseRejectBlock?
}

extension ApplePayModule: PKPaymentAuthorizationViewControllerDelegate {

  func paymentAuthorizationViewController(
    _ controller: PKPaymentAuthorizationViewController,
    didAuthorizePayment payment: PKPayment,
    handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
  ) {
    // Pass the payment token to your backend here
    let tokenData = payment.token.paymentData
    let tokenString = tokenData.base64EncodedString()
    completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
    resolve?(["status": "success", "token": tokenString])
  }

  func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
    controller.dismiss(animated: true, completion: nil)
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Objective-C Bridge File

React Native's native module system requires an Objective-C macro to register your Swift class. Create a new .m file — for example, ApplePayModuleBridge.m.

// ApplePayModuleBridge.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(ApplePayModule, NSObject)

RCT_EXTERN_METHOD(
  canMakePayments:(RCTPromiseResolveBlock)resolve
  rejecter:(RCTPromiseRejectBlock)reject
)

RCT_EXTERN_METHOD(
  initiatePayment:(NSString *)amount
  currency:(NSString *)currency
  label:(NSString *)label
  resolver:(RCTPromiseResolveBlock)resolve
  rejecter:(RCTPromiseRejectBlock)reject
)

@end
Enter fullscreen mode Exit fullscreen mode

This file is what makes your Swift class visible to the React Native JavaScript layer.


Step 3: Update the Bridging Header

Open your <ProjectName>-Bridging-Header.h file and add the React Native import:

// YourProject-Bridging-Header.h
#import <React/RCTBridgeModule.h>
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure Apple Pay in Xcode

Before the code works, your app needs the right entitlements:

  1. Go to your target's Signing & Capabilities tab in Xcode
  2. Click + Capability and add Apple Pay
  3. Add your Merchant ID (create one in Apple Developer Portal under Identifiers → Merchant IDs)
  4. Make sure the same merchant ID is used in your Swift file

Step 5: Use It in JavaScript

Now that the native module is registered, you can import and use it in your React Native code.

// useApplePay.js
import { NativeModules, Platform } from 'react-native';

const { ApplePayModule } = NativeModules;

export const useApplePay = () => {

  const isApplePayAvailable = async () => {
    if (Platform.OS !== 'ios') return false;
    try {
      return await ApplePayModule.canMakePayments();
    } catch {
      return false;
    }
  };

  const startPayment = async ({ amount, currency = 'USD', label = 'Total' }) => {
    try {
      const result = await ApplePayModule.initiatePayment(
        String(amount),
        currency,
        label
      );
      return result; // { status: 'success', token: '...' }
    } catch (error) {
      throw error;
    }
  };

  return { isApplePayAvailable, startPayment };
};
Enter fullscreen mode Exit fullscreen mode
// PaymentScreen.js
import React, { useEffect, useState } from 'react';
import { View, Button, Text } from 'react-native';
import { useApplePay } from './useApplePay';

const PaymentScreen = () => {
  const { isApplePayAvailable, startPayment } = useApplePay();
  const [applePayEnabled, setApplePayEnabled] = useState(false);

  useEffect(() => {
    isApplePayAvailable().then(setApplePayEnabled);
  }, []);

  const handlePayment = async () => {
    try {
      const result = await startPayment({
        amount: '49.99',
        currency: 'USD',
        label: 'Premium Subscription',
      });
      console.log('Payment success:', result.token);
      // Send token to your backend payment processor
    } catch (error) {
      console.error('Payment failed:', error);
    }
  };

  return (
    <View>
      {applePayEnabled ? (
        <Button title="Pay with Apple Pay" onPress={handlePayment} />
      ) : (
        <Text>Apple Pay is not available on this device.</Text>
      )}
    </View>
  );
};

export default PaymentScreen;
Enter fullscreen mode Exit fullscreen mode

What Happens Behind the Scenes

When initiatePayment is called:

  1. React Native bridges the call to the Swift module via the Objective-C bridge
  2. The Swift module creates a PKPaymentRequest with your configured merchant ID, amount, and networks
  3. iOS presents the native Apple Pay sheet to the user
  4. On authorization, the delegate receives a PKPayment object containing a cryptographically signed payment token
  5. That token is returned to JavaScript as a base64 string
  6. You pass that token to your backend, which forwards it to your payment processor (Stripe, Braintree, etc.) for actual charge processing

Important Things to Keep in Mind

Test on a real device. Apple Pay does not work on simulators. You need a physical iPhone with a card added to Wallet to test the full flow.

Never charge the card on the client side. The payment token from Apple Pay must always be sent to your backend server. Your server then uses it with your payment processor's API. Never expose secret keys or process payments in the app itself.

Merchant ID must match exactly. The merchant ID in your Swift code, your Xcode entitlements, and your Apple Developer Portal registration must all be identical. A single character difference causes a silent failure.

Handle the didFinish delegate carefully. The paymentAuthorizationViewControllerDidFinish delegate fires both when the user cancels and after a successful payment. Make sure your promise resolution logic handles both cases cleanly to avoid unresolved promises.


Why This Approach Works

Third-party packages add a layer between you and the platform. They make assumptions about your project setup, React Native version, and use case. When those assumptions break, you're stuck waiting for a maintainer.

Writing your own native module means:

  • You control the code entirely
  • You can update it independently of any package release cycle
  • You know exactly what the module does and can debug it yourself
  • It works reliably across iOS updates because you're using Apple's own PassKit framework directly

This is not the easy path. But for a payment feature in a production app, reliability is non-negotiable.


Summary

Step What you do
1 Create ApplePayModule.swift with PassKit logic
2 Create ApplePayModuleBridge.m to register the module
3 Update the bridging header
4 Add Apple Pay capability and Merchant ID in Xcode
5 Call the module from JavaScript using NativeModules

Final Thoughts

When I hit the wall with third-party packages, I almost gave up on the native approach thinking it would be too complex. It wasn't. The React Native bridge is well-documented, PassKit is a mature Apple framework, and once you understand the pattern, writing native modules becomes a genuine superpower.

If you're building a production app that handles real payments, give your users the reliability they deserve — even if that means writing a few lines of Swift.

If you have questions or ran into a different issue with Apple Pay in React Native, drop them in the comments. Happy to help.

Top comments (0)