expo 57.0.22 → 57.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +2 -2
- package/bundledNativeModules.json +14 -14
- package/ios/AppDelegates/ExpoAppSceneDelegate.swift +181 -0
- package/ios/AppDelegates/ExpoReactNativeFactoryProvider.swift +30 -0
- package/ios/AppDelegates/SceneEventForwarder.swift +154 -0
- package/ios/Tests/ExpoAppSceneDelegateTests.swift +602 -0
- package/package.json +4 -4
- package/template.tgz +0 -0
package/android/build.gradle
CHANGED
|
@@ -10,7 +10,7 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
group = 'host.exp.exponent'
|
|
13
|
-
version = '57.0.
|
|
13
|
+
version = '57.0.23'
|
|
14
14
|
|
|
15
15
|
expoModule {
|
|
16
16
|
// We can't prebuild the module because it depends on the generated files.
|
|
@@ -21,7 +21,7 @@ android {
|
|
|
21
21
|
namespace "expo.core"
|
|
22
22
|
defaultConfig {
|
|
23
23
|
versionCode 1
|
|
24
|
-
versionName "57.0.
|
|
24
|
+
versionName "57.0.23"
|
|
25
25
|
consumerProguardFiles("proguard-rules.pro")
|
|
26
26
|
}
|
|
27
27
|
testOptions {
|
|
@@ -17,19 +17,19 @@
|
|
|
17
17
|
"expo-analytics-amplitude": "~11.3.0",
|
|
18
18
|
"expo-app-auth": "~11.1.0",
|
|
19
19
|
"expo-app-loader-provider": "~8.0.0",
|
|
20
|
-
"expo-app-metrics": "~57.0.
|
|
20
|
+
"expo-app-metrics": "~57.0.19",
|
|
21
21
|
"expo-apple-authentication": "~57.0.2",
|
|
22
22
|
"expo-application": "~57.0.3",
|
|
23
23
|
"expo-asset": "~57.0.17",
|
|
24
24
|
"expo-audio": "~57.0.5",
|
|
25
25
|
"expo-auth-session": "~57.0.12",
|
|
26
|
-
"expo-background-fetch": "~57.0.
|
|
27
|
-
"expo-background-task": "~57.0.
|
|
26
|
+
"expo-background-fetch": "~57.0.18",
|
|
27
|
+
"expo-background-task": "~57.0.18",
|
|
28
28
|
"expo-battery": "~57.0.3",
|
|
29
29
|
"expo-blur": "~57.0.3",
|
|
30
30
|
"expo-brightness": "~57.0.2",
|
|
31
|
-
"expo-brownfield": "~57.0.
|
|
32
|
-
"expo-build-properties": "~57.0.
|
|
31
|
+
"expo-brownfield": "~57.0.20",
|
|
32
|
+
"expo-build-properties": "~57.0.18",
|
|
33
33
|
"expo-calendar": "~57.0.3",
|
|
34
34
|
"expo-camera": "~57.0.5",
|
|
35
35
|
"expo-cellular": "~57.0.2",
|
|
@@ -50,28 +50,28 @@
|
|
|
50
50
|
"expo-haptics": "~57.0.3",
|
|
51
51
|
"expo-image": "~57.0.5",
|
|
52
52
|
"expo-image-loader": "~57.0.1",
|
|
53
|
-
"expo-image-manipulator": "~57.0.
|
|
54
|
-
"expo-image-picker": "~57.0.
|
|
53
|
+
"expo-image-manipulator": "~57.0.18",
|
|
54
|
+
"expo-image-picker": "~57.0.18",
|
|
55
55
|
"expo-intent-launcher": "~57.0.1",
|
|
56
|
-
"expo-insights": "~57.0.
|
|
56
|
+
"expo-insights": "~57.0.18",
|
|
57
57
|
"expo-keep-awake": "~57.0.2",
|
|
58
58
|
"expo-linear-gradient": "~57.0.2",
|
|
59
59
|
"expo-linking": "~57.0.10",
|
|
60
60
|
"expo-local-authentication": "~57.0.3",
|
|
61
61
|
"expo-localization": "~57.0.2",
|
|
62
|
-
"expo-location": "~57.0.
|
|
62
|
+
"expo-location": "~57.0.18",
|
|
63
63
|
"expo-mail-composer": "~57.0.2",
|
|
64
64
|
"expo-manifests": "~57.0.2",
|
|
65
65
|
"expo-maps": "~57.0.3",
|
|
66
66
|
"expo-mcp": "~0.2.1",
|
|
67
67
|
"expo-media-library": "~57.0.5",
|
|
68
68
|
"expo-mesh-gradient": "~57.0.2",
|
|
69
|
-
"expo-module-template": "~57.0.
|
|
69
|
+
"expo-module-template": "~57.0.12",
|
|
70
70
|
"expo-modules-core": "~57.0.18",
|
|
71
71
|
"expo-navigation-bar": "~57.0.2",
|
|
72
72
|
"expo-network": "~57.0.2",
|
|
73
|
-
"expo-notifications": "~57.0.
|
|
74
|
-
"expo-observe": "~57.0.
|
|
73
|
+
"expo-notifications": "~57.0.19",
|
|
74
|
+
"expo-observe": "~57.0.22",
|
|
75
75
|
"expo-print": "~57.0.2",
|
|
76
76
|
"expo-live-photo": "~57.0.2",
|
|
77
77
|
"expo-router": "~57.0.21",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"expo-secure-store": "~57.0.4",
|
|
81
81
|
"expo-sensors": "~57.0.3",
|
|
82
82
|
"expo-server": "~57.0.3",
|
|
83
|
-
"expo-sharing": "~57.0.
|
|
83
|
+
"expo-sharing": "~57.0.20",
|
|
84
84
|
"expo-sms": "~57.0.2",
|
|
85
85
|
"expo-speech": "~57.0.3",
|
|
86
86
|
"expo-splash-screen": "~57.0.9",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"expo-store-review": "~57.0.3",
|
|
90
90
|
"expo-symbols": "~57.0.3",
|
|
91
91
|
"expo-system-ui": "~57.0.4",
|
|
92
|
-
"expo-task-manager": "~57.0.
|
|
92
|
+
"expo-task-manager": "~57.0.18",
|
|
93
93
|
"expo-tracking-transparency": "~57.0.2",
|
|
94
94
|
"expo-updates": "~57.0.22",
|
|
95
95
|
"expo-video-thumbnails": "~57.0.2",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import React
|
|
5
|
+
|
|
6
|
+
#if os(iOS) || os(tvOS)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
`UIWindowSceneDelegate` base class for Expo apps. Required by the iOS 27, which
|
|
10
|
+
asserts at launch unless the app adopts the scene-based life cycle.
|
|
11
|
+
|
|
12
|
+
Responsibilities:
|
|
13
|
+
- Create the `UIWindow` from the connecting `UIWindowScene` and start React Native into it.
|
|
14
|
+
- Re-feed scene life-cycle, URL, user-activity, and quick-action events to the app delegate.
|
|
15
|
+
|
|
16
|
+
Requires the app delegate to be an `ExpoAppDelegate`; it forwards every event to the subscribers,
|
|
17
|
+
so one call reaches both subscribers and `AppDelegate` overrides.
|
|
18
|
+
*/
|
|
19
|
+
@available(iOSApplicationExtension, unavailable)
|
|
20
|
+
@objc(EXExpoAppSceneDelegate)
|
|
21
|
+
open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
22
|
+
open var window: UIWindow?
|
|
23
|
+
|
|
24
|
+
let forwarder = SceneEventForwarder()
|
|
25
|
+
|
|
26
|
+
open func scene(
|
|
27
|
+
_ scene: UIScene,
|
|
28
|
+
willConnectTo session: UISceneSession,
|
|
29
|
+
options connectionOptions: UIScene.ConnectionOptions
|
|
30
|
+
) {
|
|
31
|
+
guard let windowScene = scene as? UIWindowScene else {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
guard let appDelegate = UIApplication.shared.delegate as? ExpoAppDelegate,
|
|
35
|
+
let provider = appDelegate as? ExpoReactNativeFactoryProvider,
|
|
36
|
+
let factory = provider.reactNativeFactory else {
|
|
37
|
+
fatalError(
|
|
38
|
+
"ExpoAppSceneDelegate couldn't start React Native because the app delegate isn't an "
|
|
39
|
+
+ "ExpoAppDelegate that provides a React Native factory. Make sure your AppDelegate subclasses "
|
|
40
|
+
+ "ExpoAppDelegate, conforms to ExpoReactNativeFactoryProvider and creates its "
|
|
41
|
+
+ "RCTReactNativeFactory in application(_:didFinishLaunchingWithOptions:)."
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let window = UIWindow(windowScene: windowScene)
|
|
46
|
+
self.window = window
|
|
47
|
+
|
|
48
|
+
// Mirror the window onto the app delegate so code that reads
|
|
49
|
+
// `UIApplication.shared.delegate?.window` keeps working (e.g. expo-system-ui).
|
|
50
|
+
provider.window = window
|
|
51
|
+
|
|
52
|
+
// Under the scene life cycle UIKit passes cold-start URLs and activities in `connectionOptions`
|
|
53
|
+
// rather than in the app delegate's launch options. React Native's `Linking.getInitialURL()`
|
|
54
|
+
// only reads them from launch options, so rebuild them here; otherwise a link that cold-starts
|
|
55
|
+
// the app is delivered to no one, because the `url` event routed below fires before JS is ready.
|
|
56
|
+
let browsingWebActivity = connectionOptions.userActivities.first {
|
|
57
|
+
$0.activityType == NSUserActivityTypeBrowsingWeb
|
|
58
|
+
}
|
|
59
|
+
factory.startReactNative(
|
|
60
|
+
withModuleName: provider.reactNativeFactoryModuleName,
|
|
61
|
+
in: window,
|
|
62
|
+
launchOptions: Self.launchOptions(
|
|
63
|
+
url: connectionOptions.urlContexts.first?.url,
|
|
64
|
+
userActivity: browsingWebActivity
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Deep links / universal links.
|
|
69
|
+
connectionOptions.urlContexts.forEach {
|
|
70
|
+
forwarder.open(url: $0.url, options: Self.openURLOptions(from: $0.options))
|
|
71
|
+
}
|
|
72
|
+
connectionOptions.userActivities.forEach { forwarder.continue($0) }
|
|
73
|
+
|
|
74
|
+
#if os(iOS)
|
|
75
|
+
// A quick action that cold-starts the app arrives here instead of in
|
|
76
|
+
// `windowScene(_:performActionFor:completionHandler:)`, which UIKit only calls while running.
|
|
77
|
+
if let shortcutItem = connectionOptions.shortcutItem {
|
|
78
|
+
forwarder.perform(shortcutItem) { _ in }
|
|
79
|
+
}
|
|
80
|
+
#endif
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
open func sceneDidDisconnect(_ scene: UIScene) {
|
|
84
|
+
window = nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
open func sceneDidBecomeActive(_ scene: UIScene) {
|
|
88
|
+
forwarder.didBecomeActive()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
open func sceneWillResignActive(_ scene: UIScene) {
|
|
92
|
+
forwarder.willResignActive()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
open func sceneWillEnterForeground(_ scene: UIScene) {
|
|
96
|
+
forwarder.willEnterForeground()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
open func sceneDidEnterBackground(_ scene: UIScene) {
|
|
100
|
+
forwarder.didEnterBackground()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
open func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
|
|
104
|
+
URLContexts.forEach { forwarder.open(url: $0.url, options: Self.openURLOptions(from: $0.options)) }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
open func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
|
|
108
|
+
forwarder.willContinueUserActivity(withType: userActivityType)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
open func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
|
112
|
+
forwarder.continue(userActivity)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
open func scene(
|
|
116
|
+
_ scene: UIScene,
|
|
117
|
+
didFailToContinueUserActivityWithType userActivityType: String,
|
|
118
|
+
error: Error
|
|
119
|
+
) {
|
|
120
|
+
forwarder.didFailToContinueUserActivity(withType: userActivityType, error: error)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
open func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) {
|
|
124
|
+
forwarder.didUpdate(userActivity)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#if os(iOS)
|
|
128
|
+
open func windowScene(
|
|
129
|
+
_ windowScene: UIWindowScene,
|
|
130
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
131
|
+
completionHandler: @escaping (Bool) -> Void
|
|
132
|
+
) {
|
|
133
|
+
forwarder.perform(shortcutItem, completionHandler: completionHandler)
|
|
134
|
+
}
|
|
135
|
+
#endif
|
|
136
|
+
// MARK: - Launch options
|
|
137
|
+
|
|
138
|
+
/// Rebuilds the launch options that `Linking.getInitialURL()` reads from a scene's connection
|
|
139
|
+
/// options. Returns `nil` when the app wasn't cold-started by a URL or a browsing-web activity,
|
|
140
|
+
/// so it can be forwarded to `startReactNative` as-is.
|
|
141
|
+
static func launchOptions(
|
|
142
|
+
url: URL?,
|
|
143
|
+
userActivity: NSUserActivity?
|
|
144
|
+
) -> [UIApplication.LaunchOptionsKey: Any]? {
|
|
145
|
+
// Build the keys from their underlying constant strings rather than the `UIApplication`
|
|
146
|
+
// accessors (`.url`, `.userActivityDictionary`): those accessors are deprecated as of iOS 26
|
|
147
|
+
// in favor of the scene APIs, but React Native's `getInitialURL` still reads the launch options
|
|
148
|
+
// by these exact keys, so this is the shape it expects.
|
|
149
|
+
var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]
|
|
150
|
+
if let url {
|
|
151
|
+
let urlKey = UIApplication.LaunchOptionsKey(rawValue: "UIApplicationLaunchOptionsURLKey")
|
|
152
|
+
launchOptions[urlKey] = url
|
|
153
|
+
}
|
|
154
|
+
if let userActivity {
|
|
155
|
+
let userActivityDictionaryKey = UIApplication.LaunchOptionsKey(
|
|
156
|
+
rawValue: "UIApplicationLaunchOptionsUserActivityDictionaryKey"
|
|
157
|
+
)
|
|
158
|
+
launchOptions[userActivityDictionaryKey] = [
|
|
159
|
+
"UIApplicationLaunchOptionsUserActivityTypeKey": userActivity.activityType,
|
|
160
|
+
"UIApplicationLaunchOptionsUserActivityKey": userActivity
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
return launchOptions.isEmpty ? nil : launchOptions
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private static func openURLOptions(
|
|
167
|
+
from sceneOptions: UIScene.OpenURLOptions
|
|
168
|
+
) -> [UIApplication.OpenURLOptionsKey: Any] {
|
|
169
|
+
var options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
|
170
|
+
if let sourceApplication = sceneOptions.sourceApplication {
|
|
171
|
+
options[.sourceApplication] = sourceApplication
|
|
172
|
+
}
|
|
173
|
+
if let annotation = sceneOptions.annotation {
|
|
174
|
+
options[.annotation] = annotation
|
|
175
|
+
}
|
|
176
|
+
options[.openInPlace] = sceneOptions.openInPlace
|
|
177
|
+
return options
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#endif
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Conformed to by the application's `AppDelegate` so that the scene delegate can retrieve
|
|
7
|
+
the React Native factory it created during `application(_:didFinishLaunchingWithOptions:)`.
|
|
8
|
+
|
|
9
|
+
In the scene-based life cycle the window is created by the scene delegate, but the factory
|
|
10
|
+
is still owned by the app delegate. This protocol is the bridge between the two.
|
|
11
|
+
*/
|
|
12
|
+
@MainActor
|
|
13
|
+
public protocol ExpoReactNativeFactoryProvider: AnyObject {
|
|
14
|
+
/// The app's window. The scene delegate creates the window and assigns it here so that code
|
|
15
|
+
/// reading `UIApplication.shared.delegate?.window` keeps working (e.g. expo-system-ui).
|
|
16
|
+
/// This is the same `var window: UIWindow?` the app delegate already declares for `UIApplicationDelegate`.
|
|
17
|
+
var window: UIWindow? { get set }
|
|
18
|
+
|
|
19
|
+
/// The factory created in `application(_:didFinishLaunchingWithOptions:)`.
|
|
20
|
+
var reactNativeFactory: RCTReactNativeFactory? { get }
|
|
21
|
+
|
|
22
|
+
/// The registered React Native module name. Defaults to `"main"`.
|
|
23
|
+
var reactNativeFactoryModuleName: String { get }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public extension ExpoReactNativeFactoryProvider {
|
|
27
|
+
var reactNativeFactoryModuleName: String {
|
|
28
|
+
return "main"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import React
|
|
5
|
+
|
|
6
|
+
#if os(iOS) || os(tvOS)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
Re-feeds scene events to the app delegate. Under the scene life cycle UIKit no longer calls the
|
|
10
|
+
app delegate's URL, user-activity, life-cycle and quick-action methods, so overrides in the app's
|
|
11
|
+
`AppDelegate: ExpoAppDelegate` would never run; `ExpoAppDelegate` forwards every event to the
|
|
12
|
+
subscribers, so one call reaches both.
|
|
13
|
+
|
|
14
|
+
Main-actor isolated: `UIWindowSceneDelegate` hands these events over on the main actor, and every
|
|
15
|
+
`ExpoAppDelegate` method they reach is main-actor isolated too.
|
|
16
|
+
*/
|
|
17
|
+
@MainActor
|
|
18
|
+
@available(iOSApplicationExtension, unavailable)
|
|
19
|
+
struct SceneEventForwarder {
|
|
20
|
+
var appDelegate: () -> ExpoAppDelegate? = { UIApplication.shared.delegate as? ExpoAppDelegate }
|
|
21
|
+
|
|
22
|
+
func open(url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) {
|
|
23
|
+
let application = UIApplication.shared
|
|
24
|
+
let delegate = appDelegate()
|
|
25
|
+
|
|
26
|
+
notifyLinkingManagerUnlessAlreadyNotified(of: url) {
|
|
27
|
+
_ = delegate?.application(application, open: url, options: options)
|
|
28
|
+
} notify: {
|
|
29
|
+
RCTLinkingManager.application(application, open: url, options: options)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func `continue`(_ userActivity: NSUserActivity) {
|
|
34
|
+
let application = UIApplication.shared
|
|
35
|
+
let delegate = appDelegate()
|
|
36
|
+
|
|
37
|
+
// `RCTLinkingManager` only announces browsing-web activities, so there is nothing to dedupe
|
|
38
|
+
// against for the other activity types.
|
|
39
|
+
notifyLinkingManagerUnlessAlreadyNotified(of: userActivity.webpageURL) {
|
|
40
|
+
_ = delegate?.application(application, continue: userActivity, restorationHandler: { _ in })
|
|
41
|
+
} notify: {
|
|
42
|
+
RCTLinkingManager.application(application, continue: userActivity, restorationHandler: { _ in })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func willContinueUserActivity(withType userActivityType: String) {
|
|
47
|
+
// The scene callback has no return value, unlike its app-delegate counterpart. Calling the app
|
|
48
|
+
// delegate still lets every subscriber prepare; its aggregated result is intentionally ignored.
|
|
49
|
+
_ = appDelegate()?.application(
|
|
50
|
+
UIApplication.shared,
|
|
51
|
+
willContinueUserActivityWithType: userActivityType
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func didFailToContinueUserActivity(withType userActivityType: String, error: Error) {
|
|
56
|
+
appDelegate()?.application(
|
|
57
|
+
UIApplication.shared,
|
|
58
|
+
didFailToContinueUserActivityWithType: userActivityType,
|
|
59
|
+
error: error
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func didUpdate(_ userActivity: NSUserActivity) {
|
|
64
|
+
appDelegate()?.application(UIApplication.shared, didUpdate: userActivity)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func didBecomeActive() {
|
|
68
|
+
appDelegate()?.applicationDidBecomeActive(UIApplication.shared)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func willResignActive() {
|
|
72
|
+
appDelegate()?.applicationWillResignActive(UIApplication.shared)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func willEnterForeground() {
|
|
76
|
+
appDelegate()?.applicationWillEnterForeground(UIApplication.shared)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func didEnterBackground() {
|
|
80
|
+
appDelegate()?.applicationDidEnterBackground(UIApplication.shared)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#if os(iOS)
|
|
84
|
+
/// UIKit waits on `completionHandler`, so it has to run exactly once.
|
|
85
|
+
func perform(
|
|
86
|
+
_ shortcutItem: UIApplicationShortcutItem,
|
|
87
|
+
completionHandler: @escaping (Bool) -> Void
|
|
88
|
+
) {
|
|
89
|
+
guard let delegate = appDelegate() else {
|
|
90
|
+
completionHandler(false)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
delegate.application(
|
|
94
|
+
UIApplication.shared,
|
|
95
|
+
performActionFor: shortcutItem,
|
|
96
|
+
completionHandler: completionHandler
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
#endif
|
|
100
|
+
|
|
101
|
+
/// Runs `body`, then calls `notify` unless `RCTLinkingManager` was already notified about `url`
|
|
102
|
+
/// while `body` ran. Always calls `notify` when `url` is `nil`.
|
|
103
|
+
///
|
|
104
|
+
/// App delegates generated by SDK 57 and older call `RCTLinkingManager` themselves, which would
|
|
105
|
+
/// deliver the JS `url` event twice now that their overrides run again, and no static check can
|
|
106
|
+
/// tell such an override apart — so listen for what `RCTLinkingManager` announces instead.
|
|
107
|
+
private func notifyLinkingManagerUnlessAlreadyNotified(
|
|
108
|
+
of url: URL?,
|
|
109
|
+
during body: () -> Void,
|
|
110
|
+
notify: () -> Void
|
|
111
|
+
) {
|
|
112
|
+
guard let url else {
|
|
113
|
+
body()
|
|
114
|
+
notify()
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
let observer = LinkingManagerObserver(url: url)
|
|
118
|
+
body()
|
|
119
|
+
if !observer.wasNotified {
|
|
120
|
+
notify()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Observes the notification `RCTLinkingManager` posts for every link it handles. The name is
|
|
126
|
+
/// private to React Native's `RCTLinkingManager.mm` — expo-router spells it out the same way in
|
|
127
|
+
/// `ExpoHeadAppDelegateSubscriber.swift`. If React Native renames it, legacy overrides deliver a
|
|
128
|
+
/// duplicate `url` event again; a link is never lost.
|
|
129
|
+
private final class LinkingManagerObserver: NSObject {
|
|
130
|
+
private let expectedURL: String
|
|
131
|
+
private(set) var wasNotified = false
|
|
132
|
+
|
|
133
|
+
init(url: URL) {
|
|
134
|
+
expectedURL = url.absoluteString
|
|
135
|
+
super.init()
|
|
136
|
+
NotificationCenter.default.addObserver(
|
|
137
|
+
self,
|
|
138
|
+
selector: #selector(linkingManagerDidOpenURL(_:)),
|
|
139
|
+
name: Notification.Name("RCTOpenURLNotification"),
|
|
140
|
+
object: nil
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
deinit {
|
|
145
|
+
NotificationCenter.default.removeObserver(self)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@objc
|
|
149
|
+
private func linkingManagerDidOpenURL(_ notification: Notification) {
|
|
150
|
+
wasNotified = wasNotified || notification.userInfo?["url"] as? String == expectedURL
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#endif
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import React
|
|
5
|
+
import Testing
|
|
6
|
+
@testable import Expo
|
|
7
|
+
|
|
8
|
+
#if os(iOS) || os(tvOS)
|
|
9
|
+
|
|
10
|
+
@Suite(.serialized)
|
|
11
|
+
struct ExpoAppSceneDelegateTests {
|
|
12
|
+
@Test
|
|
13
|
+
func `extends UIResponder`() {
|
|
14
|
+
// Assert that `ExpoAppSceneDelegate` extends from `UIResponder` so it doesn't regress in the future.
|
|
15
|
+
// UIKit instantiates the scene delegate by name and expects a responder; losing this would break
|
|
16
|
+
// responder-chain behavior such as key presses.
|
|
17
|
+
#expect(ExpoAppSceneDelegate.self is UIResponder.Type)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
func `conforms to UIWindowSceneDelegate`() {
|
|
22
|
+
// The iOS 27 SDK asserts at launch unless the app's scene delegate adopts the scene life cycle.
|
|
23
|
+
// Conforming to `UIWindowSceneDelegate` is what makes the class usable as the scene delegate.
|
|
24
|
+
#expect(ExpoAppSceneDelegate.self is UIWindowSceneDelegate.Type)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
@MainActor
|
|
29
|
+
func `exposes scene user activity callbacks to UIKit`() {
|
|
30
|
+
let sceneDelegate = ExpoAppSceneDelegate()
|
|
31
|
+
#expect(
|
|
32
|
+
sceneDelegate.responds(
|
|
33
|
+
to: #selector(UISceneDelegate.scene(_:willContinueUserActivityWithType:))
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
#expect(sceneDelegate.responds(to: #selector(UISceneDelegate.scene(_:didUpdate:))))
|
|
37
|
+
#expect(
|
|
38
|
+
sceneDelegate.responds(
|
|
39
|
+
to: #selector(UISceneDelegate.scene(_:didFailToContinueUserActivityWithType:error:))
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
@MainActor
|
|
46
|
+
func `builds launch options carrying a cold-start URL`() {
|
|
47
|
+
// Under the scene life cycle the URL that cold-started the app arrives in the scene connection
|
|
48
|
+
// options, not in the app delegate's launch options. Synthesizing launch options from it is
|
|
49
|
+
// what lets `Linking.getInitialURL()` (which reads `UIApplicationLaunchOptionsURLKey`) return
|
|
50
|
+
// the URL, matching the app-delegate life cycle it replaced.
|
|
51
|
+
let url = URL(string: "bareexpo://test-suite/run?tests=AppMetrics")!
|
|
52
|
+
let launchOptions = ExpoAppSceneDelegate.launchOptions(url: url, userActivity: nil)
|
|
53
|
+
let urlKey = UIApplication.LaunchOptionsKey(rawValue: "UIApplicationLaunchOptionsURLKey")
|
|
54
|
+
#expect(launchOptions?[urlKey] as? URL == url)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
@MainActor
|
|
59
|
+
func `builds launch options carrying a browsing-web user activity`() {
|
|
60
|
+
let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
|
|
61
|
+
userActivity.webpageURL = URL(string: "https://expo.dev/link")!
|
|
62
|
+
let launchOptions = ExpoAppSceneDelegate.launchOptions(url: nil, userActivity: userActivity)
|
|
63
|
+
let userActivityDictionaryKey = UIApplication.LaunchOptionsKey(
|
|
64
|
+
rawValue: "UIApplicationLaunchOptionsUserActivityDictionaryKey"
|
|
65
|
+
)
|
|
66
|
+
let activityDictionary = launchOptions?[userActivityDictionaryKey] as? [AnyHashable: Any]
|
|
67
|
+
#expect(activityDictionary?["UIApplicationLaunchOptionsUserActivityTypeKey"] as? String == NSUserActivityTypeBrowsingWeb)
|
|
68
|
+
#expect((activityDictionary?["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity) === userActivity)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
@MainActor
|
|
73
|
+
func `returns nil launch options without a URL or user activity`() {
|
|
74
|
+
#expect(ExpoAppSceneDelegate.launchOptions(url: nil, userActivity: nil) == nil)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
@MainActor
|
|
79
|
+
func `an app delegate can provide the React Native factory`() {
|
|
80
|
+
let appDelegate = FactoryProvidingAppDelegate()
|
|
81
|
+
#expect(appDelegate.reactNativeFactory == nil)
|
|
82
|
+
#expect(appDelegate.reactNativeFactoryModuleName == "main")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test
|
|
86
|
+
@MainActor
|
|
87
|
+
func `routes an opened URL through the app delegate`() {
|
|
88
|
+
let spy = SpyAppDelegate()
|
|
89
|
+
let url = URL(string: "bareexpo://scene-delegate/open-url")!
|
|
90
|
+
SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [.sourceApplication: "dev.expo.Payments"])
|
|
91
|
+
#expect(spy.openedURLs.count == 1)
|
|
92
|
+
#expect(spy.openedURLs.first?.url == url)
|
|
93
|
+
#expect(spy.openedURLs.first?.options[.sourceApplication] as? String == "dev.expo.Payments")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
@MainActor
|
|
98
|
+
func `routes a continued user activity through the app delegate`() {
|
|
99
|
+
let spy = SpyAppDelegate()
|
|
100
|
+
let subscriber = UserActivityRecordingSubscriber()
|
|
101
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
|
|
102
|
+
let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
|
|
103
|
+
userActivity.webpageURL = URL(string: "https://expo.dev/scene-delegate")!
|
|
104
|
+
SceneEventForwarder(appDelegate: { spy }).continue(userActivity)
|
|
105
|
+
#expect(spy.continuedUserActivities.count == 1)
|
|
106
|
+
#expect(spy.continuedUserActivities.first === userActivity)
|
|
107
|
+
#expect(subscriber.continuations.filter { $0 === userActivity }.count == 1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
@MainActor
|
|
112
|
+
func `asks every subscriber to prepare for a scene user activity`() {
|
|
113
|
+
let activityType = "dev.expo.scene-delegate.prepare.\(UUID().uuidString)"
|
|
114
|
+
let firstSubscriber = UserActivityRecordingSubscriber(willContinueResult: false)
|
|
115
|
+
let handlingSubscriber = UserActivityRecordingSubscriber(willContinueResult: true)
|
|
116
|
+
let lastSubscriber = UserActivityRecordingSubscriber(willContinueResult: false)
|
|
117
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(firstSubscriber)
|
|
118
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(handlingSubscriber)
|
|
119
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(lastSubscriber)
|
|
120
|
+
|
|
121
|
+
let spy = SpyAppDelegate()
|
|
122
|
+
SceneEventForwarder(appDelegate: { spy }).willContinueUserActivity(withType: activityType)
|
|
123
|
+
|
|
124
|
+
#expect(spy.preparations.count == 1)
|
|
125
|
+
#expect(isSharedApplication(spy.preparations.first?.application))
|
|
126
|
+
#expect(spy.preparations.first?.activityType == activityType)
|
|
127
|
+
#expect(spy.continuedUserActivities.isEmpty)
|
|
128
|
+
for subscriber in [firstSubscriber, handlingSubscriber, lastSubscriber] {
|
|
129
|
+
let preparations = subscriber.preparations.filter { $0.activityType == activityType }
|
|
130
|
+
#expect(preparations.count == 1)
|
|
131
|
+
#expect(isSharedApplication(preparations.first?.application))
|
|
132
|
+
#expect(subscriber.continuations.isEmpty)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
@MainActor
|
|
138
|
+
func `forwards scene user activity updates and failures without continuing`() {
|
|
139
|
+
let activityType = "dev.expo.scene-delegate.failure.\(UUID().uuidString)"
|
|
140
|
+
let userActivity = NSUserActivity(
|
|
141
|
+
activityType: "dev.expo.scene-delegate.update.\(UUID().uuidString)"
|
|
142
|
+
)
|
|
143
|
+
let error = NSError(
|
|
144
|
+
domain: "dev.expo.scene-delegate.handoff",
|
|
145
|
+
code: 17,
|
|
146
|
+
userInfo: [NSLocalizedDescriptionKey: "Handoff transfer failed"]
|
|
147
|
+
)
|
|
148
|
+
let subscriber = UserActivityRecordingSubscriber()
|
|
149
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
|
|
150
|
+
|
|
151
|
+
let spy = SpyAppDelegate()
|
|
152
|
+
let forwarder = SceneEventForwarder(appDelegate: { spy })
|
|
153
|
+
forwarder.didUpdate(userActivity)
|
|
154
|
+
forwarder.didFailToContinueUserActivity(withType: activityType, error: error)
|
|
155
|
+
|
|
156
|
+
#expect(spy.updatedUserActivities.count == 1)
|
|
157
|
+
#expect(isSharedApplication(spy.updatedUserActivities.first?.application))
|
|
158
|
+
#expect(spy.updatedUserActivities.first?.userActivity === userActivity)
|
|
159
|
+
#expect(spy.userActivityFailures.count == 1)
|
|
160
|
+
#expect(isSharedApplication(spy.userActivityFailures.first?.application))
|
|
161
|
+
#expect(spy.userActivityFailures.first?.activityType == activityType)
|
|
162
|
+
#expect((spy.userActivityFailures.first?.error as NSError?) === error)
|
|
163
|
+
#expect(spy.preparations.isEmpty)
|
|
164
|
+
#expect(spy.continuedUserActivities.isEmpty)
|
|
165
|
+
#expect(subscriber.updates.count == 1)
|
|
166
|
+
#expect(isSharedApplication(subscriber.updates.first?.application))
|
|
167
|
+
#expect(subscriber.updates.first?.userActivity === userActivity)
|
|
168
|
+
#expect(subscriber.failures.count == 1)
|
|
169
|
+
#expect(isSharedApplication(subscriber.failures.first?.application))
|
|
170
|
+
#expect(subscriber.failures.first?.activityType == activityType)
|
|
171
|
+
#expect((subscriber.failures.first?.error as NSError?) === error)
|
|
172
|
+
#expect(subscriber.preparations.isEmpty)
|
|
173
|
+
#expect(subscriber.continuations.isEmpty)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
@MainActor
|
|
178
|
+
func `routes life cycle events through the app delegate`() {
|
|
179
|
+
let spy = SpyAppDelegate()
|
|
180
|
+
let forwarder = SceneEventForwarder(appDelegate: { spy })
|
|
181
|
+
forwarder.didBecomeActive()
|
|
182
|
+
forwarder.willResignActive()
|
|
183
|
+
forwarder.willEnterForeground()
|
|
184
|
+
forwarder.didEnterBackground()
|
|
185
|
+
#expect(spy.lifeCycleEvents == [
|
|
186
|
+
"applicationDidBecomeActive",
|
|
187
|
+
"applicationWillResignActive",
|
|
188
|
+
"applicationWillEnterForeground",
|
|
189
|
+
"applicationDidEnterBackground"
|
|
190
|
+
])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#if os(iOS)
|
|
194
|
+
@Test
|
|
195
|
+
@MainActor
|
|
196
|
+
func `routes a quick action through the app delegate`() async {
|
|
197
|
+
let spy = SpyAppDelegate()
|
|
198
|
+
let shortcutItem = UIApplicationShortcutItem(
|
|
199
|
+
type: "dev.expo.bareexpo.run-tests",
|
|
200
|
+
localizedTitle: "Run tests"
|
|
201
|
+
)
|
|
202
|
+
var handled: [Bool] = []
|
|
203
|
+
// The subscriber manager completes on the next main-queue turn when a subscriber handles the
|
|
204
|
+
// action, so wait for the first reply instead of assuming a synchronous one.
|
|
205
|
+
await withCheckedContinuation { continuation in
|
|
206
|
+
var didResume = false
|
|
207
|
+
SceneEventForwarder(appDelegate: { spy }).perform(shortcutItem) { succeeded in
|
|
208
|
+
handled.append(succeeded)
|
|
209
|
+
if !didResume {
|
|
210
|
+
didResume = true
|
|
211
|
+
continuation.resume()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
await drainMainQueue()
|
|
216
|
+
#expect(spy.shortcutItemTypes == [shortcutItem.type])
|
|
217
|
+
#expect(handled == [false])
|
|
218
|
+
}
|
|
219
|
+
#endif
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
@MainActor
|
|
223
|
+
func `notifies RCTLinkingManager once per routed URL`() {
|
|
224
|
+
let url = URL(string: "bareexpo://scene-delegate/linking")!
|
|
225
|
+
let recorder = OpenURLNotificationRecorder()
|
|
226
|
+
let spy = SpyAppDelegate()
|
|
227
|
+
SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [:])
|
|
228
|
+
#expect(recorder.count(of: url) == 1)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Test
|
|
232
|
+
@MainActor
|
|
233
|
+
func `notifies RCTLinkingManager for a universal link without a legacy linking override`() {
|
|
234
|
+
let delegate = SpyAppDelegate()
|
|
235
|
+
let url = URL(string: "https://expo.dev/scene-delegate/fallback-activity")!
|
|
236
|
+
let activity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
|
|
237
|
+
activity.webpageURL = url
|
|
238
|
+
let recorder = OpenURLNotificationRecorder()
|
|
239
|
+
|
|
240
|
+
SceneEventForwarder(appDelegate: { delegate }).continue(activity)
|
|
241
|
+
|
|
242
|
+
#expect(delegate.continuedUserActivities.first === activity)
|
|
243
|
+
#expect(recorder.count(of: url) == 1)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Test
|
|
247
|
+
@MainActor
|
|
248
|
+
func `delivers repeated opens of the same URL once for each invocation`() {
|
|
249
|
+
let delegate = LegacyLinkingAppDelegate()
|
|
250
|
+
let url = URL(string: "bareexpo://scene-delegate/repeated-url")!
|
|
251
|
+
let recorder = OpenURLNotificationRecorder()
|
|
252
|
+
let forwarder = SceneEventForwarder(appDelegate: { delegate })
|
|
253
|
+
|
|
254
|
+
forwarder.open(url: url, options: [:])
|
|
255
|
+
forwarder.open(url: url, options: [:])
|
|
256
|
+
|
|
257
|
+
#expect(delegate.openedURLs == [url, url])
|
|
258
|
+
#expect(recorder.count(of: url) == 2)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@Test
|
|
262
|
+
@MainActor
|
|
263
|
+
func `notifies RCTLinkingManager once when the delegate notifies it too`() {
|
|
264
|
+
let delegate = LegacyLinkingAppDelegate()
|
|
265
|
+
let url = URL(string: "bareexpo://scene-delegate/legacy-open-url")!
|
|
266
|
+
let recorder = OpenURLNotificationRecorder()
|
|
267
|
+
SceneEventForwarder(appDelegate: { delegate }).open(url: url, options: [:])
|
|
268
|
+
#expect(delegate.openedURLs == [url])
|
|
269
|
+
#expect(recorder.count(of: url) == 1)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Test
|
|
273
|
+
@MainActor
|
|
274
|
+
func `notifies RCTLinkingManager once per user activity when the delegate notifies it too`() {
|
|
275
|
+
let delegate = LegacyLinkingAppDelegate()
|
|
276
|
+
let webpageURL = URL(string: "https://expo.dev/scene-delegate/legacy-activity")!
|
|
277
|
+
let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
|
|
278
|
+
userActivity.webpageURL = webpageURL
|
|
279
|
+
let recorder = OpenURLNotificationRecorder()
|
|
280
|
+
SceneEventForwarder(appDelegate: { delegate }).continue(userActivity)
|
|
281
|
+
#expect(delegate.continuedUserActivities.count == 1)
|
|
282
|
+
#expect(recorder.count(of: webpageURL) == 1)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@Test
|
|
286
|
+
@MainActor
|
|
287
|
+
func `does not notify subscribers without an ExpoAppDelegate`() {
|
|
288
|
+
let subscriber = URLRecordingSubscriber()
|
|
289
|
+
ExpoAppDelegateSubscriberRepository.registerSubscriber(subscriber)
|
|
290
|
+
|
|
291
|
+
let url = URL(string: "bareexpo://scene-delegate/no-app-delegate")!
|
|
292
|
+
let recorder = OpenURLNotificationRecorder()
|
|
293
|
+
SceneEventForwarder(appDelegate: { nil }).open(url: url, options: [:])
|
|
294
|
+
#expect(subscriber.count(of: url) == 0)
|
|
295
|
+
#expect(recorder.count(of: url) == 1)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#if os(iOS)
|
|
299
|
+
@Test
|
|
300
|
+
@MainActor
|
|
301
|
+
func `completes a quick action with false without an ExpoAppDelegate`() async {
|
|
302
|
+
let shortcutItem = UIApplicationShortcutItem(
|
|
303
|
+
type: "dev.expo.bareexpo.unhandled-action",
|
|
304
|
+
localizedTitle: "Unhandled action"
|
|
305
|
+
)
|
|
306
|
+
var handled: [Bool] = []
|
|
307
|
+
await withCheckedContinuation { continuation in
|
|
308
|
+
var didResume = false
|
|
309
|
+
SceneEventForwarder(appDelegate: { nil }).perform(shortcutItem) { succeeded in
|
|
310
|
+
handled.append(succeeded)
|
|
311
|
+
if !didResume {
|
|
312
|
+
didResume = true
|
|
313
|
+
continuation.resume()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await drainMainQueue()
|
|
318
|
+
#expect(handled == [false])
|
|
319
|
+
}
|
|
320
|
+
#endif
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
@MainActor
|
|
324
|
+
func `forwards to the app delegate of the running application`() {
|
|
325
|
+
let forwarder = ExpoAppSceneDelegate().forwarder
|
|
326
|
+
#expect(forwarder.appDelegate() === UIApplication.shared.delegate as? ExpoAppDelegate)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@MainActor
|
|
331
|
+
private func isSharedApplication(_ application: UIApplication?) -> Bool {
|
|
332
|
+
// A hostless XCTest runner can bridge `UIApplication.shared` as nil despite its nonoptional
|
|
333
|
+
// Swift declaration. Evaluate identity outside `#expect` so the macro does not capture it.
|
|
334
|
+
let sharedApplication: UIApplication? = UIApplication.shared
|
|
335
|
+
return application === sharedApplication
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Records user-activity callbacks delivered through the app-delegate subscriber compatibility
|
|
339
|
+
/// layer. Subscribers cannot be unregistered, so tests use unique activity types and retain these
|
|
340
|
+
/// instances for the remainder of the test process.
|
|
341
|
+
private final class UserActivityRecordingSubscriber: NSObject, ExpoAppDelegateSubscriberProtocol {
|
|
342
|
+
struct Preparation {
|
|
343
|
+
let application: UIApplication?
|
|
344
|
+
let activityType: String
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
struct ActivityUpdate {
|
|
348
|
+
let application: UIApplication?
|
|
349
|
+
let userActivity: NSUserActivity
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
struct Failure {
|
|
353
|
+
let application: UIApplication?
|
|
354
|
+
let activityType: String
|
|
355
|
+
let error: Error
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let willContinueResult: Bool
|
|
359
|
+
var preparations: [Preparation] = []
|
|
360
|
+
var continuations: [NSUserActivity] = []
|
|
361
|
+
var updates: [ActivityUpdate] = []
|
|
362
|
+
var failures: [Failure] = []
|
|
363
|
+
|
|
364
|
+
init(willContinueResult: Bool = false) {
|
|
365
|
+
self.willContinueResult = willContinueResult
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func application(
|
|
369
|
+
_ application: UIApplication,
|
|
370
|
+
willContinueUserActivityWithType userActivityType: String
|
|
371
|
+
) -> Bool {
|
|
372
|
+
preparations.append(Preparation(application: application, activityType: userActivityType))
|
|
373
|
+
return willContinueResult
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func application(
|
|
377
|
+
_ application: UIApplication,
|
|
378
|
+
continue userActivity: NSUserActivity,
|
|
379
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
380
|
+
) -> Bool {
|
|
381
|
+
continuations.append(userActivity)
|
|
382
|
+
restorationHandler(nil)
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
|
|
387
|
+
updates.append(ActivityUpdate(application: application, userActivity: userActivity))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
func application(
|
|
391
|
+
_ application: UIApplication,
|
|
392
|
+
didFailToContinueUserActivityWithType userActivityType: String,
|
|
393
|
+
error: Error
|
|
394
|
+
) {
|
|
395
|
+
failures.append(Failure(application: application, activityType: userActivityType, error: error))
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// App delegate shaped like the SDK ≤ 57 templates, which call `RCTLinkingManager` from their own
|
|
400
|
+
/// overrides. See `packages/expo-updates/e2e/fixtures/custom_init/AppDelegate.swift`.
|
|
401
|
+
private final class LegacyLinkingAppDelegate: ExpoAppDelegate {
|
|
402
|
+
var openedURLs: [URL] = []
|
|
403
|
+
var continuedUserActivities: [NSUserActivity] = []
|
|
404
|
+
|
|
405
|
+
override func application(
|
|
406
|
+
_ app: UIApplication,
|
|
407
|
+
open url: URL,
|
|
408
|
+
options: [UIApplication.OpenURLOptionsKey: Any]
|
|
409
|
+
) -> Bool {
|
|
410
|
+
openedURLs.append(url)
|
|
411
|
+
let handled = super.application(app, open: url, options: options)
|
|
412
|
+
RCTLinkingManager.application(app, open: url, options: options)
|
|
413
|
+
return handled
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
override func application(
|
|
417
|
+
_ application: UIApplication,
|
|
418
|
+
continue userActivity: NSUserActivity,
|
|
419
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
420
|
+
) -> Bool {
|
|
421
|
+
continuedUserActivities.append(userActivity)
|
|
422
|
+
let handled = super.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
|
423
|
+
RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
|
424
|
+
return handled
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Lets the main-queue blocks scheduled so far run, so a late duplicate reply is recorded before
|
|
429
|
+
/// the assertions.
|
|
430
|
+
@MainActor
|
|
431
|
+
private func drainMainQueue() async {
|
|
432
|
+
await withCheckedContinuation { continuation in
|
|
433
|
+
DispatchQueue.main.async { continuation.resume() }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Records the URLs a module subscriber receives. Registration is permanent for the process, so it
|
|
438
|
+
/// reports not having handled the URL to leave the other delegates' behavior untouched.
|
|
439
|
+
private final class URLRecordingSubscriber: NSObject, ExpoAppDelegateSubscriberProtocol {
|
|
440
|
+
private var urls: [URL] = []
|
|
441
|
+
|
|
442
|
+
func application(
|
|
443
|
+
_ app: UIApplication,
|
|
444
|
+
open url: URL,
|
|
445
|
+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
|
446
|
+
) -> Bool {
|
|
447
|
+
urls.append(url)
|
|
448
|
+
return false
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
func count(of url: URL) -> Int {
|
|
452
|
+
return urls.filter { $0 == url }.count
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private final class FactoryProvidingAppDelegate: ExpoAppDelegate, ExpoReactNativeFactoryProvider {
|
|
457
|
+
var window: UIWindow?
|
|
458
|
+
var reactNativeFactory: RCTReactNativeFactory?
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// Records the events an `ExpoAppDelegate` subclass receives, then forwards them to `super` the way
|
|
462
|
+
/// an app's own `AppDelegate` override would.
|
|
463
|
+
private final class SpyAppDelegate: ExpoAppDelegate {
|
|
464
|
+
struct OpenedURL {
|
|
465
|
+
let url: URL
|
|
466
|
+
let options: [UIApplication.OpenURLOptionsKey: Any]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
struct Preparation {
|
|
470
|
+
let application: UIApplication?
|
|
471
|
+
let activityType: String
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
struct ActivityUpdate {
|
|
475
|
+
let application: UIApplication?
|
|
476
|
+
let userActivity: NSUserActivity
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
struct UserActivityFailure {
|
|
480
|
+
let application: UIApplication?
|
|
481
|
+
let activityType: String
|
|
482
|
+
let error: Error
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
var openedURLs: [OpenedURL] = []
|
|
486
|
+
var continuedUserActivities: [NSUserActivity] = []
|
|
487
|
+
var preparations: [Preparation] = []
|
|
488
|
+
var updatedUserActivities: [ActivityUpdate] = []
|
|
489
|
+
var userActivityFailures: [UserActivityFailure] = []
|
|
490
|
+
var lifeCycleEvents: [String] = []
|
|
491
|
+
var shortcutItemTypes: [String] = []
|
|
492
|
+
|
|
493
|
+
override func application(
|
|
494
|
+
_ app: UIApplication,
|
|
495
|
+
open url: URL,
|
|
496
|
+
options: [UIApplication.OpenURLOptionsKey: Any]
|
|
497
|
+
) -> Bool {
|
|
498
|
+
openedURLs.append(OpenedURL(url: url, options: options))
|
|
499
|
+
return super.application(app, open: url, options: options)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
override func application(
|
|
503
|
+
_ application: UIApplication,
|
|
504
|
+
willContinueUserActivityWithType userActivityType: String
|
|
505
|
+
) -> Bool {
|
|
506
|
+
preparations.append(Preparation(application: application, activityType: userActivityType))
|
|
507
|
+
return super.application(application, willContinueUserActivityWithType: userActivityType)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
override func application(
|
|
511
|
+
_ application: UIApplication,
|
|
512
|
+
continue userActivity: NSUserActivity,
|
|
513
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
514
|
+
) -> Bool {
|
|
515
|
+
continuedUserActivities.append(userActivity)
|
|
516
|
+
return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
override func application(_ application: UIApplication, didUpdate userActivity: NSUserActivity) {
|
|
520
|
+
updatedUserActivities.append(ActivityUpdate(application: application, userActivity: userActivity))
|
|
521
|
+
super.application(application, didUpdate: userActivity)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
override func application(
|
|
525
|
+
_ application: UIApplication,
|
|
526
|
+
didFailToContinueUserActivityWithType userActivityType: String,
|
|
527
|
+
error: Error
|
|
528
|
+
) {
|
|
529
|
+
userActivityFailures.append(
|
|
530
|
+
UserActivityFailure(application: application, activityType: userActivityType, error: error)
|
|
531
|
+
)
|
|
532
|
+
super.application(
|
|
533
|
+
application,
|
|
534
|
+
didFailToContinueUserActivityWithType: userActivityType,
|
|
535
|
+
error: error
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
override func applicationDidBecomeActive(_ application: UIApplication) {
|
|
540
|
+
lifeCycleEvents.append("applicationDidBecomeActive")
|
|
541
|
+
super.applicationDidBecomeActive(application)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
override func applicationWillResignActive(_ application: UIApplication) {
|
|
545
|
+
lifeCycleEvents.append("applicationWillResignActive")
|
|
546
|
+
super.applicationWillResignActive(application)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
override func applicationWillEnterForeground(_ application: UIApplication) {
|
|
550
|
+
lifeCycleEvents.append("applicationWillEnterForeground")
|
|
551
|
+
super.applicationWillEnterForeground(application)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
override func applicationDidEnterBackground(_ application: UIApplication) {
|
|
555
|
+
lifeCycleEvents.append("applicationDidEnterBackground")
|
|
556
|
+
super.applicationDidEnterBackground(application)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#if os(iOS)
|
|
560
|
+
override func application(
|
|
561
|
+
_ application: UIApplication,
|
|
562
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
563
|
+
completionHandler: @escaping (Bool) -> Void
|
|
564
|
+
) {
|
|
565
|
+
shortcutItemTypes.append(shortcutItem.type)
|
|
566
|
+
super.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
|
|
567
|
+
}
|
|
568
|
+
#endif
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// Counts the notifications `RCTLinkingManager` posts for handled URLs. React Native keeps the
|
|
572
|
+
/// notification name private to its implementation file, so it's spelled out here.
|
|
573
|
+
private final class OpenURLNotificationRecorder: NSObject {
|
|
574
|
+
private var urls: [String] = []
|
|
575
|
+
|
|
576
|
+
override init() {
|
|
577
|
+
super.init()
|
|
578
|
+
NotificationCenter.default.addObserver(
|
|
579
|
+
self,
|
|
580
|
+
selector: #selector(record(_:)),
|
|
581
|
+
name: Notification.Name("RCTOpenURLNotification"),
|
|
582
|
+
object: nil
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
deinit {
|
|
587
|
+
NotificationCenter.default.removeObserver(self)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
func count(of url: URL) -> Int {
|
|
591
|
+
return urls.filter { $0 == url.absoluteString }.count
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@objc
|
|
595
|
+
private func record(_ notification: Notification) {
|
|
596
|
+
if let url = notification.userInfo?["url"] as? String {
|
|
597
|
+
urls.append(url)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#endif
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo",
|
|
3
|
-
"version": "57.0.
|
|
3
|
+
"version": "57.0.23",
|
|
4
4
|
"description": "The Expo SDK",
|
|
5
5
|
"main": "src/Expo.ts",
|
|
6
6
|
"module": "src/Expo.ts",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"homepage": "https://github.com/expo/expo/tree/main/packages/expo",
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@babel/runtime": "^7.20.0",
|
|
71
|
-
"@expo/cli": "^57.0.
|
|
71
|
+
"@expo/cli": "^57.0.25",
|
|
72
72
|
"@expo/config": "~57.0.9",
|
|
73
73
|
"@expo/config-plugins": "~57.0.9",
|
|
74
74
|
"@expo/devtools": "~57.0.1",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@expo/metro": "~56.0.2",
|
|
80
80
|
"@expo/metro-config": "~57.0.12",
|
|
81
81
|
"@ungap/structured-clone": "^1.3.0",
|
|
82
|
-
"babel-preset-expo": "~57.0.
|
|
82
|
+
"babel-preset-expo": "~57.0.12",
|
|
83
83
|
"expo-asset": "~57.0.17",
|
|
84
84
|
"expo-constants": "~57.0.18",
|
|
85
85
|
"expo-file-system": "~57.0.7",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"scripts/resolveAppEntry.js"
|
|
140
140
|
]
|
|
141
141
|
},
|
|
142
|
-
"gitHead": "
|
|
142
|
+
"gitHead": "f6f5af06b95cb7d511b52641885b2fb5adad525d",
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc",
|
|
145
145
|
"clean": "rimraf build",
|
package/template.tgz
CHANGED
|
Binary file
|