diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 60f47fc..a760f46 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -8,6 +8,8 @@ import { hasLocale, NextIntlClientProvider } from 'next-intl' import { notFound } from 'next/navigation' import { routing } from '@/i18n/routing' import MatomoTracking from '@/components/Matomo/MatomoTracking' +import PwaContentProvider from '@/components/PwaContentProvider' +import PwaInstallationDialog from '@/components/PwaInstallationDialog' const inter = Inter({ weight: ['300', '400', '600', '700'], @@ -48,17 +50,20 @@ export default async function LocaleLayout({ - - - {children} - + + + + + {children} + + diff --git a/src/components/PwaContentProvider.tsx b/src/components/PwaContentProvider.tsx new file mode 100644 index 0000000..e742a35 --- /dev/null +++ b/src/components/PwaContentProvider.tsx @@ -0,0 +1,101 @@ +'use client' + +import usePwaInfo from '@/components/hooks/usePwaInfo' +import { CustomDimensions } from '@/components/Matomo/constants' +import useMatomo from '@/components/Matomo/useMatomo' +import { + createContext, + MouseEventHandler, + ReactNode, + useCallback, + useEffect, + useState, +} from 'react' + +interface PwaContextType { + handleInstallClick?: MouseEventHandler + showPwaInstallOptions: boolean + showSafariInstructions: boolean + prompt?: Event +} + +export const PwaContext = createContext({ + showPwaInstallOptions: false, + showSafariInstructions: false, +}) + +/** + * this provider contains PWA information needed for components + */ +export const PwaContentProvider = ({ children }: { children: ReactNode }) => { + const [showPwaInstallOptions, setShowPwaInstallOptions] = + useState(false) + const [showSafariInstructions, setShowSafariInstructions] = + useState(false) + const [prompt, setPrompt] = useState(undefined) + + const { isPwa, hasChrome, hasSafari } = usePwaInfo() + + const { trackEvent, setCustomDimension } = useMatomo() + + useEffect(() => { + // set PWA state custom dimension + setCustomDimension(CustomDimensions.IS_PWA, isPwa ? 1 : 0) + + // this is needed because chrome on mac contains both strings + if (!isPwa && !hasChrome && hasSafari) { + // safari + setShowSafariInstructions(true) + } + + const handleBeforeInstallPrompt = (event: Event) => { + event.preventDefault() + + if (!isPwa && hasChrome) { + // chrome + setShowPwaInstallOptions(true) + // is needed in order to open the installation prompt + setPrompt(event) + } + } + + const handleAfterInstallPrompt = () => { + trackEvent('PwaInstallation', 'PWA installed') + setShowPwaInstallOptions(false) + } + + window?.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt) + window?.addEventListener('appinstalled', handleAfterInstallPrompt) + + return () => { + window?.removeEventListener( + 'beforeinstallprompt', + handleBeforeInstallPrompt + ) + window?.removeEventListener('appinstalled', handleAfterInstallPrompt) + } + }, [hasChrome, hasSafari, isPwa, setCustomDimension, trackEvent]) + + const handleInstallClick = useCallback(() => { + if (prompt) { + trackEvent('PwaInstallation', 'click', 'installButton') + // @ts-expect-error actually we expect here the BeforeInstallPromptEvent but it is not supported by at least firefox + prompt?.prompt() + } + }, [prompt, trackEvent]) + + return ( + + {children} + + ) +} + +export default PwaContentProvider diff --git a/src/components/PwaInstallation.tsx b/src/components/PwaInstallation.tsx index c866386..d8c6af1 100644 --- a/src/components/PwaInstallation.tsx +++ b/src/components/PwaInstallation.tsx @@ -1,87 +1,20 @@ 'use client' -import { CustomDimensions } from '@/components/Matomo/constants' -import useMatomo from '@/components/Matomo/useMatomo' -import { AddBoxOutlined, InstallMobile, IosShare } from '@mui/icons-material' -import { - Alert, - Button, - Grid, - List, - ListItem, - ListItemText, - Paper, - Typography, -} from '@mui/material' +import { PwaContext } from '@/components/PwaContentProvider' +import PwaInstallationButtonChromium from '@/components/PwaInstallationButtonChromium' +import PwaInstallationInstructionIos from '@/components/PwaInstallationInstructionIos' +import { Alert, Grid, Typography } from '@mui/material' import { useTranslations } from 'next-intl' -import { useCallback, useEffect, useState } from 'react' +import { useContext } from 'react' /** * this component contains the PWA installation */ const PwaInstallation = () => { - const [showInstallButton, setShowInstallButton] = useState(false) - const [showInstallationInstruction, setShowInstallationInstruction] = - useState(false) - const [prompt, setPrompt] = useState(null) + const { handleInstallClick, showPwaInstallOptions, showSafariInstructions } = + useContext(PwaContext) - const browser = window.navigator.userAgent - const isPWA = window.matchMedia('(display-mode: standalone)').matches - const hasChrome = browser.includes('Chrome') - const hasSafari = browser.includes('Safari') - - const { trackEvent, setCustomDimension } = useMatomo() - - useEffect(() => { - // set PWA state custom dimension - setCustomDimension(CustomDimensions.IS_PWA, isPWA ? 1 : 0) - - // this is needed because chrome on mac contains both strings - if (!isPWA && !hasChrome && hasSafari) { - // safari - setShowInstallButton(true) - } - - const handleBeforeInstallPrompt = (event: Event) => { - event.preventDefault() - - if (!isPWA && hasChrome) { - // chrome - setShowInstallButton(true) - // is needed in order to open the installation prompt - setPrompt(event) - } - } - - const handleAfterInstallPrompt = () => { - trackEvent('PwaInstallation', 'PWA installed') - setShowInstallButton(false) - } - - window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt) - window.addEventListener('appinstalled', handleAfterInstallPrompt) - - return () => { - window.removeEventListener( - 'beforeinstallprompt', - handleBeforeInstallPrompt - ) - window.removeEventListener('appinstalled', handleAfterInstallPrompt) - } - }, [hasChrome, hasSafari, isPWA, setCustomDimension, trackEvent]) - - const handleInstallClick = useCallback(() => { - if (prompt) { - trackEvent('PwaInstallation', 'click', 'installButton') - // @ts-expect-error actually we expect here the BeforeInstallPromptEvent but it is not supported by at least firefox - prompt?.prompt() - } else if (!hasChrome && hasSafari) { - setShowInstallButton(false) - setShowInstallationInstruction(true) - } - }, [prompt, hasChrome, hasSafari, trackEvent]) - - const t = useTranslations('PWAInstallation') + const t = useTranslations('pwa.install') return ( { pb={7.5} sx={{ height: '100%' }} > - {showInstallButton && ( + {showPwaInstallOptions && ( - + {/* @ts-expect-error ignore undefined */} + )} - {showInstallationInstruction && ( + {showSafariInstructions && ( { })} > - {t('install.instruction.title')} + {t('instruction.title')} - - - - - - - {t('install.instruction.step1')}{' '} - - - - - - - - - - - - - - {t('install.instruction.step2')} - - - - - {t('install.instruction.toHomeScreen')} - - - - - - - + diff --git a/src/components/PwaInstallationButtonChromium.tsx b/src/components/PwaInstallationButtonChromium.tsx new file mode 100644 index 0000000..d9c8ae1 --- /dev/null +++ b/src/components/PwaInstallationButtonChromium.tsx @@ -0,0 +1,33 @@ +'use client' + +import { InstallMobile } from '@mui/icons-material' +import { Button, Typography } from '@mui/material' +import { useTranslations } from 'next-intl' +import { MouseEventHandler } from 'react' + +/** + * this component contains the PWA installation button + * shown on chromium based browsers + */ +const PwaInstallationButtonChromium = ({ + onClick, +}: { + onClick: MouseEventHandler +}) => { + const t = useTranslations('pwa.install') + + return ( + // @ts-expect-error for some reason now it does not like missing href + + ) +} + +export default PwaInstallationButtonChromium diff --git a/src/components/PwaInstallationDialog.tsx b/src/components/PwaInstallationDialog.tsx new file mode 100644 index 0000000..53e2bd6 --- /dev/null +++ b/src/components/PwaInstallationDialog.tsx @@ -0,0 +1,113 @@ +'use client' + +import useMatomo from '@/components/Matomo/useMatomo' +import { PwaContext } from '@/components/PwaContentProvider' +import PwaInstallationButtonChromium from '@/components/PwaInstallationButtonChromium' +import PwaInstallationInstructionIos from '@/components/PwaInstallationInstructionIos' +import { Close } from '@mui/icons-material' +import { + Avatar, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid, + IconButton, + Typography, +} from '@mui/material' +import { useTranslations } from 'next-intl' +import { useContext, useState } from 'react' + +/** + * this component contains the PWA installation + */ +const PwaInstallationDialog = () => { + const { handleInstallClick, showPwaInstallOptions, showSafariInstructions } = + useContext(PwaContext) + const [open, setOpen] = useState(true) + const { trackEvent } = useMatomo() + + const t = useTranslations('pwa.dialog') + + return ( + <> + {open && (showPwaInstallOptions || showSafariInstructions) && ( + { + trackEvent('PwaInstallationDialog', 'close', 'other') + setOpen(false) + }} + sx={() => ({ + '& .MuiDialog-paper': { + borderRadius: 2, + }, + })} + aria-labelledby='pwa-installation-dialog-title' + > + ({ '&.MuiDialogTitle-root': { p: 1.5, pb: 1 } })} + > + + + + + + {t('title')} + + + + { + trackEvent('PWaInstallationDialog', 'close', 'button') + setOpen(false) + }} + sx={{ + position: 'absolute', + right: 12, // align with checkboxes + top: 8, // align with title + }} + > + + + + + + {showPwaInstallOptions && ( + + + {t('chromiumText')} + + + )} + {showSafariInstructions && ( + + {t('iosText')} + + + )} + + + {showPwaInstallOptions && ( + + {/* @ts-expect-error ignore undefined */} + + + )} + + )} + + ) +} + +export default PwaInstallationDialog diff --git a/src/components/PwaInstallationInstructionIos.tsx b/src/components/PwaInstallationInstructionIos.tsx new file mode 100644 index 0000000..0e3b9ae --- /dev/null +++ b/src/components/PwaInstallationInstructionIos.tsx @@ -0,0 +1,69 @@ +'use client' + +import { AddBoxOutlined, IosShare } from '@mui/icons-material' +import { + Grid, + List, + ListItem, + ListItemText, + Paper, + Typography, +} from '@mui/material' +import { useTranslations } from 'next-intl' + +/** + * this component contains the PWA installation instruction for safari + */ +const PwaInstallationInstructionIos = () => { + const t = useTranslations('pwa.install.instruction') + + return ( + + + + + + + {t('step1')}{' '} + + + + + + + + + + + + + + {t('step2')} + + + + {t('toHomeScreen')} + + + + + + + ) +} + +export default PwaInstallationInstructionIos diff --git a/src/components/hooks/usePwaInfo.ts b/src/components/hooks/usePwaInfo.ts new file mode 100644 index 0000000..7e2a304 --- /dev/null +++ b/src/components/hooks/usePwaInfo.ts @@ -0,0 +1,16 @@ +import { useMemo } from 'react' + +export default function usePwaInfo() { + const pwaInfos = useMemo(() => { + if (typeof window !== 'undefined') { + const browser = window.navigator.userAgent + const isPwa = window.matchMedia('(display-mode: standalone)').matches + const hasChrome = browser.includes('Chrome') + const hasSafari = browser.includes('Safari') + return { isPwa, hasChrome, hasSafari } + } + return {} + }, []) + + return { ...pwaInfos } +} diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index e8ab553..f09d2a6 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -39,7 +39,12 @@ "BackButton": { "label": "zurück" }, - "PWAInstallation": { + "pwa": { + "dialog": { + "title": "appgefahren! WebApp installieren", + "iosText": "Füge die appgefahren! WebApp deinem Home-Bildschirm hinzu, um sie immer dabei zu haben.", + "chromiumText": "Installiere die appgefahren! WebApp, um sie immer dabei zu haben." + }, "install": { "button": { "label": "WebApp installieren" @@ -49,8 +54,8 @@ "step1": "Tippe auf ", "step2": "wähle ", "toHomeScreen": "Zum Home-Bildschirm ", - "iosShareIcon": "Teilen-Button", - "addBoxIconOutlinedIcon": "hinzufügen-Button" + "titleAccessIosShareIcon": "Teilen-Button", + "titleAccessAddBoxIconOutlinedIcon": "hinzufügen-Button" } } },