52

I'm trying to create an "Add To Home Screen" button on my progressive web app, as described in Chrome's documentation.

I'm generally following the prescribed pattern, where I have some hidden button which is displayed when Chrome's beforeinstallprompt event fires. I capture the event once it fires, and then use the event to begin the native install dialogue once my own install button is clicked. The sample code is below:

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
  // Update UI notify the user they can add to home screen
  btnAdd.style.display = 'block';
});

btnAdd.addEventListener('click', (e) => {
  // hide our user interface that shows our A2HS button
  btnAdd.style.display = 'none';
  // Show the prompt
  deferredPrompt.prompt();
  // Wait for the user to respond to the prompt
  deferredPrompt.userChoice
    .then((choiceResult) => {
      if (choiceResult.outcome === 'accepted') {
        console.log('User accepted the A2HS prompt');
      } else {
        console.log('User dismissed the A2HS prompt');
      }
      deferredPrompt = null;
    });
});

The issue I'm running into is that I don't want to show my install button (btnAdd) if the user has already installed the web app to thier home screen, and I'm having trouble figuring out how to check for that scenario.

I was hoping to modify the above code as follows:

window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67 and earlier from automatically showing the prompt
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;

  // If the user has not already installed...
  deferredPrompt.userChoice
    .then(choiceResult => {
      if (choiceResult === undefined) {
        // Update UI notify the user they can add to home screen
        btnAdd.style.display = 'block';
      }
    });
});

So that the install button won't be displayed if the user has already installed. But this doesn't seem to work. It appears that if they haven't made a choice already, accessing userChoice just prompts the user directly with the native dialogue.

I'm not really sure how the beforeinstallevent works, so this might not even be a good strategy. Ideally I was hoping this would work something like something like navigator.serviceWorker.ready(), which returns a Promise rather than using browser events to try and figure out when stuff is ready.

In any case, are there any ideas on how I can check that the user has installed to home screen before I show my own home screen install button?

Edit: As Mathias has commented, checking for the event before showing the button should be sufficient. I believe the issue I was having is a result of using localhost, which appears to continually fire the beforeinstallprompt event even after installation, which is not the intended behavior. Hosting the code solved the issue.

David
  • 2,846
  • 3
  • 22
  • 34
  • Wait. I just re-read your issue. It looks like you are already only showing the button after you intercept the prompt, correct? Once they install, you should not receive another prompt to intercept from Chrome? – Mathias Aug 07 '18 at 22:11
  • 3
    I believe that's correct. I think I just didn't fully understand the API, and I was testing on localhost, which for some reason appears to continually fire the `beforeinstallprompt` event even after the app has been installed. Once I hosted my code, the `beforeinstallprompt` stopped firing post-install, which resolves my issue. – David Aug 08 '18 at 20:28

6 Answers6

66

Perhaps, don't show the button until you intercept the automatic pop-up?

or
In your code, check to see if the window is standalone
If it is, you need not show the button

if (window.matchMedia('(display-mode: standalone)').matches) {  
    // do things here  
    // set a variable to be used when calling something  
    // e.g. call Google Analytics to track standalone use   
}  

My example tester here
https://a2hs.glitch.me

Source code for my tester
https://github.com/ng-chicago/AddToHomeScreen

Mathias
  • 4,243
  • 4
  • 25
  • 34
  • 2
    I am already checking for the event before showing the pop-up, but I do believe that is the correct solution. See my comment above. Checking for standalone mode is definitely a handy trick, but it would mean the install button still shows in the browser view (non-standalone) post-install. – David Aug 08 '18 at 20:28
  • This code detects if the website was launched as an app - it does not detect if, say, the user has the app installed but is viewing the website from their browser. – Alex Walker Nov 04 '19 at 14:21
  • 2
    @AlexWalker Correct. Web pages in the browser do not have permission to view apps installed on a device. In most cases users who have installed the PWA from the installed Icon will use that icon and not the URL. So this is not a problem that will ahppen very often (I Think) – Mathias Nov 04 '19 at 18:50
  • @AlexWalker but... You could detect if the beforeinstallprompt fired to tell you if the user has NOT installed the PWA yet and is using the browser version. This will work for everyone who has NOT declined an install prompt. – Mathias Nov 04 '19 at 18:52
  • True, provided they're using Chrome that would be sufficient. – Alex Walker Nov 05 '19 at 10:08
  • Excellent solution, thanks, solved my problem. :) – Mitch Williams Jun 18 '23 at 17:39
  • Worked for me, thanks for the answer! – Brandon Waring Aug 26 '23 at 23:32
26

To answer original question. With latest versions of Chrome you can use window.navigator.getInstalledRelatedApps(). It returns a promise with an array of installed apps that your web app specifies as related in the manifest.json. To enable this to work you need to add related_applications field to manifest.json

  "related_applications": [{
    "platform": "webapp",
    "url": "https://app.example.com/manifest.json"
  }]

And then you can use it like:

//check if browser version supports the api
if ('getInstalledRelatedApps' in window.navigator) {
  const relatedApps = await navigator.getInstalledRelatedApps();
  relatedApps.forEach((app) => {
    //if your PWA exists in the array it is installed
    console.log(app.platform, app.url);
  });
}

Source: API docs

Now you can display some elements depending if your app is installed. E.g: you can display "Open app" button and redirect user to PWA. But remember to disable it when the user is already in the app using @Mathias's answer and checking (display-mode: standalone)

However, regarding your use case. You should display install button only when beforeinstallprompt is intercepted. Browser does not fire this event if the PWA is already installed on the device. And when prompt is fired and choiceResult.outcome === 'accepted' you hide the button again.

DLesjak
  • 267
  • 3
  • 5
  • This should be the accepted answer. Great write! I also tried to test it on desktop (Ubuntu 20.04 and Chrome 84). I use devtools to mimic the mobile device. It installs the app, but the getInstalledRelatedApps does not detect it is installed. – croraf Jul 31 '20 at 21:56
  • 1
    It is stated on the page that it works for _**Android**: Chrome 84 or later_. So probably your **desktop** Chrome doesn't support it despite of mimicking mobile device. – DLesjak Aug 21 '20 at 07:52
  • I tried your answer on chrome for desktop, but didn't work – shamaseen Nov 17 '20 at 21:50
  • @DLesjak can you explain how to do this ? "Now you can display some elements depending if your app is installed. E.g: you can display "Open app" button and redirect user to PWA. " – Rahul Vyas Jan 11 '21 at 11:58
  • @RahulVyas You should check `getIntstalledRelatedApps()` and after you encounter your app in `relatedApps` array you can update your UI and display the button. On button click you can redirect the user like `window.open(window.location.href, "_blank");` Chrome will open the app. – DLesjak Jan 12 '21 at 15:17
  • @DLesjak thanks buddy. I'll try and let you know. Also I assume to open native apps we need deep linking and Universal links thing right ? – Rahul Vyas Jan 18 '21 at 04:49
  • @RahulVyas it can be a normal web link. But it only works for Chrome (Android) and potentially for Samsung Internet. Those browsers install PWA using WebAPK. From docs: `When a Progressive Web App is installed on Android, it will register a set of intent filters for all URLs within the scope of the app. When a user clicks on a link that is within the scope of the app, the app will be opened, rather than opening within a browser tab.` [WebAPKs](https://developers.google.com/web/fundamentals/integration/webapks) – DLesjak Jan 19 '21 at 08:46
  • Even on Samsung or version of Chrome, it seems not to work. It works for real app, but not for PWA. With a PWA, the getInstalledRelatedApps() returned array is empty. – Peter Mar 23 '21 at 18:24
  • Current support for `getInstalledRelatedApps` is [here](https://caniuse.com/mdn-api_navigator_getinstalledrelatedapps) – Jan Turoň Nov 06 '22 at 08:58
7

Here is the simple function that tells you, this app is open in browser or in pwa. original source link

function getPWADisplayMode() {
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
  if (document.referrer.startsWith('android-app://')) {
    return 'twa';
  } else if (navigator.standalone || isStandalone) {
    return 'standalone';
  }
  return 'browser';
}
Sanjeet kumar
  • 3,333
  • 3
  • 17
  • 26
6

I don't see how this is the correct answer, because this is basically a check if user uses the App already, but the behavior we wan't is "When the user is on the web and tries to install the app again to tell him that he already has the app in his device". Upon me this is not an answer that solves this.

What we can do is: 1. When the user clicks install but has the application on his device In this case the beforeinstallprompt event WON'T BE fired so this event will return null. We store the result in global variable and when the result is null we show this to user that he already has the app installed. 2. When the user clicks install but doesn't have the application on his device In this case the beforeinstallprompt event WILL be fired so this event will return access to show the prompt. We can store the result in global variable and if it is not NULL (which won't be) because beforeinstallprompt will be fired if the user don't have the app on his device we show the prompt() to the user.

I doubt if mine solution is good too but I think that the Question and the correct answer don't have nothing in common

window.addEventListener("beforeinstallprompt", event => {
  window.deferedPrompt = event;
});

handleButtonClick = () => {
 const promptEvent = window.deferedPrompt;
 if(!promptEvent){
 // DO SOMETHING
 }
//Show the add to home screen prompt
promptEvent.prompt()

promptEvent.userChoice.then((result: any) => {
      // Reset the deferred prompt variable, since
      // prompt() can only be called once.
      window.deferedPrompt = null;.
    });
}



<button onClick={handleButtonClick}>Install</button>
Boris Dedejski
  • 287
  • 2
  • 4
  • 17
4

HTML

<!-- start with hidden button -->
<button id="install" style="display:none;">install</button>

JAVASCRIPT

// variable store event
window.deferredPrompt = {};

// get button with id
const install_button = document.querySelector('#install');

// if the app can be installed emit beforeinstallprompt
window.addEventListener('beforeinstallprompt', e => {
  // this event does not fire if the application is already installed
  // then your button still hidden ;)

  // show button with display:block;
  install_button.style.display = 'block';

  // prevent default event
  e.preventDefault();

  // store install avaliable event
  window.deferredPrompt = e;

  // wait for click install button by user
  install_button.addEventListener('click', e => {
    window.deferredPrompt.prompt();
    window.deferredPrompt.userChoice.then(choiceResult => {
      if (choiceResult.outcome === 'accepted') {
        // user accept the prompt

        // lets hidden button
        install_button.style.display = 'none';
      } else {
        console.log('User dismissed the prompt');
      }
      window.deferredPrompt = null;
    });
  });
});

// if are standalone android OR safari
if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true) {
  // hidden the button
  install_button.style.display = 'none';
}

// do action when finished install
window.addEventListener('appinstalled', e => {
  console.log("success app install!");
});
Leonardo Filipe
  • 1,534
  • 15
  • 9
  • 1
    As in many articles and blog, you write "do action when finished install". Unfortunatly, the event 'appinstalled' don't have the correct name as this event if fired at the begining of the installation and NOT at the end. – Peter Mar 23 '21 at 11:59
0

Another way I've found to do this is to use IndexedDB. I import "idb-keyval" (https://www.npmjs.com/package/idb-keyval) to make it simple to get/set to the IndexedDb. Then I store a value that gets checked on the next page load. One difference with this method, is that it will let you check if your application is installed already if you visit the application webpage from the browser instead of the installed app shortcut.

import * as IDB from "idb-keyval";

let deferredPropt;

// Get the stored value from the IndexedDb
IDB.get("isInstalled").then((val) => {
  if (val) {
    // If it exists, run code based on an installed pwa
  } else {
    log({ isInstalled: false });
  }
});

window.addEventListener("beforeinstallprompt", (e) => {
  e.preventDefault();
  deferredPrompt = e;
  document.getElementById("installApp").addEventListener("click", async () => {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    if (outcome == "accepted") {
      // Set the value into the IndexedDb on installation of the PWA
      IDB.set("isInstalled", true);
    }
  });
});
TK421
  • 801
  • 5
  • 16
  • 24