Skip to content

feat(auth, expo): add support for AppDelegate.swift #8490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIApplicationOpenURLOptionsKey,id> *)options {
// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) sync-5e029a87ac71df3ca5665387eb712d1b32274c6a
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
42 changes: 42 additions & 0 deletions packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk53.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 16 additions & 10 deletions packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ensureFirebaseSwizzlingIsEnabled,
appDelegateOpenUrlInsertionPointAfter,
multiline_appDelegateOpenUrlInsertionPointAfter,
modifyAppDelegate,
} from '../src/ios/openUrlFix';
import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix';

Expand Down Expand Up @@ -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();
});

Expand All @@ -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' },
Expand All @@ -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' });
Expand All @@ -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' },
Expand All @@ -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' },
Expand Down
91 changes: 65 additions & 26 deletions packages/auth/plugin/src/ios/openUrlFix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,40 @@
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}"`);
}
}
Expand Down Expand Up @@ -147,6 +152,40 @@
}).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(

Check warning on line 173 in packages/auth/plugin/src/ios/openUrlFix.ts

View check run for this annotation

Codecov / codecov/patch

packages/auth/plugin/src/ios/openUrlFix.ts#L173

Added line #L173 was not covered by tests
[
"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
Expand Down
Loading