From 1ce0b811fd035e961ddd55603907e78c7a1b792d Mon Sep 17 00:00:00 2001 From: austin43 Date: Tue, 1 Nov 2022 14:57:19 -0700 Subject: [PATCH 1/8] feat(dynamic-links): add expo config plugin for dynamic-links - Related #2660 --- packages/dynamic-links/app.plugin.js | 1 + .../dynamic-links/plugin/__tests__/README.md | 19 + .../__snapshots__/iosPlugin.test.ts.snap | 483 ++++++++++++++++++ .../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 +++++ .../plugin/__tests__/iosPlugin.test.ts | 107 ++++ packages/dynamic-links/plugin/src/index.ts | 15 + .../plugin/src/ios/appDelegate.ts | 92 ++++ .../dynamic-links/plugin/src/ios/index.ts | 3 + packages/dynamic-links/plugin/tsconfig.json | 9 + 13 files changed, 1171 insertions(+) create mode 100644 packages/dynamic-links/app.plugin.js create mode 100644 packages/dynamic-links/plugin/__tests__/README.md create mode 100644 packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap create mode 100644 packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m create mode 100644 packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_fallback.m create mode 100644 packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk42.m create mode 100644 packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk44.m create mode 100644 packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk45.mm create mode 100644 packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts create mode 100644 packages/dynamic-links/plugin/src/index.ts create mode 100644 packages/dynamic-links/plugin/src/ios/appDelegate.ts create mode 100644 packages/dynamic-links/plugin/src/ios/index.ts create mode 100644 packages/dynamic-links/plugin/tsconfig.json diff --git a/packages/dynamic-links/app.plugin.js b/packages/dynamic-links/app.plugin.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/packages/dynamic-links/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/packages/dynamic-links/plugin/__tests__/README.md b/packages/dynamic-links/plugin/__tests__/README.md new file mode 100644 index 0000000000..13b9757295 --- /dev/null +++ b/packages/dynamic-links/plugin/__tests__/README.md @@ -0,0 +1,19 @@ +## Expo Config Plugin unit tests + +To test the changes to native code applied by config plugins, [snapshot tests](https://jestjs.io/docs/snapshot-testing) are used. Plugin test flow, in short: + +1. A test fixture is loaded. In this case, fixtures are template files (`build.gradle`, `AppDelegate.m` etc.) from [`expo-template-bare-minimum`](https://github.com/expo/expo/tree/master/templates/expo-template-bare-minimum). +2. Plugin changes are applied (e.g. gradle dependency is added). +3. Modified file is compared with previously saved snapshot. If they're equal, the test passes. If not, the test fails and the difference (actual vs expected) is shown. + +You can preview the snapshot files manually, by opening `__snapshots__/*.snap` files. + +### Updating the snapshots + +Snapshot tests are designed to ensure the plugin result will not change. In case you intentionally modified the plugin behavior (e.g. updated gradle dependency versions), you have to update the snapshots, otherwise the tests will fail. There are two ways to do it: + +- Update all snapshots by running `npm run tests:jest -u`. +- Update snapshots interactively, one by one: + 1. Run `yarn tests:jest --watchAll` + 2. Press `i` to let `jest` display changes and prompt you for updating each snapshot. + > This option is not available, when there are no failing snapshots diff --git a/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap b/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap new file mode 100644 index 0000000000..4fe28833f5 --- /dev/null +++ b/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap @@ -0,0 +1,483 @@ +// 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 +// 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 +#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 + +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-5b7813c3af090f886568429140e982730142dbe7 +[RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + 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 +" +`; + +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 + +#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 + +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-5b7813c3af090f886568429140e982730142dbe7 +[RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; +// @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> *)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 +" +`; + +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 + +#import +#import +#import +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-5b7813c3af090f886568429140e982730142dbe7 +[RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions-fallback + +// 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 +" +`; + +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) + +#import \\"AppDelegate.h\\" +#import + +#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 + +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-5b7813c3af090f886568429140e982730142dbe7 +[RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + 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 +" +`; + +exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 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 + +#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); + +// @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions - expo prebuild (DO NOT MODIFY) sync-5b7813c3af090f886568429140e982730142dbe7 +[RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; +// @generated end @react-native-firebase/app-didFinishLaunchingWithOptions + 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/dynamic-links/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m b/packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_bare_sdk43.m new file mode 100644 index 0000000000..74eb997d9d --- /dev/null +++ b/packages/dynamic-links/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/dynamic-links/plugin/__tests__/fixtures/AppDelegate_fallback.m b/packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_fallback.m new file mode 100644 index 0000000000..360359a7c8 --- /dev/null +++ b/packages/dynamic-links/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/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk42.m b/packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk42.m new file mode 100644 index 0000000000..3c06d90e66 --- /dev/null +++ b/packages/dynamic-links/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/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk44.m b/packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk44.m new file mode 100644 index 0000000000..7c4864d7d8 --- /dev/null +++ b/packages/dynamic-links/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/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk45.mm b/packages/dynamic-links/plugin/__tests__/fixtures/AppDelegate_sdk45.mm new file mode 100644 index 0000000000..59e8153756 --- /dev/null +++ b/packages/dynamic-links/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/dynamic-links/plugin/__tests__/iosPlugin.test.ts b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts new file mode 100644 index 0000000000..8d0d29ff93 --- /dev/null +++ b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts @@ -0,0 +1,107 @@ +import { IOSConfig } from '@expo/config-plugins'; +import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import fs from 'fs/promises'; +import path from 'path'; + +import { modifyAppDelegateAsync, modifyObjcAppDelegate } from '../src/ios/appDelegate'; + +describe('Config Plugin iOS Tests', function () { + beforeEach(function () { + jest.resetAllMocks(); + }); + + it('tests changes made to old AppDelegate.m (SDK 42)', async function () { + const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk42.m'), { + encoding: 'utf8', + }); + const result = modifyObjcAppDelegate(appDelegate); + expect(result).toMatchSnapshot(); + }); + + it('tests changes made to AppDelegate.m (SDK 43)', async function () { + const appDelegate = await fs.readFile( + path.join(__dirname, './fixtures/AppDelegate_bare_sdk43.m'), + { + encoding: 'utf8', + }, + ); + 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(); + }); + + it('works with AppDelegate.mm (RN 0.68+)', async function () { + const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'), { + encoding: 'utf8', + }); + const result = modifyObjcAppDelegate(appDelegate); + expect(result).toMatchSnapshot(); + }); + + it('detects Objective-C++ AppDelegate.mm', async function () { + jest.spyOn(fs, 'writeFile').mockImplementation(); + + const appDelegatePath = path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'); + const appDelegateFileInfo = IOSConfig.Paths.getFileInfo( + appDelegatePath, + ) as AppDelegateProjectFile; + + await modifyAppDelegateAsync(appDelegateFileInfo); + + // expect file contents to be modified + expect(fs.writeFile).toHaveBeenCalledWith( + appDelegateFileInfo.path, + expect.not.stringContaining(appDelegateFileInfo.contents), + ); + }); + + it("doesn't support Swift AppDelegate", async function () { + jest.spyOn(fs, 'writeFile').mockImplementation(); + + const appDelegateFileInfo: AppDelegateProjectFile = { + path: '.', + language: 'swift', + contents: 'some dummy content', + }; + + await expect(modifyAppDelegateAsync(appDelegateFileInfo)).rejects.toThrow(); + expect(fs.writeFile).not.toHaveBeenCalled(); + }); + + it('does not add the firebase import multiple times', async function () { + const singleImport = + '#import "AppDelegate.h"\n#import '; + const doubleImport = singleImport + '\n#import '; + + const appDelegate = await fs.readFile(path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'), { + encoding: 'utf8', + }); + expect(appDelegate).not.toContain(singleImport); + + const onceModifiedAppDelegate = modifyObjcAppDelegate(appDelegate); + expect(onceModifiedAppDelegate).toContain(singleImport); + expect(onceModifiedAppDelegate).not.toContain(doubleImport); + + const twiceModifiedAppDelegate = modifyObjcAppDelegate(onceModifiedAppDelegate); + expect(twiceModifiedAppDelegate).toContain(singleImport); + expect(twiceModifiedAppDelegate).not.toContain(doubleImport); + }); +}); diff --git a/packages/dynamic-links/plugin/src/index.ts b/packages/dynamic-links/plugin/src/index.ts new file mode 100644 index 0000000000..1e9925dbb8 --- /dev/null +++ b/packages/dynamic-links/plugin/src/index.ts @@ -0,0 +1,15 @@ +import { ConfigPlugin, withPlugins, createRunOncePlugin } from '@expo/config-plugins'; +import { withFirebaseAppDelegate } from './ios'; + +/** + * A config plugin for configuring `@react-native-firebase/dynamic-links` + */ +const withRnFirebaseDynamicLinks: ConfigPlugin = config => { + return withPlugins(config, [ + // iOS + withFirebaseAppDelegate, + ]); +}; + +const pak = require('@react-native-firebase/dynamic-links/package.json'); +export default createRunOncePlugin(withRnFirebaseDynamicLinks, pak.name, pak.version); diff --git a/packages/dynamic-links/plugin/src/ios/appDelegate.ts b/packages/dynamic-links/plugin/src/ios/appDelegate.ts new file mode 100644 index 0000000000..15e35ccc55 --- /dev/null +++ b/packages/dynamic-links/plugin/src/ios/appDelegate.ts @@ -0,0 +1,92 @@ +import { ConfigPlugin, IOSConfig, WarningAggregator, withDangerousMod } from '@expo/config-plugins'; +import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; +import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; +import fs from 'fs'; + +const methodInvocationBlock = `[RNFBDynamicLinksAppDelegateInterceptor sharedInstance];`; +// https://regex101.com/r/mPgaq6/1 +const methodInvocationLineMatcher = + /(?:(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 + if (!contents.includes('#import ')) { + contents = contents.replace( + /#import "AppDelegate.h"/g, + `#import "AppDelegate.h" +#import `, + ); + } + + // To avoid potential issues with existing changes from older plugin versions + if (contents.includes(methodInvocationBlock)) { + return contents; + } + + if ( + !methodInvocationLineMatcher.test(contents) && + !fallbackInvocationLineMatcher.test(contents) + ) { + WarningAggregator.addWarningIOS( + '@react-native-firebase/dynamic-links', + 'Unable to determine correct Firebase insertion point in AppDelegate.m. Skipping Firebase addition.', + ); + return contents; + } + + // Add invocation + 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 async function modifyAppDelegateAsync(appDelegateFileInfo: AppDelegateProjectFile) { + const { language, path, contents } = appDelegateFileInfo; + + if (['objc', 'objcpp'].includes(language)) { + const newContents = modifyObjcAppDelegate(contents); + await fs.promises.writeFile(path, newContents); + } else { + // TODO: Support Swift + throw new Error(`Cannot add Firebase code to AppDelegate of language "${language}"`); + } +} + +export const withFirebaseAppDelegate: ConfigPlugin = config => { + return withDangerousMod(config, [ + 'ios', + async config => { + const fileInfo = IOSConfig.Paths.getAppDelegate(config.modRequest.projectRoot); + await modifyAppDelegateAsync(fileInfo); + return config; + }, + ]); +}; diff --git a/packages/dynamic-links/plugin/src/ios/index.ts b/packages/dynamic-links/plugin/src/ios/index.ts new file mode 100644 index 0000000000..4bb98efe8d --- /dev/null +++ b/packages/dynamic-links/plugin/src/ios/index.ts @@ -0,0 +1,3 @@ +import { withFirebaseAppDelegate } from './appDelegate'; + +export { withFirebaseAppDelegate }; diff --git a/packages/dynamic-links/plugin/tsconfig.json b/packages/dynamic-links/plugin/tsconfig.json new file mode 100644 index 0000000000..a68b89f126 --- /dev/null +++ b/packages/dynamic-links/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "src", + "declaration": true + }, + "include": ["./src"] +} From 87bfbfab3fea489250add7208b0e69c9706d2708 Mon Sep 17 00:00:00 2001 From: austin43 Date: Tue, 1 Nov 2022 14:57:41 -0700 Subject: [PATCH 2/8] docs(dynamic-links): add note for iOS swizzling workaround --- docs/dynamic-links/usage/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/dynamic-links/usage/index.md b/docs/dynamic-links/usage/index.md index cd5889511f..145effbe91 100644 --- a/docs/dynamic-links/usage/index.md +++ b/docs/dynamic-links/usage/index.md @@ -50,6 +50,9 @@ a Dynamic Link on iOS or Android, they can be taken directly to the linked conte ## iOS Setup +> Notes: Currently, iOS requires a workaround to make method swizzling work. The workaround is described in [this Github comment](https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059). Without this, dynamic link matching behavior may be inconsistent. +> If you are using Expo Managed Workflow, be sure to load the [@react-native-firebase/dynamic-links config plugin](https://rnfirebase.io/#managed-workflow) to automatically apply the workaround. + To setup Dynamic Links on iOS, it is a **prerequisite** that you have an Apple developer account [setup](https://developer.apple.com/programs/enroll/). 1. Add an `App Store ID` & `Team ID` to your app in your Firebase console. If you do not have an `App Store ID` yet, you can put any number in here for now. Your `Team ID` can be found in your Apple developer console. From d89ddba2afd3b39323971b923c4a51917dad0b58 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 06:54:57 -0500 Subject: [PATCH 3/8] Update docs/dynamic-links/usage/index.md --- docs/dynamic-links/usage/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dynamic-links/usage/index.md b/docs/dynamic-links/usage/index.md index 145effbe91..9f09687c48 100644 --- a/docs/dynamic-links/usage/index.md +++ b/docs/dynamic-links/usage/index.md @@ -50,7 +50,7 @@ a Dynamic Link on iOS or Android, they can be taken directly to the linked conte ## iOS Setup -> Notes: Currently, iOS requires a workaround to make method swizzling work. The workaround is described in [this Github comment](https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059). Without this, dynamic link matching behavior may be inconsistent. +> Notes: Currently, iOS requires a workaround to make method swizzling work. The workaround is described in [this github comment](https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059). Without this, dynamic link matching behavior may be inconsistent. > If you are using Expo Managed Workflow, be sure to load the [@react-native-firebase/dynamic-links config plugin](https://rnfirebase.io/#managed-workflow) to automatically apply the workaround. To setup Dynamic Links on iOS, it is a **prerequisite** that you have an Apple developer account [setup](https://developer.apple.com/programs/enroll/). From 65775224f47093220693a3c983c9ee9d785d1f10 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 06:56:20 -0500 Subject: [PATCH 4/8] Update packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts --- packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts index 8d0d29ff93..38b73b8b1a 100644 --- a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts +++ b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts @@ -1,3 +1,4 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { IOSConfig } from '@expo/config-plugins'; import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import fs from 'fs/promises'; From 6cfbafd595fa384fa63ae1d92ecf7072611d33fa Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 07:03:53 -0500 Subject: [PATCH 5/8] Update packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts --- packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts index 38b73b8b1a..33ce1f93e9 100644 --- a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts +++ b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest, mockImplementation } from '@jest/globals'; import { IOSConfig } from '@expo/config-plugins'; import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import fs from 'fs/promises'; From fe630bde469b64f5f4822ecd50f480a3702558c8 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 07:08:50 -0500 Subject: [PATCH 6/8] Apply suggestions from code review --- packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts index 33ce1f93e9..204d5c57d7 100644 --- a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts +++ b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, jest, mockImplementation } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { IOSConfig } from '@expo/config-plugins'; import { AppDelegateProjectFile } from '@expo/config-plugins/build/ios/Paths'; import fs from 'fs/promises'; @@ -58,7 +58,7 @@ describe('Config Plugin iOS Tests', function () { }); it('detects Objective-C++ AppDelegate.mm', async function () { - jest.spyOn(fs, 'writeFile').mockImplementation(); + jest.spyOn(fs, 'writeFile').mockImplementation(() => {}); const appDelegatePath = path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'); const appDelegateFileInfo = IOSConfig.Paths.getFileInfo( @@ -75,7 +75,7 @@ describe('Config Plugin iOS Tests', function () { }); it("doesn't support Swift AppDelegate", async function () { - jest.spyOn(fs, 'writeFile').mockImplementation(); + jest.spyOn(fs, 'writeFile').mockImplementation(() => {}); const appDelegateFileInfo: AppDelegateProjectFile = { path: '.', From 30d450587225341a89b4f34032fa7f013fd615c7 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 07:22:16 -0500 Subject: [PATCH 7/8] test: fix jest mockImplementation --- packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts index 204d5c57d7..0640c33308 100644 --- a/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts +++ b/packages/dynamic-links/plugin/__tests__/iosPlugin.test.ts @@ -58,7 +58,7 @@ describe('Config Plugin iOS Tests', function () { }); it('detects Objective-C++ AppDelegate.mm', async function () { - jest.spyOn(fs, 'writeFile').mockImplementation(() => {}); + jest.spyOn(fs, 'writeFile').mockImplementation(async () => {}); const appDelegatePath = path.join(__dirname, './fixtures/AppDelegate_sdk45.mm'); const appDelegateFileInfo = IOSConfig.Paths.getFileInfo( @@ -75,7 +75,7 @@ describe('Config Plugin iOS Tests', function () { }); it("doesn't support Swift AppDelegate", async function () { - jest.spyOn(fs, 'writeFile').mockImplementation(() => {}); + jest.spyOn(fs, 'writeFile').mockImplementation(async () => {}); const appDelegateFileInfo: AppDelegateProjectFile = { path: '.', From 882fdc65f52693d0b34401a53878c8880fa57402 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Wed, 2 Nov 2022 07:42:34 -0500 Subject: [PATCH 8/8] test: update snapshots from new jest version --- .../__snapshots__/iosPlugin.test.ts.snap | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap b/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap index 4fe28833f5..d736d3c4db 100644 --- a/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap +++ b/packages/dynamic-links/plugin/__tests__/__snapshots__/iosPlugin.test.ts.snap @@ -5,7 +5,7 @@ exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m (SDK 43) 1` // It is (nearly) identical to the pure template used when // creating a bare React Native app (without Expo) -#import \\"AppDelegate.h\\" +#import "AppDelegate.h" #import #import @@ -45,8 +45,8 @@ static void InitializeFlipper(UIApplication *application) { [RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; // @generated end @react-native-firebase/app-didFinishLaunchingWithOptions RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil]; - id rootViewBackgroundColor = [[NSBundle mainBundle] objectForInfoDictionaryKey:@\\"RCTRootViewBackgroundColor\\"]; + 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 { @@ -72,9 +72,9 @@ static void InitializeFlipper(UIApplication *application) { - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #ifdef DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else - return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"]; + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } @@ -98,7 +98,7 @@ exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with Expo R "// This AppDelegate prebuild template is used in Expo SDK 44+ // It has the RCTBridge to be created by Expo ReactDelegate -#import \\"AppDelegate.h\\" +#import "AppDelegate.h" #import #import @@ -138,7 +138,7 @@ static void InitializeFlipper(UIApplication *application) { [RNFBDynamicLinksAppDelegateInterceptor sharedInstance]; // @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]; + 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]; @@ -159,9 +159,9 @@ static void InitializeFlipper(UIApplication *application) { - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #ifdef DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else - return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"]; + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } @@ -188,7 +188,7 @@ exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with fallba // some parts omitted to be short -#import \\"AppDelegate.h\\" +#import "AppDelegate.h" #import #import @@ -212,8 +212,8 @@ exports[`Config Plugin iOS Tests tests changes made to AppDelegate.m with fallba // 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\\"]; + 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 { @@ -239,7 +239,7 @@ exports[`Config Plugin iOS Tests tests changes made to old AppDelegate.m (SDK 42 "// 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 "AppDelegate.h" #import #import @@ -309,7 +309,7 @@ static void InitializeFlipper(UIApplication *application) { - (RCTBridge *)initializeReactNativeApp { RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:self.launchOptions]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@\\"main\\" initialProperties:nil]; + 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]; @@ -329,7 +329,7 @@ static void InitializeFlipper(UIApplication *application) { - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #ifdef DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\" fallbackResource:nil]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[EXUpdatesAppController sharedInstance] launchAssetUrl]; #endif @@ -350,7 +350,7 @@ exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 1`] = ` // 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 "AppDelegate.h" #import #import @@ -394,12 +394,12 @@ exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 1`] = ` #if RCT_NEW_ARCH_ENABLED _contextContainer = std::make_shared(); _reactNativeConfig = std::make_shared(); - _contextContainer->insert(\\"ReactNativeConfig\\", _reactNativeConfig); + _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]; + UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:nil]; rootView.backgroundColor = [UIColor whiteColor]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; @@ -422,9 +422,9 @@ exports[`Config Plugin iOS Tests works with AppDelegate.mm (RN 0.68+) 1`] = ` - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\\"index\\"]; + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else - return [[NSBundle mainBundle] URLForResource:@\\"main\\" withExtension:@\\"jsbundle\\"]; + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif }