Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(app, expo): Update AppDelegate config plugin for Expo SDK 44 #5940

Merged
merged 4 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 143 additions & 2 deletions packages/app/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43+) 1`] = `
"// This AppDelegate template is used in Expo SDK 43 and newer
exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43) 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)

Expand Down Expand Up @@ -94,6 +94,147 @@ static void InitializeFlipper(UIApplication *application) {
"
`;

exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+) 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 Firebase;

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

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(<FlipperKit/FlipperClient.h>)
InitializeFlipper(application);
#endif

// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
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<id<RCTBridgeModule>> *)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<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:application openURL:url options:options];
}

// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}

@end
"
`;

exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with fallback regex (if the original one fails) 1`] = `
"// 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 Firebase;

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
[FIRApp configure];
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions-fallback

// The generated code should appear above ^^^
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
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
"
`;

exports[`Config Plugin iOS Tests tests changes made to old AppDelegate.m (SDK 42) 1`] = `
"// This AppDelegate prebuild template is used in Expo SDK 42 and older
// It expects the old react-native-unimodules architecture (UM* prefix)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// This AppDelegate template is used in Expo SDK 43 and newer
// 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)

Expand Down
46 changes: 46 additions & 0 deletions packages/app/plugin/__tests__/fixtures/AppDelegate_fallback.m
Original file line number Diff line number Diff line change
@@ -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 <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

// The generated code should appear above ^^^
#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
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
79 changes: 79 additions & 0 deletions packages/app/plugin/__tests__/fixtures/AppDelegate_sdk44.m
Original file line number Diff line number Diff line change
@@ -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 <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

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(<FlipperKit/FlipperClient.h>)
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<id<RCTBridgeModule>> *)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<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:application openURL:url options:options];
}

// Universal Links
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}

@end
21 changes: 20 additions & 1 deletion packages/app/plugin/__tests__/iosPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Config Plugin iOS Tests', function () {
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m (SDK 43+)', async function () {
it('tests changes made to AppDelegate.m (SDK 43)', async function () {
const appDelegate = await fs.readFile(
path.join(__dirname, './fixtures/AppDelegate_bare_sdk43.m'),
{
Expand All @@ -22,4 +22,23 @@ describe('Config Plugin iOS Tests', function () {
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m with Expo ReactDelegate support (SDK 44+)', async function () {
const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk44.m'), {
encoding: 'utf8',
});
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});

it('tests changes made to AppDelegate.m with fallback regex (if the original one fails)', async function () {
const appDelegate = await fs.readFile(
path.join(__dirname, './fixtures/AppDelegate_fallback.m'),
{
encoding: 'utf8',
},
);
const result = modifyObjcAppDelegate(appDelegate);
expect(result).toMatchSnapshot();
});
});
56 changes: 45 additions & 11 deletions packages/app/plugin/src/ios/appDelegate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { ConfigPlugin, IOSConfig, withDangerousMod } from '@expo/config-plugins';
import { ConfigPlugin, IOSConfig, WarningAggregator, withDangerousMod } from '@expo/config-plugins';
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
import fs from 'fs';

const methodInvocationBlock = `[FIRApp configure];`;
// https://regex101.com/r/Imm3E8/1
// https://regex101.com/r/mPgaq6/1
const methodInvocationLineMatcher =
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[\[RCTBridge alloc\])/g;
/(?:(self\.|_)(\w+)\s?=\s?\[\[UMModuleRegistryAdapter alloc\])|(?:RCTBridge\s?\*\s?(\w+)\s?=\s?\[(\[RCTBridge alloc\]|self\.reactDelegate))/g;

// https://regex101.com/r/nHrTa9/1/
// if the above regex fails, we can use this one as a fallback:
const fallbackInvocationLineMatcher =
/-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g;

export function modifyObjcAppDelegate(contents: string): string {
// Add import
Expand All @@ -22,15 +27,44 @@ export function modifyObjcAppDelegate(contents: string): string {
return contents;
}

if (
!methodInvocationLineMatcher.test(contents) &&
!fallbackInvocationLineMatcher.test(contents)
) {
WarningAggregator.addWarningIOS(
'@react-native-firebase/app',
'Unable to determine correct Firebase insertion point in AppDelegate.m. Skipping Firebase addition.',
);
return contents;
}

// Add invocation
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
src: contents,
newSrc: methodInvocationBlock,
anchor: methodInvocationLineMatcher,
offset: 0, // new line will be inserted right before matched anchor
comment: '//',
}).contents;
try {
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions',
src: contents,
newSrc: methodInvocationBlock,
anchor: methodInvocationLineMatcher,
offset: 0, // new line will be inserted right above matched anchor
comment: '//',
}).contents;
} catch (e: any) {
// tests if the opening `{` is in the new line
const multilineMatcher = new RegExp(fallbackInvocationLineMatcher.source + '.+\\n*{');
const isHeaderMultiline = multilineMatcher.test(contents);

// we fallback to another regex if the first one fails
return mergeContents({
tag: '@react-native-firebase/app-didFinishLaunchingWithOptions-fallback',
src: contents,
newSrc: methodInvocationBlock,
anchor: fallbackInvocationLineMatcher,
// new line will be inserted right below matched anchor
// or two lines, if the `{` is in the new line
offset: isHeaderMultiline ? 2 : 1,
comment: '//',
}).contents;
}
}

export const withFirebaseAppDelegate: ConfigPlugin = config => {
Expand Down