vite-pwa / vite-plugin-pwa

Zero-config PWA for Vite
https://vite-pwa-org.netlify.app/
MIT License
2.93k stars 199 forks source link

Unable to get custom install prompt to work Nuxt 3 #649

Open chronicadventure opened 5 months ago

chronicadventure commented 5 months ago

I have a component that shows up with an install button. Once the user clicks the install button I'm using await $pwa.install() but no install prompt shows up. There are no console errors and there are no manifest errors in the chrome application tab. Here is my manifest file:

    manifest: {
      name: 'Chronic Adventure',
      description: "Your road life companion for van lifers and RV enthusiasts. Map your travels, share your favorite spots, and discover new adventures with a community that travels with you.",
      theme_color: '#E8ECF5',
      short_name: 'Chronic',
      start_url: '/',
      display: 'standalone',
      "icons": [
        {
          "src": "pwa/android/android-launchericon-512-512.png",
          "sizes": "512x512"
        },
        {
          "src": "pwa/android/android-launchericon-512-512.png",
          "sizes": "512x512",
          "purpose": "any"
        },
        {
          "src": "pwa/android/android-launchericon-512-512.png",
          "sizes": "512x512",
          "purpose": "maskable"
        },
        {
          "src": "pwa/android/android-launchericon-192-192.png",
          "sizes": "192x192"
        },
        {
          "src": "pwa/android/android-launchericon-144-144.png",
          "sizes": "144x144"
        },
        {
          "src": "pwa/android/android-launchericon-96-96.png",
          "sizes": "96x96"
        },
        {
          "src": "pwa/android/android-launchericon-72-72.png",
          "sizes": "72x72"
        },
        {
          "src": "pwa/android/android-launchericon-48-48.png",
          "sizes": "48x48"
        },
        {
          "src": "pwa/ios/16.png",
          "sizes": "16x16"
        },
        {
          "src": "pwa/ios/20.png",
          "sizes": "20x20"
        },
        {
          "src": "pwa/ios/29.png",
          "sizes": "29x29"
        },
        {
          "src": "pwa/ios/32.png",
          "sizes": "32x32"
        },
        {
          "src": "pwa/ios/40.png",
          "sizes": "40x40"
        },
        {
          "src": "pwa/ios/50.png",
          "sizes": "50x50"
        },
        {
          "src": "pwa/ios/57.png",
          "sizes": "57x57"
        },
        {
          "src": "pwa/ios/58.png",
          "sizes": "58x58"
        },
        {
          "src": "pwa/ios/60.png",
          "sizes": "60x60"
        },
        {
          "src": "pwa/ios/64.png",
          "sizes": "64x64"
        },
        {
          "src": "pwa/ios/72.png",
          "sizes": "72x72"
        },
        {
          "src": "pwa/ios/76.png",
          "sizes": "76x76"
        },
        {
          "src": "pwa/ios/80.png",
          "sizes": "80x80"
        },
        {
          "src": "pwa/ios/87.png",
          "sizes": "87x87"
        },
        {
          "src": "pwa/ios/100.png",
          "sizes": "100x100"
        },
        {
          "src": "pwa/ios/114.png",
          "sizes": "114x114"
        },
        {
          "src": "pwa/ios/120.png",
          "sizes": "120x120"
        },
        {
          "src": "pwa/ios/128.png",
          "sizes": "128x128"
        },
        {
          "src": "pwa/ios/144.png",
          "sizes": "144x144"
        },
        {
          "src": "pwa/ios/152.png",
          "sizes": "152x152"
        },
        {
          "src": "pwa/ios/167.png",
          "sizes": "167x167"
        },
        {
          "src": "pwa/ios/180.png",
          "sizes": "180x180"
        },
        {
          "src": "pwa/ios/192.png",
          "sizes": "192x192"
        },
        {
          "src": "pwa/ios/256.png",
          "sizes": "256x256"
        },
        {
          "src": "pwa/ios/512.png",
          "sizes": "512x512"
        },
        {
          "src": "pwa/ios/1024.png",
          "sizes": "1024x1024"
        }
      ],
      screenshots: [
        {
          "src": "profileScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316"
        },
        {
          "src": "bookmarksScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316"
        },
        {
          "src": "feedScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316"
        },
        {
          "src": "profileScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316",
          "form_factor" : "wide"
        },
        {
          "src": "bookmarksScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316",
          "form_factor" : "wide"
        },
        {
          "src": "feedScreenShot.jpg",
          "type": "image/jpg",
          "sizes": "1080x2316",
          "form_factor" : "wide"
        }
      ]

    },
    devOptions: {
      enabled: true,
      suppressWarnings: true,
      navigateFallbackAllowlist: [/^\/$/],
      type: 'module',
    },
    registerType: "autoUpdate",
    client: {
      installPrompt: true
    },
    workbox: {
      //globPatterns: ['**/*.{js,css,html,ico,png,svg}']
    }
  }

Here is the vue component:

<template>
    <div v-if="needsToSeeThePrompt() & isPromptOpen" class="fixed inset-0 flex items-center justify-center p-4 z-50">
        <!-- Overlay -->
        <div class="absolute inset-0 bg-black bg-opacity-50 z-0"></div>

        <!-- Prompt Box -->
        <transition name="fade">
            <div class="bg-white rounded-lg shadow-lg" style="z-index: 2001;">
                <!-- Close Button -->
                <div class="flex justify-end">
                    <button @click="closePrompt" class="text-black">
                        <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>
                <!-- Content -->
                <div class="text-center p-6 m-4">
                    <img src="~/assets/images/logo-icon.png" alt="App Icon" class="mx-auto mb-4 h-20 w-20">
                    <!-- Replace with your icon path -->
                    <h2 class="text-xl font-bold mb-2">Install Chronic Adventure</h2>
                    <p class="mb-4">Install this application on your home screen for quick and easy access when you're on
                        the go.</p>

                </div>
                <div v-if="isIos" class="flex justify-center items-center bg-secondary flex-col py-2 rounded-b-lg">
                    <span class="text-sm">Just tap </span>
                    <span class="text-sm mx-1"><img class="h-5" src="~/assets/images/ios-share.svg" /></span>
                    <span class="text-sm"> then 'Add to Home Screen'</span>
                </div>
                <div v-else class="flex justify-center items-center bg-secondary flex-col py-2 rounded-b-lg">
                    <button @click="installOnNonIos" class="btn-primary">Install?</button>
                </div>
            </div>

        </transition>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isPromptOpen: true,
            deferredPrompt: null
        }
    },
    created() {
        window.addEventListener("appinstalled", () => {
            console.log('app installed');
        });
    },
    methods: {
        async installOnNonIos() {
            const {$pwa} = useNuxtApp();
            await $pwa.install();
        },
        closePrompt() {
            // Your logic to hide the prompt
            this.isPromptOpen = false;
            //pwaHelper.savePromptShownDate();
        },
        getLastShownTimestamp() {
            const pwaHelper = usePwaHelper();
            return pwaHelper.getLastShownTimestamp();
        },
        savePromptShownDate() {
            const pwaHelper = usePwaHelper();
            pwaHelper.savePromptShownDate();
        },
        isMoreThanTwoWeeksAgo(lastShownDate) {
            const pwaHelper = usePwaHelper();
            return pwaHelper.isMoreThanTwoWeeksAgo(lastShownDate);
        },
        needsToSeeThePrompt() {
            const authHelper = useAuthHelper();
            if (!authHelper.isLoggedIn())
                return false;

            var lastShown = this.getLastShownTimestamp();
            //if we've shown the prompt before but it hasn't been two weeks then the 
            //prompt does not need to be shown again
            if (lastShown && !this.isMoreThanTwoWeeksAgo(lastShown)) {
                return false;
            }

            if (this.isIos) {
                //app already installed on most recent ios device, legacy devices this won't work
                //you can't have standalone pwa installed
                if (('standalone' in window.navigator) && (window.navigator.standalone)) {
                    return false;
                }

                return true;

            }

            return true;
        }
    },
    computed: {
        isIos() {
            const pwaHelper = usePwaHelper();
            return pwaHelper.isIos();
        }
    }
}
</script>

<style>
/* You can add your global styles here */
.fade-enter-active,
.fade-leave-active {
    transition: opacity .5s;
}

.fade-enter,
.fade-leave-to

/* .fade-leave-active in <2.1.8 */
    {
    opacity: 0;
}
</style>
Jessuhh commented 1 month ago

+1, did you ever find a solution to this problem @chronicadventure?

userquin commented 1 month ago

you can enable pwa.client.installPrompt option in pwa nuxt module options, then use $pwa?.showInstallPrompt to activate the custom install badge/dialog in the ui, when user click on install call $pwa?.install()

https://vite-pwa-org.netlify.app/frameworks/nuxt.html

Jessuhh commented 1 month ago

If I just want to add a way to manually trigger the install prompt, is that possible? I don't necessarily want to disable the default browser prompt.

Currently I'm trying to add a button that simply calls the prompt, however calling await $pwa?.install(); does absolutely nothing. It doesn't throw an error nor does the prompt appear.

My button:

<UButton @click="installPwa">Installeer</UButton>

installPwa:

async function installPwa() {
    console.log("Installing PWA"); // To make sure this function gets called (it does)
    await $pwa?.install();
}

My nuxt config:

pwa: {
  registerType: "prompt",
  manifest: {
      // Removed for simplicity (it is valid, I can install the app manually just fine)
  },
  workbox: {
      globPatterns: ["**/*.{js,css,html,wasm}"],
      maximumFileSizeToCacheInBytes: 8000000, // 8MB, needed for the wasm files
  },
},
Jessuhh commented 1 month ago

I got it working now. It seems that $pwa?.install() won't work unless you've setclient.installPrompt to true in your nuxt config. It seems that what I was trying to achieve (have the default install prompt and manually open one) isn't possible. It's either one, not both.

Thanks for your help and time userquin!

userquin commented 1 month ago

You cannot install PWA, you need to intercept the beforeinstallprompt replacing the callback using the deferredPrompt from beforeinstallprompt listener (preventing the browser behavior) , iirc the sw must be also installed and activated.

You can check the logic here: https://github.com/vite-pwa/nuxt/blob/main/src/runtime/plugins/pwa.client.ts#L90-L126

userquin commented 1 month ago

Check this for example: https://github.com/elk-zone/elk/blob/main/components/pwa/PwaInstallPrompt.client.vue

From https://elk.zone

imagen

userquin commented 1 month ago

if you're using Nuxt it is fine, otherwise you'll need to use $pwa instead useNuxtApp().$pwa (both should work in nuxt)

userquin commented 1 month ago

If you want to customize the browser PWA prompt I cannot help you, check https://whatpwacando.today/ (click on the install pwa button)