| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- /** @typedef {Event & { prompt: () => Promise<void>, userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }> }} BeforeInstallPromptEvent */
- const PROMPT_KEY = '__recipesPwaDeferredPrompt';
- const LISTENER_KEY = '__recipesPwaInstallListening';
- let ready = $state(false);
- let installed = $state(false);
- let alreadyOnDevice = $state(false);
- function getDeferredPrompt() {
- if (typeof window === 'undefined') return null;
- return /** @type {BeforeInstallPromptEvent | null} */ (window[PROMPT_KEY] ?? null);
- }
- function setDeferredPrompt(/** @type {BeforeInstallPromptEvent | null} */ event) {
- if (typeof window === 'undefined') return;
- window[PROMPT_KEY] = event;
- ready = event !== null;
- }
- function isStandaloneDisplay() {
- if (typeof window === 'undefined') return false;
- return (
- window.matchMedia('(display-mode: standalone)').matches ||
- window.matchMedia('(display-mode: window-controls-overlay)').matches ||
- /** @type {Navigator & { standalone?: boolean }} */ (window.navigator).standalone === true
- );
- }
- function isIosDevice() {
- if (typeof navigator === 'undefined') return false;
- const ua = navigator.userAgent;
- return /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
- }
- async function isBrave() {
- if (typeof navigator === 'undefined') return false;
- const brave = /** @type {Navigator & { brave?: { isBrave?: () => Promise<boolean> } }} */ (navigator).brave;
- if (brave?.isBrave) {
- try {
- return await brave.isBrave();
- } catch {
- /* fall through */
- }
- }
- return /Brave/i.test(navigator.userAgent);
- }
- /**
- * Detect an existing install of this origin's PWA (desktop/Android Chromium).
- */
- async function detectRelatedInstall() {
- if (typeof navigator === 'undefined' || !('getInstalledRelatedApps' in navigator)) {
- return false;
- }
- try {
- const apps = await /** @type {{ getInstalledRelatedApps: () => Promise<unknown[]> }} */ (
- navigator
- ).getInstalledRelatedApps();
- return Array.isArray(apps) && apps.length > 0;
- } catch {
- return false;
- }
- }
- async function refreshInstallFlags() {
- installed = isStandaloneDisplay();
- alreadyOnDevice = installed || (await detectRelatedInstall());
- if (alreadyOnDevice) {
- setDeferredPrompt(null);
- } else if (getDeferredPrompt()) {
- ready = true;
- }
- }
- /**
- * Start listening for install eligibility. Safe to call multiple times.
- * Prompt event is stored on `window` so Vite HMR does not drop it.
- */
- export function initPwaInstall() {
- if (typeof window === 'undefined') return;
- // Rehydrate after HMR / remount.
- if (getDeferredPrompt()) ready = true;
- void refreshInstallFlags();
- if (window[LISTENER_KEY]) return;
- window[LISTENER_KEY] = true;
- window.addEventListener('beforeinstallprompt', (event) => {
- event.preventDefault();
- setDeferredPrompt(/** @type {BeforeInstallPromptEvent} */ (event));
- installed = false;
- alreadyOnDevice = false;
- });
- window.addEventListener('appinstalled', () => {
- setDeferredPrompt(null);
- installed = true;
- alreadyOnDevice = true;
- });
- const media = window.matchMedia('(display-mode: standalone)');
- media.addEventListener?.('change', () => {
- void refreshInstallFlags();
- });
- }
- export function isInstallVisible() {
- // Hide only while running inside the installed app window.
- return !installed;
- }
- export function isInstallReady() {
- return getDeferredPrompt() !== null && !installed;
- }
- export function isAlreadyInstalledOnDevice() {
- return alreadyOnDevice && !installed;
- }
- /**
- * Triggers the native browser install prompt. Must be called from a user gesture.
- */
- export async function promptInstall() {
- await refreshInstallFlags();
- if (alreadyOnDevice && !getDeferredPrompt()) {
- return { ok: false, reason: /** @type {const} */ ('already-installed') };
- }
- const event = getDeferredPrompt();
- if (!event) {
- const brave = await isBrave();
- const ios = isIosDevice();
- return {
- ok: false,
- reason: /** @type {const} */ ('unavailable'),
- brave,
- ios
- };
- }
- try {
- await event.prompt();
- const choice = await event.userChoice;
- setDeferredPrompt(null);
- if (choice.outcome === 'accepted') {
- installed = true;
- alreadyOnDevice = true;
- return { ok: true, reason: /** @type {const} */ ('accepted') };
- }
- return { ok: false, reason: /** @type {const} */ ('dismissed') };
- } catch {
- const brave = await isBrave();
- const ios = isIosDevice();
- return {
- ok: false,
- reason: /** @type {const} */ ('unavailable'),
- brave,
- ios
- };
- }
- }
- if (typeof window !== 'undefined') {
- initPwaInstall();
- }
|