PWA - Progressive Web App / Installation et manifest

Web App Manifest et installation PWA : A2HS sans App Store

Le Web App Manifest est un fichier JSON qui déclare votre application comme installable : icône sur l'écran d'accueil, mode plein écran sans barre de navigation, couleur de thème. C'est la différence entre un favori dans le navigateur et une vraie application qui s'ouvre comme une app native.

Nous expliquons ici comment configurer ce manifest avec vite-plugin-pwa, gérer le prompt d'installation sur Android et le cas particulier d'iOS Safari avec notre composable useInstallPrompt utilisé dans nos projets PWA.

CHAMPS DU MANIFEST

Ce que le manifest déclare au navigateur

Chaque champ a une incidence directe sur l'expérience d'installation et sur l'apparence de l'application une fois installée.

Anatomie du manifest d'une PWA et son effet sur l'application installée

name / short_name

Le nom affiché sur l'écran d'accueil. name est le nom complet (dialogue d'installation), short_name est le nom court sous l'icône (limité à 12 caractères sur Android).

display: standalone

L'application s'ouvre sans barre d'adresse ni navigation du navigateur. L'utilisateur a l'impression d'une app native. fullscreen supprime aussi la barre de statut (heure, batterie), rarement souhaitable.

icons (192px et 512px)

Deux tailles minimum : 192x192 pour Android et 512x512 pour le splash screen. L'icône maskable permet à Android de découper l'icône dans n'importe quelle forme (rond, carré arrondi...) selon le launcher.

theme_color / background_color

theme_color colorise la barre d'état et le switcher d'apps sur Android. background_color est la couleur du splash screen pendant le chargement initial. Elle doit correspondre à la couleur de fond de l'app pour éviter le flash blanc.

start_url

L'URL ouverte quand l'utilisateur lance l'app depuis l'écran d'accueil. Mettre / dans la plupart des cas. Peut inclure un paramètre de tracking (/?source=pwa) pour distinguer les sessions PWA dans les analytics.

orientation

portrait pour les apps mobiles classiques, landscape pour les tablettes de présentation ou les jeux. La valeur any laisse l'appareil décider, rarement recommandé pour les interfaces métier qui n'ont pas été conçues pour les deux orientations.

COMPORTEMENT PAR PLATEFORME

Android et iOS : deux comportements très différents

La gestion du prompt d'installation est la principale différence entre les deux plateformes. Android est standardisé, iOS nécessite une instruction manuelle.

Android Chrome

Automatisable

Chrome déclenche l'événement beforeinstallprompt automatiquement quand les critères PWA sont remplis. On peut intercepeter cet événement et déclencher le prompt natif Chrome au moment souhaité via promptInstall().

  • Prompt natif : boîte de dialogue Chrome reconnue par les utilisateurs
  • Événement appinstalled pour confirmer l'installation
  • display-mode: standalone pour détecter l'état installé

iOS Safari

Instruction manuelle

iOS Safari ne supporte pas beforeinstallprompt. L'installation est manuelle : l'utilisateur appuie sur le bouton Partager (icône rectangle avec flèche) puis "Ajouter à l'écran d'accueil". L'app doit afficher cette instruction.

  • Pas de beforeinstallprompt sur iOS
  • showIosHint pour afficher le guide d'installation
  • Supporté sur iOS 11.3+ (disponible sur tous les iPhones récents)

Notre approche : le composable useInstallPrompt expose trois valeurs : deferredPrompt (non nul si Android et l'événement est disponible), showIosHint (vrai si iOS et non installée), isInstalled (vrai si déjà en mode standalone). Le composant parent adapte l'interface selon ces trois cas sans logique supplémentaire.

Documentation développeurs

Code source : manifest et installation de nos POC

La configuration du manifest ce fait dans vite.config.ts et l'invite d'installation via le composable useInstallPrompt. C'est la base que nous réutilisons sur chaque nouveau projet PWA.

Configuration du manifest : vite-plugin-pwa

Extrait de vite.config.ts

Tous les champs du manifest sont déclarés dans vite.config.ts. Le plugin vite-plugin-pwa génère le fichier manifest.webmanifest et injecte la balise <link rel="manifest"> dans le HTML au build.

VitePWA({
  registerType: 'autoUpdate',

  // Fichiers copiés dans dist/ et précachés par Workbox (logo SVG non inclus par défaut)
  includeAssets: ['icons/smartbooster-logo.svg'],

  manifest: {
    name:             'Démo PWA Terrain SmartBooster',
    short_name:       'Démo PWA Terrain',     // ≤ 12 caractères recommandé
    description:      'Application terrain pour techniciens, démo offline',
    theme_color:      '#2563eb',         // barre de statut Android + switcher d'apps
    background_color: '#f8fafc',         // couleur du splash screen au chargement
    display:          'standalone',      // pas de barre d'adresse, look natif
    orientation:      'portrait',
    start_url:        '/',

    icons: [
      {
        src:   '/icons/pwa-192x192.png',
        sizes: '192x192',
        type:  'image/png',
      },
      {
        src:   '/icons/pwa-512x512.png',
        sizes: '512x512',
        type:  'image/png',
      },
      {
        src:     '/icons/pwa-512x512.png',
        sizes:   '512x512',
        type:    'image/png',
        purpose: 'any maskable',   // permet à Android de découper l'icône
      },
    ],
  },
})

Icône maskable : Android adapte la forme de l'icône selon le launcher (rond, carré arrondi, goutte...). L'icône maskable doit avoir une zone de sécurité de 10% sur chaque bord pour éviter que le contenu soit découpé. L'outil maskable.app permet de tester le rendu avant déploiement.

useInstallPrompt : gestion du prompt Android et de l'hint iOS

composable partagé

Le composable intercepte l'événement beforeinstallprompt au niveau module (avant tout montage de composant) pour ne pas le rater s'il se déclenche tôt au chargement de la page. Il expose aussi showIosHint pour afficher une instruction manuelle sur iOS où cet événement n'existe pas.

import { isIOS, isAndroid } from '@/utils/deviceDetect.js'

// Singleton module-level : les listeners sont enregistrés dès le premier import,
// avant le montage du premier composant. Si beforeinstallprompt se déclenche
// pendant le chargement (fréquent), on ne le rate pas.
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null)

// Détection via display-mode : si l'app est déjà installée, ne pas afficher
// la bannière d'installation.
const isInstalled = ref(window.matchMedia('(display-mode: standalone)').matches)

const isMobile = isIOS || isAndroid

// Android Chrome : l'événement peut se déclencher avant le montage des composants
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault()   // empêche le mini infobar automatique de Chrome
  if (isMobile) deferredPrompt.value = e as BeforeInstallPromptEvent
})

// Mise à jour de isInstalled quand l'utilisateur accepte l'installation
window.addEventListener('appinstalled', () => {
  deferredPrompt.value = null
  isInstalled.value = true
})

// iOS : pas de beforeinstallprompt. On affiche un hint textuel
// ("Appuyez sur Partager → Ajouter à l'écran d'accueil") si l'app n'est pas installée.
const showIosHint = ref(isMobile && isIOS && !isInstalled.value)

export function useInstallPrompt() {
  async function promptInstall(): Promise<void> {
    if (!deferredPrompt.value) return
    await deferredPrompt.value.prompt()
    const { outcome } = await deferredPrompt.value.userChoice
    // outcome : 'accepted' | 'dismissed'
    deferredPrompt.value = null
  }

  return { deferredPrompt, isInstalled, showIosHint, promptInstall }
}

Pourquoi au niveau module : beforeinstallprompt peut se déclencher très tôt, avant que le composant qui gère la bannière ne soit monté. Si le listener est dans onMounted, l'événement est perdu et la bannière ne s'affiche jamais, même si l'app est installable.

FAQ

Les réponses à vos questions

Et si vous ne trouvez pas ce que vous cherchez, nous serons ravis de vous répondre en direct lors d'un rendez-vous entre humains !

Non. Sur Android, Chrome affiche un prompt natif 'Ajouter à l'écran d'accueil' quand les critères PWA sont remplis (HTTPS, Service Worker actif, manifest valide). Sur iOS Safari, l'utilisateur appuie sur le bouton Partager puis 'Ajouter à l'écran d'accueil'. Pas de Store, pas de validation tierce. L'installation prend 5 secondes et l'app apparaît sur l'écran d'accueil comme n'importe quelle application native.

Chrome déclenche l'événement beforeinstallprompt automatiquement quand les critères PWA sont satisfaits (HTTPS, Service Worker enregistré, manifest avec name, short_name, start_url et icône 192px). Notre composable useInstallPrompt intercepte cet événement et affiche une bannière personnalisée au moment choisi. On peut aussi appeler promptInstall() à n'importe quel moment pour déclencher le prompt natif de Chrome.

Via window.matchMedia('(display-mode: standalone)').matches. Si la PWA est lancée depuis l'écran d'accueil (mode standalone), cette valeur est true. On peut aussi écouter l'événement appinstalled qui se déclenche juste après que l'utilisateur a accepté l'installation. Notre composable useInstallPrompt combine les deux pour masquer automatiquement la bannière d'installation quand l'app est déjà installée.

En théorie oui, en pratique Android peut modifier la forme de l'icône selon le launcher (rond, carré, goutte...). L'attribut purpose: 'maskable' sur l'icône 512px permet à Android de découper l'icône dans la forme souhaitée. Sans icône maskable, Android ajoute parfois un fond blanc autour de l'icône qui rend le résultat peu soigné. iOS respecte l'icône telle quelle avec des coins arrondis automatiques.

Sur Android, oui facilement via le mécanisme TWA (Trusted Web Activity) et l'outil Bubblewrap de Google. La PWA est encapsulée dans une app Android distribuable sur le Play Store sans modifier le code. Sur iOS, des outils tiers comme PWABuilder génèrent un wrapper Swift, mais l'approbation App Store reste incertaine et certaines PWA sont refusées. Pour les applications métier internes, l'installation directe depuis Safari est la solution la plus simple et elle fonctionne parfaitement.

Oui, avec display: standalone dans le manifest. L'app s'ouvre en plein écran sans barre d'adresse ni navigation du navigateur. L'utilisateur ne peut pas voir l'URL et l'expérience ressemble à une application native. Sur iOS, la barre de statut (heure, batterie) reste visible. Sur Android, elle peut être colorisée avec theme_color pour correspondre à l'identité visuelle de l'application.

Votre projet

Vous voulez une application qui s'installe comme une app native ?

Une PWA installée sur l'écran d'accueil offre la même expérience qu'une app Store, sans les contraintes de validation et de distribution. Discutons de votre projet.

POUR ALLER PLUS LOIN

Approfondir la stack PWA

Mode hors ligne : Service Worker + IndexedDB

Workbox, Dexie.js et queue de synchronisation : l'architecture offline-first que nous utilisons pour les applications terrain.

APIs mobiles dans une PWA

GPS, scan QR, caméra, micro et torche : les composables Vue.js pour accéder aux capacités matérielles sans développement natif.

Application de visite guidée

Un projet PWA en production : installation A2HS, carte Leaflet, scan QR et contenu hors ligne pour des parcours patrimoniaux.

Application mobile terrain sur mesure

La solution complète pour les équipes terrain : GPS, photos, formulaires hors ligne et synchronisation au retour au bureau.

PWA : présentation générale

Comparatif PWA vs natif, cas d'usage, APIs mobiles disponibles et librairies clés de notre stack de développement.

Vous avez un projet ?

Contactez-nous pour savoir comment nous pouvons vous aider.