Implementing the beforeinstallprompt Event in Nuxt

How I got the `beforeinstallprompt` event working on a Nuxt app, allowing users to install it as a Progressive Web App (PWA).

I found getting the beforeinstallprompt event to work a little complicated, so I wanted to share exactly what I did to get it working in The House App. Before I start, if you are wondering what the beforeinstallprompt event is, it’s a browser event that triggers when a Progressive Web App meets certain criteria and is ready to be installed by the user. It allows developers to show a custom “Install App” button and trigger the native install prompt when the user clicks it.

I found this documentation from MDN really helpful while figuring this out.

Before I knew this was possible, I had a separate page explaining how to install the app manually. That worked, but it added unnecessary friction and wasn’t a great experience.

Note: This API currently only works in Chromium-based browsers as of the publish of this post.

About the problem

I wanted to implement the beforeinstallprompt event in The House App to allow users to install it as a PWA, but I ran into some issues getting it to work. The event wasn’t triggered, the saved prompt was always null, and there were no clear errors to help me debug.

What was actually happening

I started with listening for the beforeinstallprompt event on the Account page load. But it didn’t work, the event never triggered. Eventually after some digging I found this Stack Overflow thread. The event was always triggered but by the time the Vue page was mounted it was too late.

My fix

I made this install-pwa.js script to get the prompt and store it before Vue loaded.

install-pwa.js
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
window.pwaDeferredPrompt = e;
});
window.addEventListener("appinstalled", () => {
window.pwaDeferredPrompt = null;
});

Then I needed to add it to the Nuxt config to make sure it gets loaded before the Vue app:

nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: '/install-pwa.js',
async: false,
}
]
},
},
}

This script listens for the beforeinstallprompt event and stores it in a global variable window.pwaDeferredPrompt. It also listens for the appinstalled event to clear the stored prompt when the app is installed. This way, when my Vue component mounts, it can check if the prompt is available and show the install button.

Using it in Vue

Now that the event is stored globally, the Vue app can safely access it. Here’s the composable I ended up with:

export const usePromptInstallApp = () => {
const event = computed<BeforeInstallPromptEvent | null>(() => window.pwaDeferredPrompt);
const isSupported = computed<boolean>(() => {
return "BeforeInstallPromptEvent" in window && event.value != null;
});
const handleButtonClick = async () => {
if (event.value == null) {
console.warn("No install prompt event available");
return;
}
await event.value.prompt(); // Show the install UI
const choiceResult = await event.value.userChoice;
if (choiceResult.outcome === "accepted") {
onInstall();
}
};
const onInstall = () => {
if ("BeforeInstallPromptEvent" in window) {
// Do something when the app is installed, like showing a success message or updating the UI
}
};
const handleAppInstalled = () => {
onInstall();
};
onMounted(() => {
window.addEventListener("appinstalled", handleAppInstalled);
});
onBeforeUnmount(() => {
window.removeEventListener("appinstalled", handleAppInstalled);
});
return {
event,
handleButtonClick,
isSupported,
};
};

Final result

Once I moved the event listener outside of Vue and captured it early everything worked as expected. Thank you for reading!