> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Make your app installable (PWA)

> Detect standalone mode and show a dismissible install prompt — after enabling PWA in the app's Branding settings.

A Zite app installs to the home screen as a PWA. Your code adds the install UI and standalone-mode detection; the owner enables PWA under **Settings → Branding** (upload an icon, set an install title, publish) — until then the platform won't serve the manifest and the install prompt has no effect.

## Detect standalone (installed) mode

```tsx theme={null}
// apps/<app>/src/hooks/useIsPwaMode.ts
import { useEffect, useState } from 'react';

export function useIsPwaMode() {
  const [isPwaMode, setIsPwaMode] = useState(false);
  useEffect(() => {
    const standalone = window.matchMedia('(display-mode: standalone)');
    const fullscreen = window.matchMedia('(display-mode: fullscreen)');
    const update = () => {
      const iosStandalone =
        (window.navigator as Navigator & { standalone?: boolean }).standalone === true;
      setIsPwaMode(standalone.matches || fullscreen.matches || iosStandalone);
    };
    update();
    standalone.addEventListener('change', update);
    fullscreen.addEventListener('change', update);
    return () => {
      standalone.removeEventListener('change', update);
      fullscreen.removeEventListener('change', update);
    };
  }, []);
  return isPwaMode;
}
```

## A dismissible install prompt

Show it only when not already installed. Use `beforeinstallprompt` where available (Android / desktop Chrome), fall back to manual instructions on iOS Safari.

```tsx theme={null}
import { useEffect, useState } from 'react';
import { useIsPwaMode } from '../hooks/useIsPwaMode';
import { Button } from '@project/components/ui/button';

type BeforeInstallPromptEvent = Event & {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
};

const instructions = () => {
  const ua = navigator.userAgent.toLowerCase();
  if (/iphone|ipad|ipod/.test(ua)) return 'Tap Share, then Add to Home Screen.';
  if (/android/.test(ua)) return 'Tap Install when prompted, or use your browser menu.';
  return 'Use the install icon in your browser address bar.';
};

export function InstallAppPopover() {
  const isPwaMode = useIsPwaMode();
  const [dismissed, setDismissed] = useState(false);
  const [prompt, setPrompt] = useState<BeforeInstallPromptEvent | null>(null);

  useEffect(() => {
    const onPrompt = (e: Event) => { e.preventDefault(); setPrompt(e as BeforeInstallPromptEvent); };
    window.addEventListener('beforeinstallprompt', onPrompt);
    return () => window.removeEventListener('beforeinstallprompt', onPrompt);
  }, []);

  if (isPwaMode || dismissed) return null;

  const install = async () => {
    if (!prompt) return;
    await prompt.prompt();
    await prompt.userChoice;
    setPrompt(null);
    setDismissed(true);
  };

  return (
    <div className="fixed bottom-4 left-4 right-4 z-50 rounded-xl border bg-background p-4 shadow-lg sm:left-auto sm:w-80">
      <h2 className="font-semibold">Install this app</h2>
      <p className="mt-1 text-sm text-muted-foreground">{instructions()}</p>
      <div className="mt-3 flex gap-2">
        {prompt && <Button size="sm" onClick={install}>Install</Button>}
        <Button size="sm" variant="outline" onClick={() => setDismissed(true)}>Not now</Button>
      </div>
    </div>
  );
}
```

Mount `<InstallAppPopover />` near the app root, dismissible and out of the critical path.

## Gotchas

* **Hide install prompts when `useIsPwaMode()` is `true`** — the app is already installed.
* **Leave the manifest to the platform** — it serves `/manifest.webmanifest`, the Apple touch icon, and meta tags. Don't add your own `<link rel="manifest">` or JSON; it conflicts.
