Skip to content

Commit

Permalink
feat(app, expo): support rn77 AppDelegate.swift in config plugin (#8324)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
gausie and mikehardy authored Feb 11, 2025
1 parent 3393306 commit 6a7867c
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 27 deletions.
38 changes: 38 additions & 0 deletions packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
"
`;
30 changes: 30 additions & 0 deletions packages/app/plugin/__tests__/fixtures/AppDelegate_sdk45.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 14 additions & 12 deletions packages/app/plugin/__tests__/iosPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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 () {
Expand Down
77 changes: 62 additions & 15 deletions packages/app/plugin/src/ios/appDelegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Firebase/Firebase.h>')) {
contents = contents.replace(
Expand Down Expand Up @@ -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 => {
Expand Down

0 comments on commit 6a7867c

Please sign in to comment.