Skip to content

Commit 92c4f40

Browse files
authored
fix(ios): deliver launch urls, activities and quick actions to scene apps (#11518)
With a scene manifest, UIKit hands the payloads that launch the app only to scene:willConnectToSession:options: (connectionOptions.URLContexts, userActivities and shortcutItem). scene:openURLContexts:, scene:continueUserActivity: and windowScene:performActionForShortcutItem: fire only once the scene is connected, so opening a file or link while the app was not running, or launching it from a quick action, was dropped: no scene event, and no call into handlers registered with addDelegateHandler. The connecting scene now delivers those payloads through the same path as the warm callbacks, after its content is resolved. The activity openWindow() uses to carry window data is not forwarded as a continuation.
1 parent 45b3b99 commit 92c4f40

3 files changed

Lines changed: 172 additions & 70 deletions

File tree

‎packages/core/application/application.ios.ts‎

Lines changed: 108 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { SDK_VERSION } from '../utils/constants';
88
import { ios as iosUtils, dataSerialize, dataDeserialize } from '../utils/native-helper';
99
import { ApplicationCommon } from './application-common';
1010
import { ApplicationEventData, SceneContinueUserActivityEventData, SceneEventData, SceneOpenURLContextsEventData, ScenePerformActionForShortcutItemEventData } from './application-interfaces';
11-
import { deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, oneShotCompletion } from './scene-delegate-bridge';
11+
import { deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, getLaunchUserActivities, oneShotCompletion } from './scene-delegate-bridge';
1212
import { Observable } from '../data/observable';
1313
import type { iOSApplication as IiOSApplication } from './application';
1414
import { Trace } from '../trace';
@@ -255,6 +255,104 @@ function getSceneConnectionData(connectionOptions: UISceneConnectionOptions): Re
255255
return activity?.userInfo ? (dataDeserialize(activity.userInfo) as Record<string, any>) : undefined;
256256
}
257257

258+
/**
259+
* The activity type `openWindow()` uses to hand its data to the scene it opens.
260+
*/
261+
function getSceneActivityType(): string {
262+
return `${NSBundle.mainBundle.bundleIdentifier}.scene`;
263+
}
264+
265+
function deliverOpenURLContexts(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, URLContexts: NSSet<UIOpenURLContext>): void {
266+
if (nativeWindow) {
267+
nativeWindow.notify({
268+
eventName: NativeWindowEvents.sceneOpenURLContexts,
269+
object: nativeWindow,
270+
window: nativeWindow,
271+
scene: windowScene,
272+
urlContexts: URLContexts,
273+
} as SceneOpenURLContextsEventData);
274+
}
275+
276+
Application.ios.notify({
277+
eventName: NativeWindowEvents.sceneOpenURLContexts,
278+
object: Application.ios,
279+
window: nativeWindow,
280+
scene: windowScene,
281+
urlContexts: URLContexts,
282+
} as SceneOpenURLContextsEventData);
283+
284+
forwardOpenURLContexts(Application.ios.delegate, UIApplication.sharedApplication, URLContexts);
285+
}
286+
287+
function deliverUserActivity(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, userActivity: NSUserActivity): void {
288+
if (nativeWindow) {
289+
nativeWindow.notify({
290+
eventName: NativeWindowEvents.sceneContinueUserActivity,
291+
object: nativeWindow,
292+
window: nativeWindow,
293+
scene: windowScene,
294+
userActivity,
295+
} as SceneContinueUserActivityEventData);
296+
}
297+
298+
Application.ios.notify({
299+
eventName: NativeWindowEvents.sceneContinueUserActivity,
300+
object: Application.ios,
301+
window: nativeWindow,
302+
scene: windowScene,
303+
userActivity,
304+
} as SceneContinueUserActivityEventData);
305+
306+
forwardContinueUserActivity(Application.ios.delegate, UIApplication.sharedApplication, userActivity);
307+
}
308+
309+
function deliverShortcutAction(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, shortcutItem: UIApplicationShortcutItem, completionHandler: ((handled: boolean) => void) | undefined): void {
310+
// Shared by the listeners and the legacy handler, so it has to tolerate several callers.
311+
const deliver = oneShotCompletion(completionHandler);
312+
313+
if (nativeWindow) {
314+
nativeWindow.notify({
315+
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
316+
object: nativeWindow,
317+
window: nativeWindow,
318+
scene: windowScene,
319+
shortcutItem,
320+
completionHandler: deliver,
321+
} as ScenePerformActionForShortcutItemEventData);
322+
}
323+
324+
Application.ios.notify({
325+
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
326+
object: Application.ios,
327+
window: nativeWindow,
328+
scene: windowScene,
329+
shortcutItem,
330+
completionHandler: deliver,
331+
} as ScenePerformActionForShortcutItemEventData);
332+
333+
deliverShortcutItem(Application.ios.delegate, UIApplication.sharedApplication, shortcutItem, deliver);
334+
}
335+
336+
/**
337+
* UIKit hands the URLs, continuation activities and quick action that launched a scene only to
338+
* `scene:willConnectToSession:options:`; the scene callbacks that carry them later never fire for
339+
* the launch itself, so they are delivered here the same way.
340+
*/
341+
function deliverLaunchPayloads(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow, connectionOptions: UISceneConnectionOptions): void {
342+
if (connectionOptions?.URLContexts?.count) {
343+
deliverOpenURLContexts(windowScene, nativeWindow, connectionOptions.URLContexts);
344+
}
345+
346+
for (const userActivity of getLaunchUserActivities(connectionOptions, getSceneActivityType())) {
347+
deliverUserActivity(windowScene, nativeWindow, userActivity);
348+
}
349+
350+
if (connectionOptions?.shortcutItem) {
351+
// A launch has no completion handler to report back to.
352+
deliverShortcutAction(windowScene, nativeWindow, connectionOptions.shortcutItem, undefined);
353+
}
354+
}
355+
258356
@NativeClass
259357
class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
260358
static ObjCProtocols = [UIWindowSceneDelegate];
@@ -351,6 +449,10 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
351449
ios: { connectionOptions },
352450
});
353451
}
452+
453+
// After the content is resolved, matching the legacy order where `openURL` and friends
454+
// arrived once launch had finished and the root view existed.
455+
deliverLaunchPayloads(windowScene, nativeWindow, connectionOptions);
354456
}
355457

356458
sceneDidBecomeActive(scene: UIScene): void {
@@ -505,80 +607,18 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
505607

506608
sceneOpenURLContexts(scene: UIScene, URLContexts: NSSet<UIOpenURLContext>): void {
507609
const windowScene = scene as UIWindowScene;
508-
const nativeWindow = Application.ios._getWindowForScene(windowScene);
509-
510-
if (nativeWindow) {
511-
nativeWindow.notify({
512-
eventName: NativeWindowEvents.sceneOpenURLContexts,
513-
object: nativeWindow,
514-
window: nativeWindow,
515-
scene: windowScene,
516-
urlContexts: URLContexts,
517-
} as SceneOpenURLContextsEventData);
518-
}
519-
520-
Application.ios.notify({
521-
eventName: NativeWindowEvents.sceneOpenURLContexts,
522-
object: Application.ios,
523-
window: nativeWindow,
524-
scene: windowScene,
525-
urlContexts: URLContexts,
526-
} as SceneOpenURLContextsEventData);
527610

528-
forwardOpenURLContexts(Application.ios.delegate, UIApplication.sharedApplication, URLContexts);
611+
deliverOpenURLContexts(windowScene, Application.ios._getWindowForScene(windowScene), URLContexts);
529612
}
530613

531614
sceneContinueUserActivity(scene: UIScene, userActivity: NSUserActivity): void {
532615
const windowScene = scene as UIWindowScene;
533-
const nativeWindow = Application.ios._getWindowForScene(windowScene);
534616

535-
if (nativeWindow) {
536-
nativeWindow.notify({
537-
eventName: NativeWindowEvents.sceneContinueUserActivity,
538-
object: nativeWindow,
539-
window: nativeWindow,
540-
scene: windowScene,
541-
userActivity,
542-
} as SceneContinueUserActivityEventData);
543-
}
544-
545-
Application.ios.notify({
546-
eventName: NativeWindowEvents.sceneContinueUserActivity,
547-
object: Application.ios,
548-
window: nativeWindow,
549-
scene: windowScene,
550-
userActivity,
551-
} as SceneContinueUserActivityEventData);
552-
553-
forwardContinueUserActivity(Application.ios.delegate, UIApplication.sharedApplication, userActivity);
617+
deliverUserActivity(windowScene, Application.ios._getWindowForScene(windowScene), userActivity);
554618
}
555619

556620
windowScenePerformActionForShortcutItemCompletionHandler(windowScene: UIWindowScene, shortcutItem: UIApplicationShortcutItem, completionHandler: (p1: boolean) => void): void {
557-
const nativeWindow = Application.ios._getWindowForScene(windowScene);
558-
// Shared by the listeners and the legacy handler, so it has to tolerate several callers.
559-
const deliver = oneShotCompletion(completionHandler);
560-
561-
if (nativeWindow) {
562-
nativeWindow.notify({
563-
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
564-
object: nativeWindow,
565-
window: nativeWindow,
566-
scene: windowScene,
567-
shortcutItem,
568-
completionHandler: deliver,
569-
} as ScenePerformActionForShortcutItemEventData);
570-
}
571-
572-
Application.ios.notify({
573-
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
574-
object: Application.ios,
575-
window: nativeWindow,
576-
scene: windowScene,
577-
shortcutItem,
578-
completionHandler: deliver,
579-
} as ScenePerformActionForShortcutItemEventData);
580-
581-
deliverShortcutItem(Application.ios.delegate, UIApplication.sharedApplication, shortcutItem, deliver);
621+
deliverShortcutAction(windowScene, Application.ios._getWindowForScene(windowScene), shortcutItem, completionHandler);
582622
}
583623
}
584624
// ensure available globally
@@ -1550,7 +1590,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
15501590
try {
15511591
request = UISceneSessionActivationRequest.requestWithRole(UIWindowSceneSessionRoleApplication);
15521592

1553-
const activity = NSUserActivity.alloc().initWithActivityType(`${NSBundle.mainBundle.bundleIdentifier}.scene`);
1593+
const activity = NSUserActivity.alloc().initWithActivityType(getSceneActivityType());
15541594
activity.userInfo = dataSerialize(options?.data ?? {});
15551595
request.userActivity = activity;
15561596

@@ -1737,7 +1777,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
17371777
}
17381778

17391779
// Create user activity for the new scene
1740-
const userActivity = NSUserActivity.alloc().initWithActivityType(`${NSBundle.mainBundle.bundleIdentifier}.scene`);
1780+
const userActivity = NSUserActivity.alloc().initWithActivityType(getSceneActivityType());
17411781
userActivity.userInfo = dataSerialize(data ?? {});
17421782

17431783
// Use the legacy API

‎packages/core/application/scene-delegate-bridge.spec.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2-
import { buildOpenURLOptions, deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, oneShotCompletion } from './scene-delegate-bridge';
2+
import { buildOpenURLOptions, deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, getLaunchUserActivities, oneShotCompletion } from './scene-delegate-bridge';
33

44
const SOURCE_APPLICATION_KEY = 'UIApplicationOpenURLOptionsSourceApplicationKey';
55
const ANNOTATION_KEY = 'UIApplicationOpenURLOptionsAnnotationKey';
@@ -204,6 +204,41 @@ describe('forwardContinueUserActivity', () => {
204204
});
205205
});
206206

207+
describe('getLaunchUserActivities', () => {
208+
const SCENE_ACTIVITY_TYPE = 'com.example.app.scene';
209+
210+
function createConnectionOptions(activities: any[]) {
211+
return {
212+
userActivities: {
213+
count: activities.length,
214+
allObjects: {
215+
count: activities.length,
216+
objectAtIndex: (index: number) => activities[index],
217+
},
218+
},
219+
} as any;
220+
}
221+
222+
it('returns every activity the scene was launched with', () => {
223+
const browsing = { activityType: 'NSUserActivityTypeBrowsingWeb' };
224+
const handoff = { activityType: 'com.example.handoff' };
225+
226+
expect(getLaunchUserActivities(createConnectionOptions([browsing, handoff]), SCENE_ACTIVITY_TYPE)).toEqual([browsing, handoff]);
227+
});
228+
229+
it('leaves out the activity openWindow() uses to carry window data', () => {
230+
const browsing = { activityType: 'NSUserActivityTypeBrowsingWeb' };
231+
232+
expect(getLaunchUserActivities(createConnectionOptions([{ activityType: SCENE_ACTIVITY_TYPE }, browsing]), SCENE_ACTIVITY_TYPE)).toEqual([browsing]);
233+
});
234+
235+
it('returns nothing when the scene was launched without activities', () => {
236+
expect(getLaunchUserActivities(createConnectionOptions([]), SCENE_ACTIVITY_TYPE)).toEqual([]);
237+
expect(getLaunchUserActivities({} as any, SCENE_ACTIVITY_TYPE)).toEqual([]);
238+
expect(getLaunchUserActivities(undefined, SCENE_ACTIVITY_TYPE)).toEqual([]);
239+
});
240+
});
241+
207242
describe('oneShotCompletion', () => {
208243
it('delivers only the first result', () => {
209244
const completionHandler = vi.fn();
@@ -223,6 +258,13 @@ describe('oneShotCompletion', () => {
223258
deliver(true);
224259
expect(deliver.delivered).toBe(true);
225260
});
261+
262+
it('accepts a launch that has no completion handler to report to', () => {
263+
const deliver = oneShotCompletion(undefined);
264+
265+
expect(() => deliver(true)).not.toThrow();
266+
expect(deliver.delivered).toBe(true);
267+
});
226268
});
227269

228270
describe('deliverShortcutItem', () => {

‎packages/core/application/scene-delegate-bridge.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ export function forwardContinueUserActivity(delegate: AppDelegateClass, applicat
116116
return true;
117117
}
118118

119+
/**
120+
* The continuation activities a scene was launched with. The activity `openWindow()` attaches
121+
* to carry window data is left out: it addresses the new window, not the app's handlers.
122+
*/
123+
export function getLaunchUserActivities(connectionOptions: UISceneConnectionOptions | undefined | null, sceneActivityType: string): NSUserActivity[] {
124+
const activities = connectionOptions?.userActivities?.allObjects;
125+
const count = activities?.count ?? 0;
126+
const result: NSUserActivity[] = [];
127+
128+
for (let i = 0; i < count; i++) {
129+
const activity = activities.objectAtIndex(i) as NSUserActivity;
130+
131+
if (activity && activity.activityType !== sceneActivityType) {
132+
result.push(activity);
133+
}
134+
}
135+
136+
return result;
137+
}
138+
119139
/**
120140
* Hands a scene quick action to `applicationPerformActionForShortcutItemCompletionHandler`,
121141
* or reports it unhandled when no such handler is registered.
@@ -145,7 +165,7 @@ export function deliverShortcutItem(delegate: AppDelegateClass, application: UIA
145165
* completion handler invoked twice as a programming error, and the same handler is exposed
146166
* to both event listeners and legacy delegate handlers.
147167
*/
148-
export function oneShotCompletion(completionHandler: (handled: boolean) => void): OneShotCompletion {
168+
export function oneShotCompletion(completionHandler: ((handled: boolean) => void) | undefined): OneShotCompletion {
149169
let delivered = false;
150170

151171
const deliver = ((handled: boolean) => {

0 commit comments

Comments
 (0)