This as of April 2023.
Put your button wherever you want, but keep it invisible for now. Then we will display it if necessary.
<button type=button id=installbutton style='display:none'>Install</button>
Regardless of our logic to display the button, we will always hide it if the app is opened in standalone mode. We can do this using css, and so this will essentially override our hide/show logic. This works in all browsers. That's the easy part.
@media all and (display-mode: standalone) {
#installbutton {display:none}
}
So with the css in place, the following only applies when the app is opened in a browser, unfortunately, in a browser there's no way to know for sure if the app is installed. Saving the install state in localStorage or a cookie is pointless because those can be deleted and then your app knows nothing. (Plus see point #4 below)
The best we can do is check the 'beforeinstallprompt' event. If we can add a listener to this event then that means the app is not yet installed, and the device is not iOS. Those are the only two things we know for sure, but it could also be another browser that doesn't support it, like Firefox on Android. (In my code I'm not addressing Firefox on Android)
For iOS, instead of prompting for the install, we will popup a message with instructions on how to install manually.
var prompt;
var beforeinstallpromptsupport = false;
window.addEventListener('beforeinstallprompt', function(e){
// We now know the app is not yet installed and this is not ios
// Note: If the app is installed through Edge on Android, this event will still fire in Edge.
// If however the app is installed through Chrome on Android, this event will not fire in Edge.
// So it appears that edge only consider the app installed if it is installed through Chrome. Sigh.
beforeinstallpromptsupport = true;
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Show button
$('#installbutton').show();
// Bind native install prompt to click of button
// Then hide button so it can not be clicked again (or if you prefer, disable it)
$('#installbutton').on('click', function(){
e.prompt();
$('#installbutton').hide();
})
});
function alternatePrompt(){
// This function only matters if we can not bind the native prompt to the 'beforeinstallprompt' event.
// So this code only runs if we are on iOS or if the app is already installed
if(!beforeinstallpromptsupport){
// Detects if device is on iOS
const ios = () => {
const userAgent = window.navigator.userAgent.toLowerCase();
return /iphone|ipad|ipod/.test(userAgent);
}
if (ios()){
// Show button
$('#installbutton').show();
// Change what we do on click of our button
// ie. Instead of a native install prompt, we popup instructions for a manual install
$('#installbutton').off('click').on('click', function(){
promptPopup('iosinstructions');
})
}else{
// This is not iOS and the beforeinstallprompt event is not supported
// Therefore, app is likely already installed, so do not display button
}
}
}
// Check for event support after half a second (Without this delay our code will not have had the chance to set 'beforeinstallpromptsupport')
window.setTimeout(alternatePrompt,500);
Note 1: This does not handle Firefox on Android, nor I'm sure other less popular browsers.
Note 2: This was not tested on Mac.
Note 3: iOS will always show the button because there is no way to tell if it is installed or not.
Note 4: The reason I don't keep track of installed state in localStorage is because if the 'beforeinstallprompt' event exists, that event only executes if the app is not yet installed, so that is a better method of determination. And when the event does not exist (iOS) there's no good time to set the localStorage as there's no way to know when the app was installed (unless you want to assume the app was installed when the user clicked the button).
Note 5: My code "promptPopup('iosinstructions')"
is based on the mobiscroll framework. You can generate a popup however you like.