From 97dc2a92e3989abb35c272c5216fd23513b213d3 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 06:26:32 -0800 Subject: [PATCH 01/19] fix(auth, iOS): Expo plugin: don't invoke expo-router for openURL from recaptcha --- packages/auth/plugin/src/ios/urlTypes.ts | 54 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/auth/plugin/src/ios/urlTypes.ts b/packages/auth/plugin/src/ios/urlTypes.ts index da31a70229..f1a59ac3af 100644 --- a/packages/auth/plugin/src/ios/urlTypes.ts +++ b/packages/auth/plugin/src/ios/urlTypes.ts @@ -2,17 +2,24 @@ import { ConfigPlugin, IOSConfig, withInfoPlist, + withAppDelegate, ExportedConfigWithProps, } from '@expo/config-plugins'; +import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import fs from 'fs'; import path from 'path'; import plist from 'plist'; // does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project export const withIosCaptchaUrlTypes: ConfigPlugin = config => { - return withInfoPlist(config, config => { + config = withInfoPlist(config, config => { return setUrlTypesForCaptcha({ config }); }); + config = withAppDelegate(config, config => { + return patchOpenUrlForCaptcha({ config }); + }); + return config; }; function getReversedClientId(googleServiceFilePath: string): string { @@ -88,3 +95,48 @@ export function setUrlTypesForCaptcha({ return config; } + +const skipOpenUrlForFirebaseAuthBlock = `\ + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + }\ +`; + +// NOTE: `mergeContents()` requires that this pattern not match newlines +const appDelegateOpenUrlInsertionPointAfter = + /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 + +function patchOpenUrlForCaptcha({ config }: { + config: ExportedConfigWithProps; +}) { + const {contents} = config.modResults; + const multilineMatcher = new RegExp(appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n'); + const fullMatch = contents.match(multilineMatcher); + if(!fullMatch) { + throw new Error("Failed to find insertion point; expected newline after '{'"); + } + const fullMatchNumLines = fullMatch[0].split('\n').length; + const offset = fullMatchNumLines - 1; + if(offset < 0) { + throw new Error(`Failed to find insertion point; fullMatchNumLines=${fullMatchNumLines}`); + } + + const newContents = mergeContents({ + tag: '@react-native-firebase/auth-openURL', + src: contents, + newSrc: skipOpenUrlForFirebaseAuthBlock , + anchor: appDelegateOpenUrlInsertionPointAfter, + offset, + comment: '//', + }).contents; + + const newConfig = { + ...config, + modResults: { + ...config.modResults, + contents: newContents, + }, + }; + return newConfig; +} From 8980b5d28d9cfcca58a44001378c7fa84d298bc0 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 16:48:12 -0800 Subject: [PATCH 02/19] add a check for expo-router before patching AppDelegate --- packages/auth/plugin/src/ios/urlTypes.ts | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/auth/plugin/src/ios/urlTypes.ts b/packages/auth/plugin/src/ios/urlTypes.ts index f1a59ac3af..1e87a21fce 100644 --- a/packages/auth/plugin/src/ios/urlTypes.ts +++ b/packages/auth/plugin/src/ios/urlTypes.ts @@ -5,6 +5,7 @@ import { withAppDelegate, ExportedConfigWithProps, } from '@expo/config-plugins'; +import type { ExpoConfig } from '@expo/config/build/Config.types'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import fs from 'fs'; @@ -16,9 +17,13 @@ export const withIosCaptchaUrlTypes: ConfigPlugin = config => { config = withInfoPlist(config, config => { return setUrlTypesForCaptcha({ config }); }); - config = withAppDelegate(config, config => { - return patchOpenUrlForCaptcha({ config }); - }); + + if(isPluginEnabled(config, "expo-router")) { + config = withAppDelegate(config, config => { + return patchOpenUrlForCaptcha({ config }); + }); + } + return config; }; @@ -140,3 +145,19 @@ function patchOpenUrlForCaptcha({ config }: { }; return newConfig; } + +// Search the ExpoConfig plugins array to see if `pluginName` is present +function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { + if(config.plugins === undefined) { + return false; + } + return config.plugins.some((plugin: string | [] | [string] | [string, any]) => { + if(plugin === pluginName) { + return true; + } else if(Array.isArray(plugin) && plugin.length >= 1 && plugin[0] === pluginName) { + return true; + } else { + return false; + } + }); +} From df4e5b5db1f145e293bd667edb238ef33faef8aa Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 17:19:11 -0800 Subject: [PATCH 03/19] move ios openUrl fix to separate file --- packages/auth/plugin/src/index.ts | 3 +- packages/auth/plugin/src/ios/index.ts | 3 +- packages/auth/plugin/src/ios/openUrlFix.ts | 78 ++++++++++++++++++++++ packages/auth/plugin/src/ios/urlTypes.ts | 75 +-------------------- 4 files changed, 83 insertions(+), 76 deletions(-) create mode 100644 packages/auth/plugin/src/ios/openUrlFix.ts diff --git a/packages/auth/plugin/src/index.ts b/packages/auth/plugin/src/index.ts index 15e82ad889..685179030f 100644 --- a/packages/auth/plugin/src/index.ts +++ b/packages/auth/plugin/src/index.ts @@ -1,6 +1,6 @@ import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; -import { withIosCaptchaUrlTypes } from './ios'; +import { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix } from './ios'; /** * A config plugin for configuring `@react-native-firebase/auth` @@ -9,6 +9,7 @@ const withRnFirebaseAuth: ConfigPlugin = config => { return withPlugins(config, [ // iOS withIosCaptchaUrlTypes, + withIosCaptchaOpenUrlFix, ]); }; diff --git a/packages/auth/plugin/src/ios/index.ts b/packages/auth/plugin/src/ios/index.ts index 611ed02250..d63f3a971b 100644 --- a/packages/auth/plugin/src/ios/index.ts +++ b/packages/auth/plugin/src/ios/index.ts @@ -1,3 +1,4 @@ import { withIosCaptchaUrlTypes } from './urlTypes'; +import { withIosCaptchaOpenUrlFix } from './openUrlFix'; -export { withIosCaptchaUrlTypes }; +export { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix }; diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts new file mode 100644 index 0000000000..ea310b57e3 --- /dev/null +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -0,0 +1,78 @@ +import { ConfigPlugin, withAppDelegate, ExportedConfigWithProps } from '@expo/config-plugins'; +import type { ExpoConfig } from '@expo/config/build/Config.types'; +import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; + +export const withIosCaptchaOpenUrlFix: ConfigPlugin = config => { + if (isPluginEnabled(config, 'expo-router')) { + config = withAppDelegate(config, config => { + return patchOpenUrlForCaptcha({ config }); + }); + } + return config; +}; + +const skipOpenUrlForFirebaseAuthBlock = `\ + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + }\ +`; + +// NOTE: `mergeContents()` requires that this pattern not match newlines +const appDelegateOpenUrlInsertionPointAfter = + /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 + +function patchOpenUrlForCaptcha({ + config, +}: { + config: ExportedConfigWithProps; +}) { + const { contents } = config.modResults; + const multilineMatcher = new RegExp( + appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', + ); + const fullMatch = contents.match(multilineMatcher); + if (!fullMatch) { + throw new Error("Failed to find insertion point; expected newline after '{'"); + } + const fullMatchNumLines = fullMatch[0].split('\n').length; + const offset = fullMatchNumLines - 1; + if (offset < 0) { + throw new Error(`Failed to find insertion point; fullMatchNumLines=${fullMatchNumLines}`); + } + + const newContents = mergeContents({ + tag: '@react-native-firebase/auth-openURL', + src: contents, + newSrc: skipOpenUrlForFirebaseAuthBlock, + anchor: appDelegateOpenUrlInsertionPointAfter, + offset, + comment: '//', + }).contents; + + const newConfig = { + ...config, + modResults: { + ...config.modResults, + contents: newContents, + }, + }; + return newConfig; +} + +// Search the ExpoConfig plugins array to see if `pluginName` is present +function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { + if (config.plugins === undefined) { + return false; + } + return config.plugins.some((plugin: string | [] | [string] | [string, any]) => { + if (plugin === pluginName) { + return true; + } else if (Array.isArray(plugin) && plugin.length >= 1 && plugin[0] === pluginName) { + return true; + } else { + return false; + } + }); +} diff --git a/packages/auth/plugin/src/ios/urlTypes.ts b/packages/auth/plugin/src/ios/urlTypes.ts index 1e87a21fce..da31a70229 100644 --- a/packages/auth/plugin/src/ios/urlTypes.ts +++ b/packages/auth/plugin/src/ios/urlTypes.ts @@ -2,29 +2,17 @@ import { ConfigPlugin, IOSConfig, withInfoPlist, - withAppDelegate, ExportedConfigWithProps, } from '@expo/config-plugins'; -import type { ExpoConfig } from '@expo/config/build/Config.types'; -import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; -import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import fs from 'fs'; import path from 'path'; import plist from 'plist'; // does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project export const withIosCaptchaUrlTypes: ConfigPlugin = config => { - config = withInfoPlist(config, config => { + return withInfoPlist(config, config => { return setUrlTypesForCaptcha({ config }); }); - - if(isPluginEnabled(config, "expo-router")) { - config = withAppDelegate(config, config => { - return patchOpenUrlForCaptcha({ config }); - }); - } - - return config; }; function getReversedClientId(googleServiceFilePath: string): string { @@ -100,64 +88,3 @@ export function setUrlTypesForCaptcha({ return config; } - -const skipOpenUrlForFirebaseAuthBlock = `\ - if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { - // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router - return NO; - }\ -`; - -// NOTE: `mergeContents()` requires that this pattern not match newlines -const appDelegateOpenUrlInsertionPointAfter = - /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 - -function patchOpenUrlForCaptcha({ config }: { - config: ExportedConfigWithProps; -}) { - const {contents} = config.modResults; - const multilineMatcher = new RegExp(appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n'); - const fullMatch = contents.match(multilineMatcher); - if(!fullMatch) { - throw new Error("Failed to find insertion point; expected newline after '{'"); - } - const fullMatchNumLines = fullMatch[0].split('\n').length; - const offset = fullMatchNumLines - 1; - if(offset < 0) { - throw new Error(`Failed to find insertion point; fullMatchNumLines=${fullMatchNumLines}`); - } - - const newContents = mergeContents({ - tag: '@react-native-firebase/auth-openURL', - src: contents, - newSrc: skipOpenUrlForFirebaseAuthBlock , - anchor: appDelegateOpenUrlInsertionPointAfter, - offset, - comment: '//', - }).contents; - - const newConfig = { - ...config, - modResults: { - ...config.modResults, - contents: newContents, - }, - }; - return newConfig; -} - -// Search the ExpoConfig plugins array to see if `pluginName` is present -function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { - if(config.plugins === undefined) { - return false; - } - return config.plugins.some((plugin: string | [] | [string] | [string, any]) => { - if(plugin === pluginName) { - return true; - } else if(Array.isArray(plugin) && plugin.length >= 1 && plugin[0] === pluginName) { - return true; - } else { - return false; - } - }); -} From 064bc810a290cf5bf6c0b439cbe402e17811c16c Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 18:02:27 -0800 Subject: [PATCH 04/19] make openUrl fix configurable --- packages/auth/plugin/src/index.ts | 7 ++++--- packages/auth/plugin/src/ios/openUrlFix.ts | 19 +++++++++++++++++-- packages/auth/plugin/src/ios/urlTypes.ts | 3 ++- packages/auth/plugin/src/pluginConfig.ts | 7 +++++++ 4 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 packages/auth/plugin/src/pluginConfig.ts diff --git a/packages/auth/plugin/src/index.ts b/packages/auth/plugin/src/index.ts index 685179030f..be33c86e0a 100644 --- a/packages/auth/plugin/src/index.ts +++ b/packages/auth/plugin/src/index.ts @@ -1,15 +1,16 @@ import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; import { withIosCaptchaUrlTypes, withIosCaptchaOpenUrlFix } from './ios'; +import { PluginConfigType } from './pluginConfig'; /** * A config plugin for configuring `@react-native-firebase/auth` */ -const withRnFirebaseAuth: ConfigPlugin = config => { +const withRnFirebaseAuth: ConfigPlugin = (config, props) => { return withPlugins(config, [ // iOS - withIosCaptchaUrlTypes, - withIosCaptchaOpenUrlFix, + [withIosCaptchaUrlTypes, props], + [withIosCaptchaOpenUrlFix, props], ]); }; diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index ea310b57e3..0d63409bc0 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -2,9 +2,10 @@ import { ConfigPlugin, withAppDelegate, ExportedConfigWithProps } from '@expo/co import type { ExpoConfig } from '@expo/config/build/Config.types'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; +import { PluginConfigType } from '../pluginConfig'; -export const withIosCaptchaOpenUrlFix: ConfigPlugin = config => { - if (isPluginEnabled(config, 'expo-router')) { +export const withIosCaptchaOpenUrlFix: ConfigPlugin = (config, props) => { + if (shouldApplyIosOpenUrlFix(config, props)) { config = withAppDelegate(config, config => { return patchOpenUrlForCaptcha({ config }); }); @@ -12,6 +13,20 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = config => { return config; }; +// Interpret the plugin config to determine whether this fix should be applied +function shouldApplyIosOpenUrlFix(config: ExpoConfig, props: PluginConfigType): boolean { + const flag = props.ios?.captchaOpenUrlFix; + if (flag === undefined || flag === 'default') { + // by default, apply the fix whenever 'expo-router' is detected in the same project + return isPluginEnabled(config, 'expo-router'); + } else if (flag === true || flag === false) { + const isEnabled: boolean = flag; + return isEnabled; + } else { + throw new Error(`Unexpected value for 'captchaOpenUrlFix' config option`); + } +} + const skipOpenUrlForFirebaseAuthBlock = `\ if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router diff --git a/packages/auth/plugin/src/ios/urlTypes.ts b/packages/auth/plugin/src/ios/urlTypes.ts index da31a70229..14f9452e22 100644 --- a/packages/auth/plugin/src/ios/urlTypes.ts +++ b/packages/auth/plugin/src/ios/urlTypes.ts @@ -7,9 +7,10 @@ import { import fs from 'fs'; import path from 'path'; import plist from 'plist'; +import { PluginConfigType } from '../pluginConfig'; // does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project -export const withIosCaptchaUrlTypes: ConfigPlugin = config => { +export const withIosCaptchaUrlTypes: ConfigPlugin = (config, {}) => { return withInfoPlist(config, config => { return setUrlTypesForCaptcha({ config }); }); diff --git a/packages/auth/plugin/src/pluginConfig.ts b/packages/auth/plugin/src/pluginConfig.ts new file mode 100644 index 0000000000..72f00e8853 --- /dev/null +++ b/packages/auth/plugin/src/pluginConfig.ts @@ -0,0 +1,7 @@ +export interface PluginConfigType { + ios?: PluginConfigTypeIos; +} + +export interface PluginConfigTypeIos { + captchaOpenUrlFix?: undefined | boolean | 'default'; +} From 113c1c7366d389774fa7b9960661e03df9699471 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 19:58:01 -0800 Subject: [PATCH 05/19] unit tests for shouldApplyIosOpenUrlFix --- ...s.snap => iosPlugin_urlTypes.test.ts.snap} | 2 +- .../__tests__/iosPlugin_openUrlFix.test.ts | 155 ++++++++++++++++++ ...gin.test.ts => iosPlugin_urlTypes.test.ts} | 2 +- packages/auth/plugin/src/ios/openUrlFix.ts | 16 +- 4 files changed, 169 insertions(+), 6 deletions(-) rename packages/auth/plugin/__tests__/__snapshots__/{iosPlugin.test.ts.snap => iosPlugin_urlTypes.test.ts.snap} (69%) create mode 100644 packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts rename packages/auth/plugin/__tests__/{iosPlugin.test.ts => iosPlugin_urlTypes.test.ts} (97%) diff --git a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_urlTypes.test.ts.snap similarity index 69% rename from packages/auth/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap rename to packages/auth/plugin/__tests__/__snapshots__/iosPlugin_urlTypes.test.ts.snap index 601fd118d3..1e002ff39b 100644 --- a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap +++ b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_urlTypes.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Config Plugin iOS Tests adds url types to the Info.plist 1`] = ` +exports[`Config Plugin iOS Tests - urlTypes adds url types to the Info.plist 1`] = ` { "CFBundleURLTypes": [ { diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts new file mode 100644 index 0000000000..4769bba803 --- /dev/null +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -0,0 +1,155 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { shouldApplyIosOpenUrlFix } from '../src/ios/openUrlFix'; +import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix'; + +describe('Config Plugin iOS Tests - openUrlFix', () => { + beforeEach(function () { + jest.resetAllMocks(); + }); + + it('is disabled by default', async () => { + const config = { + name: 'TestName', + slug: 'TestSlug', + }; + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: {}, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: {}, + }, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: undefined, + }, + }, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: 'default', + }, + }, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: false, + }, + }, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: true, + }, + }, + }), + ).toBe(true); + }); + + it('is enabled by default when expo-router is found', async () => { + const cases: ExpoConfigPluginEntry[] = ['expo-router', ['expo-router'], ['expo-router', {}]]; + + for (const routerPluginEntry of cases) { + const plugins: ExpoConfigPluginEntry[] = [ + 'something', + [], + ['something-else', { foo: 'bar' }], + routerPluginEntry, + ]; + + const config = { + name: 'TestName', + slug: 'TestSlug', + plugins, + }; + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: {}, + }), + ).toBe(true); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: {}, + }, + }), + ).toBe(true); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: undefined, + }, + }, + }), + ).toBe(true); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: 'default', + }, + }, + }), + ).toBe(true); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: false, + }, + }, + }), + ).toBe(false); + + expect( + shouldApplyIosOpenUrlFix({ + config, + props: { + ios: { + captchaOpenUrlFix: true, + }, + }, + }), + ).toBe(true); + } + }); +}); diff --git a/packages/auth/plugin/__tests__/iosPlugin.test.ts b/packages/auth/plugin/__tests__/iosPlugin_urlTypes.test.ts similarity index 97% rename from packages/auth/plugin/__tests__/iosPlugin.test.ts rename to packages/auth/plugin/__tests__/iosPlugin_urlTypes.test.ts index 734b4152c1..3e71e21ba4 100644 --- a/packages/auth/plugin/__tests__/iosPlugin.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_urlTypes.test.ts @@ -2,7 +2,7 @@ import path from 'path'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { setUrlTypesForCaptcha } from '../src/ios/urlTypes'; -describe('Config Plugin iOS Tests', () => { +describe('Config Plugin iOS Tests - urlTypes', () => { beforeEach(function () { jest.resetAllMocks(); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 0d63409bc0..10ba990fcd 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -5,7 +5,7 @@ import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import { PluginConfigType } from '../pluginConfig'; export const withIosCaptchaOpenUrlFix: ConfigPlugin = (config, props) => { - if (shouldApplyIosOpenUrlFix(config, props)) { + if (shouldApplyIosOpenUrlFix({ config, props })) { config = withAppDelegate(config, config => { return patchOpenUrlForCaptcha({ config }); }); @@ -14,7 +14,13 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = (config, }; // Interpret the plugin config to determine whether this fix should be applied -function shouldApplyIosOpenUrlFix(config: ExpoConfig, props: PluginConfigType): boolean { +export function shouldApplyIosOpenUrlFix({ + config, + props, +}: { + config: ExpoConfig; + props: PluginConfigType; +}): boolean { const flag = props.ios?.captchaOpenUrlFix; if (flag === undefined || flag === 'default') { // by default, apply the fix whenever 'expo-router' is detected in the same project @@ -38,7 +44,7 @@ const skipOpenUrlForFirebaseAuthBlock = `\ const appDelegateOpenUrlInsertionPointAfter = /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 -function patchOpenUrlForCaptcha({ +export function patchOpenUrlForCaptcha({ config, }: { config: ExportedConfigWithProps; @@ -76,12 +82,14 @@ function patchOpenUrlForCaptcha({ return newConfig; } +export type ExpoConfigPluginEntry = string | [] | [string] | [string, any]; + // Search the ExpoConfig plugins array to see if `pluginName` is present function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { if (config.plugins === undefined) { return false; } - return config.plugins.some((plugin: string | [] | [string] | [string, any]) => { + return config.plugins.some((plugin: ExpoConfigPluginEntry) => { if (plugin === pluginName) { return true; } else if (Array.isArray(plugin) && plugin.length >= 1 && plugin[0] === pluginName) { From 275697567cafaa939535f005bc6c6693ad21374b Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 20:38:03 -0800 Subject: [PATCH 06/19] add initial AppDelegate tests using fixtures from packages/app/plugin/__tests__ --- .../iosPlugin_openUrlFix.test.ts.snap | 325 ++++++++++++++++++ .../fixtures/AppDelegate_bare_sdk43.m | 86 +++++ .../__tests__/fixtures/AppDelegate_fallback.m | 46 +++ .../__tests__/fixtures/AppDelegate_sdk42.m | 102 ++++++ .../__tests__/fixtures/AppDelegate_sdk44.m | 79 +++++ .../__tests__/fixtures/AppDelegate_sdk45.mm | 129 +++++++ .../__tests__/iosPlugin_openUrlFix.test.ts | 22 +- packages/auth/plugin/src/ios/openUrlFix.ts | 35 +- 8 files changed, 809 insertions(+), 15 deletions(-) create mode 100644 packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_fallback.m create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk42.m create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk44.m create mode 100644 packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk45.mm diff --git a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap new file mode 100644 index 0000000000..e3de802993 --- /dev/null +++ b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap @@ -0,0 +1,325 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - AppDelegate_bare_sdk43.m 1`] = ` +"// This AppDelegate template is used in Expo SDK 43 +// It is (nearly) identical to the pure template used when +// creating a bare React Native app (without Expo) + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil]; + id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTRootViewBackgroundColor"]; + if (rootViewBackgroundColor != nil) { + rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor]; + } else { + rootView.backgroundColor = [UIColor whiteColor]; + } + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) sync-5e029a87ac71df3ca5665387eb712d1b32274c6a + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + } +// @generated end @react-native-firebase/auth-openURL + return [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +@end +" +`; + +exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - AppDelegate_sdk44.m 1`] = ` +"// This AppDelegate prebuild template is used in Expo SDK 44+ +// It has the RCTBridge to be created by Expo ReactDelegate + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; + RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil]; + rootView.backgroundColor = [UIColor whiteColor]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [self.reactDelegate createRootViewController]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) sync-5e029a87ac71df3ca5665387eb712d1b32274c6a + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + } +// @generated end @react-native-firebase/auth-openURL + return [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +@end +" +`; + +exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - AppDelegate_sdk45.mm 1`] = ` +"// RN 0.68.1, Expo SDK 45 template +// The main difference between this and the SDK 44 one is that this is +// using React Native 0.68 and is written in Objective-C++ + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#import + +#if RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import +#import + +#import + +@interface AppDelegate () { + RCTTurboModuleManager *_turboModuleManager; + RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; + std::shared_ptr _reactNativeConfig; + facebook::react::ContextContainer::Shared _contextContainer; +} +@end +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + RCTAppSetupPrepareApp(application); + + RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; + +#if RCT_NEW_ARCH_ENABLED + _contextContainer = std::make_shared(); + _reactNativeConfig = std::make_shared(); + _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); + _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; + bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; +#endif + + UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil]; + + rootView.backgroundColor = [UIColor whiteColor]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [self.reactDelegate createRootViewController]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { +// @generated begin @react-native-firebase/auth-openURL - expo prebuild (DO NOT MODIFY) sync-5e029a87ac71df3ca5665387eb712d1b32274c6a + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + } +// @generated end @react-native-firebase/auth-openURL + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +#if RCT_NEW_ARCH_ENABLED + +#pragma mark - RCTCxxBridgeDelegate + +- (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge +{ + _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge + delegate:self + jsInvoker:bridge.jsCallInvoker]; + return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); +} + +#pragma mark RCTTurboModuleManagerDelegate + +- (Class)getModuleClassFromName:(const char *)name +{ + return RCTCoreModulesClassProvider(name); +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + jsInvoker:(std::shared_ptr)jsInvoker +{ + return nullptr; +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + initParams: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return nullptr; +} + +- (id)getModuleInstanceFromClass:(Class)moduleClass +{ + return RCTAppSetupDefaultModuleFromClass(moduleClass); +} + +#endif + +@end +" +`; diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m b/packages/auth/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m new file mode 100644 index 0000000000..74eb997d9d --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m @@ -0,0 +1,86 @@ +// This AppDelegate template is used in Expo SDK 43 +// It is (nearly) identical to the pure template used when +// creating a bare React Native app (without Expo) + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil]; + id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTRootViewBackgroundColor"]; + if (rootViewBackgroundColor != nil) { + rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor]; + } else { + rootView.backgroundColor = [UIColor whiteColor]; + } + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +@end diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_fallback.m b/packages/auth/plugin/__tests__/fixtures/AppDelegate_fallback.m new file mode 100644 index 0000000000..360359a7c8 --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_fallback.m @@ -0,0 +1,46 @@ +// This AppDelegate template is modified to have RCTBridge +// created in some non-standard way or not created at all. +// This should trigger the fallback regex in iOS AppDelegate Expo plugin. + +// some parts omitted to be short + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + +// The generated code should appear above ^^^ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + // the line below is malfolmed not to be matched by the Expo plugin regex + // RCTBridge* briddge = [RCTBridge new]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:briddge moduleName:@"main" initialProperties:nil]; + id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTRootViewBackgroundColor"]; + if (rootViewBackgroundColor != nil) { + rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor]; + } else { + rootView.backgroundColor = [UIColor whiteColor]; + } + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; + } + +@end diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk42.m b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk42.m new file mode 100644 index 0000000000..3c06d90e66 --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk42.m @@ -0,0 +1,102 @@ +// This AppDelegate prebuild template is used in Expo SDK 42 and older +// It expects the old react-native-unimodules architecture (UM* prefix) + +#import "AppDelegate.h" + +#import +#import +#import +#import + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@interface AppDelegate () + +@property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter; +@property (nonatomic, strong) NSDictionary *launchOptions; + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]]; + self.launchOptions = launchOptions; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + #ifdef DEBUG + [self initializeReactNativeApp]; + #else + EXUpdatesAppController *controller = [EXUpdatesAppController sharedInstance]; + controller.delegate = self; + [controller startAndShowLaunchScreen:self.window]; + #endif + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (RCTBridge *)initializeReactNativeApp +{ + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil]; + rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; + + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + return bridge; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + NSArray> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge]; + // If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here! + return extraModules; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[EXUpdatesAppController sharedInstance] launchAssetUrl]; + #endif +} + +- (void)appController:(EXUpdatesAppController *)appController didStartWithSuccess:(BOOL)success { + appController.bridge = [self initializeReactNativeApp]; + EXSplashScreenService *splashScreenService = (EXSplashScreenService *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXSplashScreenService class]]; + [splashScreenService showSplashScreenFor:self.window.rootViewController]; +} + +@end diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk44.m b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk44.m new file mode 100644 index 0000000000..7c4864d7d8 --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk44.m @@ -0,0 +1,79 @@ +// This AppDelegate prebuild template is used in Expo SDK 44+ +// It has the RCTBridge to be created by Expo ReactDelegate + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#if defined(FB_SONARKIT_ENABLED) && __has_include() +#import +#import +#import +#import +#import +#import + +static void InitializeFlipper(UIApplication *application) { + FlipperClient *client = [FlipperClient sharedClient]; + SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; + [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; + [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; + [client addPlugin:[FlipperKitReactPlugin new]]; + [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; + [client start]; +} +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +#if defined(FB_SONARKIT_ENABLED) && __has_include() + InitializeFlipper(application); +#endif + + RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; + RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil]; + rootView.backgroundColor = [UIColor whiteColor]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [self.reactDelegate createRootViewController]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; + } + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { + #ifdef DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; + #else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + #endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +@end diff --git a/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk45.mm b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk45.mm new file mode 100644 index 0000000000..59e8153756 --- /dev/null +++ b/packages/auth/plugin/__tests__/fixtures/AppDelegate_sdk45.mm @@ -0,0 +1,129 @@ +// RN 0.68.1, Expo SDK 45 template +// The main difference between this and the SDK 44 one is that this is +// using React Native 0.68 and is written in Objective-C++ + +#import "AppDelegate.h" + +#import +#import +#import +#import +#import + +#import + +#if RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import +#import + +#import + +@interface AppDelegate () { + RCTTurboModuleManager *_turboModuleManager; + RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; + std::shared_ptr _reactNativeConfig; + facebook::react::ContextContainer::Shared _contextContainer; +} +@end +#endif + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + RCTAppSetupPrepareApp(application); + + RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; + +#if RCT_NEW_ARCH_ENABLED + _contextContainer = std::make_shared(); + _reactNativeConfig = std::make_shared(); + _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); + _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; + bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; +#endif + + UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil]; + + rootView.backgroundColor = [UIColor whiteColor]; + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [self.reactDelegate createRootViewController]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + + [super application:application didFinishLaunchingWithOptions:launchOptions]; + + return YES; +} + +- (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge +{ + // If you'd like to export some custom RCTBridgeModules, add them here! + return @[]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +#if RCT_NEW_ARCH_ENABLED + +#pragma mark - RCTCxxBridgeDelegate + +- (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge +{ + _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge + delegate:self + jsInvoker:bridge.jsCallInvoker]; + return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); +} + +#pragma mark RCTTurboModuleManagerDelegate + +- (Class)getModuleClassFromName:(const char *)name +{ + return RCTCoreModulesClassProvider(name); +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + jsInvoker:(std::shared_ptr)jsInvoker +{ + return nullptr; +} + +- (std::shared_ptr)getTurboModule:(const std::string &)name + initParams: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return nullptr; +} + +- (id)getModuleInstanceFromClass:(Class)moduleClass +{ + return RCTAppSetupDefaultModuleFromClass(moduleClass); +} + +#endif + +@end diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 4769bba803..0467298b01 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -1,5 +1,7 @@ +import fs from 'fs/promises'; +import path from 'path'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -import { shouldApplyIosOpenUrlFix } from '../src/ios/openUrlFix'; +import { shouldApplyIosOpenUrlFix, modifyObjcAppDelegate } from '../src/ios/openUrlFix'; import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix'; describe('Config Plugin iOS Tests - openUrlFix', () => { @@ -152,4 +154,22 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { ).toBe(true); } }); + + const appDelegateFixtures = [ + 'AppDelegate_sdk42.m', + 'AppDelegate_bare_sdk43.m', + 'AppDelegate_sdk44.m', + 'AppDelegate_fallback.m', + 'AppDelegate_sdk45.mm', + ]; + appDelegateFixtures.forEach(fixtureName => { + it(`munges AppDelegate correctly - ${fixtureName}`, async () => { + const appDelegate = await readFixtureAsText(fixtureName); + const result = modifyObjcAppDelegate(appDelegate); + expect(result).toMatchSnapshot(); + }); + }); }); + +const readFixtureAsText = async (filepath: string): Promise => + fs.readFile(path.join(__dirname, 'fixtures', filepath), { encoding: 'utf8' }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 10ba990fcd..ace10f2875 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -7,7 +7,7 @@ import { PluginConfigType } from '../pluginConfig'; export const withIosCaptchaOpenUrlFix: ConfigPlugin = (config, props) => { if (shouldApplyIosOpenUrlFix({ config, props })) { config = withAppDelegate(config, config => { - return patchOpenUrlForCaptcha({ config }); + return withOpenUrlFixForCaptcha({ config }); }); } return config; @@ -44,12 +44,29 @@ const skipOpenUrlForFirebaseAuthBlock = `\ const appDelegateOpenUrlInsertionPointAfter = /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 -export function patchOpenUrlForCaptcha({ +export function withOpenUrlFixForCaptcha({ config, }: { config: ExportedConfigWithProps; }) { - const { contents } = config.modResults; + const { language, contents } = config.modResults; + + if (['objc', 'objcpp'].includes(language)) { + const newContents = modifyObjcAppDelegate(contents); + return { + ...config, + modResults: { + ...config.modResults, + contents: newContents, + }, + }; + } else { + // TODO: Support Swift + throw new Error(`Don't know how to apply openUrlFix to AppDelegate of language "${language}"`); + } +} + +export function modifyObjcAppDelegate(contents: string): string { const multilineMatcher = new RegExp( appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', ); @@ -62,8 +79,7 @@ export function patchOpenUrlForCaptcha({ if (offset < 0) { throw new Error(`Failed to find insertion point; fullMatchNumLines=${fullMatchNumLines}`); } - - const newContents = mergeContents({ + return mergeContents({ tag: '@react-native-firebase/auth-openURL', src: contents, newSrc: skipOpenUrlForFirebaseAuthBlock, @@ -71,15 +87,6 @@ export function patchOpenUrlForCaptcha({ offset, comment: '//', }).contents; - - const newConfig = { - ...config, - modResults: { - ...config.modResults, - contents: newContents, - }, - }; - return newConfig; } export type ExpoConfigPluginEntry = string | [] | [string] | [string, any]; From 375b42bc9ec9dbbacfa11ef0337c3355c43e229d Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 20:55:00 -0800 Subject: [PATCH 07/19] test undefined and invalid config --- .../__tests__/iosPlugin_openUrlFix.test.ts | 23 +++++++++++++++++++ packages/auth/plugin/src/ios/openUrlFix.ts | 9 +++++--- packages/auth/plugin/src/ios/urlTypes.ts | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 0467298b01..a9a2dd1be0 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -15,6 +15,12 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { slug: 'TestSlug', }; + expect( + shouldApplyIosOpenUrlFix({ + config, + }), + ).toBe(false); + expect( shouldApplyIosOpenUrlFix({ config, @@ -155,6 +161,23 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { } }); + it('throws an error for invalid config', () => { + expect(() => + shouldApplyIosOpenUrlFix({ + config: { + name: 'TestName', + slug: 'TestSlug', + }, + props: { + ios: { + // @ts-ignore testing invalid argument + captchaOpenUrlFix: Math.PI, + }, + }, + }), + ).toThrow("Unexpected value for 'captchaOpenUrlFix' config option"); + }); + const appDelegateFixtures = [ 'AppDelegate_sdk42.m', 'AppDelegate_bare_sdk43.m', diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index ace10f2875..cbcf8cb895 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -4,7 +4,10 @@ import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Path import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import { PluginConfigType } from '../pluginConfig'; -export const withIosCaptchaOpenUrlFix: ConfigPlugin = (config, props) => { +export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( + config: ExpoConfig, + props?: PluginConfigType, +) => { if (shouldApplyIosOpenUrlFix({ config, props })) { config = withAppDelegate(config, config => { return withOpenUrlFixForCaptcha({ config }); @@ -19,9 +22,9 @@ export function shouldApplyIosOpenUrlFix({ props, }: { config: ExpoConfig; - props: PluginConfigType; + props?: PluginConfigType; }): boolean { - const flag = props.ios?.captchaOpenUrlFix; + const flag = props?.ios?.captchaOpenUrlFix; if (flag === undefined || flag === 'default') { // by default, apply the fix whenever 'expo-router' is detected in the same project return isPluginEnabled(config, 'expo-router'); diff --git a/packages/auth/plugin/src/ios/urlTypes.ts b/packages/auth/plugin/src/ios/urlTypes.ts index 14f9452e22..3d7e731460 100644 --- a/packages/auth/plugin/src/ios/urlTypes.ts +++ b/packages/auth/plugin/src/ios/urlTypes.ts @@ -10,7 +10,7 @@ import plist from 'plist'; import { PluginConfigType } from '../pluginConfig'; // does this for you: https://firebase.google.com/docs/auth/ios/phone-auth#enable-phone-number-sign-in-for-your-firebase-project -export const withIosCaptchaUrlTypes: ConfigPlugin = (config, {}) => { +export const withIosCaptchaUrlTypes: ConfigPlugin = config => { return withInfoPlist(config, config => { return setUrlTypesForCaptcha({ config }); }); From ae289eb0110e7c39b740e4956907211a9a29cc01 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 21:08:59 -0800 Subject: [PATCH 08/19] minor --- packages/auth/plugin/src/ios/openUrlFix.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index cbcf8cb895..904a40f7cf 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -36,17 +36,6 @@ export function shouldApplyIosOpenUrlFix({ } } -const skipOpenUrlForFirebaseAuthBlock = `\ - if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { - // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router - return NO; - }\ -`; - -// NOTE: `mergeContents()` requires that this pattern not match newlines -const appDelegateOpenUrlInsertionPointAfter = - /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 - export function withOpenUrlFixForCaptcha({ config, }: { @@ -69,6 +58,17 @@ export function withOpenUrlFixForCaptcha({ } } +const skipOpenUrlForFirebaseAuthBlock: string = `\ + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + }\ +`; + +// NOTE: `mergeContents()` requires that this pattern not match newlines +const appDelegateOpenUrlInsertionPointAfter: RegExp = + /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 + export function modifyObjcAppDelegate(contents: string): string { const multilineMatcher = new RegExp( appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', From 75a6d129fa1369d8c4f9c8194f4cc91e734cf6e6 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 21:27:15 -0800 Subject: [PATCH 09/19] skip patching AppDelegate if no 'openURL' doesn't exist --- .../__tests__/iosPlugin_openUrlFix.test.ts | 19 +++++++--- packages/auth/plugin/src/ios/openUrlFix.ts | 35 +++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index a9a2dd1be0..823a5f5413 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -178,20 +178,31 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { ).toThrow("Unexpected value for 'captchaOpenUrlFix' config option"); }); - const appDelegateFixtures = [ - 'AppDelegate_sdk42.m', + const appDelegateFixturesPatch = [ 'AppDelegate_bare_sdk43.m', 'AppDelegate_sdk44.m', - 'AppDelegate_fallback.m', 'AppDelegate_sdk45.mm', ]; - appDelegateFixtures.forEach(fixtureName => { + appDelegateFixturesPatch.forEach(fixtureName => { it(`munges AppDelegate correctly - ${fixtureName}`, async () => { const appDelegate = await readFixtureAsText(fixtureName); const result = modifyObjcAppDelegate(appDelegate); expect(result).toMatchSnapshot(); }); }); + + const appDelegateFixturesNoop = ['AppDelegate_sdk42.m', 'AppDelegate_fallback.m']; + appDelegateFixturesNoop.forEach(fixtureName => { + it(`skips AppDelegate without openURL - ${fixtureName}`, async () => { + const appDelegate = await readFixtureAsText(fixtureName); + const spy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const result = modifyObjcAppDelegate(appDelegate); + expect(result).toBe(null); + expect(spy).toHaveBeenCalledWith( + "Skipping iOS openURL fix for captcha because no 'openURL' method was found", + ); + }); + }); }); const readFixtureAsText = async (filepath: string): Promise => diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 904a40f7cf..a9fb57ec6e 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -45,13 +45,17 @@ export function withOpenUrlFixForCaptcha({ if (['objc', 'objcpp'].includes(language)) { const newContents = modifyObjcAppDelegate(contents); - return { - ...config, - modResults: { - ...config.modResults, - contents: newContents, - }, - }; + if (newContents === null) { + return config; + } else { + return { + ...config, + modResults: { + ...config.modResults, + contents: newContents, + }, + }; + } } else { // TODO: Support Swift throw new Error(`Don't know how to apply openUrlFix to AppDelegate of language "${language}"`); @@ -69,13 +73,22 @@ const skipOpenUrlForFirebaseAuthBlock: string = `\ const appDelegateOpenUrlInsertionPointAfter: RegExp = /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 -export function modifyObjcAppDelegate(contents: string): string { - const multilineMatcher = new RegExp( +// Returns AppDelegate with modification applied, or null if no change needed. +export function modifyObjcAppDelegate(contents: string): string | null { + const pattern = appDelegateOpenUrlInsertionPointAfter; + const multilinePattern = new RegExp( appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', ); - const fullMatch = contents.match(multilineMatcher); + const fullMatch = contents.match(multilinePattern); if (!fullMatch) { - throw new Error("Failed to find insertion point; expected newline after '{'"); + if (contents.match(pattern)) { + throw new Error("Failed to find insertion point; expected newline after '{'"); + } else if (contents.match(/openURL\s*:/)) { + throw new Error("Failed to find insertion point but detected 'openURL' method"); + } else { + console.warn("Skipping iOS captcha openURL fix because no 'openURL' method was found"); + return null; + } } const fullMatchNumLines = fullMatch[0].split('\n').length; const offset = fullMatchNumLines - 1; From 18bcd5aa71a174a9f5acbdc11d269b6f34e6fd24 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 22:20:27 -0800 Subject: [PATCH 10/19] throw Error if explicitly enabled but failed to find 'openURL' method --- .../__tests__/iosPlugin_openUrlFix.test.ts | 53 +++++++++++++++++-- packages/auth/plugin/src/ios/openUrlFix.ts | 24 ++++++--- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 823a5f5413..8c93723f44 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -1,7 +1,12 @@ import fs from 'fs/promises'; import path from 'path'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; -import { shouldApplyIosOpenUrlFix, modifyObjcAppDelegate } from '../src/ios/openUrlFix'; +import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import { + shouldApplyIosOpenUrlFix, + modifyObjcAppDelegate, + withOpenUrlFixForCaptcha, +} from '../src/ios/openUrlFix'; import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix'; describe('Config Plugin iOS Tests - openUrlFix', () => { @@ -194,12 +199,50 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { const appDelegateFixturesNoop = ['AppDelegate_sdk42.m', 'AppDelegate_fallback.m']; appDelegateFixturesNoop.forEach(fixtureName => { it(`skips AppDelegate without openURL - ${fixtureName}`, async () => { - const appDelegate = await readFixtureAsText(fixtureName); + const fixturePath = path.join(__dirname, 'fixtures', fixtureName); + const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); + const config = { + name: 'TestName', + slug: 'TestSlug', + plugins: ['expo-router'], + modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, + modResults: { + path: fixturePath, + language: 'objc', + contents: appDelegate, + } as AppDelegateProjectFile, + modRawConfig: { name: 'TestName', slug: 'TestSlug' }, + }; + const props = undefined; const spy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - const result = modifyObjcAppDelegate(appDelegate); - expect(result).toBe(null); + const result = withOpenUrlFixForCaptcha({ config, props }); + expect(result.modResults.contents).toBe(appDelegate); expect(spy).toHaveBeenCalledWith( - "Skipping iOS openURL fix for captcha because no 'openURL' method was found", + "Skipping iOS openURL fix because no 'openURL' method was found", + ); + }); + + it(`errors when enabled but openURL not found - ${fixtureName}`, async () => { + const fixturePath = path.join(__dirname, 'fixtures', fixtureName); + const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); + const config = { + name: 'TestName', + slug: 'TestSlug', + modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, + modResults: { + path: fixturePath, + language: 'objc', + contents: appDelegate, + } as AppDelegateProjectFile, + modRawConfig: { name: 'TestName', slug: 'TestSlug' }, + }; + const props = { + ios: { + captchaOpenUrlFix: true, + }, + }; + expect(() => withOpenUrlFixForCaptcha({ config, props })).toThrow( + "Failed to apply iOS openURL fix because no 'openURL' method was found", ); }); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index a9fb57ec6e..db768b7322 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -8,12 +8,9 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( config: ExpoConfig, props?: PluginConfigType, ) => { - if (shouldApplyIosOpenUrlFix({ config, props })) { - config = withAppDelegate(config, config => { - return withOpenUrlFixForCaptcha({ config }); - }); - } - return config; + return withAppDelegate(config, config => { + return withOpenUrlFixForCaptcha({ config, props }); + }); }; // Interpret the plugin config to determine whether this fix should be applied @@ -38,15 +35,27 @@ export function shouldApplyIosOpenUrlFix({ export function withOpenUrlFixForCaptcha({ config, + props, }: { config: ExportedConfigWithProps; + props?: PluginConfigType; }) { const { language, contents } = config.modResults; + if (!shouldApplyIosOpenUrlFix({ config, props })) { + return config; + } + if (['objc', 'objcpp'].includes(language)) { const newContents = modifyObjcAppDelegate(contents); if (newContents === null) { - return config; + if (props?.ios?.captchaOpenUrlFix === true) { + throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found"); + } else { + // eslint-disable-next-line no-console + console.warn("Skipping iOS openURL fix because no 'openURL' method was found"); + return config; + } } else { return { ...config, @@ -86,7 +95,6 @@ export function modifyObjcAppDelegate(contents: string): string | null { } else if (contents.match(/openURL\s*:/)) { throw new Error("Failed to find insertion point but detected 'openURL' method"); } else { - console.warn("Skipping iOS captcha openURL fix because no 'openURL' method was found"); return null; } } From 7ec0d865786f288621b5950dd3edeb26ed525373 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Thu, 26 Dec 2024 22:54:55 -0800 Subject: [PATCH 11/19] fixup! throw Error if explicitly enabled but failed to find 'openURL' method --- packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 8c93723f44..548d4aca20 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -190,7 +190,8 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { ]; appDelegateFixturesPatch.forEach(fixtureName => { it(`munges AppDelegate correctly - ${fixtureName}`, async () => { - const appDelegate = await readFixtureAsText(fixtureName); + const fixturePath = path.join(__dirname, 'fixtures', fixtureName); + const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); const result = modifyObjcAppDelegate(appDelegate); expect(result).toMatchSnapshot(); }); @@ -247,6 +248,3 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { }); }); }); - -const readFixtureAsText = async (filepath: string): Promise => - fs.readFile(path.join(__dirname, 'fixtures', filepath), { encoding: 'utf8' }); From 266c27bf7d1af9d95f9227f286eecea577c3173f Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 00:43:08 -0800 Subject: [PATCH 12/19] move shouldApplyIosOpenUrlFix test to withIosCaptchaOpenUrlFix --- packages/auth/plugin/src/ios/openUrlFix.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index db768b7322..d21a91d17e 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -8,6 +8,10 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( config: ExpoConfig, props?: PluginConfigType, ) => { + if (!shouldApplyIosOpenUrlFix({ config, props })) { + return config; + } + return withAppDelegate(config, config => { return withOpenUrlFixForCaptcha({ config, props }); }); @@ -42,10 +46,6 @@ export function withOpenUrlFixForCaptcha({ }) { const { language, contents } = config.modResults; - if (!shouldApplyIosOpenUrlFix({ config, props })) { - return config; - } - if (['objc', 'objcpp'].includes(language)) { const newContents = modifyObjcAppDelegate(contents); if (newContents === null) { From d3f49a6def90697dbc6db547312723b7627bd87d Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 17:09:52 -0800 Subject: [PATCH 13/19] check that swizzling is enabled before patching --- packages/auth/plugin/src/ios/openUrlFix.ts | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index d21a91d17e..75104da699 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -1,6 +1,12 @@ -import { ConfigPlugin, withAppDelegate, ExportedConfigWithProps } from '@expo/config-plugins'; +import { + ConfigPlugin, + withAppDelegate, + withInfoPlist, + ExportedConfigWithProps, +} from '@expo/config-plugins'; import type { ExpoConfig } from '@expo/config/build/Config.types'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import type { InfoPlist } from '@expo/config-plugins/build/ios/IosConfig.types'; import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; import { PluginConfigType } from '../pluginConfig'; @@ -8,10 +14,27 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( config: ExpoConfig, props?: PluginConfigType, ) => { + // check configuration if (!shouldApplyIosOpenUrlFix({ config, props })) { return config; } + // check that swizzling is enabled; otherwise patch must be customized and applied manually + withInfoPlist(config, config => { + if (isFirebaseSwizzlingDisabled(config)) { + throw new Error( + [ + 'Your app has disabled swizzling by setting FirebaseAppDelegateProxyEnabled=false in its Info.plist.', + "Please update your app.config.json to configure the '@react-native-firebase/auth' plugin to set `captchaOpenUrlFix: false`", + 'and see https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling for instructions.', + ].join(' '), + ); + } else { + return config; + } + }); + + // apply patch return withAppDelegate(config, config => { return withOpenUrlFixForCaptcha({ config, props }); }); @@ -130,3 +153,7 @@ function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { } }); } + +export function isFirebaseSwizzlingDisabled(config: ExportedConfigWithProps): boolean { + return config.ios?.infoPlist?.['FirebaseAppDelegateProxyEnabled'] === false; +} From 7fea7ad0fe5179ee698378ab70f4772be20ab36f Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 20:05:27 -0800 Subject: [PATCH 14/19] add tests for parsing edgecases --- .../iosPlugin_openUrlFix.test.ts.snap | 25 ++++++++++++ .../__tests__/iosPlugin_openUrlFix.test.ts | 40 +++++++++++++++++++ packages/auth/plugin/src/ios/openUrlFix.ts | 19 ++++++--- 3 files changed, 79 insertions(+), 5 deletions(-) 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 e3de802993..7d6290c03a 100644 --- a/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap +++ b/packages/auth/plugin/__tests__/__snapshots__/iosPlugin_openUrlFix.test.ts.snap @@ -323,3 +323,28 @@ exports[`Config Plugin iOS Tests - openUrlFix munges AppDelegate correctly - App @end " `; + +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 + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + } +// @generated end @react-native-firebase/auth-openURL + int x=3;" +`; + +exports[`Config Plugin iOS Tests - openUrlFix must match positiveTemplateCases[3] 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 + if ([url.host caseInsensitiveCompare:@"firebaseauth"] == NSOrderedSame) { + // invocations for Firebase Auth are handled elsewhere and should not be forwarded to Expo Router + return NO; + } +// @generated end @react-native-firebase/auth-openURL +" +`; diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 548d4aca20..3eff92dde6 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -6,6 +6,8 @@ import { shouldApplyIosOpenUrlFix, modifyObjcAppDelegate, withOpenUrlFixForCaptcha, + appDelegateOpenUrlInsertionPointAfter, + multiline_appDelegateOpenUrlInsertionPointAfter, } from '../src/ios/openUrlFix'; import type { ExpoConfigPluginEntry } from '../src/ios/openUrlFix'; @@ -247,4 +249,42 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { ); }); }); + + it(`should issue error when openURL is found but patching fails`, () => { + const snippet = [ + '// preamble goes here\nint theAnswer()\n{\n\treturn 42;\n}', + '- (BOOL)application:(UIApplication *)application options:(NSDictionary *)options openURL:(NSURL *)url\n{', + 'int x = theAnswer();\n', + ].join(''); + expect(() => modifyObjcAppDelegate(snippet)).toThrow( + "Failed to apply 'captchaOpenUrlFix' but detected 'openURL' method.", + ); + }); + + const positiveTemplateCases = [ + '- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options {\n\tint x=3;', + '- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options', + '\t- (\tBOOL ) application : ( UIApplication*\t) application openURL : ( NSURL*) url options : ( NSDictionary < UIApplicationOpenURLOptionsKey , id > *)\toptions', + ' - ( BOOL ) application : ( UIApplication*\t) application openURL : ( NSURL*) url options : ( NSDictionary < UIApplicationOpenURLOptionsKey , id > *)\toptions\n\n{\n\n', + ]; + positiveTemplateCases.forEach((snippet, caseIdx) => { + it(`must match positiveTemplateCases[${caseIdx}]`, () => { + expect(appDelegateOpenUrlInsertionPointAfter.test(snippet)).toBe(true); + if (snippet.match(/{/)) { + expect(multiline_appDelegateOpenUrlInsertionPointAfter.test(snippet)).toBe(true); + expect(modifyObjcAppDelegate(snippet)).toMatchSnapshot(); + } + }); + }); + + const negativeTemplateCases = [ + '- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity {', + '- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url', + '\t( BOOL ) application : ( UIApplication*\t) application openURL : ( NSURL*) url options : ( NSDictionary < UIApplicationOpenURLOptionsKey , id > *)\toptions', + ]; + negativeTemplateCases.forEach((snippet, caseIdx) => { + it(`must not match negativeTemplateCases[${caseIdx}]`, () => { + expect(appDelegateOpenUrlInsertionPointAfter.test(snippet)).toBe(false); + }); + }); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 75104da699..916e5cc4b5 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -102,21 +102,30 @@ const skipOpenUrlForFirebaseAuthBlock: string = `\ `; // NOTE: `mergeContents()` requires that this pattern not match newlines -const appDelegateOpenUrlInsertionPointAfter: RegExp = +export const appDelegateOpenUrlInsertionPointAfter: RegExp = /-\s*\(\s*BOOL\s*\)\s*application\s*:\s*\(\s*UIApplication\s*\*\s*\)\s*application\s+openURL\s*:\s*\(\s*NSURL\s*\*\s*\)\s*url\s+options\s*:\s*\(\s*NSDictionary\s*<\s*UIApplicationOpenURLOptionsKey\s*,\s*id\s*>\s*\*\s*\)\s*options\s*/; // 🙈 +export const multiline_appDelegateOpenUrlInsertionPointAfter = new RegExp( + appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', +); + // Returns AppDelegate with modification applied, or null if no change needed. export function modifyObjcAppDelegate(contents: string): string | null { const pattern = appDelegateOpenUrlInsertionPointAfter; - const multilinePattern = new RegExp( - appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', - ); + const multilinePattern = multiline_appDelegateOpenUrlInsertionPointAfter; const fullMatch = contents.match(multilinePattern); if (!fullMatch) { if (contents.match(pattern)) { throw new Error("Failed to find insertion point; expected newline after '{'"); } else if (contents.match(/openURL\s*:/)) { - throw new Error("Failed to find insertion point but detected 'openURL' method"); + 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 { return null; } From c35be1e9b95a90257f23753be90495909c34c6d2 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 21:01:28 -0800 Subject: [PATCH 15/19] rename withOpenUrlFixForCaptcha to withOpenUrlFixForAppDelegate --- packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts | 6 +++--- packages/auth/plugin/src/ios/openUrlFix.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 3eff92dde6..61cb1a5d15 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -5,7 +5,7 @@ import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Path import { shouldApplyIosOpenUrlFix, modifyObjcAppDelegate, - withOpenUrlFixForCaptcha, + withOpenUrlFixForAppDelegate, appDelegateOpenUrlInsertionPointAfter, multiline_appDelegateOpenUrlInsertionPointAfter, } from '../src/ios/openUrlFix'; @@ -218,7 +218,7 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { }; const props = undefined; const spy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); - const result = withOpenUrlFixForCaptcha({ config, props }); + const result = withOpenUrlFixForAppDelegate({ config, props }); expect(result.modResults.contents).toBe(appDelegate); expect(spy).toHaveBeenCalledWith( "Skipping iOS openURL fix because no 'openURL' method was found", @@ -244,7 +244,7 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { captchaOpenUrlFix: true, }, }; - expect(() => withOpenUrlFixForCaptcha({ config, props })).toThrow( + expect(() => withOpenUrlFixForAppDelegate({ config, props })).toThrow( "Failed to apply iOS openURL fix because no 'openURL' method was found", ); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 916e5cc4b5..31134e644e 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -36,7 +36,7 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( // apply patch return withAppDelegate(config, config => { - return withOpenUrlFixForCaptcha({ config, props }); + return withOpenUrlFixForAppDelegate({ config, props }); }); }; @@ -60,7 +60,7 @@ export function shouldApplyIosOpenUrlFix({ } } -export function withOpenUrlFixForCaptcha({ +export function withOpenUrlFixForAppDelegate({ config, props, }: { From 379d3bf74ecab59e72c827c4002567b0bc23b196 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 21:25:31 -0800 Subject: [PATCH 16/19] add test for FirebaseAppDelegateProxyEnabled=false --- .../__tests__/iosPlugin_openUrlFix.test.ts | 24 +++++++++++++++ packages/auth/plugin/src/ios/openUrlFix.ts | 29 ++++++++++++------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 61cb1a5d15..f197c789e6 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -6,6 +6,7 @@ import { shouldApplyIosOpenUrlFix, modifyObjcAppDelegate, withOpenUrlFixForAppDelegate, + ensureFirebaseSwizzlingIsEnabled, appDelegateOpenUrlInsertionPointAfter, multiline_appDelegateOpenUrlInsertionPointAfter, } from '../src/ios/openUrlFix'; @@ -287,4 +288,27 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { expect(appDelegateOpenUrlInsertionPointAfter.test(snippet)).toBe(false); }); }); + + it(`rejects projects with swizzling disabled`, () => { + const config = { + name: 'TestName', + slug: 'TestSlug', + plugins: ['expo-router'], + modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, + modResults: { + path: path.join(__dirname, 'fixtures', '/path/to/Info.plist'), + language: 'plist', + contents: '', + }, + modRawConfig: { name: 'TestName', slug: 'TestSlug' }, + ios: { + infoPlist: { + FirebaseAppDelegateProxyEnabled: false, + }, + }, + }; + expect(() => ensureFirebaseSwizzlingIsEnabled(config)).toThrow( + 'Your app has disabled swizzling by setting FirebaseAppDelegateProxyEnabled=false in its Info.plist.', + ); + }); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 31134e644e..b913a4cd3f 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -21,17 +21,8 @@ export const withIosCaptchaOpenUrlFix: ConfigPlugin = ( // check that swizzling is enabled; otherwise patch must be customized and applied manually withInfoPlist(config, config => { - if (isFirebaseSwizzlingDisabled(config)) { - throw new Error( - [ - 'Your app has disabled swizzling by setting FirebaseAppDelegateProxyEnabled=false in its Info.plist.', - "Please update your app.config.json to configure the '@react-native-firebase/auth' plugin to set `captchaOpenUrlFix: false`", - 'and see https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling for instructions.', - ].join(' '), - ); - } else { - return config; - } + ensureFirebaseSwizzlingIsEnabled(config); + return config; }); // apply patch @@ -166,3 +157,19 @@ function isPluginEnabled(config: ExpoConfig, pluginName: string): boolean { export function isFirebaseSwizzlingDisabled(config: ExportedConfigWithProps): boolean { return config.ios?.infoPlist?.['FirebaseAppDelegateProxyEnabled'] === false; } + +export function ensureFirebaseSwizzlingIsEnabled( + config: ExportedConfigWithProps, +): boolean { + if (isFirebaseSwizzlingDisabled(config)) { + throw new Error( + [ + 'Your app has disabled swizzling by setting FirebaseAppDelegateProxyEnabled=false in its Info.plist.', + "Please update your app.config.json to configure the '@react-native-firebase/auth' plugin to set `captchaOpenUrlFix: false`", + 'and see https://firebase.google.com/docs/auth/ios/phone-auth#appendix:-using-phone-sign-in-without-swizzling for instructions.', + ].join(' '), + ); + } else { + return true; + } +} From 5627a4f5a7faecaa728f876403af2e84cddd7c07 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Fri, 27 Dec 2024 22:33:07 -0800 Subject: [PATCH 17/19] use WarningAggregator instead of console.warn --- .../auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts | 4 +++- packages/auth/plugin/src/ios/openUrlFix.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index f197c789e6..530fb366d7 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -1,6 +1,7 @@ import fs from 'fs/promises'; import path from 'path'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { WarningAggregator } from '@expo/config-plugins'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import { shouldApplyIosOpenUrlFix, @@ -218,10 +219,11 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { modRawConfig: { name: 'TestName', slug: 'TestSlug' }, }; const props = undefined; - const spy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const spy = jest.spyOn(WarningAggregator, 'addWarningIOS'); const result = withOpenUrlFixForAppDelegate({ config, props }); expect(result.modResults.contents).toBe(appDelegate); expect(spy).toHaveBeenCalledWith( + '@react-native-firebase/auth', "Skipping iOS openURL fix because no 'openURL' method was found", ); }); diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index b913a4cd3f..9610b3c0d9 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -3,6 +3,7 @@ import { withAppDelegate, withInfoPlist, ExportedConfigWithProps, + WarningAggregator, } from '@expo/config-plugins'; import type { ExpoConfig } from '@expo/config/build/Config.types'; import type { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; @@ -66,8 +67,10 @@ export function withOpenUrlFixForAppDelegate({ if (props?.ios?.captchaOpenUrlFix === true) { throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found"); } else { - // eslint-disable-next-line no-console - console.warn("Skipping iOS openURL fix because no 'openURL' method was found"); + WarningAggregator.addWarningIOS( + '@react-native-firebase/auth', + "Skipping iOS openURL fix because no 'openURL' method was found", + ); return config; } } else { From 11a36ab5688a6af0c6ed5b02065a88a8b2080e52 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Sat, 28 Dec 2024 03:53:41 -0800 Subject: [PATCH 18/19] add comments to explain 'null' returned from 'modifyObjcAppDelegate' --- packages/auth/plugin/src/ios/openUrlFix.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 9610b3c0d9..0e0b5adfe8 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -103,7 +103,7 @@ export const multiline_appDelegateOpenUrlInsertionPointAfter = new RegExp( appDelegateOpenUrlInsertionPointAfter.source + '\\s*{\\s*\\n', ); -// Returns AppDelegate with modification applied, or null if no change needed. +// Returns contents of new AppDelegate with modification applied, or returns null if this patch is not applicable because the AppDelegate doesn't have an 'openURL' method to handle deep links. export function modifyObjcAppDelegate(contents: string): string | null { const pattern = appDelegateOpenUrlInsertionPointAfter; const multilinePattern = multiline_appDelegateOpenUrlInsertionPointAfter; @@ -121,6 +121,7 @@ export function modifyObjcAppDelegate(contents: string): string | null { ].join(' '), ); } else { + // openURL method was not found in AppDelegate return null; } } From 7f0412e4a450696603bd1a2eac3957c21188c3c6 Mon Sep 17 00:00:00 2001 From: Jey Kottalam Date: Wed, 1 Jan 2025 21:08:22 -0800 Subject: [PATCH 19/19] print a warning when config is default and AppDelegate is modified --- .../__tests__/iosPlugin_openUrlFix.test.ts | 29 ++++++++++++++++++- packages/auth/plugin/src/ios/openUrlFix.ts | 9 +++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts index 530fb366d7..fa2f16fa4c 100644 --- a/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts +++ b/packages/auth/plugin/__tests__/iosPlugin_openUrlFix.test.ts @@ -199,6 +199,31 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { const result = modifyObjcAppDelegate(appDelegate); expect(result).toMatchSnapshot(); }); + + it(`prints warning message when configured to default and AppDelegate is modified`, async () => { + const fixturePath = path.join(__dirname, 'fixtures', fixtureName); + const appDelegate = await fs.readFile(fixturePath, { encoding: 'utf-8' }); + const config = { + name: 'TestName', + slug: 'TestSlug', + modRequest: { projectRoot: path.join(__dirname, 'fixtures') } as any, + modResults: { + path: fixturePath, + language: 'objc', + contents: appDelegate, + } as AppDelegateProjectFile, + modRawConfig: { name: 'TestName', slug: 'TestSlug' }, + }; + const props = undefined; + const spy = jest + .spyOn(WarningAggregator, 'addWarningIOS') + .mockImplementation(() => undefined); + withOpenUrlFixForAppDelegate({ config, props }); + expect(spy).toHaveBeenCalledWith( + '@react-native-firebase/auth', + 'modifying iOS AppDelegate openURL method to ignore firebaseauth reCAPTCHA redirect URLs', + ); + }); }); const appDelegateFixturesNoop = ['AppDelegate_sdk42.m', 'AppDelegate_fallback.m']; @@ -219,7 +244,9 @@ describe('Config Plugin iOS Tests - openUrlFix', () => { modRawConfig: { name: 'TestName', slug: 'TestSlug' }, }; const props = undefined; - const spy = jest.spyOn(WarningAggregator, 'addWarningIOS'); + const spy = jest + .spyOn(WarningAggregator, 'addWarningIOS') + .mockImplementation(() => undefined); const result = withOpenUrlFixForAppDelegate({ config, props }); expect(result.modResults.contents).toBe(appDelegate); expect(spy).toHaveBeenCalledWith( diff --git a/packages/auth/plugin/src/ios/openUrlFix.ts b/packages/auth/plugin/src/ios/openUrlFix.ts index 0e0b5adfe8..eb05f21edf 100644 --- a/packages/auth/plugin/src/ios/openUrlFix.ts +++ b/packages/auth/plugin/src/ios/openUrlFix.ts @@ -60,11 +60,12 @@ export function withOpenUrlFixForAppDelegate({ props?: PluginConfigType; }) { const { language, contents } = config.modResults; + const configValue = props?.ios?.captchaOpenUrlFix || 'default'; if (['objc', 'objcpp'].includes(language)) { const newContents = modifyObjcAppDelegate(contents); if (newContents === null) { - if (props?.ios?.captchaOpenUrlFix === true) { + if (configValue === true) { throw new Error("Failed to apply iOS openURL fix because no 'openURL' method was found"); } else { WarningAggregator.addWarningIOS( @@ -74,6 +75,12 @@ export function withOpenUrlFixForAppDelegate({ 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: {