1import type { VNode } from "preact";2import { useEffect, useState } from "preact/hooks";3import {4 type PwaController,5 pwaController,6 pwaFailureMessage,7 type PwaState,8} from "../runtime/pwa.ts";910export { pwaFailureMessage } from "../runtime/pwa.ts";11export type { PwaController, PwaFailureCode, PwaState, PwaUpdateState } from "../runtime/pwa.ts";1213/**14 * Subscribes a Preact component to an isolated or shared PWA controller.15 *16 * @example17 * ```tsx18 * import { usePwaState } from "@nzip/lofi/preact";19 *20 * const pwa = usePwaState();21 * const updateReady = pwa.update === "ready";22 * ```23 *24 * @param controller The controller to observe; defaults to the package-wide PWA controller.25 * @returns The current install/update state, kept live via subscription.26 */27export function usePwaState(controller: PwaController = pwaController): PwaState {28 const [state, setState] = useState<PwaState>(controller.getState());29 useEffect(() => controller.subscribe(setState), [controller]);30 return state;31}3233/** Optional controller and heading text for {@link PwaActions}. */34export interface PwaActionsProps {35 /** Controller to observe; defaults to the package-wide PWA controller. */36 readonly controller?: PwaController;37 /** Heading rendered above install or update actions. */38 readonly title?: string;39}4041/**42 * A composable install/update surface that keeps browser event handling package-owned.43 *44 * @example45 * ```tsx46 * import { PwaActions } from "@nzip/lofi/preact";47 *48 * <PwaActions title="Install this app" />;49 * ```50 *51 * @param props An optional controller override and heading text.52 * @returns The install/update section, or `null` when no action or status is relevant.53 */54export function PwaActions({55 controller = pwaController,56 title = "Install & updates",57}: PwaActionsProps): VNode | null {58 const state = usePwaState(controller);59 const visible = state.install === "available" || state.install === "manual-ios" ||60 state.install === "manual-browser" || state.install === "unsupported" ||61 state.install === "accepted" || state.install === "dismissed" ||62 state.update !== "idle" || Boolean(state.failure);63 if (!visible) return null;6465 return (66 <section class="pwa-actions" aria-labelledby="pwa-actions-title">67 <h3 id="pwa-actions-title">{title}</h3>68 {state.install === "available" && (69 <button70 type="button"71 onClick={() => void controller.requestInstall()}72 >73 Install app74 </button>75 )}76 {state.install === "accepted" && (77 <p role="status">Installation accepted. Follow the browser prompt to finish.</p>78 )}79 {state.install === "dismissed" && (80 <p role="status">Installation dismissed. You can use the browser install menu later.</p>81 )}82 {state.install === "manual-ios" && (83 <div class="pwa-ios-guidance">84 <p>Install this app on your iPhone or iPad:</p>85 <ol>86 <li>Open the Share menu.</li>87 <li>Choose Add to Home Screen.</li>88 <li>Confirm Add.</li>89 </ol>90 </div>91 )}92 {state.install === "manual-browser" && (93 <div class="pwa-browser-guidance">94 <p>No in-page install button is available right now.</p>95 <p>Look in its menu for Install app or Add to Home Screen, if offered.</p>96 </div>97 )}98 {state.install === "unsupported" && (99 <p>Installation is not supported in this browser or browsing context.</p>100 )}101 {state.update === "checking" && <p role="status">Checking for updates…</p>}102 {state.update === "installing" && <p role="status">Installing update…</p>}103 {state.update === "ready" && (104 <button105 type="button"106 onClick={() => controller.applyUpdate()}107 >108 Update app109 </button>110 )}111 {state.update === "applying" && <p role="status">Applying update…</p>}112 {state.failure && (113 <p class="pwa-error" role="alert">{pwaFailureMessage(state.failure.code)}</p>114 )}115 </section>116 );117}