evOo-local/apps/mobile/plugins/with-scene-lifecycle.js
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

95 lines
3.4 KiB
JavaScript

const { withAppDelegate, withInfoPlist } = require("expo/config-plugins");
const LAUNCHING_BLOCK = ` reactNativeDelegate = delegate
reactNativeFactory = factory
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "main",
in: window,
launchOptions: launchOptions)
#endif
return super.application(application, didFinishLaunchingWithOptions: launchOptions)`;
const LAUNCHING_BLOCK_REPLACEMENT = ` reactNativeDelegate = delegate
reactNativeFactory = factory
return super.application(application, didFinishLaunchingWithOptions: launchOptions)`;
const CLASS_DECLARATION = "class AppDelegate: ExpoAppDelegate {";
const CLASS_DECLARATION_REPLACEMENT =
"class AppDelegate: ExpoAppDelegate, ExpoReactNativeFactoryProvider {";
// The ObjC runtime name of Expo's `ExpoAppSceneDelegate`; referencing it by
// runtime name avoids Swift module-name mangling in the generated Info.plist.
const SCENE_DELEGATE_CLASS_NAME = "EXExpoAppSceneDelegate";
/**
* iOS 26 refuses to launch apps built with the iOS 26 SDK unless they adopt
* the UIScene life cycle. Expo's SDK 57 AppDelegate template still creates a
* bare UIWindow in didFinishLaunching, so a prebuild from a current Xcode
* crashes at startup on iOS 26 devices. Point the scene manifest at Expo's
* own scene delegate and let it own window creation: it starts React Native
* from the factory the app delegate builds, and re-feeds deep links,
* universal links, and quick actions through Expo's linking path (which a
* hand-rolled delegate forwarding to RCTLinkingManager would bypass).
*/
function withSceneLifecycle(config) {
config = withAppDelegate(config, (config) => {
config.modResults.contents = applySceneLifecycleToAppDelegate(config.modResults.contents);
return config;
});
config = withInfoPlist(config, (config) => {
applySceneManifest(config.modResults);
return config;
});
return config;
}
function applySceneLifecycleToAppDelegate(contents) {
// Only treat the file as already migrated when no legacy window startup is
// left; a file with both would double-start React Native (the app delegate
// and the scene delegate each call startReactNative).
if (contents.includes("ExpoReactNativeFactoryProvider")) {
if (contents.includes(LAUNCHING_BLOCK)) {
throw new Error(
"with-scene-lifecycle: AppDelegate already conforms to ExpoReactNativeFactoryProvider but still has legacy window startup; refusing to guess.",
);
}
return contents;
}
if (!contents.includes(LAUNCHING_BLOCK) || !contents.includes(CLASS_DECLARATION)) {
throw new Error(
"with-scene-lifecycle: Expo AppDelegate template changed; update the plugin's anchor strings.",
);
}
return contents
.replace(CLASS_DECLARATION, CLASS_DECLARATION_REPLACEMENT)
.replace(LAUNCHING_BLOCK, LAUNCHING_BLOCK_REPLACEMENT);
}
function applySceneManifest(infoPlist) {
infoPlist.UIApplicationSceneManifest = {
UIApplicationSupportsMultipleScenes: false,
UISceneConfigurations: {
UIWindowSceneSessionRoleApplication: [
{
UISceneConfigurationName: "main",
UISceneDelegateClassName: SCENE_DELEGATE_CLASS_NAME,
},
],
},
};
return infoPlist;
}
module.exports = withSceneLifecycle;
module.exports.applySceneLifecycleToAppDelegate = applySceneLifecycleToAppDelegate;
module.exports.applySceneManifest = applySceneManifest;