Added onboarding + a lightweight install banner to both the mobile login screen and the settings screen, with Android/Chromium
install prompt support and iOS “Share → Add to Home Screen” guidance. Also added a small helper + tests to decide when/which banner variant should show, and shared copy in common.json.
This commit is contained in:
91
resources/js/admin/mobile/hooks/useInstallPrompt.ts
Normal file
91
resources/js/admin/mobile/hooks/useInstallPrompt.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BeforeInstallPromptEvent,
|
||||
getStandaloneStatus,
|
||||
isIosDevice,
|
||||
type InstallOutcome,
|
||||
} from '../lib/installPrompt';
|
||||
|
||||
type InstallPromptState = {
|
||||
canInstall: boolean;
|
||||
isInstalled: boolean;
|
||||
isStandalone: boolean;
|
||||
isIos: boolean;
|
||||
outcome: InstallOutcome | null;
|
||||
promptInstall: () => Promise<InstallOutcome | null>;
|
||||
};
|
||||
|
||||
export function useInstallPrompt(): InstallPromptState {
|
||||
const [deferredPrompt, setDeferredPrompt] = React.useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [isInstalled, setIsInstalled] = React.useState(false);
|
||||
const [isStandalone, setIsStandalone] = React.useState(false);
|
||||
const [isIos, setIsIos] = React.useState(false);
|
||||
const [outcome, setOutcome] = React.useState<InstallOutcome | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateStandalone = () => {
|
||||
const standalone = getStandaloneStatus();
|
||||
setIsStandalone(standalone);
|
||||
if (standalone) {
|
||||
setIsInstalled(true);
|
||||
}
|
||||
};
|
||||
|
||||
setIsIos(isIosDevice(navigator.userAgent ?? ''));
|
||||
updateStandalone();
|
||||
|
||||
const handleBeforeInstallPrompt = (event: Event) => {
|
||||
event.preventDefault();
|
||||
setDeferredPrompt(event as BeforeInstallPromptEvent);
|
||||
};
|
||||
|
||||
const handleAppInstalled = () => {
|
||||
setIsInstalled(true);
|
||||
setDeferredPrompt(null);
|
||||
setOutcome('accepted');
|
||||
};
|
||||
|
||||
const mediaQuery = window.matchMedia?.('(display-mode: standalone)');
|
||||
const handleDisplayModeChange = () => updateStandalone();
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.addEventListener('appinstalled', handleAppInstalled);
|
||||
mediaQuery?.addEventListener?.('change', handleDisplayModeChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||||
mediaQuery?.removeEventListener?.('change', handleDisplayModeChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const promptInstall = React.useCallback(async () => {
|
||||
if (!deferredPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await deferredPrompt.prompt();
|
||||
const choice = await deferredPrompt.userChoice;
|
||||
setOutcome(choice.outcome ?? 'unknown');
|
||||
|
||||
if (choice.outcome === 'accepted') {
|
||||
setIsInstalled(true);
|
||||
setDeferredPrompt(null);
|
||||
}
|
||||
|
||||
return choice.outcome ?? 'unknown';
|
||||
}, [deferredPrompt]);
|
||||
|
||||
return {
|
||||
canInstall: Boolean(deferredPrompt),
|
||||
isInstalled,
|
||||
isStandalone,
|
||||
isIos,
|
||||
outcome,
|
||||
promptInstall,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user