Add PWA
parent
fddc5cc22d
commit
15eceb3eee
|
@ -6,4 +6,5 @@ cypress/screenshots
|
|||
cypress/downloads
|
||||
.deps.json
|
||||
netlify*.toml
|
||||
source/public/sitemap.*.txt
|
||||
source/public/sitemap.*.txt
|
||||
dev-dist
|
|
@ -142,9 +142,11 @@
|
|||
"ts-morph": "^13.0.3",
|
||||
"ts-node": "^10.8.0",
|
||||
"typescript": "^4.7.2",
|
||||
"vite": "^2.9.9",
|
||||
"vite": "^2.9.13",
|
||||
"vite-plugin-pwa": "^0.12.1",
|
||||
"vite-plugin-shim-react-pdf": "^1.0.5",
|
||||
"vitest": "^0.9.4",
|
||||
"workbox-window": "^6.5.3",
|
||||
"xml2js": "^0.4.23",
|
||||
"yaml": "^1.9.2"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import { I18nextProvider } from 'react-i18next'
|
|||
import { Provider as ReduxProvider } from 'react-redux'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { CompatRouter } from 'react-router-dom-v5-compat'
|
||||
import { ServiceWorker } from './ServiceWorker'
|
||||
import * as safeLocalStorage from './storage/safeLocalStorage'
|
||||
import { store } from './store'
|
||||
import { inIframe } from './utils'
|
||||
|
@ -30,26 +31,6 @@ import { inIframe } from './utils'
|
|||
import { TrackingContext } from './ATInternetTracking'
|
||||
import { createTracker } from './ATInternetTracking/Tracker'
|
||||
|
||||
if (
|
||||
!import.meta.env.SSR &&
|
||||
import.meta.env.MODE === 'production' &&
|
||||
'serviceWorker' in navigator &&
|
||||
!inIframe()
|
||||
) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then((registration) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SW registered: ', registration)
|
||||
})
|
||||
.catch((registrationError) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SW registration failed: ', registrationError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type SiteName = 'mon-entreprise' | 'infrance' | 'publicodes'
|
||||
|
||||
export const SiteNameContext = createContext<SiteName | null>(null)
|
||||
|
@ -100,6 +81,10 @@ export default function Provider({
|
|||
</Container>
|
||||
}
|
||||
>
|
||||
{!import.meta.env.SSR &&
|
||||
import.meta.env.MODE === 'production' &&
|
||||
'serviceWorker' in navigator &&
|
||||
!inIframe() && <ServiceWorker />}
|
||||
<OverlayProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<IsEmbeddedProvider>
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { Message } from './design-system'
|
||||
import { HideButton } from './design-system/banner'
|
||||
import { Button } from './design-system/buttons'
|
||||
import { Body } from './design-system/typography/paragraphs'
|
||||
|
||||
const PromptContainer = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
min-height: initial !important;
|
||||
`
|
||||
|
||||
const StyledSmallBody = styled(Body)`
|
||||
margin: 0.5rem 0.75rem 0.5rem 0;
|
||||
`
|
||||
|
||||
const StyledMessage = styled(Message)`
|
||||
margin: 0 0.5rem 0.5rem 0.5rem !important;
|
||||
max-width: 450px;
|
||||
|
||||
${Message.Wrapper} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledHideButton = styled.div`
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
right: 0.375rem;
|
||||
`
|
||||
|
||||
export const ServiceWorker = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
offlineReady: [offlineReady, setOfflineReady],
|
||||
needRefresh: [needRefresh, setNeedRefresh],
|
||||
updateServiceWorker,
|
||||
} = useRegisterSW({
|
||||
onRegistered: (r) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('=> SW Registered: ', r)
|
||||
},
|
||||
onRegisterError: (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SW registration error', error)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<PromptContainer>
|
||||
{offlineReady && (
|
||||
<StyledMessage type="info">
|
||||
<StyledSmallBody>
|
||||
<Trans>L'application est prête à fonctionner hors ligne.</Trans>
|
||||
</StyledSmallBody>
|
||||
|
||||
<StyledHideButton>
|
||||
<HideButton
|
||||
onClick={() => setOfflineReady(false)}
|
||||
aria-label={t('Fermer')}
|
||||
>
|
||||
×
|
||||
</HideButton>
|
||||
</StyledHideButton>
|
||||
</StyledMessage>
|
||||
)}
|
||||
|
||||
{needRefresh && (
|
||||
<StyledMessage type="info">
|
||||
<StyledSmallBody>
|
||||
<Trans>
|
||||
Nouveau contenu disponible, cliquez sur recharger pour mettre à
|
||||
jour la page.
|
||||
</Trans>{' '}
|
||||
<Button
|
||||
light
|
||||
size="XXS"
|
||||
onClick={() => void updateServiceWorker(true)}
|
||||
>
|
||||
{t('Recharger')}
|
||||
</Button>
|
||||
</StyledSmallBody>
|
||||
|
||||
<StyledHideButton>
|
||||
<HideButton
|
||||
onClick={() => setNeedRefresh(false)}
|
||||
aria-label={t('Fermer')}
|
||||
>
|
||||
×
|
||||
</HideButton>
|
||||
</StyledHideButton>
|
||||
</StyledMessage>
|
||||
)}
|
||||
</PromptContainer>
|
||||
)
|
||||
}
|
|
@ -39,7 +39,9 @@ export default function Banner({
|
|||
<FadeIn>
|
||||
<Container className={className}>
|
||||
<Emoji emoji={icon} />
|
||||
<Content>{children}</Content>
|
||||
<Content as={typeof children === 'string' ? undefined : 'div'}>
|
||||
{children}
|
||||
</Content>
|
||||
</Container>
|
||||
</FadeIn>
|
||||
) : null
|
||||
|
|
|
@ -7,7 +7,7 @@ import { wrapperDebounceEvents } from '@/utils'
|
|||
import React, { ForwardedRef, forwardRef } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
type Size = 'XL' | 'MD' | 'XS'
|
||||
type Size = 'XL' | 'MD' | 'XS' | 'XXS'
|
||||
type Color = 'primary' | 'secondary' | 'tertiary'
|
||||
|
||||
type ButtonProps = GenericButtonOrLinkProps & {
|
||||
|
@ -69,6 +69,9 @@ export const StyledButton = styled.button<StyledButtonProps>`
|
|||
if ($size === 'XS') {
|
||||
return '0.5rem 2rem'
|
||||
}
|
||||
if ($size === 'XXS') {
|
||||
return '0.25rem 1rem'
|
||||
}
|
||||
}};
|
||||
@media (max-width: ${({ theme }) => theme.breakpointsWidth.sm}) {
|
||||
width: 100%;
|
||||
|
|
|
@ -35,6 +35,7 @@ export const Label = styled.label`
|
|||
}
|
||||
`}
|
||||
`
|
||||
|
||||
interface ButtonProps {
|
||||
isFocusVisible?: boolean
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ type MessageProps = {
|
|||
border?: boolean
|
||||
type?: MessageType
|
||||
light?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Message({
|
||||
|
@ -22,6 +23,7 @@ export function Message({
|
|||
border = true,
|
||||
light = false,
|
||||
children,
|
||||
className,
|
||||
}: MessageProps) {
|
||||
if (typeof children !== 'object') {
|
||||
children = <Body>{children}</Body>
|
||||
|
@ -29,7 +31,12 @@ export function Message({
|
|||
|
||||
return (
|
||||
<ThemeProvider theme={(theme) => ({ ...theme, darkMode: false })}>
|
||||
<StyledMessage type={type} border={border} light={light}>
|
||||
<StyledMessage
|
||||
className={className}
|
||||
type={type}
|
||||
border={border}
|
||||
light={light}
|
||||
>
|
||||
{icon &&
|
||||
(type === 'success' ? (
|
||||
<StyledIcon
|
||||
|
@ -56,13 +63,7 @@ export function Message({
|
|||
alt="icône signalant un texte informatif"
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
css={`
|
||||
flex: 1;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Wrapper>{children}</Wrapper>
|
||||
</StyledMessage>
|
||||
</ThemeProvider>
|
||||
)
|
||||
|
@ -111,3 +112,9 @@ const StyledIcon = styled.img`
|
|||
margin-top: calc(${theme.spacings.lg});
|
||||
`}
|
||||
`
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
Message.Wrapper = Wrapper
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "Mon entreprise",
|
||||
"short_name": "Mon entreprise",
|
||||
"description": "L'assistant officiel du créateur d'entreprise",
|
||||
"display": "standalone",
|
||||
"lang": "fr",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#2975d1",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// A simple, no-op service worker that takes immediate control.
|
||||
|
||||
self.addEventListener('install', () => {
|
||||
// Skip over the "waiting" lifecycle state, to ensure that our
|
||||
// new service worker is activated immediately, even if there's
|
||||
// another tab open controlled by our older service worker code.
|
||||
self.skipWaiting()
|
||||
})
|
|
@ -3,30 +3,46 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
<link rel="icon" href="/favicon/favicon.svg?v=1.0" type="image/svg+xml" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicon/apple-touch-icon.png"
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon/favicon-16x16.png?v=1.0"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon/favicon-32x32.png"
|
||||
href="/favicon/favicon-32x32.png?v=1.0"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
rel="alternate icon"
|
||||
href="/favicon/favicon.ico?v=1.0"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/favicon/apple-touch-icon.png?v=1.0"
|
||||
/>
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/favicon/safari-pinned-tab.svg?v=1.0"
|
||||
color="#98deed"
|
||||
/>
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico?v=1.0" />
|
||||
<meta name="apple-mobile-web-app-title" content="Mon-entreprise" />
|
||||
<meta name="application-name" content="Mon-entreprise" />
|
||||
<meta name="msapplication-TileColor" content="#603cba" />
|
||||
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
|
||||
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<!--app-helmet-tags:start-->
|
||||
<meta name="description" content="{{ description }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
@ -35,8 +51,6 @@
|
|||
<meta property="og:image" content="{{ shareImage }}" />
|
||||
<!--app-helmet-tags:end-->
|
||||
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
|
||||
<style>
|
||||
html[data-useragent*='MSIE'] #outdated-browser,
|
||||
html[data-useragent*='Safari'][data-useragent*='Version/8']
|
||||
|
|
|
@ -8,8 +8,13 @@ import fs from 'fs/promises'
|
|||
import path from 'path'
|
||||
import serveStatic from 'serve-static'
|
||||
import { defineConfig, loadEnv, Plugin } from 'vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import shimReactPdf from 'vite-plugin-shim-react-pdf'
|
||||
|
||||
const HOUR = 60 * 60
|
||||
const DAY = HOUR * 24
|
||||
const YEAR = DAY * 365
|
||||
|
||||
const env = (mode: string) => loadEnv(mode, process.cwd(), '')
|
||||
|
||||
export default defineConfig(({ command, mode }) => ({
|
||||
|
@ -57,10 +62,121 @@ export default defineConfig(({ command, mode }) => ({
|
|||
},
|
||||
},
|
||||
}),
|
||||
VitePWA({
|
||||
registerType: 'prompt',
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
sourcemap: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: (options) => {
|
||||
if (
|
||||
!(
|
||||
options.sameOrigin &&
|
||||
options.url.pathname.startsWith('/twemoji/')
|
||||
) &&
|
||||
!(
|
||||
options.sameOrigin &&
|
||||
options.url.pathname.startsWith('/twemoji/')
|
||||
)
|
||||
) {
|
||||
console.log('=>', options.url.pathname)
|
||||
}
|
||||
|
||||
return (
|
||||
options.sameOrigin && options.url.pathname.startsWith('/fonts/')
|
||||
)
|
||||
},
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'fonts-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 1 * YEAR,
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: (options) => {
|
||||
return (
|
||||
options.sameOrigin &&
|
||||
options.url.pathname.startsWith('/twemoji/')
|
||||
)
|
||||
},
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'twemoji-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 1 * YEAR,
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: (options) => {
|
||||
return (
|
||||
!options.sameOrigin && options.url.hostname === 'polyfill.io'
|
||||
)
|
||||
},
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'external-cache',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 1 * DAY,
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
srcDir: 'public/favicon',
|
||||
includeAssets: [
|
||||
'favicon.svg',
|
||||
'favicon.ico',
|
||||
'robots.txt',
|
||||
'apple-touch-icon.png',
|
||||
],
|
||||
manifest: {
|
||||
name: 'Mon entreprise',
|
||||
short_name: 'Mon entreprise',
|
||||
description: "L'assistant officiel du créateur d'entreprise",
|
||||
lang: 'fr',
|
||||
orientation: 'portrait-primary',
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon/android-chrome-192x192.png?v=1.0',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/favicon/android-chrome-512x512.png?v=1.0',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
},
|
||||
}),
|
||||
legacy({
|
||||
targets: ['defaults', 'not IE 11'],
|
||||
}),
|
||||
],
|
||||
esbuild: {
|
||||
logOverride: { 'this-is-undefined-in-esm': 'silent' }, // will be fixed in next version of @vitejs/plugin-react (actualy 1.3.2), issue https://github.com/vitejs/vite/pull/8674
|
||||
},
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort:
|
||||
|
@ -138,7 +254,7 @@ function multipleSPA(options: MultipleSPAOptions): Plugin {
|
|||
return next()
|
||||
}
|
||||
|
||||
if (url === '/') {
|
||||
if (url && ['/', '/index.html'].includes(url)) {
|
||||
res.writeHead(302, { Location: '/' + options.defaultSite }).end()
|
||||
} else if (
|
||||
firstLevelDir &&
|
||||
|
|
Loading…
Reference in New Issue