Detecting if a PWA/TWA is Installed
Support this website by purchasing prints of my photographs! Check them out here.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.