UNPKG

react-native-expo-braintree

Version:

React native and expo wrapper around braintree sdk fro android and ios

310 lines (282 loc) 10.7 kB
/* eslint-disable no-bitwise */ import { IOSConfig, WarningAggregator, withAppDelegate, withInfoPlist, type ConfigPlugin, type ExportedConfigWithProps, } from '@expo/config-plugins'; import eol from 'eol'; import type { ExpoBraintreePluginProps } from './withExpoBraintree'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; export type AppleLanguage = 'objc' | 'objcpp' | 'swift' | 'rb'; /* * Mods for Info.plist */ export const withExpoBraintreePlist: ConfigPlugin = (expoConfig) => { return withInfoPlist(expoConfig, (config) => { const bundleIdentifier = config.ios?.bundleIdentifier ?? ''; const bundleIdentifierWithBraintreeSchema = `${bundleIdentifier}.braintree`; const bundleUrlTypes = config.modResults.CFBundleURLTypes ?? []; // Check if an entry with the specific Braintree URL scheme already exists const isBraintreeEntryNotExist = !bundleUrlTypes.find((urlType) => { return urlType.CFBundleURLSchemes?.includes( bundleIdentifierWithBraintreeSchema ); }); // If Braintree entry doesn't exist, add a new one if (isBraintreeEntryNotExist) { bundleUrlTypes.push({ CFBundleURLSchemes: [bundleIdentifierWithBraintreeSchema], }); } // Assign the modified bundleUrlTypes back to the config config.modResults.CFBundleURLTypes = bundleUrlTypes; return config; }); }; /* * Add allowlist Venmo URL scheme * @see https://developer.paypal.com/braintree/docs/guides/venmo/client-side/ios/v6#allowlist-venmo-url-scheme */ export const withVenmoScheme: ConfigPlugin = (expoConfig) => { return withInfoPlist(expoConfig, (config) => { // Ensure LSApplicationQueriesSchemes exists in Info.plist config.modResults.LSApplicationQueriesSchemes = config.modResults.LSApplicationQueriesSchemes || []; // Hardcoded scheme for Venmo const venmoScheme = 'com.venmo.touch.v2'; // Add the Venmo scheme to the LSApplicationQueriesSchemes array if not already present if (!config.modResults.LSApplicationQueriesSchemes.includes(venmoScheme)) { config.modResults.LSApplicationQueriesSchemes.push(venmoScheme); } return config; }); }; /* * Mods for Info.plist */ /** * Mods for AppDelegate.swift React Native above 0.77.x and Expo above 53 */ export const modifyAppDelegateSwift = ( config: ExportedConfigWithProps<AppDelegateProjectFile> ) => { const appDelegate = config.modResults; let contents = eol.split(appDelegate.contents); // Step 1 Add method to properly handle openUrl method in AppDelegate.m // func application( // _ application: UIApplication, // open url: URL, // options: [UIApplication.OpenURLOptionsKey : Any] = [:] // ) -> Bool { // if url.scheme?.localizedCaseInsensitiveCompare( // ExpoBraintreeConfig.paymentURLScheme // ) == .orderedSame { // return ExpoBraintreeConfig.handleUrl(url: url) // } // return RCTLinkingManager.application( // application, // open: url, // options: options // ) // } // Step 1.1 // We need to try to find if in AppDelegate.swift there is already an application method to handle url // we also need to be sure that we do not break other plugins as well const openUrlIdentifier = `options: [UIApplication.OpenURLOptionsKey: Any] = [:]`; const expoBraintreeConfigHandleUrl = `return ExpoBraintreeConfig.handleUrl(url: url)`; const expoBraintreeOpenUrlLines = [ ` // @generated by react-native-expo-braintree (DO NOT MODIFY)`, ` if url.scheme?.localizedCaseInsensitiveCompare(`, ` ExpoBraintreeConfig.paymentURLScheme`, ` ) == .orderedSame {`, ` ${expoBraintreeConfigHandleUrl}`, ` }`, ` // @generated by react-native-expo-braintree (DO NOT MODIFY)`, ]; const openUrlIdentifierElementIndex = contents.findIndex((content) => content.includes(openUrlIdentifier) ); // Step 2 If openUrlIdentifierElementIndex exist in AppDelegate.swift then we only need to add expoBraintreeOpenUrlLines at the top of that method /* eslint-disable no-extra-boolean-cast */ if (!!~openUrlIdentifierElementIndex) { contents.splice( // We are adding +1 to the index to insert content after ') -> Bool {' block openUrlIdentifierElementIndex + 2, 0, ...expoBraintreeOpenUrlLines ); } // Step 3 If openUrlIdentifierElementIndex do not exist in AppDelegate.swift add a warning that something went wrong else { WarningAggregator.addWarningIOS( 'withExpoBraintree', `Unable to find "options: [UIApplication.OpenURLOptionsKey: Any] = [:]" in AppDelegate.swift, automatic changes not applied expo-braintree might not working as expected` ); } const expoBraintreeConfigHandleUrlElementIndex = contents.findIndex( (content) => content.includes(expoBraintreeConfigHandleUrl) ); // If openUrlMethodElementIndex exist in AppDelegate.mm and expoBraintreeOpenUrlLineIndex do not exist if (!~expoBraintreeConfigHandleUrlElementIndex) { WarningAggregator.addWarningIOS( 'withExpoBraintree', `Unable to find "return ExpoBraintreeConfig.handleUrl(url: url)" in AppDelegate.swift, automatic changes not applied expo-braintree might not working as expected` ); } return contents; }; /** * Mods for AppDelegate.swift */ /** * Mods for AppDelegate.mm / AppDelegate.m React Native below 0.77.x and Expo below 53 */ export const modifyAppDelegateObjectiveC = ( config: ExportedConfigWithProps<AppDelegateProjectFile>, xCodeProjectAppName?: string ) => { if (!xCodeProjectAppName) { WarningAggregator.addWarningIOS( 'withExpoBraintree', `xCodeProjectAppName props not provided but in case of using plugin in objective c world it is required` ); } const appDelegate = config.modResults; let contents = eol.split(appDelegate.contents); // Step 1 Edit Import part // Editing import part for -swift.h file to be able to use Braintree const importSwiftHeaderFileContent = `#import "${xCodeProjectAppName}-Swift.h"`; const importSwiftHeaderFileIndex = contents.findIndex((content) => content.includes(importSwiftHeaderFileContent) ); // If importSwiftHeaderFileContent do not exist in AppDelegate.mm if (!~importSwiftHeaderFileIndex) { contents = [importSwiftHeaderFileContent, ...contents]; } const importExpoModulesSwiftHeader = `#import "ExpoModulesCore-Swift.h"`; const importExpoModulesSwiftHeaderFileIndex = contents.findIndex((content) => content.includes(importExpoModulesSwiftHeader) ); // If importExpoModulesSwiftHeader do not exist in AppDelegate.mm if (!~importExpoModulesSwiftHeaderFileIndex) { contents = [importExpoModulesSwiftHeader, ...contents]; } // Step 2 Add method to properly handle openUrl method in AppDelegate.m const openUrlMethod = '- (BOOL)application:(UIApplication *)application openURL'; const expoBraintreeOpenUrlLines = [ ' if ([url.scheme localizedCaseInsensitiveCompare:[BraintreeExpoConfig getPaymentUrlScheme]] == NSOrderedSame) {', ' return [BraintreeExpoConfig handleUrl:url];', ' }', ]; const openUrlMethodElementIndex = contents.findIndex((content) => content.includes(openUrlMethod) ); const expoBraintreeOpenUrlLineIndex = contents.findIndex((content) => content.includes(expoBraintreeOpenUrlLines?.[0] ?? '') ); // If openUrlMethodElementIndex exist in AppDelegate.mm and expoBraintreeOpenUrlLineIndex do not exist if (!~expoBraintreeOpenUrlLineIndex && !!~openUrlMethodElementIndex) { contents.splice( // We are adding +1 to the index to insert content after '{' block openUrlMethodElementIndex + 1, 0, ...expoBraintreeOpenUrlLines ); } return contents; }; /** * Mods for AppDelegate.mm */ export const withExpoBraintreeAppDelegate: ConfigPlugin< ExpoBraintreePluginProps > = (expoConfig, { xCodeProjectAppName }) => { let appDelegateLanguage: AppleLanguage | null = null; return withAppDelegate(expoConfig, (config) => { appDelegateLanguage = config.modResults.language; switch (appDelegateLanguage) { case 'objc': case 'objcpp': const resultObjectiVeC = modifyAppDelegateObjectiveC( config, xCodeProjectAppName ); config.modResults.contents = resultObjectiVeC.join('\n'); return config; case 'swift': const resultSwift = modifyAppDelegateSwift(config); config.modResults.contents = resultSwift.join('\n'); return config; default: WarningAggregator.addWarningIOS( 'withExpoBraintree', `${appDelegateLanguage} AppDelegate file is not supported yet` ); return config; } }); }; // Add a new wrapper Swift file to the Xcode project for Swift compatibility. export const withBraintreeWrapperFile: ConfigPlugin<{ appDelegateLanguage: AppleLanguage | null; }> = (config, { appDelegateLanguage }) => { const braintreeWrapperObjectiveC = [ 'import Braintree', 'import Foundation', '', '@objc public class BraintreeExpoConfig: NSObject {', '', '@objc(getPaymentUrlScheme)', 'public static func getPaymentUrlScheme() -> String {', ' let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""', ' return bundleIdentifier + ".braintree"', '}', '', '@objc(handleUrl:)', 'public static func handleUrl(url: URL) -> Bool {', ' return BTAppContextSwitcher.sharedInstance.handleOpen(url)', '}', '}', ]; const braintreeWrapperSwift = [ 'import Braintree', 'import Foundation', '', 'public final class ExpoBraintreeConfig {', '', ' private init() {}', '', ' public static var paymentURLScheme: String {', ' let bundleIdentifier = Bundle.main.bundleIdentifier ?? ""', ' return bundleIdentifier + ".braintree"', ' }', '', ' public static func handleUrl(url: URL) -> Bool {', ' return BTAppContextSwitcher.sharedInstance.handleOpen(url)', ' }', '}', ]; switch (appDelegateLanguage) { case 'objc': case 'objcpp': return IOSConfig.XcodeProjectFile.withBuildSourceFile(config, { filePath: 'ExpoBraintreeConfig.swift', contents: braintreeWrapperObjectiveC.join('\n'), }); case 'swift': return IOSConfig.XcodeProjectFile.withBuildSourceFile(config, { filePath: 'ExpoBraintreeConfig.swift', contents: braintreeWrapperSwift.join('\n'), }); default: WarningAggregator.addWarningIOS( 'withExpoBraintree', `${appDelegateLanguage} AppDelegate file is not supported yet` ); return config; } };