androidprogressive-web-appsservice-worker

How to install a Progressive Web App (PWA) on Android


I have a PWA that works on a desktop browser and shows the install icon in the address bar (Brave and Chrome browsers). There is a manifest.json and the service worker (sw.js) is recognised, and DevTools show it as running. So I am reasonably sure my code is complete.

This is my sw.js file:

// Service Worker
const cacheName = "paul-address";
const filesToCache = [
  "/test/",
  "/test/index.html",
  "/test/js/index.js",
  "/test/styles/styles.css"
];

self.addEventListener("install", e => {
  console.log("[ServiceWorker**] Install");
  e.waitUntil(
    caches.open(cacheName).then(cache => {
      console.log("[ServiceWorker**] Caching app shell");
      return cache.addAll(filesToCache);
    })
  );
});

self.addEventListener("activate", event => {
  caches.keys().then(keyList => {
    return Promise.all(
      keyList.map(key => {
        if (key !== cacheName) {
          console.log("[ServiceWorker] - Removing old cache", key);
          return caches.delete(key);
        }
      })
    );
  });
});

// sw.js - Cache-first strategy for static assets
self.addEventListener('fetch', event => {
  // Only handle GET requests
  if (event.request.method !== 'GET') return;
  
  const url = new URL(event.request.url);
  
  // Cache-first for static assets
  if (url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico)$/)) {
    event.respondWith(
      caches.match(event.request)
        .then(response => {
          if (response) {
            console.log('Serving from cache:', event.request.url);
            return response;
          }
          
          // Fetch and cache new resources
          return fetch(event.request)
            .then(response => {
              // Don't cache non-successful responses
              if (!response || response.status !== 200 || response.type !== 'basic') {
                return response;
              }
              
              const responseToCache = response.clone();
              caches.open(CACHE_NAME)
                .then(cache => {
                  cache.put(event.request, responseToCache);
                });
              
              return response;
            });
        })
        .catch(() => {
          // Return fallback for images
          if (event.request.destination === 'image') {
            return caches.match('/images/fallback.png');
          }
        })
    );
  }
});

However, when I test it on an Android device, I get no Install prompt on either Brave or Chrome browsers.

If I manually go to "Add to home screen" it doesn't quite work as I hoped.

On Brave it presents me with two options: Install and Add a shortcut. Even when I select Install despite the screen saying 'Installing ..." it actually only creates a home screen shortcut.

On Chrome it both Installs or Adds a shortcut, depending on which option I select, but not without manually selecting.

What I really want is for Install to work as expected, i.e. to actually install the PWA like an app and a prompt to appear automatically for the user to start the install of the app.


Solution

  • On chrome on mobile devices, the install button doesn't appear every time. This depends on machine learning algorithm as explain here :
    developer.chrome.com/blog/how_chrome_helps_users_install_the_apps_they_value

    To help user to install the PWA without using "Add to home screen", you can try to use the event beforeinstallprompt.

    There are some information here : https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable#triggering_the_install_prompt

    And next is an example of code from https://whatpwacando.today/installation

    let deferredEvent;
    
    window.addEventListener('beforeinstallprompt', (e) => {
      // prevent the browser from displaying the default install dialog
      e.preventDefault();
      
      // Stash the event so it can be triggered later when the user clicks the button
      deferredEvent = e;
    });
    
    installButton.addEventListener('click', () => {
      // if the deferredEvent exists, call its prompt method to display the install dialog
      if(deferredEvent) {
        deferredEvent.prompt();
      }
    });