Detecting if a PWA/TWA is Installed

Multithreaded JavaScript, O'Reilly 2021, has been published!

I've been starting a product/company lately, Radar Chat. Currently it's built as a PWA (Progressive Web App) for mobile devices that users can install via the iOS / Android "Add to Homescreen" functionality. This process is not a familiar one to most users so to make things smoother I'm displaying instructions in the app. These instructions depend on the user's platform. If the user already installed the app then the instructions shouldn't be displayed at all.

Detecting the platform is rather straightforward. Essentially, if the word "Android" is present in the user agent, then it's an Android device, and if "iPad" or "iPhone" or "iPod" is present, it's an iOS device. The code to check this looks like the following:

const IOS = UA.match(/iPhone|iPad|iPod/);
const ANDROID = UA.match(/Android/);
export const PLATFORM = IOS ? 'ios' : ANDROID ? 'android' : 'unknown';

Detecting if the page is executed via an installed "app" takes a little more effort. Specifically, if the user is launching the "app" by tapping an icon on their homescreen then I'm considering it installed. While googling for an answer I came across this Stack Overflow solution: Javascript to check if PWA or Mobile Web.

The answer comes down to checking three values in the current JavaScript environment. If any one of them is true then the app is presumably running after having been installed in some manner:

// Adapted from Stack Overflow answer
const media = window.matchMedia('(display-mode: standalone)').matches;
const navigator = navigator.standalone;
const andref = document.referrer.includes('android-app://');

(Note that if you're using a different display setting in your manifest.json file, such as fullscreen, then you'll need to adapt the code.)

The original answer checks all three values in an if statement, but here I've broken them apart into individual checks. I did this to make sure the checks were correct for a myriad of situations. Depending on the situation, the result of each of these checks is different, and I've documented them in the next table.

First, lets define some terms. I'll slightly abuse the phrase PWA (Progressive Web Application) to mean a mobile web app that has a service worker and has all of the other requirements so that a mobile browser allows the user to add it to their homescreen. I then assume the PWA has been launched from the homescreen. Next, I'll slightly abuse the phrase TWA (Trusted Web Activities) to mean a packaged PWA that has been installed as a native app via the platform's app store, and that the TWA is properly configured with a .well-known/assetlinks.json file. I'm using Microsoft's PWA Builder to simplify the app store TWA generation process.

Another thing to complicate these tests is that Firefox can be installed as the default system browser on Android. When this happens, tapping either a PWA or TWA icon on the homescreen will launch the application using Firefox as the underlying browser. Chrome and Firefox browsers behave differently enough to warrant documentation.

Here is the result of my testing so far:

Situation media navigator andref OS Ver Browser OK
Android Chrome Web false false false 11 96.0
Android Firefox Web false false false 11 95.1.0
Android Installed PWA Chrome true false false 11 96.0
Android Installed PWA Firefox true false false 11 95.1.0
Android Installed TWA Chrome true false true 11 96.0
Android Installed TWA Firefox false false false 11 95.1.0
iOS Web false false false 14.8.1 604.1?
iOS Installed PWA true true false 14.8.1 604.1?
iOS Installed TWA false false false 14.8.1 604.1?

The situation column describes the OS and browser pair and how the user is running the page. The three media, navigator, and andref columns contain the value of the above variables. The final OK column lets us know if the outcome is correct. Notably, the checks described in the Stack Overflow answer don't work 100% of the time. The first issue is with launching a TWA via Firefox on Android (a rare scenario). The second is when launching a TWA on iOS (more common).

To get a working solution we'll need to dig a little deeper. Specifically, the user agent contains some more hints for us. Let's look at a few more scenarios.

This is the user agent string when iOS is running the installed TWA app:

Mozilla/5.0 (iPhone; CPU iPhone OS 14_8_1 like Mac OS X)
  AppleWebKit/605.1.15 (KHTML, like Gecko)
  Mobile/15E148

And this is the user agent string when iOS is running via the browser or via installed PWA:

Mozilla/5.0 (iPhone; CPU iPhone OS 14_8_1 like Mac OS X)
  AppleWebKit/605.1.15 (KHTML, like Gecko)
  Version/14.1.2 Mobile/15E148 Safari/604.1

And this is the user agent string when iOS is rendering the page via Chrome, which is really just Safari with a different wrapper:

Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X)
  AppleWebKit/605.1.15 (KHTML, like Gecko)
  CriOS/96.0.4664.101 Mobile/15E148 Safari/604.1

Notice that the Safari moniker is missing in the TWA! Luckily, the existing platform check still works. We can then deduce that if the device is running iOS, and if the word "Safari" is missing in the user agent, then it must be installed as a TWA.

In the end, this is the code that I'm using to detect platform and if the current context has been launched via the homescreen:

const UA = navigator.userAgent;

const IOS = UA.match(/iPhone|iPad|iPod/);
const ANDROID = UA.match(/Android/);

export const PLATFORM = IOS ? 'ios' : ANDROID ? 'android' : 'unknown';

const standalone = window.matchMedia('(display-mode: standalone)').matches;

export const INSTALLED = !!(standalone || (IOS && !UA.match(/Safari/)));

There are two main takeaways from my findings. The first is that if Firefox is installed as the system browser on Android then it's currently not possible to know if a page is running as a TWA! I would consider it a bug that the display-mode: standalone check fails. While I've used Firefox as a system browser for multiple years I suspect it's uncommon enough that most users won't encounter it. The second takeaway that only the media check from the Stack overflow answer is useful when detecting installation status. While I'd love to use the cleaner navigator.standalone everywhere it's just not as common.

Thomas Hunter II Avatar

Thomas has contributed to dozens of enterprise Node.js services and has worked for a company dedicated to securing Node.js. He has spoken at several conferences on Node.js and JavaScript, is an O'Reilly published author, and is an organizer of NodeSchool SF.