first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
Base class for app delegate subscribers. Ensures the class
inherits from `UIResponder` and has `required init()` initializer.
*/
@objc(EXBaseAppDelegateSubscriber)
open class BaseExpoAppDelegateSubscriber: UIResponder {
public override required init() {
super.init()
}
#if os(macOS)
public required init?(coder: NSCoder) {
super.init(coder: coder)
}
#endif // os(macOS)
}
/**
Typealias to `UIApplicationDelegate` protocol.
Might be useful for compatibility reasons if we decide to add more things here.
*/
@objc(EXAppDelegateSubscriberProtocol)
public protocol ExpoAppDelegateSubscriberProtocol: UIApplicationDelegate {
@objc optional func customizeRootView(_ rootView: UIView)
/**
Function that is called automatically by the `ExpoAppDelegate` once the subscriber is successfully registered.
If the subscriber is loaded from the modules provider, it is executed just before the main code of the app,
thus even before any other `UIApplicationDelegate` function. Use it if your subscriber needs to run some code as early as possible,
but keep in mind that this affects the application loading time.
*/
@objc
@MainActor
optional func subscriberDidRegister()
/**
Function that is called at the beginning of the `AppDelegate` initialization,
i.e. during the `main` function and before the application starts launching.
*/
@objc
@MainActor
optional func appDelegateWillBeginInitialization()
}
/**
Typealias merging the base class for app delegate subscribers and protocol inheritance to `UIApplicationDelegate`.
*/
public typealias ExpoAppDelegateSubscriber = BaseExpoAppDelegateSubscriber & ExpoAppDelegateSubscriberProtocol

View File

@@ -0,0 +1,510 @@
import Dispatch
import Foundation
@MainActor
@preconcurrency
public class ExpoAppDelegateSubscriberManager: NSObject {
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:willFinishLaunchingWithOptions:)))
}
// If we can't find a subscriber that implements `willFinishLaunchingWithOptions`, we will delegate the decision if we can handle the passed URL to
// the `didFinishLaunchingWithOptions` method by returning `true` here.
// You can read more about how iOS handles deep links here: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623112-application#discussion
if parsedSubscribers.isEmpty {
return true
}
return parsedSubscribers.reduce(false) { result, subscriber in
return subscriber.application?(application, willFinishLaunchingWithOptions: launchOptions) ?? false || result
}
}
@objc
public static func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.application?(application, didFinishLaunchingWithOptions: launchOptions)
}
return true
}
#elseif os(macOS)
@objc
public static func applicationWillFinishLaunching(_ notification: Notification) {
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(NSApplicationDelegate.applicationWillFinishLaunching(_:)))
}
parsedSubscribers.forEach { subscriber in
subscriber.applicationWillFinishLaunching?(notification)
}
}
@objc
public static func applicationDidFinishLaunching(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { subscriber in
// Subscriber result is ignored as it doesn't matter if any subscriber handled the incoming URL we always return `true` anyway.
_ = subscriber.applicationDidFinishLaunching?(notification)
}
}
// TODO: - Configuring and Discarding Scenes
#endif
// MARK: - Responding to App Life-Cycle Events
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidBecomeActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(application) }
}
@objc
public static func applicationWillResignActive(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(application) }
}
@objc
public static func applicationDidEnterBackground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidEnterBackground?(application) }
}
@objc
public static func applicationWillEnterForeground(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillEnterForeground?(application) }
}
@objc
public static func applicationWillTerminate(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(application) }
}
#elseif os(macOS)
@objc
public static func applicationDidBecomeActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidBecomeActive?(notification) }
}
@objc
public static func applicationWillResignActive(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillResignActive?(notification) }
}
@objc
public static func applicationDidHide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidHide?(notification) }
}
@objc
public static func applicationWillUnhide(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillUnhide?(notification) }
}
@objc
public static func applicationWillTerminate(_ notification: Notification) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationWillTerminate?(notification) }
}
#endif
// MARK: - Responding to Environment Changes
#if os(iOS) || os(tvOS)
@objc
public static func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.applicationDidReceiveMemoryWarning?(application) }
}
#endif
// TODO: - Managing App State Restoration
// MARK: - Downloading Data in the Background
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
if subs.isEmpty {
completionHandler()
return
}
var subscribersLeft = subs.count
let aggregatedHandler = {
DispatchQueue.main.async {
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler()
}
}
}
subs.forEach {
$0.application?(application, handleEventsForBackgroundURLSession: identifier, completionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Handling Remote Notification Registration
@objc
public static func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) }
}
@objc
public static func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didFailToRegisterForRemoteNotificationsWithError: error) }
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
if subs.isEmpty {
completionHandler(.noData)
return
}
var subscribersLeft = subs.count
var failedCount = 0
var newDataCount = 0
let aggregatedHandler = { (result: UIBackgroundFetchResult) in
DispatchQueue.main.async {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: aggregatedHandler)
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
didReceiveRemoteNotification userInfo: [String: Any]
) {
let selector = #selector(NSApplicationDelegate.application(_:didReceiveRemoteNotification:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
subs.forEach { subscriber in
subscriber.application?(application, didReceiveRemoteNotification: userInfo)
}
}
#endif
// MARK: - Continuing User Activity and Handling Quick Actions
@objc
public static func application(_ application: UIApplication, willContinueUserActivityWithType userActivityType: String) -> Bool {
return ExpoAppDelegateSubscriberRepository
.subscribers
.reduce(false) { result, subscriber in
return subscriber.application?(application, willContinueUserActivityWithType: userActivityType) ?? false || result
}
}
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
let selector = #selector(UIApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var allRestorableObjects = [UIUserActivityRestoring]()
let aggregatedHandler = { (restorableObjects: [UIUserActivityRestoring]?) in
DispatchQueue.main.async {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: aggregatedHandler) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(
_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void
) -> Bool {
let selector = #selector(NSApplicationDelegate.application(_:continue:restorationHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var allRestorableObjects = [NSUserActivityRestoring]()
let aggregatedHandler = { (restorableObjects: [NSUserActivityRestoring]?) in
DispatchQueue.main.async {
if let restorableObjects = restorableObjects {
allRestorableObjects.append(contentsOf: restorableObjects)
}
subscribersLeft -= 1
if subscribersLeft == 0 {
restorationHandler(allRestorableObjects)
}
}
}
return subs.reduce(false) { result, subscriber in
return subscriber.application?(application, continue: userActivity, restorationHandler: aggregatedHandler) ?? false || result
}
}
#endif
@objc
public static func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach { $0.application?(application, didUpdate: userActivity) }
}
@objc
public static func application(_ application: UIApplication, didFailToContinueUserActivityWithType userActivityType: String, error: Error) {
return ExpoAppDelegateSubscriberRepository
.subscribers
.forEach {
$0.application?(application, didFailToContinueUserActivityWithType: userActivityType, error: error)
}
}
#if os(iOS)
@objc
public static func application(
_ application: UIApplication,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performActionFor:completionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
var result: Bool = false
if subs.isEmpty {
completionHandler(result)
return
}
let aggregatedHandler = { (succeeded: Bool) in
DispatchQueue.main.async {
result = result || succeeded
subscribersLeft -= 1
if subscribersLeft == 0 {
completionHandler(result)
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, performActionFor: shortcutItem, completionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Background Fetch
#if os(iOS) || os(tvOS)
@objc
public static func application(
_ application: UIApplication,
performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let selector = #selector(UIApplicationDelegate.application(_:performFetchWithCompletionHandler:))
let subs = ExpoAppDelegateSubscriberRepository.subscribers.filter { $0.responds(to: selector) }
var subscribersLeft = subs.count
if subs.isEmpty {
completionHandler(.noData)
return
}
var failedCount = 0
var newDataCount = 0
let aggregatedHandler = { (result: UIBackgroundFetchResult) in
DispatchQueue.main.async {
if result == .failed {
failedCount += 1
} else if result == .newData {
newDataCount += 1
}
subscribersLeft -= 1
if subscribersLeft == 0 {
if newDataCount > 0 {
completionHandler(.newData)
} else if failedCount > 0 {
completionHandler(.failed)
} else {
completionHandler(.noData)
}
}
}
}
subs.forEach { subscriber in
subscriber.application?(application, performFetchWithCompletionHandler: aggregatedHandler)
}
}
#endif
// MARK: - Opening a URL-Specified Resource
#if os(iOS) || os(tvOS)
@objc
public static func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return ExpoAppDelegateSubscriberRepository.subscribers.reduce(false) { result, subscriber in
return subscriber.application?(app, open: url, options: options) ?? false || result
}
}
#elseif os(macOS)
@objc
public static func application(_ app: NSApplication, open urls: [URL]) {
ExpoAppDelegateSubscriberRepository.subscribers.forEach { subscriber in
subscriber.application?(app, open: urls)
}
}
#endif
#if os(iOS)
/**
* Sets allowed orientations for the application. It will use the values from `Info.plist`as the orientation mask unless a subscriber requested
* a different orientation.
*/
@objc
public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
let universalOrientationMask = allowedOrientations(for: .unspecified)
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
$0.responds(to: #selector(UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)))
}
// We want to create an intersection of all orientations set by subscribers.
let subscribersMask: UIInterfaceOrientationMask = parsedSubscribers.reduce(.all) { result, subscriber in
guard let requestedOrientation = subscriber.application?(application, supportedInterfaceOrientationsFor: window) else {
return result
}
return requestedOrientation.intersection(result)
}
return parsedSubscribers.isEmpty ? infoPlistOrientations : subscribersMask
}
#endif
}
#if os(iOS)
private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
// For now only iPad-specific orientations are supported
let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
var mask: UIInterfaceOrientationMask = []
guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
return mask
}
for orientation in orientations {
switch orientation {
case "UIInterfaceOrientationPortrait":
mask.insert(.portrait)
case "UIInterfaceOrientationLandscapeLeft":
mask.insert(.landscapeLeft)
case "UIInterfaceOrientationLandscapeRight":
mask.insert(.landscapeRight)
case "UIInterfaceOrientationPortraitUpsideDown":
mask.insert(.portraitUpsideDown)
default:
break
}
}
return mask
}
#endif // os(iOS)

View File

@@ -0,0 +1,59 @@
// Copyright 2018-present 650 Industries. All rights reserved.
@MainActor private var _subscribers = [ExpoAppDelegateSubscriberProtocol]()
@MainActor private var _reactDelegateHandlers = [ExpoReactDelegateHandler]()
/**
Class responsible for managing access to app delegate subscribers and react delegates.
It should be used to access subscribers without depending on the `Expo` package where they are registered.
*/
@MainActor
@preconcurrency
@objc(EXExpoAppDelegateSubscriberRepository)
public class ExpoAppDelegateSubscriberRepository: NSObject {
@objc
public static var subscribers: [ExpoAppDelegateSubscriberProtocol] {
return _subscribers
}
@objc
public static var reactDelegateHandlers: [ExpoReactDelegateHandler] {
return _reactDelegateHandlers
}
@objc
public static func registerSubscribersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getAppDelegateSubscribers().forEach { subscriberType in
registerSubscriber(subscriberType.init())
}
}
@objc
public static func registerSubscriber(_ subscriber: ExpoAppDelegateSubscriberProtocol) {
if _subscribers.contains(where: { $0 === subscriber }) {
fatalError("Given app delegate subscriber `\(String(describing: subscriber))` is already registered.")
}
_subscribers.append(subscriber)
subscriber.subscriberDidRegister?()
}
@objc
public static func getSubscriber(_ name: String) -> ExpoAppDelegateSubscriberProtocol? {
return _subscribers.first { String(describing: $0) == name }
}
public static func getSubscriberOfType<Subscriber>(_ type: Subscriber.Type) -> Subscriber? {
return _subscribers.first { $0 is Subscriber } as? Subscriber
}
@objc
public static func registerReactDelegateHandlersFrom(modulesProvider: ModulesProvider) {
modulesProvider.getReactDelegateHandlers()
.sorted { tuple1, tuple2 -> Bool in
return ModulePriorities.get(tuple1.packageName) > ModulePriorities.get(tuple2.packageName)
}
.forEach { handlerTuple in
_reactDelegateHandlers.append(handlerTuple.handler.init())
}
}
}

View File

@@ -0,0 +1,12 @@
public protocol ExpoReactNativeFactoryProtocol: AnyObject {
/**
To decouple RCTAppDelegate dependency from expo-modules-core,
expo-modules-core doesn't include the concrete `RCTReactNativeFactory` type and let the callsite to include the type
*/
func recreateRootView(
withBundleURL: URL?,
moduleName: String?,
initialProps: [AnyHashable: Any]?,
launchOptions: [AnyHashable: Any]?
) -> UIView
}

View File

@@ -0,0 +1,20 @@
// Copyright 2018-present 650 Industries. All rights reserved.
/**
This class determines the order of `ExpoReactDelegateHandler`.
The priority is only for internal use and we maintain a pre-defined `SUPPORTED_MODULE` map.
*/
internal struct ModulePriorities {
static let SUPPORTED_MODULE = [
// {key}: {value}
// key: node package name
// value: priority value, the higher value takes precedence
"expo-screen-orientation": 10,
"expo-updates": 5
]
static func get(_ packageName: String) -> Int {
return SUPPORTED_MODULE[packageName] ?? 0
}
}