diff --git a/resources/js/app.ts b/resources/js/app.ts index 1db690c..0083c31 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,5 +1,6 @@ import { createInertiaApp, router } from '@inertiajs/vue3'; import { registerSW } from 'virtual:pwa-register'; +import { initPwa } from './lib/usePwa'; import AdminLayout from './layouts/AdminLayout.vue'; import AppLayout from './layouts/AppLayout.vue'; @@ -17,6 +18,7 @@ if ( } registerSW({ immediate: true }); +initPwa(); router.on('navigate', () => { if (typeof window !== 'undefined' && (window as any).posthog) { diff --git a/resources/js/components/NavBar.vue b/resources/js/components/NavBar.vue index 567f574..4cc8b44 100644 --- a/resources/js/components/NavBar.vue +++ b/resources/js/components/NavBar.vue @@ -19,6 +19,7 @@ import { HeartHandshake, Search, MessageCircle, + Download, } from 'lucide-vue-next'; import { computed, @@ -30,6 +31,7 @@ import { } from 'vue'; import { globalSearchQuery } from '@/lib/searchStore'; import { useDarkMode } from '@/lib/useDarkMode'; +import { usePwa } from '@/lib/usePwa'; import AppLogo from './AppLogo.vue'; import AuthModal from './AuthModal.vue'; @@ -41,6 +43,15 @@ defineProps({ }); const { theme, toggle } = useDarkMode(); +const { deferredPrompt, isInstalled, promptInstall } = usePwa(); + +const canInstallApp = computed( + () => !isInstalled.value && Boolean(deferredPrompt.value), +); + +const handleInstallApp = async () => { + await promptInstall(); +}; const page = usePage(); const user = computed(() => page.props.auth?.user); @@ -452,6 +463,21 @@ onBeforeUnmount(() => { /> {{ isAdmin ? 'Home' : 'Staff Panel' }} + + @@ -619,6 +645,44 @@ onBeforeUnmount(() => { + +
+ +
+

-import { ref, shallowRef, onMounted, onUnmounted } from 'vue'; +import { Download, X, CheckCircle2, Loader2 } from 'lucide-vue-next'; +import { ref, onMounted, watch } from 'vue'; +import { usePwa } from '@/lib/usePwa'; defineOptions({ inheritAttrs: false, @@ -10,14 +12,26 @@ const props = defineProps({ type: String, default: 'modal', }, + title: { + type: String, + default: 'HSCStack অ্যাপ ইনস্টল করুন', + }, + description: { + type: String, + default: 'দ্রুতগতির ব্রাউজিং ও নিরবচ্ছিন্ন স্টাডি এক্সপেরিয়েন্স', + }, }); +const { deferredPrompt, isInstalled, promptInstall } = usePwa(); const isVisible = ref(false); -const deferredPrompt = shallowRef(null); +const isInstalling = ref(false); -const handleBeforeInstall = (e: Event) => { - e.preventDefault(); - deferredPrompt.value = e; +const updateVisibility = () => { + if (isInstalled.value || !deferredPrompt.value) { + isVisible.value = false; + + return; + } if (props.variant === 'modal') { if (!sessionStorage.getItem('pwa_prompt_dismissed')) { @@ -28,27 +42,12 @@ const handleBeforeInstall = (e: Event) => { } }; -const handleAppInstalled = () => { - isVisible.value = false; - deferredPrompt.value = null; -}; - onMounted(() => { - const isStandalone = - window.matchMedia('(display-mode: standalone)').matches || - (window.navigator as Record).standalone === true; - - if (isStandalone) { - return; - } - - window.addEventListener('beforeinstallprompt', handleBeforeInstall); - window.addEventListener('appinstalled', handleAppInstalled); + updateVisibility(); }); -onUnmounted(() => { - window.removeEventListener('beforeinstallprompt', handleBeforeInstall); - window.removeEventListener('appinstalled', handleAppInstalled); +watch([deferredPrompt, isInstalled], () => { + updateVisibility(); }); const handleInstall = async () => { @@ -56,16 +55,16 @@ const handleInstall = async () => { return; } + isInstalling.value = true; + try { - await deferredPrompt.value.prompt(); - const { outcome } = await deferredPrompt.value.userChoice; + const success = await promptInstall(); - if (outcome === 'accepted') { + if (success) { isVisible.value = false; - deferredPrompt.value = null; } - } catch (error) { - console.error('PWA install error:', error); + } finally { + isInstalling.value = false; } }; @@ -79,94 +78,204 @@ const handleDismiss = () => { diff --git a/resources/js/lib/usePwa.ts b/resources/js/lib/usePwa.ts new file mode 100644 index 0000000..b3b333a --- /dev/null +++ b/resources/js/lib/usePwa.ts @@ -0,0 +1,67 @@ +import { ref, shallowRef } from 'vue'; + +const deferredPrompt = shallowRef(null); +const isInstalled = ref(false); + +let isInitialized = false; + +export function initPwa() { + if (typeof window === 'undefined' || isInitialized) { + return; + } + + isInitialized = true; + + const checkStandalone = () => { + const isStandalone = + window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as Record).standalone === true; + isInstalled.value = isStandalone; + }; + + checkStandalone(); + + window.addEventListener('beforeinstallprompt', (e: Event) => { + e.preventDefault(); + deferredPrompt.value = e; + }); + + window.addEventListener('appinstalled', () => { + isInstalled.value = true; + deferredPrompt.value = null; + }); +} + +export function usePwa() { + if (!isInitialized) { + initPwa(); + } + + const promptInstall = async (): Promise => { + if (!deferredPrompt.value) { + return false; + } + + try { + await deferredPrompt.value.prompt(); + const { outcome } = await deferredPrompt.value.userChoice; + + if (outcome === 'accepted') { + isInstalled.value = true; + deferredPrompt.value = null; + + return true; + } + } catch (err) { + console.error('PWA install error:', err); + } + + return false; + }; + + return { + deferredPrompt, + isInstalled, + promptInstall, + }; +} diff --git a/resources/js/pages/Chat/Index.vue b/resources/js/pages/Chat/Index.vue index 5472b02..54f33a4 100644 --- a/resources/js/pages/Chat/Index.vue +++ b/resources/js/pages/Chat/Index.vue @@ -22,6 +22,7 @@ import { import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import ChatBanModal from '@/components/ChatBanModal.vue'; import type { ChatBanUser } from '@/components/ChatBanModal.vue'; +import PwaInstallPrompt from '@/components/PwaInstallPrompt.vue'; import VerifiedBadge from '@/components/VerifiedBadge.vue'; import { getEcho } from '@/lib/echo'; import { usePermissions } from '@/lib/usePermissions'; @@ -1735,6 +1736,9 @@ onUnmounted(() => { @close="isBanModalOpen = false" /> + + +