Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 108 additions & 68 deletions packages/core/application/application.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SDK_VERSION } from '../utils/constants';
import { ios as iosUtils, dataSerialize, dataDeserialize } from '../utils/native-helper';
import { ApplicationCommon } from './application-common';
import { ApplicationEventData, SceneContinueUserActivityEventData, SceneEventData, SceneOpenURLContextsEventData, ScenePerformActionForShortcutItemEventData } from './application-interfaces';
import { deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, oneShotCompletion } from './scene-delegate-bridge';
import { deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, getLaunchUserActivities, oneShotCompletion } from './scene-delegate-bridge';
import { Observable } from '../data/observable';
import type { iOSApplication as IiOSApplication } from './application';
import { Trace } from '../trace';
Expand Down Expand Up @@ -255,6 +255,104 @@ function getSceneConnectionData(connectionOptions: UISceneConnectionOptions): Re
return activity?.userInfo ? (dataDeserialize(activity.userInfo) as Record<string, any>) : undefined;
}

/**
* The activity type `openWindow()` uses to hand its data to the scene it opens.
*/
function getSceneActivityType(): string {
return `${NSBundle.mainBundle.bundleIdentifier}.scene`;
}

function deliverOpenURLContexts(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, URLContexts: NSSet<UIOpenURLContext>): void {
if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.sceneOpenURLContexts,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
urlContexts: URLContexts,
} as SceneOpenURLContextsEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.sceneOpenURLContexts,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
urlContexts: URLContexts,
} as SceneOpenURLContextsEventData);

forwardOpenURLContexts(Application.ios.delegate, UIApplication.sharedApplication, URLContexts);
}

function deliverUserActivity(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, userActivity: NSUserActivity): void {
if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.sceneContinueUserActivity,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
userActivity,
} as SceneContinueUserActivityEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.sceneContinueUserActivity,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
userActivity,
} as SceneContinueUserActivityEventData);

forwardContinueUserActivity(Application.ios.delegate, UIApplication.sharedApplication, userActivity);
}

function deliverShortcutAction(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow | undefined, shortcutItem: UIApplicationShortcutItem, completionHandler: ((handled: boolean) => void) | undefined): void {
// Shared by the listeners and the legacy handler, so it has to tolerate several callers.
const deliver = oneShotCompletion(completionHandler);

if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
shortcutItem,
completionHandler: deliver,
} as ScenePerformActionForShortcutItemEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
shortcutItem,
completionHandler: deliver,
} as ScenePerformActionForShortcutItemEventData);

deliverShortcutItem(Application.ios.delegate, UIApplication.sharedApplication, shortcutItem, deliver);
}

/**
* UIKit hands the URLs, continuation activities and quick action that launched a scene only to
* `scene:willConnectToSession:options:`; the scene callbacks that carry them later never fire for
* the launch itself, so they are delivered here the same way.
*/
function deliverLaunchPayloads(windowScene: UIWindowScene, nativeWindow: IOSNativeWindow, connectionOptions: UISceneConnectionOptions): void {
if (connectionOptions?.URLContexts?.count) {
deliverOpenURLContexts(windowScene, nativeWindow, connectionOptions.URLContexts);
}

for (const userActivity of getLaunchUserActivities(connectionOptions, getSceneActivityType())) {
deliverUserActivity(windowScene, nativeWindow, userActivity);
}

if (connectionOptions?.shortcutItem) {
// A launch has no completion handler to report back to.
deliverShortcutAction(windowScene, nativeWindow, connectionOptions.shortcutItem, undefined);
}
}

@NativeClass
class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
static ObjCProtocols = [UIWindowSceneDelegate];
Expand Down Expand Up @@ -351,6 +449,10 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
ios: { connectionOptions },
});
}

// After the content is resolved, matching the legacy order where `openURL` and friends
// arrived once launch had finished and the root view existed.
deliverLaunchPayloads(windowScene, nativeWindow, connectionOptions);
}

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

sceneOpenURLContexts(scene: UIScene, URLContexts: NSSet<UIOpenURLContext>): void {
const windowScene = scene as UIWindowScene;
const nativeWindow = Application.ios._getWindowForScene(windowScene);

if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.sceneOpenURLContexts,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
urlContexts: URLContexts,
} as SceneOpenURLContextsEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.sceneOpenURLContexts,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
urlContexts: URLContexts,
} as SceneOpenURLContextsEventData);

forwardOpenURLContexts(Application.ios.delegate, UIApplication.sharedApplication, URLContexts);
deliverOpenURLContexts(windowScene, Application.ios._getWindowForScene(windowScene), URLContexts);
}

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

if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.sceneContinueUserActivity,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
userActivity,
} as SceneContinueUserActivityEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.sceneContinueUserActivity,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
userActivity,
} as SceneContinueUserActivityEventData);

forwardContinueUserActivity(Application.ios.delegate, UIApplication.sharedApplication, userActivity);
deliverUserActivity(windowScene, Application.ios._getWindowForScene(windowScene), userActivity);
}

windowScenePerformActionForShortcutItemCompletionHandler(windowScene: UIWindowScene, shortcutItem: UIApplicationShortcutItem, completionHandler: (p1: boolean) => void): void {
const nativeWindow = Application.ios._getWindowForScene(windowScene);
// Shared by the listeners and the legacy handler, so it has to tolerate several callers.
const deliver = oneShotCompletion(completionHandler);

if (nativeWindow) {
nativeWindow.notify({
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
object: nativeWindow,
window: nativeWindow,
scene: windowScene,
shortcutItem,
completionHandler: deliver,
} as ScenePerformActionForShortcutItemEventData);
}

Application.ios.notify({
eventName: NativeWindowEvents.scenePerformActionForShortcutItem,
object: Application.ios,
window: nativeWindow,
scene: windowScene,
shortcutItem,
completionHandler: deliver,
} as ScenePerformActionForShortcutItemEventData);

deliverShortcutItem(Application.ios.delegate, UIApplication.sharedApplication, shortcutItem, deliver);
deliverShortcutAction(windowScene, Application.ios._getWindowForScene(windowScene), shortcutItem, completionHandler);
}
}
// ensure available globally
Expand Down Expand Up @@ -1550,7 +1590,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
try {
request = UISceneSessionActivationRequest.requestWithRole(UIWindowSceneSessionRoleApplication);

const activity = NSUserActivity.alloc().initWithActivityType(`${NSBundle.mainBundle.bundleIdentifier}.scene`);
const activity = NSUserActivity.alloc().initWithActivityType(getSceneActivityType());
activity.userInfo = dataSerialize(options?.data ?? {});
request.userActivity = activity;

Expand Down Expand Up @@ -1737,7 +1777,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
}

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

// Use the legacy API
Expand Down
44 changes: 43 additions & 1 deletion packages/core/application/scene-delegate-bridge.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { buildOpenURLOptions, deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, oneShotCompletion } from './scene-delegate-bridge';
import { buildOpenURLOptions, deliverShortcutItem, forwardContinueUserActivity, forwardOpenURLContexts, getLaunchUserActivities, oneShotCompletion } from './scene-delegate-bridge';

const SOURCE_APPLICATION_KEY = 'UIApplicationOpenURLOptionsSourceApplicationKey';
const ANNOTATION_KEY = 'UIApplicationOpenURLOptionsAnnotationKey';
Expand Down Expand Up @@ -204,6 +204,41 @@ describe('forwardContinueUserActivity', () => {
});
});

describe('getLaunchUserActivities', () => {
const SCENE_ACTIVITY_TYPE = 'com.example.app.scene';

function createConnectionOptions(activities: any[]) {
return {
userActivities: {
count: activities.length,
allObjects: {
count: activities.length,
objectAtIndex: (index: number) => activities[index],
},
},
} as any;
}

it('returns every activity the scene was launched with', () => {
const browsing = { activityType: 'NSUserActivityTypeBrowsingWeb' };
const handoff = { activityType: 'com.example.handoff' };

expect(getLaunchUserActivities(createConnectionOptions([browsing, handoff]), SCENE_ACTIVITY_TYPE)).toEqual([browsing, handoff]);
});

it('leaves out the activity openWindow() uses to carry window data', () => {
const browsing = { activityType: 'NSUserActivityTypeBrowsingWeb' };

expect(getLaunchUserActivities(createConnectionOptions([{ activityType: SCENE_ACTIVITY_TYPE }, browsing]), SCENE_ACTIVITY_TYPE)).toEqual([browsing]);
});

it('returns nothing when the scene was launched without activities', () => {
expect(getLaunchUserActivities(createConnectionOptions([]), SCENE_ACTIVITY_TYPE)).toEqual([]);
expect(getLaunchUserActivities({} as any, SCENE_ACTIVITY_TYPE)).toEqual([]);
expect(getLaunchUserActivities(undefined, SCENE_ACTIVITY_TYPE)).toEqual([]);
});
});

describe('oneShotCompletion', () => {
it('delivers only the first result', () => {
const completionHandler = vi.fn();
Expand All @@ -223,6 +258,13 @@ describe('oneShotCompletion', () => {
deliver(true);
expect(deliver.delivered).toBe(true);
});

it('accepts a launch that has no completion handler to report to', () => {
const deliver = oneShotCompletion(undefined);

expect(() => deliver(true)).not.toThrow();
expect(deliver.delivered).toBe(true);
});
});

describe('deliverShortcutItem', () => {
Expand Down
22 changes: 21 additions & 1 deletion packages/core/application/scene-delegate-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ export function forwardContinueUserActivity(delegate: AppDelegateClass, applicat
return true;
}

/**
* The continuation activities a scene was launched with. The activity `openWindow()` attaches
* to carry window data is left out: it addresses the new window, not the app's handlers.
*/
export function getLaunchUserActivities(connectionOptions: UISceneConnectionOptions | undefined | null, sceneActivityType: string): NSUserActivity[] {
const activities = connectionOptions?.userActivities?.allObjects;
const count = activities?.count ?? 0;
const result: NSUserActivity[] = [];

for (let i = 0; i < count; i++) {
const activity = activities.objectAtIndex(i) as NSUserActivity;

if (activity && activity.activityType !== sceneActivityType) {
result.push(activity);
}
}

return result;
}

/**
* Hands a scene quick action to `applicationPerformActionForShortcutItemCompletionHandler`,
* or reports it unhandled when no such handler is registered.
Expand Down Expand Up @@ -145,7 +165,7 @@ export function deliverShortcutItem(delegate: AppDelegateClass, application: UIA
* completion handler invoked twice as a programming error, and the same handler is exposed
* to both event listeners and legacy delegate handlers.
*/
export function oneShotCompletion(completionHandler: (handled: boolean) => void): OneShotCompletion {
export function oneShotCompletion(completionHandler: ((handled: boolean) => void) | undefined): OneShotCompletion {
let delivered = false;

const deliver = ((handled: boolean) => {
Expand Down
Loading