mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
Carries over #33221 and #33210. Replaces our #34484 Duo support with the community design, on top of main's rebuilt terminal key bar (#36166). Fixes #33200 Co-authored-by: Takeshi Matsumoto <zytakeshi520@gmail.com> Co-authored-by: Lars Keggenhoff <lars@kghf.io>
182 lines
6.1 KiB
JavaScript
182 lines
6.1 KiB
JavaScript
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const {
|
|
IOSConfig,
|
|
withAppDelegate,
|
|
withDangerousMod,
|
|
withInfoPlist,
|
|
withXcodeProject
|
|
} = require('expo/config-plugins')
|
|
|
|
// Why: UIKit stops an app built with the iOS 27 SDK at launch unless it adopts the UIScene
|
|
// lifecycle (TN3187), and Expo SDK 55's AppDelegate template still creates the window itself.
|
|
// The iOS 27 SDK is what lets the app resize on iPhone Duo, so the window moves into a scene
|
|
// delegate here. Drop this plugin once Expo's template adopts scenes (SDK 58).
|
|
const MARKER = 'orca: UIScene lifecycle'
|
|
|
|
const SCENE_MANIFEST = {
|
|
UIApplicationSupportsMultipleScenes: false,
|
|
UISceneConfigurations: {
|
|
UIWindowSceneSessionRoleApplication: [
|
|
{
|
|
UISceneConfigurationName: 'Default Configuration',
|
|
UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
const FACTORY_PROPERTY = ' var reactNativeFactory: RCTReactNativeFactory?\n'
|
|
const WINDOW_BLOCK =
|
|
/#if os\(iOS\) \|\| os\(tvOS\)\s*\n\s*window = UIWindow\(frame: UIScreen\.main\.bounds\)\s*\n\s*factory\.startReactNative\(\s*withModuleName: "main",\s*in: window,\s*launchOptions: launchOptions\)\s*\n#endif\n/
|
|
|
|
function patchAppDelegate(contents) {
|
|
if (contents.includes(MARKER)) {
|
|
return contents
|
|
}
|
|
// A changed template must fail prebuild rather than ship an app that cannot launch.
|
|
if (!contents.includes(FACTORY_PROPERTY) || !WINDOW_BLOCK.test(contents)) {
|
|
throw new Error(
|
|
"ios-scene-lifecycle: Expo's AppDelegate template changed; update the plugin before building."
|
|
)
|
|
}
|
|
return contents
|
|
.replace(
|
|
FACTORY_PROPERTY,
|
|
`${FACTORY_PROPERTY} // ${MARKER}: SceneDelegate starts React Native with these.\n var launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n`
|
|
)
|
|
.replace(
|
|
WINDOW_BLOCK,
|
|
` // ${MARKER}: SceneDelegate creates the window.\n self.launchOptions = launchOptions\n`
|
|
)
|
|
}
|
|
|
|
const SCENE_DELEGATE_SWIFT = `import React
|
|
import UIKit
|
|
|
|
// ${MARKER}, generated by plugins/ios-scene-lifecycle.js.
|
|
// Under scenes UIKit stops calling the AppDelegate URL callbacks, where RCTLinkingManager and
|
|
// Expo's subscribers listen, so every URL and user activity is forwarded to them.
|
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
var window: UIWindow?
|
|
|
|
private var appDelegate: AppDelegate? {
|
|
UIApplication.shared.delegate as? AppDelegate
|
|
}
|
|
|
|
func scene(
|
|
_ scene: UIScene,
|
|
willConnectTo session: UISceneSession,
|
|
options connectionOptions: UIScene.ConnectionOptions
|
|
) {
|
|
guard let windowScene = scene as? UIWindowScene, let appDelegate,
|
|
let factory = appDelegate.reactNativeFactory
|
|
else { return }
|
|
|
|
let window = UIWindow(windowScene: windowScene)
|
|
self.window = window
|
|
// A scene iOS dropped in the background and reconnects keeps the running app, not a fresh one.
|
|
let runningRoot = appDelegate.window?.rootViewController
|
|
appDelegate.window = window
|
|
|
|
let launchURL = connectionOptions.urlContexts.first
|
|
// Universal links are the only activity the app handles.
|
|
let launchActivity = connectionOptions.userActivities.first {
|
|
$0.activityType == NSUserActivityTypeBrowsingWeb
|
|
}
|
|
|
|
if let runningRoot {
|
|
window.rootViewController = runningRoot
|
|
window.makeKeyAndVisible()
|
|
} else {
|
|
// A cold-start URL arrives here rather than in the launch options getInitialURL reads.
|
|
var launchOptions = appDelegate.launchOptions ?? [:]
|
|
if let launchURL {
|
|
launchOptions[.url] = launchURL.url
|
|
if let source = launchURL.options.sourceApplication {
|
|
launchOptions[.sourceApplication] = source
|
|
}
|
|
}
|
|
if let launchActivity {
|
|
launchOptions[.userActivityDictionary] = [
|
|
UIApplication.LaunchOptionsKey.userActivityType.rawValue: launchActivity.activityType,
|
|
"UIApplicationLaunchOptionsUserActivityKey": launchActivity,
|
|
]
|
|
}
|
|
factory.startReactNative(withModuleName: "main", in: window, launchOptions: launchOptions)
|
|
}
|
|
|
|
if let launchURL {
|
|
forward(launchURL)
|
|
}
|
|
if let launchActivity {
|
|
_ = appDelegate.application(UIApplication.shared, continue: launchActivity) { _ in }
|
|
}
|
|
}
|
|
|
|
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
for context in URLContexts {
|
|
forward(context)
|
|
}
|
|
}
|
|
|
|
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
_ = appDelegate?.application(UIApplication.shared, continue: userActivity) { _ in }
|
|
}
|
|
|
|
private func forward(_ context: UIOpenURLContext) {
|
|
var options: [UIApplication.OpenURLOptionsKey: Any] = [
|
|
.openInPlace: context.options.openInPlace
|
|
]
|
|
if let source = context.options.sourceApplication {
|
|
options[.sourceApplication] = source
|
|
}
|
|
if let annotation = context.options.annotation {
|
|
options[.annotation] = annotation
|
|
}
|
|
_ = appDelegate?.application(UIApplication.shared, open: context.url, options: options)
|
|
}
|
|
}
|
|
`
|
|
|
|
module.exports = function withIosSceneLifecycle(config) {
|
|
config = withInfoPlist(config, (cfg) => {
|
|
cfg.modResults.UIApplicationSceneManifest = SCENE_MANIFEST
|
|
return cfg
|
|
})
|
|
|
|
config = withAppDelegate(config, (cfg) => {
|
|
if (cfg.modResults.language !== 'swift') {
|
|
throw new Error(
|
|
`ios-scene-lifecycle: expected a Swift AppDelegate, got ${cfg.modResults.language}`
|
|
)
|
|
}
|
|
cfg.modResults.contents = patchAppDelegate(cfg.modResults.contents)
|
|
return cfg
|
|
})
|
|
|
|
config = withDangerousMod(config, [
|
|
'ios',
|
|
(cfg) => {
|
|
const { platformProjectRoot, projectName } = cfg.modRequest
|
|
fs.writeFileSync(
|
|
path.join(platformProjectRoot, projectName, 'SceneDelegate.swift'),
|
|
SCENE_DELEGATE_SWIFT
|
|
)
|
|
return cfg
|
|
}
|
|
])
|
|
|
|
return withXcodeProject(config, (cfg) => {
|
|
const filepath = `${cfg.modRequest.projectName}/SceneDelegate.swift`
|
|
if (!cfg.modResults.hasFile(filepath)) {
|
|
IOSConfig.XcodeUtils.addBuildSourceFileToGroup({
|
|
filepath,
|
|
groupName: cfg.modRequest.projectName,
|
|
project: cfg.modResults
|
|
})
|
|
}
|
|
return cfg
|
|
})
|
|
}
|