Why Build Custom Solutions Over Libraries in React Native?
While libraries simplify integrations in React Native, they often come with limitations:
Deprecated Libraries: Updates and support can cease, leaving projects vulnerable.
Feature Limitations: Libraries may not meet all use cases.
Flexibility: Custom solutions offer more control for unique requirements.
Custom native modules ensure scalability and precision. Here’s how to integrate Apple Pay natively in React Native for iOS.
Why Use Apple Pay in React Native?
Apple Pay enables secure, seamless payments with encrypted card details. A React Native integration aligns with Apple’s security standards while enhancing the user experience.
Note: Before proceeding, you must create a Merchant ID and configure your project in Xcode. Follow Apple’s official documentation to complete prerequisites.
Steps to Create a Custom Apple Pay Module
1. Create a Swift Module
Add an ApplePayModule.swift file to your iOS project:
Payment Request Parameters: Define merchant ID, supported networks, and payment details.
Can Make Payments: Check device capability with PKPaymentAuthorizationController.canMakePayments().
Request Payment: Present the Apple Pay sheet and handle authorization using React Native callbacks (RCTPromiseResolveBlock/RCTPromiseRejectBlock).
import Foundation | |
import PassKit | |
import React | |
struct PaymentRequestParams: Codable { | |
let merchantIdentifier: String | |
let supportedNetworks: [String] | |
let countryCode: String | |
let currencyCode: String | |
let label: String | |
let amount: String | |
} | |
@objc(ApplePayModule) | |
class ApplePayModule: NSObject { | |
private var paymentResolver: RCTPromiseResolveBlock? | |
private var paymentRejecter: RCTPromiseRejectBlock? | |
private var completionHandler: ((PKPaymentAuthorizationResult) -> Void)? | |
@objc | |
static func requiresMainQueueSetup() -> Bool { | |
return true | |
} | |
@objc | |
func canMakePayments(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { | |
resolver(PKPaymentAuthorizationController.canMakePayments()) | |
} | |
@objc | |
func requestPayment(_ paramsJSON: NSDictionary, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { | |
guard let jsonData = try? JSONSerialization.data(withJSONObject: paramsJSON, options: []), | |
let params = try? JSONDecoder().decode(PaymentRequestParams.self, from: jsonData) else { | |
rejecter("E_INVALID_PARAMS", "Invalid payment request parameters", nil) | |
return | |
} | |
let paymentRequest = PKPaymentRequest() | |
paymentRequest.merchantIdentifier = params.merchantIdentifier | |
paymentRequest.supportedNetworks = mapSupportedNetworks(params.supportedNetworks) | |
paymentRequest.merchantCapabilities = .capability3DS | |
paymentRequest.countryCode = params.countryCode | |
paymentRequest.currencyCode = params.currencyCode | |
paymentRequest.paymentSummaryItems = [ | |
PKPaymentSummaryItem(label: params.label, amount: NSDecimalNumber(string: params.amount)) | |
] | |
let paymentAuthorizationController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) | |
paymentAuthorizationController.delegate = self | |
paymentAuthorizationController.present { (presented: Bool) in | |
if !presented { | |
rejecter("E_PAYMENT_ERROR", "Unable to present Apple Pay authorization.", nil) | |
} else { | |
self.paymentResolver = resolver | |
self.paymentRejecter = rejecter | |
} | |
} | |
} | |
@objc | |
func completePayment(_ success: Bool) { | |
guard let completionHandler = self.completionHandler else { | |
return | |
} | |
let status: PKPaymentAuthorizationStatus = success ? .success : .failure | |
let result = PKPaymentAuthorizationResult(status: status, errors: nil) | |
completionHandler(result) | |
self.completionHandler = nil | |
} | |
private func mapSupportedNetworks(_ networks: [String]) -> [PKPaymentNetwork] { | |
return networks.compactMap { network in | |
switch network.lowercased() { | |
case "visa": | |
return .visa | |
case "mastercard": | |
return .masterCard | |
case "mada": | |
return .mada | |
case "amex": | |
return .amex | |
case "discover": | |
return .discover | |
case "jcb": | |
return .JCB | |
case "maestro": | |
return .maestro | |
case "electron": | |
return .electron | |
case "vpay": | |
return .vPay | |
default: | |
return nil | |
} | |
} | |
} | |
} | |
extension ApplePayModule: PKPaymentAuthorizationControllerDelegate { | |
func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) { | |
// Store the completion handler | |
self.completionHandler = completion | |
// Handle the payment authorization | |
if let resolver = self.paymentResolver { | |
do { | |
let paymentData = try JSONSerialization.jsonObject(with: payment.token.paymentData, options: []) as? [String: Any] ?? [:] | |
resolver(["status": "success", "paymentData": paymentData]) | |
} catch { | |
if let rejecter = self.paymentRejecter { | |
rejecter("APPLE_PAY_PAYMENT_REJECTED", "Payment was rejected", error) | |
} | |
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil)) | |
return | |
} | |
self.paymentResolver = nil | |
self.paymentRejecter = nil | |
} else { | |
// Resolver is nil, reject the payment | |
if let rejecter = self.paymentRejecter { | |
rejecter("APPLE_PAY_PAYMENT_REJECTED", "Payment was rejected", nil) | |
self.paymentResolver = nil | |
self.paymentRejecter = nil | |
} | |
completion(PKPaymentAuthorizationResult(status: .failure, errors: nil)) | |
} | |
} | |
func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) { | |
controller.dismiss { | |
// Handle the dismissal of the payment sheet | |
if let rejecter = self.paymentRejecter { | |
rejecter("APPLE_PAY_PAYMENT_CANCELLED", "Payment was cancelled", nil) | |
self.paymentResolver = nil | |
self.paymentRejecter = nil | |
} | |
} | |
} | |
} |
2. Bridge the Module
Use ApplePayModule.m to bridge Swift and React Native, exposing methods like canMakePayments and requestPayment to JavaScript.
#import <React/RCTBridgeModule.h> | |
@interface RCT_EXTERN_MODULE(ApplePayModule, NSObject) | |
RCT_EXTERN_METHOD(canMakePayments:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) | |
RCT_EXTERN_METHOD(requestPayment:(NSDictionary *)paramsJSON resolver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) | |
RCT_EXTERN_METHOD(completePayment:(BOOL)success) | |
@end |
3. Create a Hook
The useApplePay hook consolidates Apple Pay operations, feel free to add currencies and countries.
Key Functions:
canMakePayments: Validates device compatibility.
requestPayment: Handles the payment flow.
handleApplePay: Combines the above for a seamless user experience.
Centralized logic for checking availability and managing payments.
Comprehensive error handling.
import {Alert, NativeModules, Platform} from 'react-native'; | |
export enum Country { | |
AE = 'AE', // United Arab Emirates | |
BH = 'BH', // Bahrain | |
KW = 'KW', // Kuwait | |
OM = 'OM', // Oman | |
QA = 'QA', // Qatar | |
SA = 'SA', // Saudi Arabia | |
US = 'US', // United States | |
GB = 'GB', // United Kingdom | |
IN = 'IN', // India | |
CA = 'CA', // Canada | |
AU = 'AU', // Australia | |
DE = 'DE', // Germany | |
FR = 'FR', // France | |
SG = 'SG', // Singapore | |
} | |
export enum Currency { | |
AED = 'AED', // UAE Dirham | |
BHD = 'BHD', // Bahraini Dinar | |
KWD = 'KWD', // Kuwaiti Dinar | |
OMR = 'OMR', // Omani Rial | |
QAR = 'QAR', // Qatari Riyal | |
SAR = 'SAR', // Saudi Riyal | |
GBP = 'GBP', // British Pound | |
USD = 'USD', // US Dollar | |
INR = 'INR', // Indian Rupee | |
CAD = 'CAD', // Canadian Dollar | |
AUD = 'AUD', // Australian Dollar | |
EUR = 'EUR', // Euro | |
SGD = 'SGD', // Singapore Dollar | |
} | |
export enum APPLE_PAY_SUPPORTED_NETWORKS { | |
AMEX = 'amex', // American Express | |
BANCONTACT = 'bancontact', // Bancontact | |
CARTES_BANCAIRES = 'cartesBancaires', // Cartes Bancaires | |
CHINA_UNION_PAY = 'chinaUnionPay', // China Union Pay | |
DANKORT = 'dankort', // Dankort | |
DISCOVER = 'discover', // Discover | |
EFTPOS = 'eftpos', // EFTPOS | |
ELECTRON = 'electron', // Visa Electron | |
ELO = 'elo', // ELO | |
GIROCARD = 'girocard', // Girocard | |
INTERAC = 'interac', // Interac | |
JCB = 'jcb', // JCB | |
MADA = 'mada', // MADA | |
MAESTRO = 'maestro', // Maestro | |
MASTERCARD = 'masterCard', // MasterCard | |
MIR = 'mir', // MIR | |
PRIVATE_LABEL = 'privateLabel', // Private Label Cards | |
VISA = 'visa', // Visa | |
VPAY = 'vPay', // VPay | |
UNION_PAY = 'unionPay', // Union Pay (Global) | |
TROY = 'troy', // Troy (Turkey) | |
PULSE = 'pulse', // Pulse | |
STAR = 'star', // Star Network | |
NYCE = 'nyce', // NYCE | |
ACCEL = 'accel', // Accel | |
ZIP = 'zip', // Zip (Buy Now Pay Later) | |
BARCLAYCARD = 'barclaycard', // Barclaycard | |
} | |
export interface ApplePayToken { | |
token: string; | |
ephemeralPublicKey: string; | |
publicKeyHash: string; | |
signature: string; | |
transactionId: string; | |
version: string; | |
} | |
interface PaymentDetailsType { | |
country: Country; | |
currency: Currency; | |
amount: number; | |
label: string; | |
} | |
interface UseApplePayProps { | |
handlePayment: (values: any, paymentResponse: any) => Promise<any>; | |
isCreatSub?: boolean; | |
} | |
interface OnApplePayProps { | |
paymentValues: PaymentDetailsType; | |
paymentRequestBody?: any; | |
paymentKey?: string; | |
merchantId: string; | |
} | |
interface ApplePayRequestData { | |
merchantIdentifier: string; | |
supportedNetworks: APPLE_PAY_SUPPORTED_NETWORKS[]; | |
countryCode: string; | |
currencyCode: string; | |
label: string; | |
amount: string; | |
} | |
export enum APPLE_PAY_RESPONSE_CODES { | |
APPLE_PAY_PAYMENT_REJECTED = 'APPLE_PAY_PAYMENT_REJECTED', | |
APPLE_PAY_PAYMENT_CANCELLED = 'APPLE_PAY_PAYMENT_CANCELLED', | |
} | |
const {ApplePayModule} = NativeModules; | |
const supportedNetworks = [ | |
APPLE_PAY_SUPPORTED_NETWORKS.VISA, | |
APPLE_PAY_SUPPORTED_NETWORKS.MASTERCARD, | |
APPLE_PAY_SUPPORTED_NETWORKS.AMEX, | |
APPLE_PAY_SUPPORTED_NETWORKS.MADA, | |
]; | |
const useApplePay = ({handlePayment, isCreatSub}: UseApplePayProps) => { | |
const canMakePayments = async () => { | |
try { | |
if (Platform.OS === 'ios') { | |
return await ApplePayModule.canMakePayments(); | |
} | |
return false; | |
} catch (error) { | |
return false; | |
} | |
}; | |
const requestPayment = async (data: ApplePayRequestData) => { | |
if (Platform.OS === 'ios') { | |
return await ApplePayModule.requestPayment(data); | |
} | |
throw new Error('Apple Pay is not supported on this platform.'); | |
}; | |
const handleApplePay = async (data: ApplePayRequestData) => { | |
const isAvailable = await canMakePayments(); | |
if (!isAvailable) { | |
throw new Error('Apple Pay is not available on this device'); | |
} | |
try { | |
const paymentResponse = await requestPayment(data); | |
if (paymentResponse.status === 'success') { | |
return paymentResponse.paymentData; | |
} | |
throw new Error('Payment failed'); | |
} catch (error) { | |
throw error; | |
} | |
}; | |
const alertError = () => { | |
Alert.alert('somethingWentWrong'); | |
}; | |
const onApplePay = async ({ | |
paymentRequestBody = {}, | |
paymentValues, | |
paymentKey = isCreatSub ? 'token' : 'source', | |
merchantId, | |
}: OnApplePayProps) => { | |
try { | |
if (!paymentValues.currency || !paymentValues.amount) { | |
alertError(); | |
} else { | |
const merchantIdentifier = merchantId; | |
const paymentInternalInfo = { | |
merchantIdentifier, | |
country: paymentValues.country, | |
currencyCode: paymentValues.currency, | |
}; | |
const isPaymentPossible = await canMakePayments(); | |
if (isPaymentPossible) { | |
const paymentResponse = await handleApplePay({ | |
merchantIdentifier, | |
supportedNetworks, | |
countryCode: paymentValues.country, | |
currencyCode: paymentValues.currency, | |
label: 'Label', | |
amount: paymentValues.amount.toString(), | |
}); | |
console.log(JSON.stringify(paymentResponse, null, 2)); | |
if (paymentResponse) { | |
const {version, data: token, header, signature} = paymentResponse; | |
const {transactionId, publicKeyHash, ephemeralPublicKey} = header; | |
const values = { | |
version, | |
token, | |
transactionId, | |
publicKeyHash, | |
ephemeralPublicKey, | |
signature, | |
} as ApplePayToken; | |
try { | |
await handlePayment( | |
{ | |
[paymentKey]: values, | |
...paymentRequestBody, | |
}, | |
paymentResponse, | |
); | |
await ApplePayModule.completePayment(true); | |
} catch (error) { | |
throw new Error( | |
`handle payment error ${{...paymentInternalInfo, ...values}}`, | |
); | |
} | |
} else { | |
throw new Error('Empty Payment Response'); | |
} | |
} else { | |
throw new Error( | |
`Payment is not possible ${{...paymentInternalInfo}}`, | |
); | |
} | |
} | |
} catch (error: any) { | |
if (error.code !== APPLE_PAY_RESPONSE_CODES.APPLE_PAY_PAYMENT_CANCELLED) { | |
ApplePayModule.completePayment(false); | |
alertError(); | |
} | |
} | |
}; | |
return { | |
onApplePay, | |
}; | |
}; | |
export default useApplePay; |
4. Use the Custom Module
Here’s a React Native component example leveraging the module:
Conclusion
Custom native modules are ideal when libraries fall short or become deprecated. A native Apple Pay integration:
Ensures a secure payment flow.
Aligns with Apple’s standards.
Offers control and flexibility for unique project needs.
This approach equips you with a robust, scalable solution for seamless payments in React Native apps.
Top comments (0)