From 6a7867c9366b851a6de62cc37b7834090caad98b Mon Sep 17 00:00:00 2001 From: Samuel Gaus Date: Tue, 11 Feb 2025 20:54:13 +0000 Subject: [PATCH] feat(app, expo): support rn77 AppDelegate.swift in config plugin (#8324) * Support Swift in expo plugin for @react-native-firebase/app * Remove fallback tester as we have no example code that would require it currently * Add tests for swift appdelegate * style(lint): `yarn lint:js --fix` --------- Co-authored-by: Mike Hardy --- .../__snapshots__/iosPlugin.test.ts.snap | 38 +++++++++ .../fixtures/AppDelegate_sdk45.swift | 30 ++++++++ .../app/plugin/__tests__/iosPlugin.test.ts | 26 ++++--- packages/app/plugin/src/ios/appDelegate.ts | 77 +++++++++++++++---- 4 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 packages/app/plugin/__tests__/fixtures/AppDelegate_sdk45.swift diff --git a/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap b/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap index e56203d81f..5afffcc8c7 100644 --- a/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap +++ b/packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap @@ -481,3 +481,41 @@ exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 1`] = ` @end " `; + +exports[`Config Plugin iOS Tests works with Swift AppDelegate (RN 0.77+) 1`] = ` +"import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import FirebaseCore + +@main +class AppDelegate: RCTAppDelegate { + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-10e8520570672fd76b2403b7e1e27f5198a6349a +FirebaseApp.configure() +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + self.moduleName = "HelloWorld" + self.dependencyProvider = RCTAppDependencyProvider() + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = [:] + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} +" +`; diff --git a/packages/app/plugin/__tests__/fixtures/AppDelegate_sdk45.swift b/packages/app/plugin/__tests__/fixtures/AppDelegate_sdk45.swift new file mode 100644 index 0000000000..86b299bcce --- /dev/null +++ b/packages/app/plugin/__tests__/fixtures/AppDelegate_sdk45.swift @@ -0,0 +1,30 @@ +import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider + +@main +class AppDelegate: RCTAppDelegate { + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + self.moduleName = "HelloWorld" + self.dependencyProvider = RCTAppDependencyProvider() + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = [:] + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/packages/app/plugin/__tests__/iosPlugin.test.ts b/packages/app/plugin/__tests__/iosPlugin.test.ts index 7724dc3bb2..b1edef9ae0 100644 --- a/packages/app/plugin/__tests__/iosPlugin.test.ts +++ b/packages/app/plugin/__tests__/iosPlugin.test.ts @@ -4,7 +4,11 @@ import fs from 'fs/promises'; import path from 'path'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -import { modifyAppDelegateAsync, modifyObjcAppDelegate } from '../src/ios/appDelegate'; +import { + modifyAppDelegateAsync, + modifyObjcAppDelegate, + modifySwiftAppDelegate, +} from '../src/ios/appDelegate'; describe('Config Plugin iOS Tests', function () { beforeEach(function () { @@ -74,17 +78,15 @@ describe('Config Plugin iOS Tests', function () { ); }); - it("doesn't support Swift AppDelegate", async function () { - jest.spyOn(fs, 'writeFile').mockImplementation(async () => {}); - - const appDelegateFileInfo: AppDelegateProjectFile = { - path: '.', - language: 'swift', - contents: 'some dummy content', - }; - - await expect(modifyAppDelegateAsync(appDelegateFileInfo)).rejects.toThrow(); - expect(fs.writeFile).not.toHaveBeenCalled(); + it('works with Swift AppDelegate (RN 0.77+)', async function () { + const appDelegate = await fs.readFile( + path.join(__dirname, './fixtures/AppDelegate_sdk45.swift'), + { + encoding: 'utf8', + }, + ); + const result = modifySwiftAppDelegate(appDelegate); + expect(result).toMatchSnapshot(); }); it('does not add the firebase import multiple times', async function () { diff --git a/packages/app/plugin/src/ios/appDelegate.ts b/packages/app/plugin/src/ios/appDelegate.ts index 3f61dd5eb5..f7c740f39e 100644 --- a/packages/app/plugin/src/ios/appDelegate.ts +++ b/packages/app/plugin/src/ios/appDelegate.ts @@ -3,17 +3,17 @@ import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import fs from 'fs'; -const methodInvocationBlock = `[FIRApp configure];`; -// https://regex101.com/r/mPgaq6/1 -const methodInvocationLineMatcher = - /(?:self\.moduleName\s*=\s*@\"([^"]*)\";)|(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[(\[RCTBridge alloc\]|self\.reactDelegate))/g; +export function modifyObjcAppDelegate(contents: string): string { + const methodInvocationBlock = `[FIRApp configure];`; + // https://regex101.com/r/mPgaq6/1 + const methodInvocationLineMatcher = + /(?:self\.moduleName\s*=\s*@\"([^"]*)\";)|(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[(\[RCTBridge alloc\]|self\.reactDelegate))/g; -// https://regex101.com/r/nHrTa9/1/ -// if the above regex fails, we can use this one as a fallback: -const fallbackInvocationLineMatcher = - /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g; + // https://regex101.com/r/nHrTa9/1/ + // if the above regex fails, we can use this one as a fallback: + const fallbackInvocationLineMatcher = + /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g; -export function modifyObjcAppDelegate(contents: string): string { // Add import if (!contents.includes('#import ')) { contents = contents.replace( @@ -68,16 +68,63 @@ export function modifyObjcAppDelegate(contents: string): string { } } +export function modifySwiftAppDelegate(contents: string): string { + const methodInvocationBlock = `FirebaseApp.configure()`; + const methodInvocationLineMatcher = /(?:self\.moduleName\s*=\s*"([^"]*)")/g; + + // Add import + if (!contents.includes('import FirebaseCore')) { + contents = contents.replace( + /import ReactAppDependencyProvider/g, + `import ReactAppDependencyProvider +import FirebaseCore`, + ); + } + + // To avoid potential issues with existing changes from older plugin versions + if (contents.includes(methodInvocationBlock)) { + return contents; + } + + if (!methodInvocationLineMatcher.test(contents)) { + WarningAggregator.addWarningIOS( + '@react-native-firebase/app', + 'Unable to determine correct Firebase insertion point in AppDelegate.swift. Skipping Firebase addition.', + ); + return contents; + } + + // Add invocation + return mergeContents({ + tag: '@react-native-firebase/app-didFinishLaunchingWithOptions', + src: contents, + newSrc: methodInvocationBlock, + anchor: methodInvocationLineMatcher, + offset: 0, // new line will be inserted right above matched anchor + comment: '//', + }).contents; +} + export async function modifyAppDelegateAsync(appDelegateFileInfo: AppDelegateProjectFile) { const { language, path, contents } = appDelegateFileInfo; - if (['objc', 'objcpp'].includes(language)) { - const newContents = modifyObjcAppDelegate(contents); - await fs.promises.writeFile(path, newContents); - } else { - // TODO: Support Swift - throw new Error(`Cannot add Firebase code to AppDelegate of language "${language}"`); + let newContents = contents; + + switch (language) { + case 'objc': + case 'objcpp': { + newContents = modifyObjcAppDelegate(contents); + break; + } + case 'swift': { + newContents = modifySwiftAppDelegate(contents); + break; + } + default: + throw new Error(`Cannot add Firebase code to AppDelegate of language "${language}"`); } + + await fs.promises.writeFile(path, newContents); } export const withFirebaseAppDelegate: ConfigPlugin = config => {