Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -48,17 +50,20 @@ export default async function LocaleLayout({
<NextIntlClientProvider>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBarWithMenu />
{children}
<MatomoTracking
// @ts-expect-error prevent undefined env variable
url={process.env.MATOMO_URL}
// @ts-expect-error prevent undefined env variable
siteId={process.env.MATOMO_ID}
// @ts-expect-error this is a server side environment variable that can be changed at runtime in docker
matomoEnv={process.env.MATOMO_ENV}
/>
<PwaContentProvider>
<CssBaseline />
<PwaInstallationDialog />
<AppBarWithMenu />
{children}
<MatomoTracking
// @ts-expect-error prevent undefined env variable
url={process.env.MATOMO_URL}
// @ts-expect-error prevent undefined env variable
siteId={process.env.MATOMO_ID}
// @ts-expect-error this is a server side environment variable that can be changed at runtime in docker
matomoEnv={process.env.MATOMO_ENV}
/>
</PwaContentProvider>
</ThemeProvider>
</AppRouterCacheProvider>
</NextIntlClientProvider>
Expand Down
101 changes: 101 additions & 0 deletions src/components/PwaContentProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement>
showPwaInstallOptions: boolean
showSafariInstructions: boolean
prompt?: Event
}

export const PwaContext = createContext<PwaContextType>({
showPwaInstallOptions: false,
showSafariInstructions: false,
})

/**
* this provider contains PWA information needed for components
*/
export const PwaContentProvider = ({ children }: { children: ReactNode }) => {
const [showPwaInstallOptions, setShowPwaInstallOptions] =
useState<boolean>(false)
const [showSafariInstructions, setShowSafariInstructions] =
useState<boolean>(false)
const [prompt, setPrompt] = useState<Event | undefined>(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 (
<PwaContext.Provider
value={{
handleInstallClick,
showPwaInstallOptions,
showSafariInstructions,
prompt,
}}
>
{children}
</PwaContext.Provider>
)
}

export default PwaContentProvider
162 changes: 14 additions & 148 deletions src/components/PwaInstallation.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(false)
const [showInstallationInstruction, setShowInstallationInstruction] =
useState<boolean>(false)
const [prompt, setPrompt] = useState<Event | null>(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 (
<Grid
Expand All @@ -92,20 +25,13 @@ const PwaInstallation = () => {
pb={7.5}
sx={{ height: '100%' }}
>
{showInstallButton && (
{showPwaInstallOptions && (
<Grid container justifyContent={'center'}>
<Button
startIcon={<InstallMobile />}
onClick={handleInstallClick}
sx={{ py: 1, color: 'communication.hyperlink.main' }}
>
<Typography variant='labelMedium' sx={{ textTransform: 'none' }}>
{t('install.button.label')}
</Typography>
</Button>
{/* @ts-expect-error ignore undefined */}
<PwaInstallationButtonChromium onClick={handleInstallClick} />
</Grid>
)}
{showInstallationInstruction && (
{showSafariInstructions && (
<Grid px={2} container direction={'row'}>
<Grid size='grow'>
<Alert
Expand All @@ -123,69 +49,9 @@ const PwaInstallation = () => {
})}
>
<Typography color='textDark' variant='textLargeColored'>
{t('install.instruction.title')}
{t('instruction.title')}
</Typography>
<List sx={{ listStyle: 'decimal', pl: 2.5 }} component={'ol'}>
<ListItem
sx={{ display: 'list-item', paddingX: 0, paddingY: 0.5 }}
>
<ListItemText>
<Grid
container
direction='row'
alignItems='center'
gap={0.5}
>
<Grid>
<Typography color='textDark' variant='textLarge'>
{t('install.instruction.step1')}{' '}
</Typography>
</Grid>
<Grid>
<Paper
sx={{
borderRadius: 0,
display: 'inline-block',
px: 0.5,
}}
>
<IosShare
titleAccess={t('install.instruction.iosShareIcon')}
/>
</Paper>
</Grid>
</Grid>
</ListItemText>
</ListItem>
<ListItem
sx={{ display: 'list-item', paddingX: 0, paddingY: 0.5 }}
>
<ListItemText>
<Typography color='textDark' variant='textLarge'>
{t('install.instruction.step2')}
</Typography>
<Paper
sx={{ borderRadius: 0, display: 'inline-block', p: 0.5 }}
>
<Grid
container
direction='row'
alignItems='center'
gap={0.5}
>
<Typography variant='textLarge'>
{t('install.instruction.toHomeScreen')}
</Typography>
<AddBoxOutlined
titleAccess={t(
'install.instruction.addBoxIconOutlinedIcon'
)}
/>
</Grid>
</Paper>
</ListItemText>
</ListItem>
</List>
<PwaInstallationInstructionIos />
</Alert>
</Grid>
</Grid>
Expand Down
33 changes: 33 additions & 0 deletions src/components/PwaInstallationButtonChromium.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement>
}) => {
const t = useTranslations('pwa.install')

return (
// @ts-expect-error for some reason now it does not like missing href
<Button
startIcon={<InstallMobile />}
onClick={onClick}
sx={{ py: 1, color: 'communication.hyperlink.main' }}
>
<Typography variant='labelMedium' sx={{ textTransform: 'none' }}>
{t('button.label')}
</Typography>
</Button>
)
}

export default PwaInstallationButtonChromium
Loading