Skip to content

Commit 6f2d7e1

Browse files
authored
fix(app, expo): Support RN 0.68 Obj-C++ AppDelegate (#6213)
* [app][expo] Support RN 0.68 Obj-C++ AppDelegate * Update `@expo/config-plugins` to `4.1.1` * Make AppDelegate plugin testable * Update tests
1 parent 2e31ceb commit 6f2d7e1

File tree

8 files changed

+342
-45
lines changed

8 files changed

+342
-45
lines changed

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"react-native": "*"
5757
},
5858
"dependencies": {
59-
"@expo/config-plugins": "^4.0.18",
59+
"@expo/config-plugins": "^4.1.1",
6060
"opencollective-postinstall": "^2.0.1",
6161
"superstruct": "^0.6.2"
6262
},

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

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,140 @@ static void InitializeFlipper(UIApplication *application) {
344344
@end
345345
"
346346
`;
347+
348+
exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 1`] = `
349+
"// RN 0.68.1, Expo SDK 45 template
350+
// The main difference between this and the SDK 44 one is that this is
351+
// using React Native 0.68 and is written in Objective-C++
352+
353+
#import \\"AppDelegate.h\\"
354+
@import Firebase;
355+
356+
#import <React/RCTBridge.h>
357+
#import <React/RCTBundleURLProvider.h>
358+
#import <React/RCTRootView.h>
359+
#import <React/RCTLinkingManager.h>
360+
#import <React/RCTConvert.h>
361+
362+
#import <React/RCTAppSetupUtils.h>
363+
364+
#if RCT_NEW_ARCH_ENABLED
365+
#import <React/CoreModulesPlugins.h>
366+
#import <React/RCTCxxBridgeDelegate.h>
367+
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
368+
#import <React/RCTSurfacePresenter.h>
369+
#import <React/RCTSurfacePresenterBridgeAdapter.h>
370+
#import <ReactCommon/RCTTurboModuleManager.h>
371+
372+
#import <react/config/ReactNativeConfig.h>
373+
374+
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
375+
RCTTurboModuleManager *_turboModuleManager;
376+
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
377+
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
378+
facebook::react::ContextContainer::Shared _contextContainer;
379+
}
380+
@end
381+
#endif
382+
383+
@implementation AppDelegate
384+
385+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
386+
{
387+
RCTAppSetupPrepareApp(application);
388+
389+
// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
390+
[FIRApp configure];
391+
// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
392+
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
393+
394+
#if RCT_NEW_ARCH_ENABLED
395+
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
396+
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
397+
_contextContainer->insert(\\"ReactNativeConfig\\", _reactNativeConfig);
398+
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
399+
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
400+
#endif
401+
402+
UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil];
403+
404+
rootView.backgroundColor = [UIColor whiteColor];
405+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
406+
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
407+
rootViewController.view = rootView;
408+
self.window.rootViewController = rootViewController;
409+
[self.window makeKeyAndVisible];
410+
411+
[super application:application didFinishLaunchingWithOptions:launchOptions];
412+
413+
return YES;
414+
}
415+
416+
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
417+
{
418+
// If you'd like to export some custom RCTBridgeModules, add them here!
419+
return @[];
420+
}
421+
422+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
423+
{
424+
#if DEBUG
425+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\"];
426+
#else
427+
return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"];
428+
#endif
429+
}
430+
431+
// Linking API
432+
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
433+
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
434+
}
435+
436+
// Universal Links
437+
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
438+
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
439+
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
440+
}
441+
442+
#if RCT_NEW_ARCH_ENABLED
443+
444+
#pragma mark - RCTCxxBridgeDelegate
445+
446+
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
447+
{
448+
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
449+
delegate:self
450+
jsInvoker:bridge.jsCallInvoker];
451+
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
452+
}
453+
454+
#pragma mark RCTTurboModuleManagerDelegate
455+
456+
- (Class)getModuleClassFromName:(const char *)name
457+
{
458+
return RCTCoreModulesClassProvider(name);
459+
}
460+
461+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
462+
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
463+
{
464+
return nullptr;
465+
}
466+
467+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
468+
initParams:
469+
(const facebook::react::ObjCTurboModule::InitParams &)params
470+
{
471+
return nullptr;
472+
}
473+
474+
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
475+
{
476+
return RCTAppSetupDefaultModuleFromClass(moduleClass);
477+
}
478+
479+
#endif
480+
481+
@end
482+
"
483+
`;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// RN 0.68.1, Expo SDK 45 template
2+
// The main difference between this and the SDK 44 one is that this is
3+
// using React Native 0.68 and is written in Objective-C++
4+
5+
#import "AppDelegate.h"
6+
7+
#import <React/RCTBridge.h>
8+
#import <React/RCTBundleURLProvider.h>
9+
#import <React/RCTRootView.h>
10+
#import <React/RCTLinkingManager.h>
11+
#import <React/RCTConvert.h>
12+
13+
#import <React/RCTAppSetupUtils.h>
14+
15+
#if RCT_NEW_ARCH_ENABLED
16+
#import <React/CoreModulesPlugins.h>
17+
#import <React/RCTCxxBridgeDelegate.h>
18+
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
19+
#import <React/RCTSurfacePresenter.h>
20+
#import <React/RCTSurfacePresenterBridgeAdapter.h>
21+
#import <ReactCommon/RCTTurboModuleManager.h>
22+
23+
#import <react/config/ReactNativeConfig.h>
24+
25+
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
26+
RCTTurboModuleManager *_turboModuleManager;
27+
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
28+
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
29+
facebook::react::ContextContainer::Shared _contextContainer;
30+
}
31+
@end
32+
#endif
33+
34+
@implementation AppDelegate
35+
36+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
37+
{
38+
RCTAppSetupPrepareApp(application);
39+
40+
RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
41+
42+
#if RCT_NEW_ARCH_ENABLED
43+
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
44+
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
45+
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
46+
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
47+
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
48+
#endif
49+
50+
UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil];
51+
52+
rootView.backgroundColor = [UIColor whiteColor];
53+
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
54+
UIViewController *rootViewController = [self.reactDelegate createRootViewController];
55+
rootViewController.view = rootView;
56+
self.window.rootViewController = rootViewController;
57+
[self.window makeKeyAndVisible];
58+
59+
[super application:application didFinishLaunchingWithOptions:launchOptions];
60+
61+
return YES;
62+
}
63+
64+
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
65+
{
66+
// If you'd like to export some custom RCTBridgeModules, add them here!
67+
return @[];
68+
}
69+
70+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
71+
{
72+
#if DEBUG
73+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
74+
#else
75+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
76+
#endif
77+
}
78+
79+
// Linking API
80+
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
81+
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
82+
}
83+
84+
// Universal Links
85+
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
86+
BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
87+
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
88+
}
89+
90+
#if RCT_NEW_ARCH_ENABLED
91+
92+
#pragma mark - RCTCxxBridgeDelegate
93+
94+
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
95+
{
96+
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
97+
delegate:self
98+
jsInvoker:bridge.jsCallInvoker];
99+
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
100+
}
101+
102+
#pragma mark RCTTurboModuleManagerDelegate
103+
104+
- (Class)getModuleClassFromName:(const char *)name
105+
{
106+
return RCTCoreModulesClassProvider(name);
107+
}
108+
109+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
110+
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
111+
{
112+
return nullptr;
113+
}
114+
115+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
116+
initParams:
117+
(const facebook::react::ObjCTurboModule::InitParams &)params
118+
{
119+
return nullptr;
120+
}
121+
122+
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
123+
{
124+
return RCTAppSetupDefaultModuleFromClass(moduleClass);
125+
}
126+
127+
#endif
128+
129+
@end

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import { IOSConfig } from '@expo/config-plugins';
2+
import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths';
13
import fs from 'fs/promises';
24
import path from 'path';
35

4-
import { modifyObjcAppDelegate } from '../src/ios/appDelegate';
6+
import { modifyAppDelegateAsync, modifyObjcAppDelegate } from '../src/ios/appDelegate';
57

68
describe('Config Plugin iOS Tests', function () {
9+
beforeEach(function () {
10+
jest.resetAllMocks();
11+
});
12+
713
it('tests changes made to old AppDelegate.m (SDK 42)', async function () {
814
const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk42.m'), {
915
encoding: 'utf8',
@@ -41,4 +47,42 @@ describe('Config Plugin iOS Tests', function () {
4147
const result = modifyObjcAppDelegate(appDelegate);
4248
expect(result).toMatchSnapshot();
4349
});
50+
51+
it('works with AppDelegate.mm (RN 0.68+)', async function () {
52+
const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'), {
53+
encoding: 'utf8',
54+
});
55+
const result = modifyObjcAppDelegate(appDelegate);
56+
expect(result).toMatchSnapshot();
57+
});
58+
59+
it('detects Objective-C++ AppDelegate.mm', async function () {
60+
jest.spyOn(fs, 'writeFile').mockImplementation();
61+
62+
const appDelegatePath = path.join(__dirname, './fixtures/AppDelegate_sdk45.mm');
63+
const appDelegateFileInfo = IOSConfig.Paths.getFileInfo(
64+
appDelegatePath,
65+
) as AppDelegateProjectFile;
66+
67+
await modifyAppDelegateAsync(appDelegateFileInfo);
68+
69+
// expect file contents to be modified
70+
expect(fs.writeFile).toHaveBeenCalledWith(
71+
appDelegateFileInfo.path,
72+
expect.not.stringContaining(appDelegateFileInfo.contents),
73+
);
74+
});
75+
76+
it("doesn't support Swift AppDelegate", async function () {
77+
jest.spyOn(fs, 'writeFile').mockImplementation();
78+
79+
const appDelegateFileInfo: AppDelegateProjectFile = {
80+
path: '.',
81+
language: 'swift',
82+
contents: 'some dummy content',
83+
};
84+
85+
await expect(modifyAppDelegateAsync(appDelegateFileInfo)).rejects.toThrow();
86+
expect(fs.writeFile).not.toHaveBeenCalled();
87+
});
4488
});

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ConfigPlugin, IOSConfig, WarningAggregator, withDangerousMod } from '@expo/config-plugins';
2+
import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths';
23
import { mergeContents } from '@expo/config-plugins/build/utils/generateCode';
34
import fs from 'fs';
45

@@ -67,22 +68,24 @@ export function modifyObjcAppDelegate(contents: string): string {
6768
}
6869
}
6970

71+
export async function modifyAppDelegateAsync(appDelegateFileInfo: AppDelegateProjectFile) {
72+
const { language, path, contents } = appDelegateFileInfo;
73+
74+
if (['objc', 'objcpp'].includes(language)) {
75+
const newContents = modifyObjcAppDelegate(contents);
76+
await fs.promises.writeFile(path, newContents);
77+
} else {
78+
// TODO: Support Swift
79+
throw new Error(`Cannot add Firebase code to AppDelegate of language "${language}"`);
80+
}
81+
}
82+
7083
export const withFirebaseAppDelegate: ConfigPlugin = config => {
7184
return withDangerousMod(config, [
7285
'ios',
7386
async config => {
7487
const fileInfo = IOSConfig.Paths.getAppDelegate(config.modRequest.projectRoot);
75-
let contents = await fs.promises.readFile(fileInfo.path, 'utf-8');
76-
if (fileInfo.language === 'objc') {
77-
contents = modifyObjcAppDelegate(contents);
78-
} else {
79-
// TODO: Support Swift
80-
throw new Error(
81-
`Cannot add Firebase code to AppDelegate of language "${fileInfo.language}"`,
82-
);
83-
}
84-
await fs.promises.writeFile(fileInfo.path, contents);
85-
88+
await modifyAppDelegateAsync(fileInfo);
8689
return config;
8790
},
8891
]);

packages/crashlytics/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@react-native-firebase/app": "14.8.0"
3333
},
3434
"dependencies": {
35-
"@expo/config-plugins": "^4.0.18",
35+
"@expo/config-plugins": "^4.1.1",
3636
"stacktrace-js": "^2.0.0"
3737
},
3838
"publishConfig": {

packages/perf/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@react-native-firebase/app": "14.8.0"
3333
},
3434
"dependencies": {
35-
"@expo/config-plugins": "^4.0.18"
35+
"@expo/config-plugins": "^4.1.1"
3636
},
3737
"publishConfig": {
3838
"access": "public"

0 commit comments

Comments
 (0)