Skip to content

Commit 366399c

Browse files
committed
feat(pwa): show install prompt toast on eligible browsers (beforeinstallprompt)
1 parent 677941b commit 366399c

File tree

7 files changed

+396
-233
lines changed

7 files changed

+396
-233
lines changed

assets/index-ByYSHKaj.js

Lines changed: 228 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/index-Do3U5wD-.js

Lines changed: 0 additions & 228 deletions
This file was deleted.

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@
9999
gtag("js", new Date());
100100
gtag("config", "G-1B67VYZMXF");
101101
</script>
102-
<script type="module" crossorigin src="/assets/index-Do3U5wD-.js"></script>
103-
<link rel="stylesheet" crossorigin href="/assets/index-CfflbtDV.css">
102+
<script type="module" crossorigin src="/assets/index-ByYSHKaj.js"></script>
103+
<link rel="stylesheet" crossorigin href="/assets/index-CB2TdpUq.css">
104104
<link rel="manifest" href="/manifest.webmanifest"></head>
105105

106106
<body>

sw.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vix-site/src/App.vue

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
<template>
22
<div class="app">
33
<SiteHeader />
4+
45
<main class="main">
56
<router-view />
67
</main>
8+
79
<SiteFooter />
810

11+
<!-- PWA install prompt toast -->
12+
<PwaInstallToast
13+
:show="showInstallToast"
14+
:canInstall="canInstall"
15+
message="Install Vix.cpp?"
16+
detail="Pin it like an app. Offline support, faster launch."
17+
@install="triggerInstall"
18+
@dismiss="dismissInstall"
19+
/>
20+
921
<!-- PWA update toast -->
1022
<PwaUpdateToast
1123
:show="showPwaToast"
@@ -17,11 +29,15 @@
1729
</template>
1830

1931
<script setup>
20-
import { inject, computed } from "vue";
32+
import { inject, computed, onMounted, onBeforeUnmount, ref } from "vue";
2133
2234
import SiteHeader from "./components/SiteHeader.vue";
2335
import SiteFooter from "./components/SiteFooter.vue";
2436
37+
import PwaUpdateToast from "./components/PwaUpdateToast.vue";
38+
import PwaInstallToast from "./components/PwaInstallToast.vue";
39+
40+
//PWA Update Toast (existing)
2541
const store = inject("pwaToast");
2642
const showPwaToast = computed(() => store?.show?.value === true);
2743
@@ -34,6 +50,94 @@ function refresh() {
3450
function dismiss() {
3551
window.__vix_pwa_dismiss__?.();
3652
}
53+
54+
/* ===========================
55+
PWA Install Toast (new)
56+
- uses beforeinstallprompt
57+
- cooldown on "Not now"
58+
=========================== */
59+
const showInstallToast = ref(false);
60+
const canInstall = ref(false);
61+
62+
let deferredPrompt = null;
63+
64+
const INSTALL_DISMISS_KEY = "vix_pwa_install_dismiss_until";
65+
66+
function nowMs() {
67+
return Date.now();
68+
}
69+
70+
function shouldShowInstallToast() {
71+
const until = Number(localStorage.getItem(INSTALL_DISMISS_KEY) || "0");
72+
return nowMs() > until;
73+
}
74+
75+
function setDismissCooldown(days = 7) {
76+
const until = nowMs() + days * 24 * 60 * 60 * 1000;
77+
localStorage.setItem(INSTALL_DISMISS_KEY, String(until));
78+
}
79+
80+
function dismissInstall() {
81+
setDismissCooldown(7);
82+
showInstallToast.value = false;
83+
}
84+
85+
async function triggerInstall() {
86+
if (!deferredPrompt) return;
87+
88+
try {
89+
deferredPrompt.prompt();
90+
const choice = await deferredPrompt.userChoice; // { outcome: 'accepted'|'dismissed' }
91+
92+
// one-shot: reset
93+
deferredPrompt = null;
94+
canInstall.value = false;
95+
showInstallToast.value = false;
96+
97+
// if dismissed, avoid spamming
98+
if (choice?.outcome !== "accepted") setDismissCooldown(7);
99+
} catch {
100+
deferredPrompt = null;
101+
canInstall.value = false;
102+
showInstallToast.value = false;
103+
setDismissCooldown(7);
104+
}
105+
}
106+
107+
function onBeforeInstallPrompt(e) {
108+
// prevent Chrome mini-infobar
109+
e.preventDefault();
110+
111+
deferredPrompt = e;
112+
canInstall.value = true;
113+
114+
if (!shouldShowInstallToast()) return;
115+
116+
// small delay so it feels intentional (after initial paint)
117+
setTimeout(() => {
118+
// still eligible?
119+
if (deferredPrompt && canInstall.value) showInstallToast.value = true;
120+
}, 900);
121+
}
122+
123+
function onAppInstalled() {
124+
deferredPrompt = null;
125+
canInstall.value = false;
126+
showInstallToast.value = false;
127+
128+
// long cooldown (or you can clear it)
129+
setDismissCooldown(365);
130+
}
131+
132+
onMounted(() => {
133+
window.addEventListener("beforeinstallprompt", onBeforeInstallPrompt);
134+
window.addEventListener("appinstalled", onAppInstalled);
135+
});
136+
137+
onBeforeUnmount(() => {
138+
window.removeEventListener("beforeinstallprompt", onBeforeInstallPrompt);
139+
window.removeEventListener("appinstalled", onAppInstalled);
140+
});
37141
</script>
38142
39143
<style scoped>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup>
2+
import { computed } from "vue";
3+
4+
const props = defineProps({
5+
show: { type: Boolean, default: false },
6+
message: { type: String, default: "Install Vix.cpp?" },
7+
detail: {
8+
type: String,
9+
default: "Get an app-like experience: offline support, faster launch.",
10+
},
11+
canInstall: { type: Boolean, default: true },
12+
});
13+
14+
const emit = defineEmits(["install", "dismiss"]);
15+
16+
const installDisabled = computed(() => !props.canInstall);
17+
</script>
18+
19+
<template>
20+
<teleport to="body">
21+
<div v-if="show" class="pwa-toast-wrap" role="status" aria-live="polite">
22+
<div class="pwa-toast">
23+
<div class="pwa-toast-glow" aria-hidden="true"></div>
24+
25+
<div class="pwa-toast-left">
26+
<div class="pwa-toast-icon" aria-hidden="true">
27+
<span class="pwa-dot pwa-dot-red"></span>
28+
<span class="pwa-dot pwa-dot-yellow"></span>
29+
<span class="pwa-dot pwa-dot-green"></span>
30+
</div>
31+
32+
<div class="pwa-toast-text">
33+
<div class="pwa-toast-title">{{ message }}</div>
34+
<div class="pwa-toast-sub">{{ detail }}</div>
35+
</div>
36+
</div>
37+
38+
<div class="pwa-toast-actions">
39+
<button
40+
class="pwa-btn pwa-btn-ghost"
41+
type="button"
42+
@click="emit('dismiss')"
43+
>
44+
Not now
45+
</button>
46+
47+
<button
48+
class="pwa-btn pwa-btn-primary"
49+
type="button"
50+
:disabled="installDisabled"
51+
@click="emit('install')"
52+
>
53+
Install
54+
</button>
55+
</div>
56+
</div>
57+
</div>
58+
</teleport>
59+
</template>

0 commit comments

Comments
 (0)