diff --git a/apps/antalmanac/src/components/Header/Header.tsx b/apps/antalmanac/src/components/Header/Header.tsx index 279d26b0c..2d4c89cc5 100644 --- a/apps/antalmanac/src/components/Header/Header.tsx +++ b/apps/antalmanac/src/components/Header/Header.tsx @@ -3,6 +3,7 @@ import { withStyles } from '@material-ui/core/styles'; import { ClassNameMap } from '@material-ui/core/styles/withStyles'; import Import from './Import'; +import InstallPWAButton from './InstallPWAButton'; import LoadSaveScheduleFunctionality from './LoadSaveFunctionality'; import { Logo } from './Logo'; import AppDrawer from './SettingsMenu'; @@ -43,6 +44,7 @@ const Header = ({ classes }: CustomAppBarProps) => {
+
diff --git a/apps/antalmanac/src/components/Header/InstallPWAButton.tsx b/apps/antalmanac/src/components/Header/InstallPWAButton.tsx new file mode 100644 index 000000000..597b019e3 --- /dev/null +++ b/apps/antalmanac/src/components/Header/InstallPWAButton.tsx @@ -0,0 +1,69 @@ +import { Tooltip, IconButton, Button } from '@material-ui/core'; +import BrowserUpdatedIcon from '@mui/icons-material/BrowserUpdated'; +import { useMediaQuery, useTheme } from '@mui/material'; +import { useEffect } from 'react'; +import { shallow } from 'zustand/shallow'; + +import { BeforeInstallPromptEvent, usePWAStore } from '$stores/PWAStore'; + +function InstallPWAButton() { + const [setInstallPrompt, setCanInstall, canInstall, installPrompt] = usePWAStore( + (state) => [state.setInstallPrompt, state.setCanInstall, state.canInstall, state.installPrompt], + shallow + ); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + useEffect(() => { + const beforeInstallHandler = (e: BeforeInstallPromptEvent) => { + e.preventDefault(); + setCanInstall(true); + setInstallPrompt(e); + }; + + const disableInstallHandler = () => { + setCanInstall(false); + window.removeEventListener('beforeinstallprompt', beforeInstallHandler); + }; + + window.addEventListener('beforeinstallprompt', beforeInstallHandler); + window.addEventListener('appinstalled', disableInstallHandler); + + return () => { + window.removeEventListener('beforeinstallprompt', beforeInstallHandler); + window.removeEventListener('appinstalled', disableInstallHandler); + }; + }, []); + + const handleInstall = (e: React.MouseEvent) => { + if (installPrompt) { + e.preventDefault(); + if (!installPrompt) return; + installPrompt.prompt(); + } + }; + + if (!canInstall) return null; + + return ( + + {isMobile ? ( + + ) : ( + + )} + + ); +} + +export default InstallPWAButton; diff --git a/apps/antalmanac/src/stores/PWAStore.ts b/apps/antalmanac/src/stores/PWAStore.ts new file mode 100644 index 000000000..8885728c4 --- /dev/null +++ b/apps/antalmanac/src/stores/PWAStore.ts @@ -0,0 +1,43 @@ +import { create } from 'zustand'; + +// Adapted from https://stackoverflow.com/questions/51503754/typescript-type-beforeinstallpromptevent +type UserChoice = Promise<{ + outcome: 'accepted' | 'dismissed'; + platform: string; +}>; + +export interface BeforeInstallPromptEvent extends Event { + readonly platforms: string[]; + readonly userChoice: UserChoice; + prompt(): Promise; +} + +declare global { + interface WindowEventMap { + beforeinstallprompt: BeforeInstallPromptEvent; + } +} + +export interface PWAStore { + canInstall: boolean; + installPrompt: BeforeInstallPromptEvent | null; + setInstallPrompt: (e: BeforeInstallPromptEvent) => void; + setCanInstall: (canInstall: boolean) => void; +} + +export const usePWAStore = create((set) => { + return { + canInstall: false, + installPrompt: null, + setInstallPrompt: (e: BeforeInstallPromptEvent) => { + set({ + installPrompt: e, + }); + }, + setCanInstall: (canInstall: boolean) => { + set({ + canInstall, + }); + }, + }; +});