From 22ac977a21b1d3a30fa0bf7c698cde06c60ccc35 Mon Sep 17 00:00:00 2001 From: Mike Gray Date: Thu, 24 Apr 2025 11:15:01 -0400 Subject: [PATCH] feat(auth, expo): add support for AppDelegate.swift --- .../iosPlugin_openUrlFix.test.ts.snap | 52 +++++++++++ .../AppDelegate_noOpenURL_sdk53.swift | 33 +++++++ .../fixtures/AppDelegate_sdk53.swift | 42 +++++++++ .../__tests__/iosPlugin_openUrlFix.test.ts | 26 ++++-- packages/auth/plugin/src/ios/openUrlFix.ts | 91 +++++++++++++------ 5 files changed, 208 insertions(+), 36 deletions(-) create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_noOpenURL_sdk53.swift create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk53.swift diff --git a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap index 7d6290c03a..50f0890330 100644 --- a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap +++ b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap @@ -324,6 +324,58 @@ exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - App " `; +exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - AppDelegate_sdk53.swift 1`] = ` +"import React +import Expo + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + self.moduleName = "HelloWorld" + self.initialProps = [:] + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + public override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) + if url.host.toLowerCase() == "firebaseauth" { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return false + } +// @generated end @react-native-firebase/auth-openURL + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} +" +`; + exports[`Config Plugin iOS Tests - openUrlFix must match positiveTemplateCases[0] 1`] = ` "- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { // @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) sync-5e029a87ac71df3ca5665387eb712d1b32274c6a diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_noOpenURL_sdk53.swift b/packages/auth/plugin/__tests__/fixtures/AppDelegate_noOpenURL_sdk53.swift new file mode 100644 index 0000000000..fd8c4f7bde --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_noOpenURL_sdk53.swift @@ -0,0 +1,33 @@ +import React +import Expo + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + self.moduleName = "HelloWorld" + self.initialProps = [:] + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + public override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk53.swift b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk53.swift new file mode 100644 index 0000000000..4c9c927d35 --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk53.swift @@ -0,0 +1,42 @@ +import React +import Expo + +@UIApplicationMain +public class AppDelegate: ExpoAppDelegate { + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + self.moduleName = "HelloWorld" + self.initialProps = [:] + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + public override func bundleURL() -> URL? { +#if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") +#else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } + + // Linking API + public override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + public override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } +} diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index fa2f16fa4c..ee4f6e8386 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -10,6 +10,7 @@ import { ensureFirebaseSwizzlingIsEnabled, appDelegateOpenUrlInsertionPointAfter, multiline_appDelegateOpenUrlInsertionPointAfter, + modifyAppDelegate, } from '../src/ios/openUrlFix'; import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix'; @@ -188,15 +189,16 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { }); const appDelegateFixturesPatch = [ - 'AppDelegate_bare_sdk43.m', - 'AppDelegate_sdk44.m', - 'AppDelegate_sdk45.mm', + { fixtureName: 'AppDelegate_bare_sdk43.m', language: 'objc' }, + { fixtureName: 'AppDelegate_sdk44.m', language: 'objc' }, + { fixtureName: 'AppDelegate_sdk45.mm', language: 'objcpp' }, + { fixtureName: 'AppDelegate_sdk53.swift', language: 'swift' }, ]; - appDelegateFixturesPatch.forEach(fixtureName => { + appDelegateFixturesPatch.forEach(({ fixtureName, language }) => { it(`munges AppDelegate correctly - ${fixtureName}`, async () => { const fixturePath = path.join(__dirname, 'fixtures', fixtureName); const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); - const result = modifyObjcAppDelegate(appDelegate); + const result = modifyAppDelegate(appDelegate, language); expect(result).toMatchSnapshot(); }); @@ -209,7 +211,7 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, modResults: { path: fixturePath, - language: 'objc', + language: language, contents: appDelegate, } as AppDelegateProjectFile, modRawConfig: { name: 'TestName', slug: 'TestSlug' }, @@ -226,8 +228,12 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { }); }); - const appDelegateFixturesNoop = ['AppDelegate_sdk42.m', 'AppDelegate_fallback.m']; - appDelegateFixturesNoop.forEach(fixtureName => { + const appDelegateFixturesNoop = [ + { fixtureName: 'AppDelegate_sdk42.m', language: 'objc' }, + { fixtureName: 'AppDelegate_fallback.m', language: 'objc' }, + { fixtureName: 'AppDelegate_noOpenURL_sdk53.swift', language: 'swift' }, + ]; + appDelegateFixturesNoop.forEach(({ fixtureName, language }) => { it(`skips AppDelegate without openURL - ${fixtureName}`, async () => { const fixturePath = path.join(__dirname, 'fixtures', fixtureName); const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); @@ -238,7 +244,7 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, modResults: { path: fixturePath, - language: 'objc', + language: language, contents: appDelegate, } as AppDelegateProjectFile, modRawConfig: { name: 'TestName', slug: 'TestSlug' }, @@ -264,7 +270,7 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, modResults: { path: fixturePath, - language: 'objc', + language: language, contents: appDelegate, } as AppDelegateProjectFile, modRawConfig: { name: 'TestName', slug: 'TestSlug' }, diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index eb05f21edf..d68bb071c5 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -62,35 +62,40 @@ export function withOpenUrlFixForAppDelegate({ const { language, contents } = config.modResults; const configValue = props?.ios?.captchaOpenUrlFix || 'default'; - if (['objc', 'objcpp'].includes(language)) { - const newContents = modifyObjcAppDelegate(contents); - if (newContents === null) { - if (configValue === true) { - throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found"); - } else { - WarningAggregator.addWarningIOS( - '@react-native-firebase/auth', - "Skipping iOS openURL fix because no 'openURL' method was found", - ); - return config; - } + const newContents = modifyAppDelegate(contents, language); + if (newContents === null) { + if (configValue === true) { + throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found"); } else { - if (configValue === 'default') { - WarningAggregator.addWarningIOS( - '@react-native-firebase/auth', - 'modifying iOS AppDelegate openURL method to ignore firebaseauth reCAPTCHA redirect URLs', - ); - } - return { - ...config, - modResults: { - ...config.modResults, - contents: newContents, - }, - }; + WarningAggregator.addWarningIOS( + '@react-native-firebase/auth', + "Skipping iOS openURL fix because no 'openURL' method was found", + ); + return config; + } + } else { + if (configValue === 'default') { + WarningAggregator.addWarningIOS( + '@react-native-firebase/auth', + 'modifying iOS AppDelegate openURL method to ignore firebaseauth reCAPTCHA redirect URLs', + ); } + return { + ...config, + modResults: { + ...config.modResults, + contents: newContents, + }, + }; + } +} + +export function modifyAppDelegate(contents: string, language: string): string | null { + if (language === 'objc' || language === 'objcpp') { + return modifyObjcAppDelegate(contents); + } else if (language === 'swift') { + return modifySwiftAppDelegate(contents); } else { - // TODO: Support Swift throw new Error(`Don't know how to apply openUrlFix to AppDelegate of language "${language}"`); } } @@ -147,6 +152,40 @@ export function modifyObjcAppDelegate(contents: string): string | null { }).contents; } +// NOTE: `mergeContents()` doesn't support newlines for the `anchor` regex, so we have to replace it manually +const skipOpenUrlForFirebaseAuthBlockSwift: string = `\ +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) + if url.host.toLowerCase() == "firebaseauth" { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return false + } +// @generated end @react-native-firebase/auth-openURL\ +`; + +export const appDelegateSwiftOpenUrlInsertionPointAfter: RegExp = + /public\s*override\s*func\s*application\s*\(\n\s*_\s*app\s*:\s*UIApplication,\n\s*open\s*url\s*:\s*URL,\n\s*options\s*:\s*\[UIApplication\.OpenURLOptionsKey\s*:\s*Any\]\s*=\s*\[:\]\n\s*\)\s*->\s*Bool\s*{\n/; + +export function modifySwiftAppDelegate(contents: string): string | null { + const pattern = appDelegateSwiftOpenUrlInsertionPointAfter; + const fullMatch = contents.match(pattern); + if (!fullMatch) { + if (contents.match(/open url\s*:/)) { + throw new Error( + [ + "Failed to apply 'captchaOpenUrlFix' but detected 'openURL' method.", + "Please manually apply the fix to your AppDelegate's openURL method,", + "then update your app.config.json by configuring the '@react-native-firebase/auth' plugin", + 'to set `captchaOpenUrlFix: false`.', + ].join(' '), + ); + } else { + // openURL method was not found in AppDelegate + return null; + } + } + return contents.replace(pattern, `${fullMatch[0]}${skipOpenUrlForFirebaseAuthBlockSwift}\n`); +} + export type ExpoConfigPluginEntry = string | [] | [string] | [string, any]; // Search the ExpoConfig plugins array to see if `pluginName` is present