Files
cinny/src/app/utils/pwaInstall.ts
T
jaredandClaude Opus 5 bd1e61e8cd
CI / Build & Quality Checks (push) Successful in 1m59s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 5s
CI / Playwright smoke (e2e) (push) Successful in 1m59s
feat(pwa): offer to install — Chromium prompt, iOS "Add to Home Screen" hint (#116)
The client never handled beforeinstallprompt and showed no install hint, so
phone users only got the PWA if they knew to dig through Share → Add to
Home Screen (iOS never prompts; Chromium's mini-infobar is easy to miss).

- utils/pwaInstall.ts (pure, 4 tests): show from the second visit, never in
  Tauri or an installed PWA (display-mode standalone / navigator.standalone),
  30-day snooze after a dismissal; kind = real prompt when the browser
  handed us a deferred beforeinstallprompt, Share-sheet instructions on iOS
  Safari, nothing elsewhere (Firefox desktop has no install path).
- hooks/usePwaInstallPrompt.ts: captures beforeinstallprompt/appinstalled,
  counts one visit per browser session, waits 6s for the prompt event before
  deciding, then enqueues a sticky toast; tap → prompt(), X → snooze.
- ToastNotif gains onDismiss (fired by the X button only) so the snooze is
  recorded however the toast is closed. Mounted from ClientNonUIFeatures for
  signed-in users only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-17 13:31:29 -04:00

55 lines
1.8 KiB
TypeScript

/**
* [Gitea #116] PWA install hint — pure decision logic.
*
* Chromium fires `beforeinstallprompt`; iOS Safari never prompts and only
* installs via Share → "Add to Home Screen", so people on phones simply never
* find it. The hint is shown once the user has come back at least once (second
* visit), never inside the desktop app or an already-installed PWA, and snoozes
* for 30 days after a dismissal.
*/
export const INSTALL_HINT_MIN_VISITS = 2;
export const INSTALL_HINT_SNOOZE_MS = 30 * 24 * 60 * 60 * 1000;
export type InstallHintInput = {
/** Running inside the Tauri desktop app. */
isTauri: boolean;
/** `display-mode: standalone` or iOS `navigator.standalone` — already installed. */
isStandalone: boolean;
/** Distinct app visits recorded so far, including this one. */
visits: number;
/** Epoch ms of the last dismissal, or null. */
dismissedAt: number | null;
now: number;
};
export const shouldShowInstallHint = ({
isTauri,
isStandalone,
visits,
dismissedAt,
now,
}: InstallHintInput): boolean => {
if (isTauri || isStandalone) return false;
if (visits < INSTALL_HINT_MIN_VISITS) return false;
if (dismissedAt !== null && now - dismissedAt < INSTALL_HINT_SNOOZE_MS) return false;
return true;
};
export type InstallHintKind = 'prompt' | 'ios-share' | 'none';
/**
* Which hint to show: a real install prompt when the browser handed us a
* deferred `beforeinstallprompt`, the manual Share-sheet instructions on iOS
* Safari, or nothing (Firefox desktop and friends have no install path — a
* hint there would just be noise).
*/
export const installHintKind = (
hasDeferredPrompt: boolean,
isIosSafari: boolean,
): InstallHintKind => {
if (hasDeferredPrompt) return 'prompt';
if (isIosSafari) return 'ios-share';
return 'none';
};