├── 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 |
15 |

URL:

16 |

Target: 17 | 18 | Self 19 | 20 | Blank 21 | 22 | Non-launch

23 |

24 |
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 |
53 |
54 |
55 |

56 | Prerequisites 57 |

58 |

59 | In order to implement this API, the user agent MUST support 60 | [[[appmanifest]]]. 61 |

62 |
63 |
64 |

65 | Usage Example 66 |

67 |

68 | A music player app wants to direct app shortcut launches to an existing 69 | window without interrupting music playback. This music app would add a 70 | [=manifest/launch_handler=] entry to the [[[appmanifest]]], as shown: 71 |

72 |
 73 |       {
 74 |         "name": "Music Player",
 75 |         "shortcuts": [{
 76 |           "name": "Now Playing",
 77 |           "url": "/"
 78 |         }, {
 79 |           "name": "Library",
 80 |           "url": "/library"
 81 |         }, {
 82 |           "name": "Favorites",
 83 |           "url": "/favorites"
 84 |         }, {
 85 |           "name": "Discover",
 86 |           "url": "/discover"
 87 |         }],
 88 |         "launch_handler": {
 89 |           "client_mode": "focus-existing"
 90 |         }
 91 |       }
 92 |       
93 |

94 | The [=manifest/client_mode=] parameter set to 95 | [=client mode/focus-existing=] causes app launches to bring 96 | existing app instances (if any) into focus without navigating them away 97 | from their current document. 98 |

99 |

100 | A {{LaunchParams}} will be enqueued on the {{Window/launchQueue}} where 101 | the music player can read the {{LaunchParams/targetURL}} in its 102 | {{LaunchConsumer}} and handle it in script e.g.: 103 |

104 |
105 |         window.launchQueue.setConsumer((launchParams) => {
106 |           const url = launchParams.targetURL;
107 |           // If the URL is to one of the app sections, updates the app view to
108 |           // that section without interrupting currently playing music.
109 |           if (maybeFocusAppSection(url)) {
110 |             return;
111 |           }
112 |           // Could not handle the launch in-place, just navigate the page
113 |           // (interrupts any playing music).
114 |           location.href = url;
115 |         });
116 |       
117 |

118 | A user, already using the music player app to listen to music, 119 | activating the "Library" app shortcut will trigger an app launch to 120 | /library which gets routed to the existing app instance, enqueued in the 121 | page's {{Window/launchQueue}} which, through the assigned 122 | {{LaunchConsumer}}, brings the library section of the music player into 123 | focus without affecting the current music playback. 124 |

125 |
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 |
  1. Run the steps for [=processing the launch_handler member=] 138 | given [=ordered map=] |json:ordered map| and [=ordered map=] 139 | |manifest:ordered map|. 140 |
  2. 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 |
  1. If |json|["launch_handler"] does not [=map/exist=], return. 164 |
  2. 165 |
  3. If the type of |json|["launch_handler"] is not [=ordered map=], 166 | return. 167 |
  4. 168 |
  5. Set |manifest|["launch_handler"] to a new [=ordered map=]. 169 |
  6. 170 |
  7. [=Process the `client_mode` member=] passing 171 | |json|["launch_handler"], |manifest|["launch_handler"]. 172 |
  8. 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 |
  1. If |json_launch_handler|["client_mode"] does not [=map/exist=], 248 | return. 249 |
  2. 250 |
  3. If the type of |json_launch_handler|["client_mode"] is 251 | [=list=]: 252 |
      253 |
    1. [=list/For each=] |entry| of 254 | |json_launch_handler|["client_mode"]: 255 |
        256 |
      1. If the type of |entry| is not [=string=], continue. 257 |
      2. 258 |
      3. If |entry| is supported by the user agent, set 259 | |manifest_launch_handler|["client_mode"] to |entry| and 260 | return. 261 |
      4. 262 |
      263 |
    2. 264 |
    265 |
  4. 266 |
  5. 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 |
  6. 270 |
  7. Set |manifest_launch_handler|["client_mode"] to [=client mode/auto=]. 271 |
  8. 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 |
  1. If |params| is not given, set |params| to 305 | |manifest|.[=manifest/start_url=]. 306 |
  2. 307 |
  3. If |params| is a [=URL=], set |params| to a new {{LaunchParams}} 308 | with {{LaunchParams/targetURL}} set to |params|. 309 |
  4. 310 |
  5. Assert: |params|.{{LaunchParams/targetURL}} is [=manifest/within 311 | scope=] of |manifest|. 312 |
  6. 313 |
  7. Set |application context| to the result of running the steps to 314 | [=prepare an application context=] passing |manifest|, |params| 315 | and |POST resource|. 316 |
  8. 317 |
  9. Append |params| to the [=unconsumed launch params=] of the 318 | |application context|'s document's {{Window/launchQueue}}. 319 |
  10. 320 |
  11. 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 |
  12. 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 |
  1. Let [=client mode target=] |client_mode| be 339 | |manifest|.[=manifest/launch_handler=].[=manifest/client_mode=]. 340 |
  2. 341 |
  3. 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 |
  4. 346 |
  5. 347 |

    Switching on |client mode|, run the following substeps:

    348 |
    349 |
    [=client mode/navigate-new=] 350 |
    351 |
      352 |
    1. 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 |
    2. 356 |
    357 |
    [=client mode/navigate-existing=] or 358 | [=client mode/focus-existing=] 359 |
    360 |
      361 |
    1. If there is no [=application context=] that has |manifest| 362 | [=applied=]: 363 |
        364 |
      1. 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 |
      2. 370 |
      371 |
    2. 372 |
    3. 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 |
    4. 380 |
    5. 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 |
      1. Bring |application context|'s viewport into focus. 389 |
      2. 390 |
      3. Return |application context|. 391 |
      4. 392 |
      393 |
    6. 394 |
    7. 395 | [=Navigate=] |application context| to |launch 396 | params|.{{LaunchParams/targetURL}} passing |POST resource|. 397 |
    8. 398 |
    9. Return |application context|. 399 |
    10. 400 |
    401 |
    402 |
  6. 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 |
  1. If the [=assigned launch consumer=] |consumer| is set on 415 | |queue|: 416 |
      417 |
    1. [=list/For each=] |launch_params:LaunchParams| of 418 | the |queue|'s [=unconsumed launch params=]: 419 |
        420 |
      1. Invoke |consumer| with |launch_params|. 421 |
      2. 422 |
      423 |
    2. 424 |
    3. Set |queue|'s [=unconsumed launch params=] to the empty 425 | list. 426 |
    4. 427 |
    428 |
  2. 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 |
  1. Set the [=assigned launch consumer=] to |consumer|. 512 |
  2. 513 |
  3. Run the steps to [=process unconsumed launch params=]. 514 |
  4. 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 |
527 |

528 | Accessibility 529 |

530 |

531 | This specification has no known accessibility considerations. 532 |

533 |
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 | --------------------------------------------------------------------------------