Skip to content

Commit 185756d

Browse files
barthapmikehardy
andauthored
fix(app, expo): Update AppDelegate config plugin for Expo SDK 44 (#5940)
* Fix regex in AppDelegate & add fallback * Update tests * Minor regex fix * Apply suggestion Co-authored-by: Mike Hardy <[email protected]>
1 parent 7c082be commit 185756d

File tree

6 files changed

+334
-15
lines changed

6 files changed

+334
-15
lines changed

packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43+) 1`] = `
4-
"// This AppDelegate template is used in Expo SDK 43 and newer
3+
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43) 1`] = `
4+
"// This AppDelegate template is used in Expo SDK 43
55
// It is (nearly) identical to the pure template used when
66
// creating a bare React Native app (without Expo)
77
@@ -94,6 +94,147 @@ static void InitializeFlipper(UIApplication *application) {
9494
"
9595
`;
9696
97+
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+) 1`] = `
98+
"// This AppDelegate prebuild template is used in Expo SDK 44+
99+
// It has the RCTBridge to be created by Expo ReactDelegate
100+
101+
#import \\"AppDelegate.h\\"
102+
@import Firebase;
103+
104+
#import <React/RCTBridge.h>
105+
#import <React/RCTBundleURLProvider.h>
106+
#import <React/RCTRootView.h>
107+
#import <React/RCTLinkingManager.h>
108+
#import <React/RCTConvert.h>
109+
110+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
111+
#import <FlipperKit/FlipperClient.h>
112+
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
113+
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
114+
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
115+
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
116+
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
117+
118+
static void InitializeFlipper(UIApplication *application) {
119+
FlipperClient *client = [FlipperClient sharedClient];
120+
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
121+
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
122+
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
123+
[client addPlugin:[FlipperKitReactPlugin new]];
124+
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
125+
[client start];
126+
}
127+
#endif
128+
129+
@implementation AppDelegate
130+
131+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
132+
{
133+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
134+
InitializeFlipper(application);
135+
#endif
136+
137+
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
138+
[FIRApp configure];
139+
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
140+
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
141+
RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil];
142+
rootView.backgroundColor = [UIColor whiteColor];
143+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
144+
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
145+
rootViewController.view = rootView;
146+
self.window.rootViewController = rootViewController;
147+
[self.window makeKeyAndVisible];
148+
149+
[super application:application didFinishLaunchingWithOptions:launchOptions];
150+
151+
return YES;
152+
}
153+
154+
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
155+
{
156+
// If you'd like to export some custom RCTBridgeModules, add them here!
157+
return @[];
158+
}
159+
160+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
161+
#ifdef DEBUG
162+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil];
163+
#else
164+
return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"];
165+
#endif
166+
}
167+
168+
// Linking API
169+
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
170+
return [RCTLinkingManager application:application openURL:url options:options];
171+
}
172+
173+
// Universal Links
174+
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
175+
return [RCTLinkingManager application:application
176+
continueUserActivity:userActivity
177+
restorationHandler:restorationHandler];
178+
}
179+
180+
@end
181+
"
182+
`;
183+
184+
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with fallback regex (if the original one fails) 1`] = `
185+
"// This AppDelegate template is modified to have RCTBridge
186+
// created in some non-standard way or not created at all.
187+
// This should trigger the fallback regex in iOS AppDelegate Expo plugin.
188+
189+
// some parts omitted to be short
190+
191+
#import \\"AppDelegate.h\\"
192+
@import Firebase;
193+
194+
#import <React/RCTBridge.h>
195+
#import <React/RCTBundleURLProvider.h>
196+
#import <React/RCTRootView.h>
197+
#import <React/RCTLinkingManager.h>
198+
#import <React/RCTConvert.h>
199+
200+
@implementation AppDelegate
201+
202+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
203+
{
204+
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
205+
[FIRApp configure];
206+
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions-fallback
207+
208+
// The generated code should appear above ^^^
209+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
210+
InitializeFlipper(application);
211+
#endif
212+
213+
// the line below is malfolmed not to be matched by the Expo plugin regex
214+
// RCTBridge* briddge = [RCTBridge new];
215+
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:briddge moduleName:@\\"main\\" initialProperties:nil];
216+
id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\\"RCTRootViewBackgroundColor\\"];
217+
if (rootViewBackgroundColor != nil) {
218+
rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor];
219+
} else {
220+
rootView.backgroundColor = [UIColor whiteColor];
221+
}
222+
223+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
224+
UIViewController *rootViewController = [UIViewController new];
225+
rootViewController.view = rootView;
226+
self.window.rootViewController = rootViewController;
227+
[self.window makeKeyAndVisible];
228+
229+
[super application:application didFinishLaunchingWithOptions:launchOptions];
230+
231+
return YES;
232+
}
233+
234+
@end
235+
"
236+
`;
237+
97238
exports[`Config Plugin iOS Tests tests changes made to old AppDelegate.m (SDK 42) 1`] = `
98239
"// This AppDelegate prebuild template is used in Expo SDK 42 and older
99240
// It expects the old react-native-unimodules architecture (UM* prefix)

packages/app/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// This AppDelegate template is used in Expo SDK 43 and newer
1+
// This AppDelegate template is used in Expo SDK 43
22
// It is (nearly) identical to the pure template used when
33
// creating a bare React Native app (without Expo)
44

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// This AppDelegate template is modified to have RCTBridge
2+
// created in some non-standard way or not created at all.
3+
// This should trigger the fallback regex in iOS AppDelegate Expo plugin.
4+
5+
// some parts omitted to be short
6+
7+
#import "AppDelegate.h"
8+
9+
#import <React/RCTBridge.h>
10+
#import <React/RCTBundleURLProvider.h>
11+
#import <React/RCTRootView.h>
12+
#import <React/RCTLinkingManager.h>
13+
#import <React/RCTConvert.h>
14+
15+
@implementation AppDelegate
16+
17+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
18+
{
19+
20+
// The generated code should appear above ^^^
21+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
22+
InitializeFlipper(application);
23+
#endif
24+
25+
// the line below is malfolmed not to be matched by the Expo plugin regex
26+
// RCTBridge* briddge = [RCTBridge new];
27+
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:briddge moduleName:@"main" initialProperties:nil];
28+
id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"RCTRootViewBackgroundColor"];
29+
if (rootViewBackgroundColor != nil) {
30+
rootView.backgroundColor = [RCTConvert UIColor:rootViewBackgroundColor];
31+
} else {
32+
rootView.backgroundColor = [UIColor whiteColor];
33+
}
34+
35+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
36+
UIViewController *rootViewController = [UIViewController new];
37+
rootViewController.view = rootView;
38+
self.window.rootViewController = rootViewController;
39+
[self.window makeKeyAndVisible];
40+
41+
[super application:application didFinishLaunchingWithOptions:launchOptions];
42+
43+
return YES;
44+
}
45+
46+
@end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// This AppDelegate prebuild template is used in Expo SDK 44+
2+
// It has the RCTBridge to be created by Expo ReactDelegate
3+
4+
#import "AppDelegate.h"
5+
6+
#import <React/RCTBridge.h>
7+
#import <React/RCTBundleURLProvider.h>
8+
#import <React/RCTRootView.h>
9+
#import <React/RCTLinkingManager.h>
10+
#import <React/RCTConvert.h>
11+
12+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
13+
#import <FlipperKit/FlipperClient.h>
14+
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
15+
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
16+
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
17+
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
18+
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
19+
20+
static void InitializeFlipper(UIApplication *application) {
21+
FlipperClient *client = [FlipperClient sharedClient];
22+
SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
23+
[client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
24+
[client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
25+
[client addPlugin:[FlipperKitReactPlugin new]];
26+
[client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
27+
[client start];
28+
}
29+
#endif
30+
31+
@implementation AppDelegate
32+
33+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
34+
{
35+
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
36+
InitializeFlipper(application);
37+
#endif
38+
39+
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
40+
RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil];
41+
rootView.backgroundColor = [UIColor whiteColor];
42+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
43+
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
44+
rootViewController.view = rootView;
45+
self.window.rootViewController = rootViewController;
46+
[self.window makeKeyAndVisible];
47+
48+
[super application:application didFinishLaunchingWithOptions:launchOptions];
49+
50+
return YES;
51+
}
52+
53+
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
54+
{
55+
// If you'd like to export some custom RCTBridgeModules, add them here!
56+
return @[];
57+
}
58+
59+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
60+
#ifdef DEBUG
61+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
62+
#else
63+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
64+
#endif
65+
}
66+
67+
// Linking API
68+
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
69+
return [RCTLinkingManager application:application openURL:url options:options];
70+
}
71+
72+
// Universal Links
73+
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
74+
return [RCTLinkingManager application:application
75+
continueUserActivity:userActivity
76+
restorationHandler:restorationHandler];
77+
}
78+
79+
@end

packages/app/plugin/__tests__/iosPlugin.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Config Plugin iOS Tests', function () {
1212
expect(result).toMatchSnapshot();
1313
});
1414

15-
it('tests changes made to AppDelegate.m (SDK 43+)', async function () {
15+
it('tests changes made to AppDelegate.m (SDK 43)', async function () {
1616
const appDelegate = await fs.readFile(
1717
path.join(__dirname, './fixtures/AppDelegate_bare_sdk43.m'),
1818
{
@@ -22,4 +22,23 @@ describe('Config Plugin iOS Tests', function () {
2222
const result = modifyObjcAppDelegate(appDelegate);
2323
expect(result).toMatchSnapshot();
2424
});
25+
26+
it('tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+)', async function () {
27+
const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk44.m'), {
28+
encoding: 'utf8',
29+
});
30+
const result = modifyObjcAppDelegate(appDelegate);
31+
expect(result).toMatchSnapshot();
32+
});
33+
34+
it('tests changes made to AppDelegate.m with fallback regex (if the original one fails)', async function () {
35+
const appDelegate = await fs.readFile(
36+
path.join(__dirname, './fixtures/AppDelegate_fallback.m'),
37+
{
38+
encoding: 'utf8',
39+
},
40+
);
41+
const result = modifyObjcAppDelegate(appDelegate);
42+
expect(result).toMatchSnapshot();
43+
});
2544
});

packages/app/plugin/src/ios/appDelegate.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { ConfigPlugin, IOSConfig, withDangerousMod } from '@expo/config-plugins';
1+
import { ConfigPlugin, IOSConfig, WarningAggregator, withDangerousMod } from '@expo/config-plugins';
22
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
33
import fs from 'fs';
44

55
const methodInvocationBlock = `[FIRApp configure];`;
6-
// https://regex101.com/r/Imm3E8/1
6+
// https://regex101.com/r/mPgaq6/1
77
const methodInvocationLineMatcher =
8-
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[\[RCTBridge alloc\])/g;
8+
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[(\[RCTBridge alloc\]|self\.reactDelegate))/g;
9+
10+
// https://regex101.com/r/nHrTa9/1/
11+
// if the above regex fails, we can use this one as a fallback:
12+
const fallbackInvocationLineMatcher =
13+
/-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g;
914

1015
export function modifyObjcAppDelegate(contents: string): string {
1116
// Add import
@@ -22,15 +27,44 @@ export function modifyObjcAppDelegate(contents: string): string {
2227
return contents;
2328
}
2429

30+
if (
31+
!methodInvocationLineMatcher.test(contents) &&
32+
!fallbackInvocationLineMatcher.test(contents)
33+
) {
34+
WarningAggregator.addWarningIOS(
35+
'@react-native-firebase/app',
36+
'Unable to determine correct Firebase insertion point in AppDelegate.m. Skipping Firebase addition.',
37+
);
38+
return contents;
39+
}
40+
2541
// Add invocation
26-
return mergeContents({
27-
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
28-
src: contents,
29-
newSrc: methodInvocationBlock,
30-
anchor: methodInvocationLineMatcher,
31-
offset: 0, // new line will be inserted right before matched anchor
32-
comment: '//',
33-
}).contents;
42+
try {
43+
return mergeContents({
44+
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
45+
src: contents,
46+
newSrc: methodInvocationBlock,
47+
anchor: methodInvocationLineMatcher,
48+
offset: 0, // new line will be inserted right above matched anchor
49+
comment: '//',
50+
}).contents;
51+
} catch (e: any) {
52+
// tests if the opening `{` is in the new line
53+
const multilineMatcher = new RegExp(fallbackInvocationLineMatcher.source + '.+\\n*{');
54+
const isHeaderMultiline = multilineMatcher.test(contents);
55+
56+
// we fallback to another regex if the first one fails
57+
return mergeContents({
58+
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions-fallback',
59+
src: contents,
60+
newSrc: methodInvocationBlock,
61+
anchor: fallbackInvocationLineMatcher,
62+
// new line will be inserted right below matched anchor
63+
// or two lines, if the `{` is in the new line
64+
offset: isHeaderMultiline ? 2 : 1,
65+
comment: '//',
66+
}).contents;
67+
}
3468
}
3569

3670
export const withFirebaseAppDelegate: ConfigPlugin = config => {

0 commit comments

Comments
 (0)