---
title: "PWA (Progressive Web App)"
weight: 140
---

# PWA (Progressive Web App)

Documentation [Vite-PWA](https://vite-pwa-org.netlify.app/)

Une **Progressive Web App (PWA)** est une application qui combine le meilleur du web et du mobile. Elle s'installe sur l'écran d'accueil, fonctionne hors connexion et offre une expérience fluide proche d'une app native.

Cette page couvre la chaîne complète : Service Worker, manifest, origines front/API, démarrage hors ligne, mise à jour et installation.

> [!IMPORTANT]
> Deux points sont sources d'erreur récurrente et sont traités en détail plus bas : le **fallback de navigation** (sans lui, l'application ne redémarre pas hors ligne) et la **contrainte de même origine** entre le front et l'API.

## Les deux modes de Service Worker

`vite-plugin-pwa` propose deux stratégies. SmartMaker utilise les deux, selon les projets. Il faut savoir laquelle est en place avant de toucher à la configuration, car **les options ne sont pas interchangeables**.

| Mode | Qui écrit le Service Worker | Où se configure le cache |
| --- | --- | --- |
| `generateSW` (défaut) | le plugin, entièrement | bloc `workbox` de `vite.config.js` |
| `injectManifest` | vous, dans `mobile/src/sw.js` | dans votre `sw.js`, en code Workbox |

> [!WARNING]
> En mode `injectManifest`, les options `workbox.runtimeCaching`, `skipWaiting` et `clientsClaim` placées dans `vite.config.js` sont **purement et simplement ignorées**. Elles n'existent qu'en mode `generateSW`. C'est la cause la plus fréquente du symptôme "ma configuration VitePWA ne fonctionne pas".

État des projets de référence :

| Projet | Mode | Manifest |
| --- | --- | --- |
| smartboot (squelette) | `injectManifest` | dynamique (SmartAuth) |
| smartInterventions | `injectManifest` | statique (Vite) |
| capfullpos | `generateSW` | dynamique (SmartAuth) |
| offlinepropale | `generateSW` | dynamique (SmartAuth) |

Un projet créé avec SmartBoot démarre donc en mode `injectManifest`.

### Mode generateSW

```
// vite.config.js
VitePWA({
  registerType: 'autoUpdate',
  workbox: {
    globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
    cleanupOutdatedCaches: true,
    maximumFileSizeToCacheInBytes: 3000000,
    skipWaiting: true,
    clientsClaim: true,
    runtimeCaching: [
      {
        urlPattern: /\/api\/(home|profile)/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'api-cache',
          expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 },
          cacheableResponse: { statuses: [0, 200] },
          networkTimeoutSeconds: 10,
        },
      },
      {
        urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
        handler: 'CacheFirst',
        options: {
          cacheName: 'images-cache',
          expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 },
        },
      },
    ],
  },
  injectRegister: "auto",
  includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
  manifest: false,
})
```

Avec `appType: 'spa`', le plugin ajoute automatiquement un `navigateFallback` vers `index.html`. Le redémarrage hors ligne sur une route cliente fonctionne donc sans rien écrire de plus.

### Mode injectManifest

C'est le mode du squelette SmartBoot. Il est choisi parce que le Service Worker doit porter les gestionnaires Web Push de SmartCommon, ce que `generateSW` ne permet pas.

```
// vite.config.js
VitePWA({
  registerType: 'autoUpdate',
  strategies: 'injectManifest',
  srcDir: 'src',
  filename: 'sw.js',
  injectManifest: {
    globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
    maximumFileSizeToCacheInBytes: 3000000,
  },
  // Le SW est enregistre a la main dans src/main.jsx via virtual:pwa-register
  injectRegister: false,
  includeAssets: ["favicon.ico", "assets/*", "favicon.png", "apple-touch-icon.png"],
  manifest: false,
})
```

Points à retenir :
- `globPatterns` passe dans `injectManifest`, pas dans `workbox`
- `injectRegister: false` parce que l'enregistrement est fait manuellement (voir plus bas)
- tout le comportement de cache vit maintenant dans `mobile/src/sw.js`

## Le Service Worker en mode injectManifest

Le fichier `mobile/src/sw.js` **appartient au projet**. Le plugin n'y injecte que la liste des fichiers à précacher, via `self.__WB_MANIFEST`.

### Squelette complet

```
// mobile/src/sw.js
import {
    precacheAndRoute,
    cleanupOutdatedCaches,
    createHandlerBoundToURL,
} from "workbox-precaching";
import { registerRoute, NavigationRoute } from "workbox-routing";
import { NetworkFirst, CacheFirst } from "workbox-strategies";
import { ExpirationPlugin } from "workbox-expiration";
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { registerPushHandlers } from "@cap-rel/smartcommon/sw";

// 1. Precache des assets du build (obligatoire en injectManifest)
precacheAndRoute(self.__WB_MANIFEST);

// 2. Equivalent de cleanupOutdatedCaches: true
cleanupOutdatedCaches();

// 3. Fallback de navigation SPA (voir l'encadre ci-dessous)
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
    denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));

// 4. Runtime caching (equivalent de workbox.runtimeCaching)
registerRoute(
    /\/api\/(home|profile)/,
    new NetworkFirst({
        cacheName: "api-cache",
        networkTimeoutSeconds: 10,
        plugins: [
            new CacheableResponsePlugin({ statuses: [0, 200] }),
            new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 7 }),
        ],
    })
);

registerRoute(
    /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
    new CacheFirst({
        cacheName: "images-cache",
        plugins: [new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 })],
    })
);

// 5. Equivalent de skipWaiting + clientsClaim
self.addEventListener("install", () => {
    self.skipWaiting();
});

self.addEventListener("activate", (event) => {
    event.waitUntil(self.clients.claim());
});

// 6. Web Push (gestionnaires partages fournis par smartcommon)
registerPushHandlers({
    defaultIcon: "/images/pwa-192x192.png",
    defaultBadge: "/images/pwa-64x64.png",
});
```

### Le fallback de navigation, indispensable hors ligne

> [!WARNING]
> C'est le piège numéro un du mode `injectManifest`. Contrairement à `generateSW`, **aucun `navigateFallback` n'est ajouté implicitement**. Le squelette SmartBoot fournit désormais cette route ; si vous reprenez un projet créé avant, ou si vous écrivez votre `sw.js` de zéro, vérifiez qu'elle est bien présente.

Sans cette route, seul `index.html` est précaché, pas les chemins clients. Un rechargement hors ligne sur `/interventions` ou `/produit/12` ne trouve rien dans le precache, part au réseau et échoue sur `net::ERR_INTERNET_DISCONNECTED`. Symptôme observé : **écran blanc au redémarrage hors ligne**, alors que le Service Worker est bien actif et que le cache est rempli.

```
registerRoute(new NavigationRoute(createHandlerBoundToURL("index.html"), {
    // Ne jamais detourner les appels API ni les requetes de fichier
    // (tout ce qui porte une extension) : seules les vraies navigations
    // applicatives retombent sur le shell.
    denylist: [/^\/api\//, /^\/api\.php\//, /\/[^/?]+\.[^/?]+$/],
}));
```

La `denylist` est indispensable : sans elle, le Service Worker renverrait `index.html` en réponse à des appels API ou à des téléchargements de fichiers.

> [!NOTE]
> Les deux motifs d'API ne font pas doublon. Une PWA déployée dans `pwa/` est construite avec `VITE_API_URL=/api.php/` : ses appels commencent par `/api.php/`, que le motif `/^\/api\//` ne couvre pas. Seul le motif générique "tout ce qui porte une extension" les attrapait, ce qui est fragile.

Implémentation de référence : `smartInterventions/mobile/src/sw.js` et le `sw.js` du squelette SmartBoot.

### Enregistrement du Service Worker

En mode `injectManifest` avec `injectRegister: false`, l'enregistrement est explicite dans `mobile/src/main.jsx` :

```
import { registerSW } from "virtual:pwa-register";

registerSW({
  immediate: true,
});
```

> [!NOTE]
> Ne pas laisser `injectRegister: "auto"` en même temps qu'un appel manuel à `registerSW` : le Service Worker serait enregistré deux fois.

## Manifest

Deux approches coexistent dans SmartMaker. Choisissez avant de commencer, elles ne se combinent pas.

| Approche | Configuration Vite | Lien dans index.html | Personnalisation |
| --- | --- | --- | --- |
| Manifest dynamique SmartAuth | `manifest: false` | explicite, obligatoire | constantes Dolibarr, par entité |
| Manifest statique Vite | bloc `manifest: { ... }` | généré, à retirer | dans le code, au build |

### Manifest dynamique servi par SmartAuth

C'est le choix du squelette SmartBoot et de la majorité des modules. Le manifest est produit par le `PwaController` de SmartAuth à partir des constantes Dolibarr du module, ce qui permet à chaque client d'avoir son propre nom et ses propres icônes sans rebuilder l'application.

Dans `mobile/index.html` :

```
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="manifest" href="api.php/manifest.webmanifest">
    <link rel="icon" type="image/png" href="api.php/icon/64">
    <link rel="apple-touch-icon" href="api.php/icon/192">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
```

Dans `vite.config.js` : `manifest: false`.

#### Routes SmartAuth

| Route | Description |
| --- | --- |
| GET `api.php/manifest.webmanifest` | manifest JSON généré dynamiquement |
| GET `api.php/icon/{size}` | icône PWA à la taille demandée (64, 192 ou 512) |

> [!WARNING]
> L'URL est bien `manifest.webmanifest`, extension comprise. Un lien vers `api.php/manifest` renvoie une erreur de routage.

Ces deux routes ne sont pas protégées : le navigateur doit pouvoir les charger avant toute authentification.

#### Constantes Dolibarr

Le préfixe est le nom du module en majuscules.

| Constante | Description | Défaut |
| --- | --- | --- |
| `{MODULE}_PWA_NAME` | nom complet de l'application | nom de la société, sinon nom du module |
| `{MODULE}_PWA_DESCRIPTION` | description | vide |
| `{MODULE}_PWA_BG_COLOR` | couleur de fond | `#ffffff` |
| `{MODULE}_PWA_THEME_COLOR` | couleur du thème | `#000000` |

> [!NOTE]
> Il n'existe pas de constante pour le nom court : `short_name` est dérivé automatiquement des 12 premiers caractères de `{MODULE}_PWA_NAME`.

#### Champs non configurables

Le manifest dynamique fixe ces valeurs et ne les expose pas en constantes :

```
"id": "/",
"scope": "/",
"start_url": "/",
"display": "standalone",
"prefer_related_applications": false
```

> [!TIP]
> La question revient souvent : `"display": "standalone"` est **déjà actif**. Une application installée depuis un manifest dynamique s'ouvre donc bien dans sa propre fenêtre, sans barre d'adresse. Il n'y a rien à ajouter.

Ce choix est assumé et ne bougera pas : le manifest dynamique ne sert qu'à ce qui dépend réellement de l'instance Dolibarr, c'est-à-dire le nom, la description, les couleurs et les icônes. `display`, `start_url`, `scope` et `orientation` sont des propriétés de l'application, pas du client qui l'héberge : elles appartiennent au build. Les exposer en constantes reviendrait à confier au paramétrage Dolibarr des décisions que seul l'auteur de l'application peut prendre, et à multiplier les combinaisons à tester.

Si vous avez besoin de modifier `display`, `start_url`, `orientation` ou d'ajouter des `shortcuts`, passez au manifest statique.

> [!WARNING]
> `id` et `scope` ne doivent pas être modifiés à la légère. `id` est la clé sur laquelle le navigateur reconnaît une application déjà installée. Le changer fait de votre application une **nouvelle** application aux yeux du navigateur : les installations existantes ne sont plus reconnues, et la détection décrite plus bas cesse de fonctionner pour ceux qui avaient déjà installé.

#### Auto-déclaration pour la détection d'installation

Le manifest dynamique se déclare lui-même :

```
"related_applications": [
  { "platform": "webapp", "url": "https://votre-app.example.fr/pwa/api.php/manifest.webmanifest" }
]
```

C'est ce qui rend `navigator.getInstalledRelatedApps()` exploitable : sans cette entrée, l'appel renvoie une liste vide sur Android et le bandeau d'installation est reproposé à des utilisateurs qui ont déjà l'application sur leur écran d'accueil.

L'URL est **absolue** et construite depuis la requête en cours, jamais avec `dol_buildpath()` : l'application est servie sur son propre virtualhost, que Dolibarr ne connaît pas. L'en-tête `Host` étant fourni par le client, il est validé ; s'il est inutilisable, `related_applications` est simplement omis plutôt que de publier une URL pointant vers une origine choisie par un tiers.

Rien à faire de votre côté : c'est servi automatiquement. Si vous passez au manifest statique, en revanche, cette entrée est à écrire vous-même.

#### Icônes

Le `PwaController` cherche l'icône dans cet ordre :

  - icône personnalisée envoyée depuis l'administration du module, dans `{dir_output}/pwa/icon_{taille}.png`
  - icône livrée avec le module, dans `pwa/images/pwa-{taille}x{taille}.png`
  - à défaut, un carré bleu généré avec les initiales du module (nécessite GD)

Les tailles servies sont 64, 192 et 512. Toute autre valeur retombe sur 512.

### Manifest statique généré par Vite

C'est le choix de smartInterventions. Il convient quand le manifest est le même pour tous les clients et que vous voulez la main sur tous les champs.

```
// vite.config.js
VitePWA({
  // ...
  manifest: {
    name: "SmartInterventions",
    short_name: "SmartInterventions",
    start_url: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#dc2626",
    icons: [
      { src: 'images/pwa-64x64.png', sizes: '64x64', type: 'image/png' },
      { src: 'images/pwa-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
      { src: 'images/pwa-512x512.png', sizes: '512x512', type: 'image/png' },
    ],
  },
})
```

Dans ce cas, **retirez le `<link rel="manifest">` de `index.html`** : Vite l'injecte lui-même. Deux liens concurrents produisent un manifest ignoré ou incohérent.

## Origine du front et origine de l'API

C'est la question qui bloque le plus souvent en développement.

### La règle

Un manifest doit être servi depuis **la même origine** que le document HTML qui le référence. Un front sur `http://localhost:5173` et une API sur `https://dolibarr.local` sont deux origines distinctes : le navigateur refuse le manifest et affiche un message du type *"Le manifest doit avoir la même origine que la page"*.

La même contrainte s'applique au Service Worker : il ne contrôle que son origine.

> [!IMPORTANT]
> Il n'est donc **pas possible** de faire cohabiter durablement un front et une API sur deux domaines différents pour une PWA. La solution n'est pas de configurer CORS, c'est de **ramener les deux sur la même origine**.

### En production : même origine par construction

La PWA est servie depuis le dossier `pwa/` du module, sur le même hôte que Dolibarr :

```
https://erp.client.fr/custom/monmodule/pwa/            <- le front
https://erp.client.fr/custom/monmodule/pwa/api.php/    <- l'API
```

Le Makefile de build force d'ailleurs l'URL de l'API en relatif juste avant de builder :

```
echo "VITE_API_URL=/api.php/" >| ./mobile/.env
```

puis restaure la valeur d'origine du `.env` pour ne pas casser l'environnement de développement. La PWA de production n'a donc aucune URL absolue de backend.

### En développement : deux serveurs, deux origines

En développement, `vite dev` sert le front sur le port 5173 et l'API reste sur le Dolibarr local. Deux origines, donc :
- `api.php/manifest.webmanifest` est introuvable sur le port 5173, puisque rien ne sert `api.php` à cette adresse
- pointer le lien vers `https://dolibarr.local/custom/monmodule/pwa/api.php/manifest.webmanifest` fait rejeter le manifest pour cause d'origine différente

La solution est de proxifier l'API depuis le serveur de développement Vite, pour que tout soit servi depuis `localhost:5173`.

**Avec le squelette SmartBoot, il n'y a rien à coder** : le proxy est déjà en place et n'attend qu'une variable d'environnement.

```
# mobile/.env
# Repertoire qui CONTIENT api.php, sans slash final
VITE_DEV_PROXY_TARGET=https://dolibarr.local/custom/monmodule/pwa
```

Si la variable est absente ou vide, aucun proxy n'est enregistré et le comportement du serveur de développement est inchangé.

Pour un projet qui n'est pas issu du squelette, la configuration équivalente :

```
// vite.config.js
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  const proxyTarget = env.VITE_DEV_PROXY_TARGET;

  return {
    server: proxyTarget ? {
      proxy: {
        '/api.php': {
          target: proxyTarget,
          changeOrigin: true,
          secure: false,
        },
      },
    } : undefined,
    // ...
  };
});
```

### En hébergement mutualisé : DoliProxy

Pour les déploiements où la PWA n'est pas servie par le Dolibarr du client, DoliProxy fournit un mode `hosted_pwa` : le client accède à `{client}.{module}.doliproxy.fr`, le proxy sert les fichiers statiques de la PWA sur `/` et proxifie `/api/*` vers le Dolibarr du client. Une seule origine, donc ni problème de CORS ni problème de manifest.

La PWA lit alors son `config.json` au démarrage, qui contient `"apiUrl": "/api/"`. Elle ne connaît jamais l'URL réelle du backend.

### Et le mode preview ?

`npm run build` puis `npm run preview` sert le build sur le port 4173. C'est utile mais partiel :

| Ce que preview permet de valider | Ce qu'il ne permet pas |
| --- | --- |
| enregistrement du Service Worker | le manifest dynamique (pas d`'api.php`) |
| contenu du precache | les appels API réels |
| fallback de navigation hors ligne | les icônes servies par SmartAuth |

La validation complète se fait sur la PWA déployée dans `pwa/` et servie par Dolibarr.

## Démarrer hors ligne

Faire fonctionner une application déjà chargée sans réseau est facile. La faire **démarrer** sans réseau demande que quatre conditions soient réunies.

### 1. Le bundle est entièrement précaché

`globPatterns` doit couvrir tous les types de fichiers du build :

```
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}']
```

L'extension `json` n'est pas décorative : elle précache les fichiers de traduction de `public/locales/`. Sans eux, l'application démarre hors ligne mais affiche les clés de traduction brutes.

### 2. La taille limite est suffisante

```
maximumFileSizeToCacheInBytes: 3000000
```

> [!WARNING]
> Tout fichier plus gros que cette limite est **silencieusement exclu** du precache. Si le bundle principal dépasse la limite, l'application est inutilisable hors ligne et rien ne le signale au build. capfullpos a rencontré exactement ce cas et a dû monter la limite à 5 Mo. Vérifiez la taille de vos assets dans `pwa/assets/` et gardez de la marge.

### 3. Le fallback de navigation est en place

En `generateSW`, il est implicite. En `injectManifest`, il faut l'écrire (voir plus haut). Sans lui, un rechargement hors ligne sur une route autre que la racine donne un écran blanc.

### 4. Les données métier sont dans IndexedDB

Le Service Worker cache les **fichiers**, pas les données. Les données métier doivent être en base locale via la classe `Db` de SmartCommon et les hooks `useDb<Feature>`. Voir [Stockage de données](/front/stockage-de-donnees) et [Synchronisation offline](/front/synchronisation).

### Détecter l'état de la connexion

```
import { useOnlineStatus } from '@cap-rel/smartcommon';

const MyComponent = () => {
  const {
    isOnline,           // true si le navigateur est en ligne
    isServerReachable,  // true/false/null selon le health check
    lastOnline,         // timestamp de la derniere connexion
    checkNow,           // verification manuelle
  } = useOnlineStatus({
    healthCheckUrl: '/api/health',
    healthCheckInterval: 30000,
    stabilityDelay: 2000,
    timeout: 5000,
  });

  return !isOnline ? <div>Mode hors connexion</div> : null;
};
```

### Synchronisation

```
import { useSyncClient } from '@cap-rel/smartcommon';

const sync = useSyncClient({
  apiUrl: import.meta.env.VITE_API_URL,
  getAccessToken: () => api.user?.accessToken,
  scope: ['items'],
  autoSync: true,
  syncInterval: 60000,
});

await sync.create('items', { label: 'Nouveau' });
await sync.sync(); // push + pull

console.log(sync.pendingCount);  // operations en attente
console.log(sync.isSyncing);     // synchronisation en cours
```

Voir [Synchronisation offline](/front/synchronisation) pour le détail, y compris la résolution de conflits.

## Mise à jour de l'application

### registerType

| Valeur | Comportement |
| --- | --- |
| `autoUpdate` | le nouveau Service Worker prend la main sans demander |
| `prompt` | le nouveau Service Worker attend une action explicite |

`autoUpdate` suppose que le Service Worker appelle `skipWaiting()` et `clients.claim()`. En mode `injectManifest`, c'est à vous de les écrire (section 5 du squelette de `sw.js` ci-dessus).

### usePWAUpdate

```
import { usePWAUpdate } from '@cap-rel/smartcommon';

const MyApp = () => {
  const {
    updateAvailable,    // true quand une mise a jour est prete
    updateActivated,    // true quand la mise a jour est activee
    checkForUpdates,    // verification manuelle
    applyUpdate,        // appliquer (skip waiting + reload)
    reloadPage,         // recharger la page
  } = usePWAUpdate({
    autoReload: false,
    checkInterval: 0,
    onUpdateAvailable: () => {},
    onUpdateActivated: () => {},
  });

  return updateAvailable ? <button onClick={applyUpdate}>Mettre à jour</button> : null;
};
```

### UpdatePrompt

Composant prêt à l'emploi. Trois variantes :

| Variante | Description |
| --- | --- |
| `toast` | notification en bas de l'écran (défaut) |
| `banner` | bandeau fixe en haut ou en bas |
| `modal` | fenêtre modale centrée |

```
import { UpdatePrompt } from '@cap-rel/smartcommon';

const App = () => (
  <Provider config={appConfig}>
    <UpdatePrompt
      variant="toast"
      checkInterval={60000}
      labels={{
        title: "Mise à jour disponible",
        message: "Une nouvelle version est disponible.",
        reloadButton: "Rafraîchir",
        dismissButton: "Plus tard",
      }}
    />
    <Router />
  </Provider>
);
```

Le `Provider` accepte aussi une prop `pwaUpdate` qui monte `UpdatePrompt` automatiquement. Voir [Configuration du Provider](/front/configuration).

### Afficher la version qui tourne

Convention SmartMaker : chaque build porte un numéro incrémental, injecté par le Makefile dans `VITE_APP_VERSION` au format `<version du module>.<numéro de build>` (par exemple `2.0.1.42`, suffixé `-dev` pour un build de debug).

```
// mobile/src/utils/constants/vite.js
export const APP_VERSION = import.meta.env.VITE_APP_VERSION;
```

À afficher discrètement en pied de page de connexion, dans les réglages et dans l`'AboutModal`. C'est le premier élément à demander à un utilisateur qui signale un comportement inattendu.

### Mise à jour et données en attente de synchronisation

Une question revient souvent : que deviennent les opérations faites hors ligne quand le Service Worker se met à jour et que la page recharge ?

Elles survivent. La file d'attente n'est pas dans le cache du Service Worker : `useSyncClient` la persiste dans une base IndexedDB dédiée (`smartauth_sync`), avec ses tables `pending_changes`, `pending_conflicts` et `local_tombstones`. Vider le cache du Service Worker ou activer une nouvelle version n'y touche pas.

> [!WARNING]
> Ce qui détruit la file, ce sont les actions qui effacent les données du site : "Clear site data" dans les DevTools, la suppression des données du navigateur, la désinstallation de la PWA. Avant de conseiller l'une de ces manipulations à un utilisateur en dépannage, vérifiez `sync.pendingCount`.

### Diagnostiquer un client bloqué sur une ancienne version

  - DevTools, **Application > Service Workers** : un Service Worker en état `waiting` signale une mise à jour prête mais non activée
  - vérifier que `cleanupOutdatedCaches()` est bien appelé, sinon les anciens caches s'accumulent
  - en dernier recours, **Unregister** puis rechargement avec vidage du cache

## Proposer l'installation

Depuis SmartCommon 1.0.379, le hook `useInstallPrompt` et le composant `InstallPrompt` gèrent la détection de l'installation et l'invitation à installer.

```
import { InstallPrompt } from "@cap-rel/smartcommon";

<InstallPrompt
  labels={{
    title: t("installPrompt.title"),
    message: t("installPrompt.message"),
    installButton: t("installPrompt.install"),
    dismissButton: t("installPrompt.later"),
    gotItButton: t("installPrompt.gotIt"),
  }}
/>
```

À monter une seule fois, haut dans l'arbre, à côté de `<UpdatePrompt />` et à l'intérieur du provider d'API. Le composant ne rend rien tant qu'il n'y a rien à proposer.

> [!WARNING]
> Contrairement à la plupart des composants de SmartCommon, `InstallPrompt` **n'a pas de bundle de traductions** : ses libellés par défaut n'existent qu'en anglais et `locales.fr.InstallPrompt` n'existe pas. Une application francophone doit donc passer ses propres `labels`. Les clés des boutons sont `installButton`, `dismissButton` et `gotItButton`.

Quelques réalités à connaître :
- sur iOS, `beforeinstallprompt` n'existe pas et il n'y a pas de prompt natif : seules des instructions manuelles sont possibles
- "la session n'est pas en mode standalone" ne signifie pas "l'application n'est pas installée"
- `navigator.getInstalledRelatedApps()` ne répond que si le manifest déclare `related_applications` pointant vers lui-même. Le manifest dynamique de smartauth le fait ; avec un manifest statique, c'est à vous de l'écrire

Le détail complet est dans la documentation interne `PWA_INSTALL.md`.

## Build et déploiement

```
# Build de production
npm run build

# Previsualiser le build
npm run preview

# Le build genere :
# - dist/index.html
# - dist/assets/*.js
# - dist/assets/*.css
# - dist/sw.js (Service Worker)
```

Déploiement dans le module Dolibarr :

```
cd mobile
npm run build
cp -r dist/* ../pwa/
```

ou, avec le Makefile du module :

```
make pwa
```

La cible `make pwa` fait plus qu'un `npm run build` : elle incrémente le numéro de build, force `VITE_API_URL` en relatif, dérive la liste des langues des sous-dossiers de `public/locales/`, puis restaure le `.env` de développement.

## Vérifier une PWA

### Dans les DevTools
- **Application > Manifest** : le manifest est chargé, sans erreur d'origine, et les icônes s'affichent
- **Application > Service Workers** : le Service Worker est `activated and running`
- **Application > Cache Storage** : le precache contient le bundle, `index.html` et les fichiers de `locales/`

### Tester le démarrage hors ligne

C'est le test qui compte, et il doit être fait dans cet ordre :

  - charger l'application en ligne, attendre que le Service Worker soit actif
  - naviguer vers une page interne, par exemple `/interventions`
  - cocher **Offline** dans l'onglet Network
  - **recharger la page** (et non simplement naviguer)

Si un écran blanc apparaît à cette étape, le fallback de navigation manque.

### Lighthouse

Onglet Lighthouse des DevTools, catégorie Progressive Web App. Critères attendus : HTTPS (ou localhost), manifest valide avec icônes, Service Worker enregistré, fonctionnement hors connexion, design responsive.

## Pièges à connaître

| Symptôme | Cause |
| --- | --- |
| "ma configuration VitePWA ne fait rien" | options `workbox` utilisées en mode `injectManifest` |
| écran blanc au rechargement hors ligne | `NavigationRoute` absente du `sw.js` |
| manifest introuvable en 404 | lien vers `api.php/manifest` au lieu de `api.php/manifest.webmanifest` |
| "le manifest doit avoir la même origine" | front et API sur deux origines, pas de proxy Vite |
| application inutilisable hors ligne sans erreur | bundle plus gros que `maximumFileSizeToCacheInBytes` |
| clés de traduction brutes hors ligne | `json` absent de `globPatterns` |
| Service Worker enregistré deux fois | `injectRegister: "auto"` et appel manuel à `registerSW` |
| manifest ignoré ou incohérent | `<link rel="manifest">` conservé avec un manifest statique Vite |

## Voir aussi
- [Configuration du Provider](/front/configuration)
- [Stockage de données](/front/stockage-de-donnees)
- [Synchronisation offline](/front/synchronisation)
- [Installation SmartBoot](/howto/smartboot)
- [Vite-PWA](https://vite-pwa-org.netlify.app/)
- [Web app manifests (MDN)](https://developer.mozilla.org/fr/docs/Web/Progressive_web_apps/Manifest)
