├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── PRIVACY_AND_SECURITY.md
├── README.md
├── declarative_link_capturing.md
├── demos
├── index.html
├── index.js
├── polyfill.js
├── simulatenavigate.html
├── simulatenavigate.js
└── sw.js
├── index.html
├── launch_handler.md
├── sw_launch_event.md
└── w3c.json
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/).
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Web Platform Incubator Community Group
2 |
3 | This repository is being used for work in the W3C Web Platform Incubator
4 | Community Group, governed by the [W3C Community License Agreement
5 | (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive
6 | contributions, you must join the CG.
7 |
8 | If you are not the sole contributor to a contribution (pull request), please
9 | identify all contributors in the pull request comment.
10 |
11 | To add a contributor (other than yourself, that's automatic), mark them one per
12 | line as follows:
13 |
14 | ```
15 | +@github_username
16 | ```
17 |
18 | If you added a contributor by mistake, you can remove them in a comment with:
19 |
20 | ```
21 | -@github_username
22 | ```
23 |
24 | If you are making a pull request on behalf of someone else but you had no part
25 | in designing the feature, you can remove yourself with the above syntax.
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors
2 | under the
3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
4 |
5 | Contributions to Specifications are made under the
6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
7 |
--------------------------------------------------------------------------------
/PRIVACY_AND_SECURITY.md:
--------------------------------------------------------------------------------
1 | # Answers to [Security and Privacy Questionnaire](https://www.w3.org/TR/security-privacy-questionnaire/)
2 |
3 | ### 3.1 Does this specification deal with personally-identifiable information?
4 |
5 | No.
6 |
7 |
8 | ### 3.2 Does this specification deal with high-value data?
9 |
10 | No.
11 |
12 |
13 | ### 3.3 Does this specification introduce new state for an origin that persists across browsing sessions?
14 |
15 | No.
16 |
17 |
18 | ### 3.4 Does this specification expose persistent, cross-origin state to the web?
19 |
20 | No.
21 |
22 | ### 3.5 Does this specification expose any other data to an origin that it doesn’t currently have access to?
23 |
24 | Kind of.
25 |
26 | The new APIs will tell sites what caused their launch (e.g. 'file_handler', 'share_target'). Previously this information was not explicit, but could be inferred based on the url that was being visited (as sites are able to specify which url they want shares/file launches to open).
27 |
28 | ### 3.6 Does this specification enable new script execution/loading mechanisms?
29 |
30 | No.
31 |
32 |
33 | ### 3.7 Does this specification allow an origin access to a user’s location?
34 |
35 | No.
36 |
37 |
38 | ### 3.8 Does this specification allow an origin access to sensors on a user’s device?
39 |
40 | No.
41 |
42 |
43 | ### 3.9 Does this specification allow an origin access to aspects of a user’s local computing environment?
44 |
45 | No.
46 |
47 |
48 | ### 3.10 Does this specification allow an origin access to other devices?
49 |
50 | No.
51 |
52 |
53 | ### 3.11 Does this specification allow an origin some measure of control over a user agent’s native UI?
54 |
55 | Kind of. This API will allow sites to control what happens in some situations (such as clicking a link into a PWA, opening a file, or sharing something to the PWA).
56 |
57 | Currently, this is likely to involve an installed PWA choosing to either
58 |
59 | 1. Open a new window
60 | 2. Focus an existing window
61 | 3. Show a notification (assuming relevant permissions have been granted).
62 |
63 | In addition, this decision should only be delegated to the site in the case where the user hasn't expressed some preference (e.g. open all links in browser, open this link in a new tab/window).
64 |
65 |
66 | ### 3.12 Does this specification expose temporary identifiers to the web?
67 |
68 | No.
69 |
70 |
71 | ### 3.13 Does this specification distinguish between behavior in first-party and third-party contexts?
72 |
73 | Only first party contexts will receive launch events.
74 |
75 |
76 | ### 3.14 How should this specification work in the context of a user agent’s "incognito" mode?
77 |
78 | Launch events will behave the same in incognito and normal browsing contexts.
79 |
80 | ### 3.15 Does this specification persist data to a user’s local device?
81 |
82 | No.
83 |
84 |
85 | ### 3.16 Does this specification have a "Security Considerations" and "Privacy Considerations" section?
86 |
87 | Yes. See the [explainer](explainer.md#security-and-privacy-considerations).
88 |
89 |
90 | ### 3.17 Does this specification allow downgrading default security characteristics?
91 |
92 | No.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web App Launch Handling
2 |
3 | Author: Matt Giuca <>
4 | Author: Eric Willigers <>
5 | Author: Jay Harris <>
6 | Author: Raymes Khoury <>
7 | Author: Alan Cutter <>
8 |
9 | Last updated: 2022-03-03
10 |
11 | This repository details a proposal to add a `launch_handler` field to the [web app manifest](https://www.w3.org/TR/appmanifest):
12 | - [Explainer](launch_handler.md)
13 | - [Draft spec](https://wicg.github.io/web-app-launch/)
14 |
15 |
16 | There are two older proposals archived here:
17 | - [Declarative Link Capturing](declarative_link_capturing.md) which was replaced by `launch_handler`.
18 | - The [service worker `launch` event](sw_launch_event.md) which preceeded `launch_handler` but will likely become an extension to it in future.
--------------------------------------------------------------------------------
/declarative_link_capturing.md:
--------------------------------------------------------------------------------
1 | Author: Matt Giuca
2 |
3 | Input from: Alan Cutter , Dominick Ng
4 |
5 | Authored: 2020-05-25
6 | Updated: 2022-03-03
7 |
8 | ## Obsolete
9 |
10 | This API proposal is obsolete in favor of [`launch_handler`](launch_handler.md).
11 | Context: https://docs.google.com/document/d/1w9qHqVJmZfO07kbiRMd9lDQMW15DeK5o-p-rZyL7twk
12 |
13 | ## Overview
14 |
15 | [sw-launch](sw_launch_event.md) has been proposed for a number of years and has never made it past a proposal stage, largely due to the complexity involved in both spec and implementation (a complex effort spanning PWAs, service workers, and HTML navigation stack).
16 |
17 | We found that almost all `launch` use cases could be covered by a handful of fixed rules (for example, "choose an existing window in the same app, focus it, and navigate it to the launched URL"). Thus, this proposal, "Declarative Link Capturing", allows sites to choose one of those fixed rules without having to implement custom `launch` event logic, which should satisfy most use cases, and simplify the implementation in the browser and in all the sites. We leave open the option of expanding into the full `launch` event later on.
18 |
19 |
20 | ## Goals / use cases
21 |
22 |
23 |
24 | * Link capturing for PWAs: a PWA that wants to open in a window whenever the user clicks a link to a URL within that app’s [navigation scope](https://www.w3.org/TR/appmanifest/#nav-scope), rather than opening in a browser tab.
25 | * Capturing links and navigations from the following (non-exhaustive list of) sources:
26 | * Clicked links from other web pages.
27 | * URL launch from a native app in the operating system.
28 | * [Shortcuts API](https://www.w3.org/TR/appmanifest/#shortcuts-member) (jump lists to within the app)
29 | * [Protocol handlers](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler)
30 | * [File handlers](https://github.com/WICG/file-handling/blob/master/explainer.md)
31 | * [Share Target API](https://web.dev/web-share-target/)
32 | * Single-window PWAs: a PWA that prefers to only have a single instance of itself open at any time, with new navigations focusing the existing instance. Sub-use cases include:
33 | * Apps that generally only make sense to have one instance running (e.g., a music player, a game).
34 | * Apps that include multi-document management within a single instance (e.g., an HTML-implemented tab strip, floating sub-windows like Gmail).
35 |
36 |
37 | ### Relevant issues
38 |
39 |
40 |
41 | * [w3c/manifest#764](https://github.com/w3c/manifest/issues/764): Option for specifying browser app picker behavior
42 | * [w3c/manifest#597](https://github.com/w3c/manifest/issues/597): Single-window mode option
43 |
44 |
45 | ### Non-goals
46 |
47 |
48 |
49 | * Capturing links from origin A to open in a PWA on origin B (see “[URL Handlers](https://github.com/WICG/pwa-url-handler/blob/master/explainer.md)” proposal which has this as an explicit goal).
50 | * Defining a separate scope for link capturing, separate from the existing [navigation scope](https://www.w3.org/TR/appmanifest/#navigation-scope) (see “[URL Handlers](https://github.com/WICG/pwa-url-handler/blob/master/explainer.md)” proposal which has this as an explicit goal).
51 | * Allowing a web app to force itself to only ever be in a single window. (The user will always be able to override a single-window setting, since after all, these are just web pages and the user is in control of navigation.)
52 | * If multiple clients are open, allowing the application to choose which client to focus. [This is an extended goal of the full sw-launch design.]
53 | * Stopping a navigation from opening any windows, and handling the navigation in the background. [This is an extended goal of the full sw-launch design.]
54 |
55 |
56 | ## Proposal
57 |
58 | (All of the proposed names are subject to change / debate.)
59 |
60 | This proposal defines new manifest members that control what happens when the browser is asked to navigate to a URL that is within the application’s [navigation scope](https://www.w3.org/TR/appmanifest/#dfn-navigation-scope), from a context outside of the navigation scope.
61 |
62 | It doesn’t apply if the user is already within the navigation scope (for instance, if the user has a browser tab open that is within scope, and clicks an internal link). The user agent is also allowed to decide under what conditions this does not apply; for example, middle-clicking a link (or right-clicking -> “open in new tab”) would typically not trigger this behaviour. Note that this interception behaviour would apply to both `target=_self` and `target=_blank` links, so that links clicked in a browser window (or window of a different PWA) would be opened in the PWA even if they would normally cause a navigation within the same tab. This accords with link capturing behaviour of native apps on operating systems like Android.
63 |
64 | A new manifest field “`capture_links`” is introduced with a string or list-of-strings value. Its value dictates the launch behaviour as follows:
65 |
66 |
67 |
68 | * “`none`” (the default) — no link capturing; links clicked leading to this PWA scope navigate as normal without opening a PWA window.
69 | * Note that user agents can technically choose `new-client` or `existing-client-navigate` as the default, since the behaviour of all three of these, while different to the user, is indistinguishable to the site. The remaining options fundamentally change how the site experiences navigation, so a user agent cannot unilaterally choose those without the site explicitly opting in.
70 | * “`new-client`” — each link clicked leading to this PWA scope opens a new PWA window at that URL.
71 | * “`existing-client-navigate`” — when a link is clicked leading to this PWA scope, the user agent finds an existing PWA window (if more than one exists, the user agent may choose one arbitrarily), and causes it to navigate to the opened URL.
72 | * Behaves as `new-client` if no window is currently open.
73 | * This option potentially leads to data loss as pages can be arbitrarily navigated away from. Sites should be aware that they are opting into such behaviour by choosing this option. Works best for “read-only” sites that don’t hold user data in memory, such as music players.
74 | * If the page being navigated away from has a “`beforeunload`” event, the user would see the prompt before the navigation completes.
75 | * “`existing-client-event`” — when a link is clicked leading to this PWA scope, the user agent finds an existing PWA window (if more than one exists, the user agent may choose one arbitrarily), and fires a “[`launch`](explainer.md)” event in that window’s top-level context, containing the launched URL.
76 | * Behaves as `new-client` if no window is currently open.
77 | * This is intended for more advanced sites, which inspect the launched URL and use it to load data into the current browsing context, without a navigation. For example, a site that allows multiple documents to be loaded in the same window can pop open a new sub-window in response to the navigation.
78 | * (future) “`serviceworker`” — doesn’t open a window at all, instead firing a “[`launch`](explainer.md)” event in the service worker’s context. This is an opt in to the full originally proposed [service worker launch event](explainer.md).
79 | * This will be omitted initially (due to complexity, and the fact that it has taken us three years to neither spec nor implement this), but we can bring it in later in accordance with demand.
80 |
81 | If a list of strings is given, the user agent chooses the first supported item in the list (this is for future proofing, allowing us to add new choices later), defaulting to `none` if none of them are supported. (Much like the [proposed `display_override` member](https://github.com/dmurph/display-mode/blob/master/explainer.md); note that if we had our time again, we likely would build the explicit display fallback chain into the display member, rather than as a separate `display_override` member.) If a string is given, it is the same as a singleton list containing that string.
82 |
83 | ## Relation to existing proposals
84 |
85 |
86 | ### [Service Worker launch event](explainer.md)
87 |
88 | This proposal is forwards-compatible with the original [sw-launch](sw_launch_event.md) proposal. It covers many of the same use cases, but omits the more advanced use cases (specifically, the option to choose which client to focus, and the option to not show any UI). By adding a new “`capture_links`” mode (“`serviceworker`”), the app can explicitly opt in to receiving the “`launch`” event in the service worker, enabling those other use cases, while the majority of uses can be achieved without having to write extra service worker code.
89 |
90 |
91 | ### [WICG: URL Handlers](https://github.com/WICG/pwa-url-handler/blob/master/explainer.md)
92 |
93 | These two proposals are ostensibly both “link capturing”, but upon close inspection, they are quite orthogonal. Huang’s URL Handlers deals with _what_ is captured (i.e., the set of URLs associated with an app), which my proposal simply assumes to be the app’s navigation scope. My proposal deals with _how_ the app works once invoked via a captured link.
94 |
95 | I think these proposals can be considered independently (for example, we could enact both of them, with the optional `url_handlers` defining the set of URLs to capture, defaulting to the app’s navigation scope, while `capture_links` defines the behaviour upon navigating to such a URL).
96 |
97 | For posterity, here is my (Matt's) [initial response to that proposal](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/300#issuecomment-627759147).
98 |
99 |
100 | ### [File Handling](https://github.com/WICG/file-handling)
101 |
102 | File Handling defines a “`launchQueue`” variable that was intended to be expanded into the launch event once that proposal was finalized. This proposal is compatible with that. When a PWA is launched via a file handler, the behaviour would change based on the “`capture_links`” member:
103 |
104 |
105 |
106 | * `none`, `new-client` or `existing-client-navigate` all result in a new browsing context being created. This just works as normal (create a launchQueue and put a single item in it).
107 | * `existing-client-event` would push a new file handle to the `launchQueue` of the existing context before firing the `launch` event.
108 | * (future) `serviceworker` would put the file handle in the service worker’s `launch` event, which can then be forwarded to an existing client’s `launchQueue`.
109 |
110 |
111 | ### [Tabbed Application Mode](https://github.com/w3c/manifest/issues/737)
112 |
113 | Link capturing goes hand-in-hand with tabbed application mode, because it’s very weird to have a tabbed application that _doesn’t_ capture links (you would have links opening in new browser tabs when they could be opening in new tabs within the PWA).
114 |
115 | When this proposal is used with a tabbed mode PWA, the “`new-client`” link capture mode would cause the user agent to find an existing PWA window, and open the launched URL in a new tab within that window. It is suggested that tabbed apps use the “`new-client`” link capture mode.
116 |
117 | All of the other link capture modes would behave as defined above (e.g., “`existing-client-navigate`” would find an existing tab and navigate it), which don’t really make sense for a tabbed application.
118 |
119 |
120 | ## Issues
121 |
122 |
123 |
124 | * We have noticed that some websites (notably Google Drive) exhibit behaviour that works weirdly in conjunction with link capturing:
125 | * Clicking a link from the site opens another page in a new browser tab. However, instead of just opening the target page with a `window.open()` or a `target=_blank` link, the site first opens an `about:blank` page with a `window.open()`, and then proceeds to navigate it to the intended page.
126 | * We believe this behaviour has something to do with the originating site wanting to have a handle on the opened client.
127 | * This behaviour breaks link capturing logic, because the initial `about:blank` navigation would open a new browser tab, and then the subsequent navigation gets captured and opened in a PWA window, leaving an about:blank tab open in the browser.
128 | * Figure out how to capture POST requests in each of the above modes. (For example, “`existing-client-event`” might have to encode the POST data in the event.)
129 | * Having the user agent arbitrarily choose an existing window (in the “`existing-client-*` modes) may not be very good behaviour for any app. For example, you may have two windows open: one on the main application and one on a help article. You would want to message the main application window, not the help article. We may need to define more control over which window gets targeted.
130 |
131 | ## Security & Privacy Questionnaire
132 |
133 | https://www.w3.org/TR/security-privacy-questionnaire/
134 |
135 | **2.11. Does this specification allow an origin some measure of control over a user agent’s native UI?**
136 |
137 | Yes. This API allows sites new additional control options:
138 |
139 | * Being able to automatically open installed apps in a window (this uses existing UI but makes it possible for the site to automatically trigger it).
140 | * Focus an existing window on its own domain and fire an event containing the clicked URL. This is intended to allow the site to navigate an existing window to a new page, overriding the default HTML navigation flow.
141 |
142 | **2.14. How does this specification work in the context of a user agent’s Private Browsing or "incognito" mode?**
143 |
144 | This probably should not work in incognito mode. (We don't want a link clicked in Incognito to open in a non-Incognito app.) The browser could also give a warning that you are leaving incognito mode, which is what Chrome does when link capturing to Android apps.
145 |
146 | ## Changelog
147 |
148 | * 2021-04-28
149 | * Added Security & Privacy Questionnaire
150 | * 2020-01-08
151 | * Got rid of `auto` (just says that `none` is the default).
152 | * Converted underscores (`_`) to dashes (`-`) in enum values.
153 | * Accept either a string or a list.
154 |
--------------------------------------------------------------------------------
/demos/index.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Service Worker Launch Demo
9 |
10 |
11 |
12 |
13 | Service Worker Launch Demo
14 | This is a demo site for the Service Worker "launch" event polyfill.
15 | Because "launch" is not implemented in the browser, you need to use the
16 | Navigation Simulator. Think of that
17 | as a test UI for generating incoming navigations into your website.
18 | This page acts as the "main page" of the site; it uses the "launch" event
19 | to steal any navigations to the "/compose" URL.
20 | Note: The demo is unable to focus an existing tab or open a new one (due
21 | to user gesture restriction). Look for error messages in the console to
22 | imagine what is "supposed" to happen (or patch Chromium with this hack to remove the gesture
24 | check).
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/demos/index.js:
--------------------------------------------------------------------------------
1 | /* This work is licensed under the W3C Software and Document License
2 | * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
3 | */
4 |
5 | if ('serviceWorker' in navigator) {
6 | navigator.serviceWorker.register('sw.js');
7 |
8 | navigator.serviceWorker.addEventListener('message', event => {
9 | const logsDiv = document.querySelector('#logs');
10 | const p = document.createElement('p');
11 | p.appendChild(document.createTextNode('Got message: ' + event.data));
12 | logsDiv.appendChild(p);
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/demos/polyfill.js:
--------------------------------------------------------------------------------
1 | /* This work is licensed under the W3C Software and Document License
2 | * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
3 | */
4 |
5 | // To be included in the Service Worker.
6 |
7 | class LaunchEvent extends ExtendableEvent {
8 | constructor(type, init) {
9 | super(type, init);
10 | this.url = init ? init.url || null : null;
11 | this.clientId = init ? init.clientId || null : null;
12 | this._handlePending = null;
13 | }
14 |
15 | handleLaunch(promise) {
16 | if (this._handlePending !== null)
17 | throw new InvalidStateError('handleLaunch has already been called');
18 |
19 | // Extend the lifetime of the event until the promise completes.
20 | // This is not actually allowed on a script-constructed ExtendableEvent.
21 | //this.waitUntil(promise);
22 |
23 | // Wait until the promise resolves before applying the default.
24 | this._handlePending = promise;
25 | }
26 | }
27 |
28 | // Listen for a message from simulatenavigate.html.
29 | self.addEventListener('message', async event => {
30 | if (event.data.tag !== 'polyfill_simulatenavigate')
31 | return;
32 |
33 | // |url| is relative to Service Worker scope.
34 | const url = new URL(event.data.url, self.registration.scope).href;
35 | const target = event.data.target;
36 | console.log('[Polyfill]: simulatenavigate message: source =', event.source.id,
37 | ', url =', url, ', target =', target);
38 |
39 | if (target === 'nolaunch') {
40 | // Like target === 'blank', but do not dispatch the launch event (simulate a
41 | // user explicitly asking for a new context).
42 | clients.openWindow(url);
43 | return;
44 | }
45 |
46 | // Create and fire a LaunchEvent.
47 | // If target is self, pass the client ID into the launch event, so it can
48 | // identify the sending window. If target is blank, this navigation is not
49 | // tied to any particular context.
50 | const clientId = target === 'self' ? event.source.id : null;
51 | const launchEvent =
52 | new LaunchEvent('launch', {url, clientId, cancelable: true});
53 | self.dispatchEvent(launchEvent);
54 |
55 | if (launchEvent.defaultPrevented) {
56 | console.log('[Polyfill]: Cancelling default behavior (preventDefault)');
57 | // Do not apply the default behaviour, regardless of any pending promise.
58 | return;
59 | }
60 |
61 | defaultBehavior = () => {
62 | if (target === 'self')
63 | event.source.navigate(url);
64 | else
65 | clients.openWindow(url);
66 | }
67 |
68 | if (launchEvent._handlePending === null) {
69 | console.log('[Polyfill]: Proceeding with default behavior '
70 | + '(no preventDefault / handleLaunch)');
71 | // Launch handler did not queue a custom handler. Immediately apply the
72 | // default behavior.
73 | defaultBehavior();
74 | return;
75 | }
76 |
77 | try {
78 | await launchEvent._handlePending;
79 | } catch (e) {
80 | // Promise failed. Apply the default behavior.
81 | // NOTE: If we do not want to do this (i.e., we don't care whether it
82 | // succeeds or fails), we can just use preventDefault instead of
83 | // handleLaunch taking a promise.
84 | console.log('[Polyfill]: Proceeding with default behavior '
85 | + '(handleLaunch rejected)');
86 | defaultBehavior();
87 | return;
88 | }
89 |
90 | console.log('[Polyfill]: Cancelling default behavior '
91 | + '(handleLaunch fulfilled)');
92 | });
93 |
--------------------------------------------------------------------------------
/demos/simulatenavigate.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Navigation simulator
9 |
10 |
11 |
12 |
13 | Navigation simulator
14 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demos/simulatenavigate.js:
--------------------------------------------------------------------------------
1 | /* This work is licensed under the W3C Software and Document License
2 | * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
3 | */
4 |
5 | function getTarget() {
6 | if (document.querySelector('#targetBlank').checked)
7 | return 'blank';
8 |
9 | if (document.querySelector('#targetNoLaunch').checked)
10 | return 'nolaunch';
11 |
12 | return 'self';
13 | }
14 |
15 | function go() {
16 | const url = document.querySelector('#url').value;
17 | const target = getTarget();
18 |
19 | navigator.serviceWorker.controller.postMessage({
20 | tag: 'polyfill_simulatenavigate', url, target
21 | });
22 | }
23 |
24 | window.addEventListener('load', () => {
25 | if (!navigator.serviceWorker) {
26 | console.error('Need a browser that supports Service Workers.');
27 | return;
28 | }
29 |
30 | document.querySelector('form').addEventListener(
31 | 'submit', e => e.preventDefault());
32 | document.querySelector('#go').addEventListener('click', go);
33 | });
34 |
--------------------------------------------------------------------------------
/demos/sw.js:
--------------------------------------------------------------------------------
1 | /* This work is licensed under the W3C Software and Document License
2 | * (http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
3 | */
4 |
5 | importScripts('polyfill.js')
6 |
7 | const kMainURLPath = new URL(self.registration.scope).pathname;
8 | const kComposeURL = new URL('compose', self.registration.scope).href;
9 |
10 | self.addEventListener('launch', event => {
11 | console.log('[SW]: launch:', event);
12 |
13 | if (event.url !== kComposeURL)
14 | return;
15 |
16 | event.handleLaunch((async () => {
17 | // Get an existing client at the main page URL.
18 | const allClients = await clients.matchAll();
19 | let client;
20 | for (const c of allClients) {
21 | if (new URL(c.url).pathname === kMainURLPath) {
22 | client = c;
23 | break;
24 | }
25 | }
26 |
27 | if (!client) {
28 | console.log('[SW] Resuming normal navigation to ', event.url);
29 | // Any throw causes the normal behaviour to resume.
30 | throw new Error('Resuming normal navigation');
31 | }
32 |
33 | console.log('[SW] Focusing existing page:', client.id);
34 | client.focus();
35 | client.postMessage('compose');
36 | })());
37 | });
38 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Web App Launch Handler API
7 |
8 |
10 |
42 |
43 |
44 |
45 |
46 | This specification defines an API that allows web applications to
47 | configure the behaviour of app launches with respect to already open app
48 | instances. This API aims to cater to the needs of single instance web
49 | apps e.g. music players.
50 |
51 |
52 |
54 |
55 |
56 | Prerequisites
57 |
58 |
59 | In order to implement this API, the user agent MUST support
60 | [[[appmanifest]]].
61 |
62 |
63 |
126 |
127 |
128 | Extension to the Web App Manifest
129 |
130 |
131 | The following steps are added to the extension point in the steps for
133 | processing a
134 | manifest:
135 |
136 |
137 | - Run the steps for [=processing the launch_handler member=]
138 | given [=ordered map=] |json:ordered map| and [=ordered map=]
139 | |manifest:ordered map|.
140 |
141 |
142 |
143 |
144 | [=manifest/launch_handler=] member
145 |
146 |
147 | The `launch_handler` is a
148 | dictionary containing configurations for how web app launches should
149 | behave.
150 |
151 |
152 | [=manifest/launch_handler=] is a dictionary despite
153 | [=manifest/client_mode=] being the only member. This is to give room for
154 | more members to be added should other types of behaviors be needed in
155 | the future.
156 |
157 |
158 | The steps for processing the launch_handler member, given
159 | [=ordered map=] |json:ordered map|, [=ordered map=]
160 | |manifest:ordered map|, are as follows:
161 |
162 |
163 | - If |json|["launch_handler"] does not [=map/exist=], return.
164 |
165 | - If the type of |json|["launch_handler"] is not [=ordered map=],
166 | return.
167 |
168 | - Set |manifest|["launch_handler"] to a new [=ordered map=].
169 |
170 | - [=Process the `client_mode` member=] passing
171 | |json|["launch_handler"], |manifest|["launch_handler"].
172 |
173 |
174 |
175 |
176 |
177 | [=manifest/client_mode=] member
178 |
179 |
180 | The `client_mode` member of the
181 | [=manifest/launch_handler=] is a [=string=] or list of [=strings=]
182 | that specify one or more [=client mode targets=]. A [=client mode target=]
183 | declares a particular client selection and navigation behaviour to use
184 | for web apps launches.
185 |
186 |
187 | User agents MAY support only a subset of the [=client mode targets=]
188 | depending on the constraints of the platform (e.g. mobile devices may
189 | not support multiple app instances simultaneously).
190 |
191 |
192 |
193 | The client mode targets are as follows:
194 |
195 |
196 | -
197 | auto
198 |
199 | -
200 | The user agent's default launch routing behaviour is used.
201 |
202 | The user agent is expected to decide what works best for the
203 | platform. e.g., on mobile devices that only support single app
204 | instances the user agent may use `navigate-existing`,
205 | while on desktop devices that support multiple windows the user
206 | agent may use `navigate-new` to avoid data loss.
207 |
208 |
209 | -
210 | navigate-new
211 |
212 | -
213 | A new web app client is created to load the launch's target URL.
214 |
215 | -
216 | navigate-existing
217 |
218 | -
219 | If an existing web app client is open it is brought to focus and
220 | navigated to the launch's target URL. If there are no existing web
221 | app clients the [=client mode/navigate-new=] behaviour is used instead.
222 |
223 | -
224 | focus-existing
225 |
226 | -
227 | If an existing web app client is open it is brought to focus but
228 | not navigated to the launch's target URL, instead the target URL
229 | is communicated via {{LaunchParams}} . If there are no existing
230 | web app clients the [=client mode/navigate-new=] behaviour is used
231 | instead.
232 |
233 | It is necessary for the page to have a {{LaunchConsumer}} set on
234 | {{Window/launchQueue}} to receive the launch's {{LaunchParams}}
235 | and do something with it. If no action is taken by the page the
236 | user experience of the launch is likely going to appear broken.
237 |
238 |
239 |
240 |
241 |
242 | To process the `client_mode` member, given [=ordered
243 | map=] |json_launch_handler:ordered map|, [=ordered map=]
244 | |manifest_launch_handler:ordered map|, run the following:
245 |
246 |
247 | - If |json_launch_handler|["client_mode"] does not [=map/exist=],
248 | return.
249 |
250 | - If the type of |json_launch_handler|["client_mode"] is
251 | [=list=]:
252 |
253 | - [=list/For each=] |entry| of
254 | |json_launch_handler|["client_mode"]:
255 |
256 | - If the type of |entry| is not [=string=], continue.
257 |
258 | - If |entry| is supported by the user agent, set
259 | |manifest_launch_handler|["client_mode"] to |entry| and
260 | return.
261 |
262 |
263 |
264 |
265 |
266 | - If |json_launch_handler|["client_mode"] is [=string=] and supported
267 | by the user agent, set |manifest_launch_handler|["client_mode"] to
268 | |json_launch_handler|["client_mode"] and return.
269 |
270 | - Set |manifest_launch_handler|["client_mode"] to [=client mode/auto=].
271 |
272 |
273 |
274 | `client_mode` accepts a list of strings to allow sites to specify
275 | fallback [=client mode targets=] to use if the preferred [=client mode
276 | target=] is not supported by the user agent or platform. The first
277 | supported [=client mode target=] entry in the list gets used.
278 |
279 |
280 |
281 |
282 |
283 | Handling Web App Launches
284 |
285 |
286 |
287 | Launching a Web App with Handling
288 |
289 |
290 | This specification replaces the existing algorithm to [=launch a web
291 | application=] to include the behavior of [=manifest/launch_handler=]
292 | by replacing it with the steps to [=launch a web application with
293 | handling=].
294 |
295 |
296 | The steps to
297 | launch a web application with handling are given by the
298 | following algorithm. The algorithm takes a [=Document/processed
299 | manifest=] |manifest:processed manifest|, an optional [=URL=] or
300 | {{LaunchParams}} |params|, an optional [=POST resource=] |POST
301 | resource| and returns an [=application context=].
302 |
303 |
304 | - If |params| is not given, set |params| to
305 | |manifest|.[=manifest/start_url=].
306 |
307 | - If |params| is a [=URL=], set |params| to a new {{LaunchParams}}
308 | with {{LaunchParams/targetURL}} set to |params|.
309 |
310 | - Assert: |params|.{{LaunchParams/targetURL}} is [=manifest/within
311 | scope=] of |manifest|.
312 |
313 | - Set |application context| to the result of running the steps to
314 | [=prepare an application context=] passing |manifest|, |params|
315 | and |POST resource|.
316 |
317 | - Append |params| to the [=unconsumed launch params=] of the
318 | |application context|'s document's {{Window/launchQueue}}.
319 |
320 | - Run the steps to [=process unconsumed launch params=] on the
321 | |application context|'s [=navigable/active document=]'s
322 | {{Window/launchQueue}}.
323 |
324 | |application context| may be an existing instance with an
325 | [=assigned launch consumer=] hence it is necessary to process
326 | the newly appended {{LaunchParams}}.
327 |
328 |
329 |
330 |
331 | The steps to prepare an application context are given by
332 | the following algorithm. The algorithm takes a
333 | [=Document/processed manifest=] |manifest:processed manifest|, a
334 | {{LaunchParams}} |launch params|, an optional [=POST
335 | resource=] |POST resource| and returns an [=application context=].
336 |
337 |
338 | - Let [=client mode target=] |client_mode| be
339 | |manifest|.[=manifest/launch_handler=].[=manifest/client_mode=].
340 |
341 | - If |client_mode| is [=client mode/auto=], set |client_mode| to
342 | either [=client mode/navigate-new=] or
343 | [=client mode/navigate-existing=] according to the user agent's
344 | decision for which is most appropriate.
345 |
346 | -
347 |
Switching on |client mode|, run the following substeps:
348 |
349 | - [=client mode/navigate-new=]
350 |
-
351 |
352 | - Return the result of running the steps to [=create a new
353 | application context=] passing |manifest|, |launch
354 | params|.{{LaunchParams/targetURL}} and |POST resource|.
355 |
356 |
357 | - [=client mode/navigate-existing=] or
358 | [=client mode/focus-existing=]
359 |
-
360 |
361 | - If there is no [=application context=] that has |manifest|
362 | [=applied=]:
363 |
364 | -
365 | Return the result of running the steps to [=create a new
366 | application context=] passing |manifest|,
367 | |launch params|.{{LaunchParams/targetURL}} and |POST
368 | resource|.
369 |
370 |
371 |
372 | - Let |application context| be an [=application context=]
373 | that has |manifest| [=applied=], the user agent selects
374 | the most appropriate one if there are multiple.
375 |
376 | An appropriate selection strategy would be to pick the
377 | one that was most recently in focus.
378 |
379 |
380 | -
381 | If |client mode| is [=client mode/focus-existing=] and
382 | |application context|'s
383 | current
384 | session history entry's
385 | URL is [=manifest/within
386 | scope=] of |manifest|:
387 |
388 | - Bring |application context|'s viewport into focus.
389 |
390 | - Return |application context|.
391 |
392 |
393 |
394 | -
395 | [=Navigate=] |application context| to |launch
396 | params|.{{LaunchParams/targetURL}} passing |POST resource|.
397 |
398 | - Return |application context|.
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 | Processing [=unconsumed launch params=]
408 |
409 |
410 | The steps to process unconsumed launch params given a
411 | {{LaunchQueue}} |queue| are as follows:
412 |
413 |
414 | - If the [=assigned launch consumer=] |consumer| is set on
415 | |queue|:
416 |
417 | - [=list/For each=] |launch_params:LaunchParams| of
418 | the |queue|'s [=unconsumed launch params=]:
419 |
420 | - Invoke |consumer| with |launch_params|.
421 |
422 |
423 |
424 | - Set |queue|'s [=unconsumed launch params=] to the empty
425 | list.
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 | Script Interfaces to App Launches
435 |
436 |
437 |
438 | LaunchParams interface
439 |
440 |
441 | - This has been copied directly from
442 |
443 | Manifest Incubations without modification, this
444 | [=manifest/launch_handler=] spec should be its replacement home.
445 |
446 | - {{LaunchParams}} should be a dictionary.
447 |
448 | - {{LaunchParams/targetURL}} should not be nullable.
449 |
450 | - {{LaunchParams/files}} should be optional
451 |
452 | - For members other than {{LaunchParams/targetURL}} only one should
453 | be not undefined, indicating the type of launch.
454 |
455 |
456 |
457 | [Exposed=Window] interface LaunchParams {
458 | readonly attribute DOMString? targetURL;
459 | readonly attribute FrozenArray<FileSystemHandle> files;
460 | };
461 |
462 |
463 | {{LaunchParams/targetURL}} represents the [=URL=] of the launch which
464 | MUST be [=manifest/within scope=] of the application.
465 |
466 |
467 | For every |file handle:FileSystemHandle| in {{LaunchParams/files}},
468 | querying the file system permission with
469 | {{FileSystemPermissionDescriptor/mode}} set to
470 | {{FileSystemPermissionMode/"readwrite"}} MUST return
471 | {{PermissionState/"granted"}}.
472 |
473 |
474 |
475 |
476 | LaunchConsumer function
477 |
478 |
479 | callback LaunchConsumer = any (LaunchParams params);
480 |
481 |
482 |
483 |
484 | LaunchQueue interface
485 |
486 |
487 | partial interface Window {
488 | readonly attribute LaunchQueue launchQueue;
489 | };
490 |
491 | [Exposed=Window] interface LaunchQueue {
492 | undefined setConsumer(LaunchConsumer consumer);
493 | };
494 |
495 |
496 | {{LaunchQueue}} has an unconsumed launch params which is a
497 | [=list=] of {{LaunchParams}} that is initially empty.
498 |
499 |
500 | {{LaunchQueue}} has an assigned launch consumer which is
501 | an optional {{LaunchConsumer}} that is initially null.
502 |
503 |
504 |
505 | setConsumer method
506 |
507 |
508 | The {{LaunchQueue/setConsumer(consumer)}} method steps are:
509 |
510 |
511 | - Set the [=assigned launch consumer=] to |consumer|.
512 |
513 | - Run the steps to [=process unconsumed launch params=].
514 |
515 |
516 |
517 |
518 | {{LaunchParams}} are passed to the document via a {{LaunchQueue}}
519 | instead of via events to avoid a race condition between a launch event
520 | firing and page scripts attaching the event listener. In contrast the
521 | {{LaunchQueue}} buffers all enqueued {{LaunchParams}} until a
522 | {{LaunchConsumer}} has been set.
523 |
524 |
525 |
526 |
534 |
535 |
536 | Privacy and Security Considerations
537 |
538 |
539 | Implementations should take care when [=launching a web application with
540 | handling=] for launches where [=manifest/client_mode=] is
541 | [=client mode/focus-existing=]. These launches MUST NOT leak URLs
542 | outside of the [=manifest/navigation scope=]. This applies in both
543 | directions given a [=Document/processed manifest=] |manifest|:
544 |
545 | -
546 | Web application launches MUST NOT use a {{LaunchParams/targetURL}}
547 | that is not [=manifest/within scope=] of |manifest|.
548 |
549 | -
550 | {{LaunchParams}} MUST NOT be enqueued in an [=application
551 | context=] with a
552 | current session history entry
553 | URL that is not [=manifest/within
554 | scope=] of |manifest|.
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
--------------------------------------------------------------------------------
/launch_handler.md:
--------------------------------------------------------------------------------
1 | # Web App Launch Handling
2 |
3 | Authors: , ,
4 |
5 | Last updated: 2022-03-03
6 |
7 | ## Overview
8 |
9 | Some web apps are designed to work in a single window (e.g. a music player) and
10 | aren't intended to be used across multiple instances. Web capabilities like
11 | [share target](https://w3c.github.io/web-share-target/),
12 | [file handlers](https://github.com/WICG/file-handling/blob/main/explainer.md)
13 | and [link capturing](https://github.com/WICG/pwa-url-handler/blob/main/handle_links/explainer.md)
14 | can cause new instances of the web app to be opened even if an app window is
15 | already open, putting the user in a non-ideal state.
16 |
17 | This document describes a new `launch_handler` manifest member that lets sites
18 | redirect app launches into existing app windows to prevent duplicate windows
19 | from being opened.
20 |
21 | We found that almost all "launch" use cases could be covered by a select few
22 | fixed rules (for example, "choose an existing window in the same app, focus it,
23 | and navigate it to the launched URL"). This `launch_handler` proposal enables
24 | sites to specify a set of fixed rules without having to implement custom
25 | [service worker `launch`][sw-launch-explainer] event logic, which should satisfy
26 | most use cases, and simplify the implementation in browsers and sites.
27 |
28 | ## Use case
29 |
30 | - Single-window web apps: a web app that prefers to only have a single instance
31 | of itself open at any time, with new navigations focusing the existing
32 | instance.\
33 | Sub-use cases include:
34 | - Apps that generally only make sense to have one instance running
35 | (e.g., a music player, a game).
36 | - Apps that include multi-document management within a single instance
37 | (e.g., an HTML-implemented tab strip, floating sub-windows like Gmail).
38 |
39 | ## Non-goals
40 |
41 | - Forcing a web app to only ever appear in a single client (e.g., blocking being
42 | opened in a browser tab while already opened in an app window).
43 | - Configuring whether link navigations into the scope of a web app launch the
44 | web app (this is out of scope and may be handled by a future version of the
45 | [Declarative Link Capturing][dlc-explainer] spec).
46 |
47 | ## Background
48 |
49 | - There are several ways for a web app window to be opened:
50 | - [File handling](https://github.com/WICG/file-handling/blob/main/explainer.md)
51 | - [In scope link capturing](https://github.com/WICG/pwa-url-handler/blob/main/handle_links/explainer.md)
52 | - [Note taking](https://wicg.github.io/manifest-incubations/index.html#note_taking-member)
53 | - [Protocol handling](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/URLProtocolHandler/explainer.md)
54 | - [Share target](https://w3c.github.io/web-share-target/)
55 | - [Shortcuts](https://www.w3.org/TR/appmanifest/#dfn-shortcuts)
56 | - Platform specific app launch surface
57 |
58 | Web apps launched via these triggers will open in a new or existing app window
59 | depending on the user agent platform. There is currently no mechanism for the
60 | web app to configure this behaviour.
61 |
62 | - This is a refactor of the [Declarative Link Capturing][dlc-explainer]
63 | explainer with a reduced behaviour scope. This does not prescribe link
64 | capturing as a means of launching a web app, instead it describes how the
65 | launch of a web app (via link capturing or any other means) may be configured
66 | by the manifest and service worker.
67 |
68 | ## Proposal
69 |
70 | ### `LaunchQueue` and `LaunchParams` interfaces
71 |
72 | Add two new interfaces; `LaunchParams` and `LaunchQueue`. Add a global instance
73 | of `LaunchQueue` to the `window` object named `launchQueue`. These serve as a
74 | JavaScript API to programmatically handle launches, in particular those that
75 | were redirected into an existing window without navigating the existing
76 | document.
77 |
78 | Whenever a web app is launched (via any launch trigger), a `LaunchParams` object
79 | will be enqueued in the `launchQueue` global `LaunchQueue` instance for the
80 | browsing context that handled the launch. Scripts can provide a single
81 | `LaunchConsumer` function to receive enqueued `LaunchParams`.
82 |
83 | This functions similarly to an event listener, but avoids the race condition
84 | where scripts may "miss" events if they're too slow to register their event
85 | listeners, this problem is particularly pronounced for launch events as they
86 | occur during the page's initialization. `LaunchParams` are buffered indefinitely
87 | until they are consumed. Crucially, if any `LaunchParams` are buffered into a
88 | `LaunchQueue` before a call to `setConsumer()`, they will be immediately passed
89 | into the consumer afterwards.
90 |
91 | ```idl
92 | [Exposed=Window] dictionary LaunchParams {
93 | readonly attribute DOMString targetURL;
94 | };
95 |
96 | callback LaunchConsumer = any (LaunchParams params);
97 |
98 | [Exposed=Window] interface LaunchQueue {
99 | void setConsumer(LaunchConsumer consumer);
100 | };
101 |
102 | partial interface Window {
103 | readonly attribute LaunchQueue launchQueue;
104 | };
105 | ```
106 |
107 | Example usage for a single-window music player app:
108 | ```js
109 | launchQueue.setConsumer(launchParams => {
110 | const songID = maybeExtractSongId(launchParams.targetURL);
111 | if (songID) {
112 | // No need to navigate the current page; can start playing the launched song
113 | // directly.
114 | playSong(songID);
115 | return;
116 | }
117 | location.href = launchParams.targetURL;
118 | });
119 | ```
120 |
121 | The `targetURL` member in `LaunchParams` will only be set if the launch did not
122 | create a new browsing context or navigate an existing one.
123 |
124 | Other web app APIs that involve launching may extend `LaunchParams` with
125 | additional members containing data specific to the method of launching, e.g., a
126 | [share target](https://w3c.github.io/web-share-target/) payload.
127 |
128 |
129 | ### `launch_handler` manifest member
130 |
131 | Add a `launch_handler` member to the web app manifest specifying the default
132 | client selection and navigation behaviour for web app launches.
133 | The shape of this member is as follows:
134 |
135 | ```
136 | "launch_handler": {
137 | "client_mode": "auto" | "navigate-new" | "navigate-existing" | "focus-existing"
138 | }
139 | ```
140 |
141 | If unspecified then `launch_handler` defaults to
142 | `{ "client_mode": "auto" }`.
143 |
144 | `client_mode`:
145 | - `auto`: The behaviour is up to the user agent to decide what works best for
146 | the platform. This reflects the status quo of user agent implementations prior
147 | to this proposal: mobile agents typically navigate an existing client, while
148 | desktop agents typically create a new one and avoid clobbering state.
149 | - `navigate-new`: A new browsing context is created in a web app window to load
150 | the launch's target URL.
151 | - `navigate-existing`: The most recently interacted with browsing context
152 | in a web app window is navigated to the launch's target URL.
153 | - `focus-existing`: The most recently interacted with browsing context
154 | in a web app window is chosen to handle the launch. A new `LaunchParams` with
155 | its `targetURL` set to the launch URL will be enqueued in the document's
156 | `window.launchQueue`.
157 |
158 | `client_mode` accepts a list of values, the first valid value will be used. This
159 | is to allow new values to be added to the spec without breaking backwards
160 | compatibility with old implementations by using them.\
161 | For example, if `"focus-matching-url"` were added, sites would specify
162 | `"client_mode": ["focus-matching-url", "focus-existing"]` to continue
163 | to control the behaviour of older browsers that didn't support
164 | `"matching-url-client"`.
165 |
166 | Example manifest that choses to receive all app launches as events in existing
167 | web app windows:
168 |
169 | ```json
170 | {
171 | "name": "Example app",
172 | "start_url": "/index.html",
173 | "launch_handler": {
174 | "client_mode": "focus-existing"
175 | }
176 | }
177 | ```
178 |
179 | ## Possible extensions to this proposal
180 |
181 | - Add `"service-worker"` as a `client_mode` option. This would invoke a
182 | [service worker `launch` event][sw-launch-explainer].
183 |
184 | This would allow web apps that open documents in their own app instances but
185 | want to bring an existing document instance into focus when the same document
186 | is re-launched instead of opening a duplicate app instance for it.
187 |
188 | - Add two members:
189 | ```
190 | "launch_handler": {
191 | "navigate_new_clients": "always" | "never",
192 | "new_client_url": ,
193 | }
194 | ```
195 | This allows web apps to intercept new client navigations and provide a
196 | constant alternative URL to handle the enqueued `LaunchParams` with
197 | `targetURL` set.
198 |
199 | - Add the `launch_handler` field to other launch methods to allow sites to
200 | customise the launch behaviour for specfic launch methods. For example:
201 | ```json
202 | {
203 | "name": "Example app",
204 | "description": "This app will navigate existing clients unless it was launched via the share target API.",
205 | "launch_handler": {
206 | "client_mode": "navigate-existing"
207 | },
208 | "share_target": {
209 | "action": "share.html",
210 | "params": {
211 | "title": "name",
212 | "text": "description",
213 | "url": "link"
214 | },
215 | "launch_handler": {
216 | "client_mode": "focus-existing"
217 | }
218 | }
219 | }
220 | ```
221 |
222 | - Add `attribute readonly LaunchConsumer? consumer` to `LaunchQueue`. This will
223 | allow sites to chain `LaunchConsumers` together more independently.
224 | ```js
225 | function addLaunchConsumer(launchConsumer) {
226 | const existingLaunchConsumer = launchQueue.consumer;
227 | launchQueue.setConsumer(launchParams => {
228 | existingLaunchConsumer?.(launchParams);
229 | launchConsumer(launchParams);
230 | });
231 | }
232 | ```
233 | Or maybe the `LaunchQueue` interface should be `addConsumer`/`removeConsumer`
234 | like `addEventListener`/`removeEventListener`, but with buffering.
235 |
236 | ## Related proposals
237 |
238 | ### [Service Worker launch event][sw-launch-explainer]
239 |
240 | This proposal is declarative alternative to the [service worker `launch`](
241 | sw-launch-explainer) proposal in WICG. It covers many of the same
242 | use cases, but omits the more advanced use cases (specifically, the option to
243 | choose which client to focus).
244 |
245 | Use of `launch_handler` in the manifest would provide a "default" launch
246 | behaviour that the service worker `launch` event handler can choose to override.
247 |
248 | ### [Declarative Link Capturing][dlc-explainer]
249 |
250 | This `launch_handler` proposal is intended to be a successor to the "launch"
251 | components of DLC and decouple launch configuration from "link capturing"
252 | behaviour.
253 |
254 | `launch_handler` generalises the concept of a launch into two core primitive
255 | actions; launch client selection and navigation, and explicitly decouples them
256 | from the "link capturing" launch trigger.
257 |
258 | ### [WICG: URL Handlers](https://github.com/WICG/pwa-url-handler/blob/master/explainer.md)
259 |
260 | Similarly to Declarative Link Capturing this `launch_handler` proposal refactors
261 | out the "launch" component from the URL Handler proposal. `launch_handler`
262 | behaviour is intended for being "invoked" by URL Handlers at the point in which
263 | a web app has been chosen to handle an out-of-browser link navigation.
264 |
265 | ### [File Handling](https://github.com/WICG/file-handling/blob/main/explainer.md)
266 |
267 | This proposal takes `LaunchQueue` and `LaunchParams` from the File Handling
268 | proposal with a few changes:
269 | - Instead of enqueuing `LaunchParams` for specific types of launches they will
270 | be enqueued for every type of web app launch.
271 | - An optional `targetURL` field is added.
272 | - The interface is explicitly intended to be extended by other launch related
273 | specs to contain launch specific data, e.g., file handles or share data.
274 |
275 | ### [Tabbed Application Mode](https://github.com/w3c/manifest/issues/737)
276 |
277 | This proposal is intended to work in tandem with tabbed mode web app windows.
278 | The behaviour of `"client_mode": "navigate-new"` with an already open tabbed window
279 | is to open a new tab in that window.
280 |
281 | [sw-launch-explainer]: sw_launch_event.md
282 | [dlc-explainer]: declarative_link_capturing.md
283 |
--------------------------------------------------------------------------------
/sw_launch_event.md:
--------------------------------------------------------------------------------
1 | # `launch` Event Explainer
2 |
3 | Author: Marijn Kruisselbrink <>
4 | Former Author: Matt Giuca <>
5 | Former Author: Eric Willigers <>
6 | Former Author: Jay Harris <>
7 | Former Author: Raymes Khoury <>
8 |
9 | Created: 2017-09-22
10 | Updated: 2024-08-28
11 |
12 | ## Introduction and Motivation
13 |
14 | The `launch_handler` manifest field gives web apps limited control over what should happen when a user launches their application. While the currently available options (`navigate-existing`, `navigate-new` and `focus-existing`) address many use cases, there are a few use cases where even more flexibility is desired.
15 |
16 | Both `navigate-existing` and `focus-existing` activate the most recently used tab/window of an application, while sometimes an application might want to use a different tab or window to handle the navigation. While this behavior can be
17 | partially emulated with `focus-existing` by having javascript focus or navigate an existing client, the fact that the "wrong" window gets activated first makes this a less than ideal user experience. In other words, the existing launch handler options handle single-window and arbitrary-many-window apps pretty well, they don't support multiple-but-limited windows.
18 |
19 | Examples of such use cases:
20 | * A document editor could allow a separate window for each document, but if the user clicks a link to a document that is already open in a window, focus that window instead of opening a duplicate.
21 | * A chat web app might sometimes be embedded in a different (same origin) app. Some links to specific chat rooms should activate the already open app rather than open a new chat specific window.
22 |
23 | In some cases, web apps may not want to open a new window at all, and may be content to show a notification. e.g.
24 | * A "`magnet:`" URL is handled by a torrent client, which automatically starts downloading the file, showing a notification but not opening a new window or tab.
25 | * A "save for later" tool that has a share target. When the share target is chosen, it just shows a notification "Saved for later", but doesn't actually spawn a browsing context.
26 |
27 | Example Service Worker code to handle all launches of a web app in an existing window if one exists:
28 |
29 | ```js
30 | self.addEventListener('launch', event => {
31 | event.waitUntil(async () => {
32 | const allClients = await clients.matchAll();
33 | // If there isn't one available, open a new window.
34 | if (allClients.length === 0) {
35 | clients.openWindow(event.request.url);
36 | return;
37 | }
38 |
39 | const client = allClients[0];
40 | client.focus();
41 | }());
42 | });
43 | ```
44 |
45 | ## Background
46 |
47 | The proposed API here is an extension to launch handlers described in https://github.com/WICG/web-app-launch/blob/main/launch_handler.md#background and https://wicg.github.io/web-app-launch/.
48 |
49 | This proposal handles the same ways that web apps can be launched as described in there, such as:
50 |
51 | 1. **Navigations:** A user clicks a link into a Social Media web app.
52 | 2. **OS Shortcuts:** A user opens an Image Editor web app using an OS shortcut (e.g. on their
53 | desktop). This shortcut was created when they installed the app.
54 | 3. **Protocol Handlers:** A user clicks on a `mailto:` protocol link which a website has registered to handle using the [`registerProtocolHandler`](https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-registerprotocolhandler) API or [`"protocol_handlers"` manifest member](https://wicg.github.io/manifest-incubations/index.html#protocol_handlers-member).
55 | 4. **Web Share Target:** A user shares an image with an Image Editor web app that has registered as
56 | a share target using the [Web Share Target API](https://wicg.github.io/web-share-target/).
57 | 5. **File Handlers:** A user opens a file that an Image Editor web app has registered to handle using the [`"file_handlers"` manifest member](https://wicg.github.io/manifest-incubations/index.html#file_handlers-member)).
58 |
59 | ## `launch_handler` manifest member
60 |
61 | This explainer proposes adding a new `"service-worker"` value to the `client_mode` field of the `launch_handler` manifest member. This would invoke a `"launch"` event on a service worker instead of handling a launch as normal. This allows the site to for example redirect the navigation into an existing window or trigger a notification.
62 |
63 | Since service worker scopes and web app manifest scopes don't necessarily match, the service worker used for this event would be the active worker of whatever registration has the URL being navigated to/launched in scope. If no such service worker can be found (or if the user agent otherwise decides this particular launch should not trigger a `"launch"` event) the next supported option from the provided list of `client_mode`s will be used instead.
64 |
65 | Like with existing `launch_handler` options, this only allows *certain* navigations to be intercepted. The user is still in control of the experience, so if they really want to, they can say "Open in new tab" and the app will not be allowed to prevent the page from opening. This is only used to prevent basic navigations, such as left-clicking a link.
66 |
67 | Further, not every navigation to a web app would trigger a `launch` event, only those that indicate it is being launched like an app. Typically, only events external to the app could trigger a `launch` event (e.g. navigations from a website outside of the app's scope into the app, opening a file, sharing a link to the app).
68 |
69 | ### Example
70 |
71 | Example Service Worker code to redirect navigations into an existing window:
72 |
73 | ```js
74 | self.addEventListener('launch', event => {
75 | event.waitUntil(async () => {
76 | const allClients = await clients.matchAll();
77 | // If there isn't one available, open a new window.
78 | if (allClients.length === 0) {
79 | const client = await clients.openWindow(event.params.targetURL);
80 | return;
81 | }
82 |
83 | const client = allClients[0];
84 | client.postMessage(event.params.targetURL);
85 | client.focus();
86 | }());
87 | });
88 | ```
89 | Notes:
90 | * `waitUntil` delays the user agent from launching and waits for the promise. This is necessary because inspecting existing client windows happens asynchronously.
91 | * The `launch` event is considered to be "allowed to show a popup", so that `Clients.openWindow` and `Client.focus` can be used.
92 | * If the launch handler does not:
93 | 1. Focus a client.
94 | 2. Open a new client.
95 | 3. Show a notification (Note: permission to show notifications is required).
96 |
97 | then the user agent should assume that the launch handler did not handle the launch, and should continue as if there were no `launch` event handler.
98 |
99 | ### Event Definition
100 |
101 | ```ts
102 | interface LaunchEvent : ExtendableEvent {
103 | readonly attribute LaunchParams params;
104 | }
105 | ```
106 |
107 |
108 | ## Design Questions/Details
109 |
110 | ### Should we expose the full `Request` instead of `LaunchParams` in the `LaunchEvent`
111 |
112 | For example for web share target, the data being shared could be part of the "POST" data of the request. By exposing a `Request` instead of (or in addition to) the `LaunchParams` this could be better handled by the service worker. However at the time launch handling runs, the request hasn't been created yet, so we'd have to create a synthetic request to be able to pass it in. Additionally if we do want to expose the POST data, we'd also want to expose that to `focus-existing` launch handlers, and thus would to add it to `LaunchParams` anyway.
113 |
114 | ### Restricting launch events to installed websites
115 |
116 | By only triggering `launch` events when the manifest specifies the `service-worker` `client_mode`, we limit this functionality to installed web apps.
117 |
118 | There are 2 reasons for this:
119 | 1. It is difficult to attribute bad behavior to misbehaving websites if they aren't installed (see the section below).
120 | 2. It could be confusing if the behavior of clicking a link changes just because a user has visited a site that registered itself as a `launch` event handler.
121 |
122 | Allowing launch events to be handled on the drive-by web could be explored in the future.
123 |
124 | ### Requiring a user gesture to trigger launch events
125 |
126 | Since a launch event can result in a new window being created or an existing window being focused, a user gesture should be required. In particular, a launch event should not be able to trigger another launch event without a subsequent user gesture.
127 |
128 | ### Addressing malicious or poorly written sites
129 |
130 | not-a-great-experience.com could register a `launch` handler that just does nothing. This would result in a poor user experience as the user could click links into the site, or share files with the site and nothing would happen.
131 |
132 | Similarly, slow-experience.com may unintentionally do a lot of processing in the `launch` event handler before it opens any UI surface. The user could open a file that would be handled by the app and not see anything for a long time. This would also be a poor user experience.
133 |
134 | User agents can give feedback to users when a site is handling a `launch` event to signify that the app is loading. User agents have a lot of flexibility to experiment here but some suggestions on what could be done if the app doesn't show some UI after a small delay (e.g. 1-2 seconds):
135 | - Show a splash screen indicating the app is launching
136 | - Show an entry for the app in the taskbar/dock/shelf indicating it's loading
137 | - Focus a previously opened window in the scope of the app
138 |
139 | If the app doesn't show UI after a long delay (e.g. 10 seconds), the user agent could:
140 | - Kill the `launch` event handler and show an error message indicating the app couldn't launch
141 | - If apps behave badly on a repetitive basis, don't allow it to handle `launch` events (fallback to opening URLs directly in their default context)
142 |
143 | ### Responding with a Client vs. calling Client.focus()
144 |
145 | `fetch` events provide a response via a `FetchEvent.respondWith` function. In a similar way, `launch` events could be designed to call a `LaunchEvent.launchWith` function with a `Client` which should be focused and/or navigated.
146 |
147 | The main benefit to this approach is that it would ensure that developers don't forget to focus a client window. The main issue with this is that it removes the flexibility for doing things besides focusing windows. For example, `launch` events may just want to show a notification. We could address this by adding other methods to `LaunchEvent` for triggering notifications and other behavior we would want to support, but that ends up duplication a large amount of API surface. It seems simpler to just stick with explicit `Client` manipulation.
148 |
149 | As an aside, the `notificationclick` event has similar challenges to the `launch` event in that handlers can be written such that nothing happens when a notification is clicked. Whatever solution is decided for `launch` event should also apply to `notificationclick` for consistency.
150 |
151 | ### Register Service Workers from the manifest?
152 |
153 | Since launch events would only work if a service worker has been registered by the application, it might be nice to be able to register a service worker with fields in the manifest as well. This explainer does not attempt to define this functionality, although this could be a good follow-up.
154 |
155 | ### Enqueue a launch in an existing client
156 |
157 | A service worker might want to emulate `focus-existing` launch handler behavior. This would require enqueueing an event into the launch queue of an existing client. This is not something that is currently possible, but we could consider a future extension of the `WindowClient` API that enabled this. For now a service worker would have to `postMessage` to the client, with corresponding code in the client to handle the message from the service worker.
158 |
159 | ## Security and privacy considerations
160 |
161 | * The user agent must only fire a `launch` event for navigations to URLs inside the service worker's scope, or a service worker could spy on other navigations.
162 |
163 | ## Appendix
164 |
165 | * [Service Worker GitHub issue](https://github.com/w3c/ServiceWorker/issues/1028)
166 | * [mgiuca proposal document](https://docs.google.com/document/d/1jWLpNEFttyLTnxsHs15oT-Hn8I81N0cwUa3JjISoPV8/edit)
167 | * [Earlier version of this explainer](https://github.com/WICG/web-app-launch/blob/ddc64da204af342ed2a908d96aa1401d025fbd70/sw_launch_event.md)
168 |
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "group": [80485]
3 | , "contacts": ["marcoscaceres"]
4 | , "repo-type": "cg-report"
5 | }
6 |
--------------------------------------------------------------------------------