clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / docs / recipes / launch-handler.md
launch-handler.md 4.2K rendered
1# Launch handling and client reuse23Use this recipe when an installed single-window app should focus its existing client and4deliberately route a new launch. The5[`launch_handler` manifest member](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/launch_handler)6and `Window.launchQueue` have limited, experimental browser support. Unsupported browsers retain7their ordinary new-window or existing-window behavior.89Do not use client reuse as an authentication boundary. A target URL is external input even when the10browser obtained it from an installed-app launch.1112## Choose a client mode1314Add this opt-in member to `public/manifest.webmanifest`:1516```json17{18 "launch_handler": {19 "client_mode": ["focus-existing", "auto"]20 }21}22```2324The ordered array gives supporting browsers a fallback:2526- `focus-existing` focuses an existing app window without navigating it. The app handles the target27 through `launchQueue`; if no client exists, the browser opens one.28- `navigate-existing` focuses and navigates an existing client. Use it only when replacing that29 client's current view is expected.30- `navigate-new` opens another client for every launch.31- `auto` leaves the choice to the browser and is the portable final fallback.3233Lofi validates the optional member and rejects unknown or repeated modes. The starter omits it.3435## Register the consumer early3637Create an island mounted from the root layout so every installed route can receive a queued launch:3839```tsx40import { useEffect, useState } from "preact/hooks";41import {42 type InstalledAppLaunchIssue,43 installInstalledAppLaunchConsumer,44} from "@nzip/lofi/recipes/launch-handler";4546export default function LaunchRouter() {47 const [supported, setSupported] = useState<boolean>();48 const [issue, setIssue] = useState<InstalledAppLaunchIssue>();4950 useEffect(() => {51 const consumer = installInstalledAppLaunchConsumer({52 scope: new URL(import.meta.env.BASE_URL, location.origin),53 onLaunch(target) {54 // target.url is proven same-origin and inside the configured path scope.55 const next = new URL(target.url);56 history.replaceState(null, "", `${next.pathname}${next.search}${next.hash}`);57 dispatchEvent(new PopStateEvent("popstate"));58 },59 onRejected: setIssue,60 });61 setSupported(consumer.supported);62 return consumer.dispose;63 }, []);6465 if (supported === false) return null; // ordinary browser launch is the fallback66 if (issue) return <p role="alert">The requested app destination is not valid.</p>;67 return null;68}69```7071The browser retains launches until a consumer is registered, but mounting this island from only one72leaf route would miss launches handled elsewhere in the app. The helper allows one active consumer73per queue. A stale development/HMR cleanup cannot overwrite a newer generation, and disposing the74current owner leaves a no-op consumer.7576The helper rejects missing or malformed targets, credential-bearing URLs, other origins, and paths77outside the exact configured scope. Rejection reasons contain no received URL. Product routing must78still decide whether the in-scope view exists and whether the current user may see it.7980## Install, update, and fallback8182Manifest launch behavior may not update immediately on every platform. Test both a fresh install and83an existing installation after changing `client_mode`; some platforms require reinstalling before84the OS launch behavior changes. Removing `launch_handler` restores browser-selected launch behavior85without changing lofi data.8687Do not force focus or close duplicate windows in unsupported browsers. Ordinary links, shortcuts,88and direct navigations remain the complete fallback.8990## Verification checklist91921. Launch with a valid same-origin URL inside the deployment base and route the existing client.932. Reject another origin, an out-of-scope path, a credential-bearing URL, and malformed input with a94 fixed message that does not echo the target.953. Confirm a browser without `launchQueue` follows its ordinary launch behavior without an error.964. Register a new development generation, dispose the older one, and verify the new consumer remains97 active.985. Test fresh install, existing install, update, offline launch, and removal of the manifest member.