const fs = require("fs"); const path = require("path"); const { createRunOncePlugin, withAppDelegate, withDangerousMod, withInfoPlist, } = require("@expo/config-plugins"); const SCENE_MARKER = "Xcode 27 UIScene lifecycle compatibility"; const DEPLOYMENT_TARGET_MARKER = "Xcode 27 minimum deployment target compatibility"; const SCENE_INTERFACE = `// ${SCENE_MARKER} @interface SceneDelegate : UIResponder @property (nonatomic, strong) UIWindow *window; @end`; const SCENE_CONFIGURATION = `- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; configuration.delegateClass = [SceneDelegate class]; return configuration; } `; const SCENE_IMPLEMENTATION = `@implementation SceneDelegate - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { UIWindowScene *windowScene = (UIWindowScene *)scene; AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; self.window = appDelegate.window; self.window.windowScene = windowScene; [self.window makeKeyAndVisible]; if (connectionOptions.URLContexts.count > 0) { [self scene:scene openURLContexts:connectionOptions.URLContexts]; } for (NSUserActivity *userActivity in connectionOptions.userActivities) { [self scene:scene continueUserActivity:userActivity]; } } - (void)sceneDidBecomeActive:(UIScene *)scene { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; [appDelegate applicationDidBecomeActive:UIApplication.sharedApplication]; } - (void)sceneWillResignActive:(UIScene *)scene { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; [appDelegate applicationWillResignActive:UIApplication.sharedApplication]; } - (void)sceneWillEnterForeground:(UIScene *)scene { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; [appDelegate applicationWillEnterForeground:UIApplication.sharedApplication]; } - (void)sceneDidEnterBackground:(UIScene *)scene { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; [appDelegate applicationDidEnterBackground:UIApplication.sharedApplication]; } - (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; for (UIOpenURLContext *urlContext in URLContexts) { NSMutableDictionary *options = [NSMutableDictionary dictionary]; options[UIApplicationOpenURLOptionsOpenInPlaceKey] = @(urlContext.options.openInPlace); if (urlContext.options.sourceApplication != nil) { options[UIApplicationOpenURLOptionsSourceApplicationKey] = urlContext.options.sourceApplication; } if (urlContext.options.annotation != nil) { options[UIApplicationOpenURLOptionsAnnotationKey] = urlContext.options.annotation; } [appDelegate application:UIApplication.sharedApplication openURL:urlContext.URL options:options]; } } - (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity { AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; [appDelegate application:UIApplication.sharedApplication continueUserActivity:userActivity restorationHandler:^(NSArray> *restorableObjects) { }]; } @end`; const DEPLOYMENT_TARGET_BLOCK = ` # ${DEPLOYMENT_TARGET_MARKER} installer.pods_project.targets.each do |target| target.build_configurations.each do |build_configuration| build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = ios_deployment_target end end`; function applySceneLifecycle(contents) { if (contents.includes(SCENE_MARKER)) { return contents; } const importAnchor = "#import "; const configurationAnchor = "- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge"; if (!contents.includes(importAnchor) || !contents.includes(configurationAnchor)) { throw new Error( "withXcode27Compatibility: could not find the Objective-C AppDelegate anchors." ); } const withInterface = contents.replace( importAnchor, `${importAnchor}\n\n${SCENE_INTERFACE}` ); const withConfiguration = withInterface.replace( configurationAnchor, `${SCENE_CONFIGURATION}\n${configurationAnchor}` ); return `${withConfiguration.trimEnd()}\n\n${SCENE_IMPLEMENTATION}\n`; } function applyDeploymentTarget(contents) { let patched = contents; if (!patched.includes("ios_deployment_target =")) { const platformLine = "platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'"; if (!patched.includes(platformLine)) { throw new Error( "withXcode27Compatibility: could not find the iOS platform declaration." ); } patched = patched.replace( platformLine, "ios_deployment_target = podfile_properties['ios.deploymentTarget'] || '15.1'\n" + "platform :ios, ios_deployment_target" ); } if (!patched.includes(DEPLOYMENT_TARGET_MARKER)) { const anchor = " # This is necessary for Xcode 14, because it signs resource bundles by default"; if (!patched.includes(anchor)) { throw new Error( "withXcode27Compatibility: could not find the Podfile post-install anchor." ); } patched = patched.replace( anchor, `${DEPLOYMENT_TARGET_BLOCK}\n\n${anchor}` ); } return patched; } const withXcode27Compatibility = (config) => { config = withInfoPlist(config, (config) => { config.modResults.UIApplicationSceneManifest = { UIApplicationSupportsMultipleScenes: false, UISceneConfigurations: { UIWindowSceneSessionRoleApplication: [ { UISceneConfigurationName: "Default Configuration", UISceneDelegateClassName: "SceneDelegate", }, ], }, }; return config; }); config = withAppDelegate(config, (config) => { if (!["objc", "objcpp"].includes(config.modResults.language)) { throw new Error( "withXcode27Compatibility: expected an Objective-C AppDelegate." ); } config.modResults.contents = applySceneLifecycle(config.modResults.contents); return config; }); config = withDangerousMod(config, [ "ios", async (config) => { const podfilePath = path.join( config.modRequest.platformProjectRoot, "Podfile" ); const podfile = fs.readFileSync(podfilePath, "utf8"); const patched = applyDeploymentTarget(podfile); if (patched !== podfile) { fs.writeFileSync(podfilePath, patched); } return config; }, ]); return config; }; module.exports = createRunOncePlugin( withXcode27Compatibility, "with-xcode-27-compatibility", "1.0.0" );