diff --git a/frontend/composables/global/usePushNotifications.ts b/frontend/composables/global/usePushNotifications.ts index ce24fe0..ec4caaa 100644 --- a/frontend/composables/global/usePushNotifications.ts +++ b/frontend/composables/global/usePushNotifications.ts @@ -1,65 +1,92 @@ -// composables/usePushNotifications.ts -import { useLocalStorage, usePermission } from "@vueuse/core"; -import { onMounted, ref } from "vue"; -import useSubscribeNotification from "../api/notifications/useSubscribeNotification"; +import { API_ENDPOINTS } from "~/constants"; +import useSubscribeNotification from "~/composables/api/notifications/useSubscribeNotification"; +import { useToast } from "~/composables/global/useToast"; interface VapidKeys { publicKey: string; } export const usePushNotifications = () => { - // const isSupported = ref(false); - // const permission = usePermission("notifications"); - // const subscription = useLocalStorage( - // "push-subscription", - // null - // ); - // const vapid = ref(null); + const isSupported = ref(false); + const permission = usePermission("notifications"); + const subscription = useLocalStorage( + "push-subscription", + null + ); + const vapid = ref(null); - // const { mutateAsync: subscribeNotification } = useSubscribeNotification(); - // const toast = useToast(); + const { mutateAsync: subscribeNotification } = useSubscribeNotification(); + const { addToast } = useToast(); - // // Only run in client-side - // onMounted(async () => { - // if (typeof window !== "undefined" && "serviceWorker" in navigator) { - // isSupported.value = true; - // vapid.value = await $fetch("/api/vapid"); - // } - // }); + const unsubscribe = async () => { + const swRegistration = await navigator.serviceWorker.ready; + const existingSubscription = + await swRegistration.pushManager.getSubscription(); + if (existingSubscription) { + await existingSubscription.unsubscribe(); + } + }; - // const subscribe = async () => { - // if (!isSupported.value || !vapid.value?.publicKey) { - // throw new Error("Push notifications not supported"); - // } + onMounted(async () => { + if (typeof window !== "undefined" && "serviceWorker" in navigator) { + isSupported.value = true; + vapid.value = await $fetch("/api/vapid"); + } + }); - // const swRegistration = await navigator.serviceWorker.ready; + const subscribe = async () => { + if (!isSupported.value || !vapid.value?.publicKey) { + throw new Error("Push notifications not supported"); + } - // const applicationServerKey = vapid.value.publicKey - // .replace(/-/g, "+") - // .replace(/_/g, "/"); + const swRegistration = await navigator.serviceWorker.ready; - // const convertedKey = Uint8Array.from(atob(applicationServerKey), (c) => - // c.charCodeAt(0) - // ); + await unsubscribe(); - // const pushSubscription = await swRegistration.pushManager.subscribe({ - // userVisibleOnly: true, - // applicationServerKey: convertedKey, - // }); + const applicationServerKey = vapid.value.publicKey + .replace(/-/g, "+") + .replace(/_/g, "/"); - // const subscriptionJson = pushSubscription.toJSON(); + const convertedKey = Uint8Array.from(atob(applicationServerKey), (c) => + c.charCodeAt(0) + ); - // subscribeNotification({ - // body: subscriptionJson, - // }); + const pushSubscription = await swRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: convertedKey, + }); - // subscription.value = subscriptionJson; - // }; + const subscriptionJson = pushSubscription.toJSON(); - // return { - // isSupported, - // permission, - // subscribe, - // subscription, - // }; + subscribeNotification( + { + body: subscriptionJson, + }, + { + onSuccess: () => { + addToast({ + message: "اعلانات برای دستگاه شما فعال شد", + }); + }, + onError: () => { + addToast({ + message: "خطایی در فعال شدن اعلانات رخ داد", + options: { + status: "error", + }, + }); + }, + } + ); + + subscription.value = subscriptionJson; + }; + + return { + isSupported, + permission, + subscribe, + unsubscribe, + subscription, + }; }; diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 5845ff7..8eaf798 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -6,45 +6,45 @@ export default defineNuxtConfig({ css: [ "~/assets/css/tailwind.css", "swiper/css", - "animate.css/animate.min.css" + "animate.css/animate.min.css", ], routeRules: { - "/products": { prerender: false, ssr: false } + "/products": { prerender: false, ssr: false }, }, app: { head: { - title: "فروشگاه هی ملز" + title: "فروشگاه هی ملز", }, pageTransition: { enterActiveClass: "animate__animated animate__fadeIn animate__faster", leaveActiveClass: "animate__animated animate__fadeOut animate__faster", - mode: "out-in" + mode: "out-in", }, layoutTransition: { enterActiveClass: "animate__animated animate__fadeIn animate__faster", leaveActiveClass: "animate__animated animate__fadeOut animate__faster", - mode: "out-in" - } + mode: "out-in", + }, }, postcss: { plugins: { "@tailwindcss/postcss": {}, - autoprefixer: {} - } + autoprefixer: {}, + }, }, components: [ { path: "~/components", - pathPrefix: false - } + pathPrefix: false, + }, ], icon: { @@ -52,27 +52,37 @@ export default defineNuxtConfig({ customCollections: [ { prefix: "ci", - dir: "./public/icons" - } - ] + dir: "./public/icons", + }, + ], }, - modules: [[ - "@nuxtjs/google-fonts", - { - families: { - "DM Sans": "100..900", - Inter: "100..900", - download: true, - inject: false - } - } - ], "@nuxt/icon", "reka-ui/nuxt", "@vueuse/nuxt", "@formkit/auto-animate/nuxt", "@vite-pwa/nuxt", "@nuxt/image"], + modules: [ + [ + "@nuxtjs/google-fonts", + { + families: { + "DM Sans": "100..900", + Inter: "100..900", + download: true, + inject: false, + }, + }, + ], + "@nuxt/icon", + "reka-ui/nuxt", + "@vueuse/nuxt", + "@formkit/auto-animate/nuxt", + "@vite-pwa/nuxt", + "@nuxt/image", + ], pwa: { strategies: "injectManifest", srcDir: "public", filename: "sw.js", + registerType: + process.env.NODE_ENV === "production" ? "autoUpdate" : "prompt", manifest: { name: "Heymlz", short_name: "Heymlz", @@ -81,35 +91,38 @@ export default defineNuxtConfig({ { src: "/logo/logo-192x192.png", sizes: "192x192", - type: "image/png" + type: "image/png", }, { src: "/logo/logo-512x512.png", sizes: "512x512", - type: "image/png" - } - ] + type: "image/png", + }, + ], }, workbox: { navigateFallback: "/", clientsClaim: true, - skipWaiting: true + skipWaiting: true, + }, + devOptions: { + enabled: process.env.NODE_ENV === "production", + type: "module", }, - devOptions: { enabled: true, type: "module" } }, typescript: { - typeCheck: false + typeCheck: false, }, image: { - quality : 65 + quality: 65, }, runtimeConfig: { public: { API_BASE_URL: process.env.API_BASE_URL, - DEBUG: process.env.DEBUG - } - } -}); \ No newline at end of file + DEBUG: process.env.DEBUG, + }, + }, +}); diff --git a/frontend/plugins/pwaUpdate.client.ts b/frontend/plugins/pwaUpdate.client.ts index ac9ce3c..dc162ab 100644 --- a/frontend/plugins/pwaUpdate.client.ts +++ b/frontend/plugins/pwaUpdate.client.ts @@ -8,29 +8,73 @@ export default defineNuxtPlugin(() => { const { isInstalledAsPWA } = usePWA(); if ("serviceWorker" in navigator && isInstalledAsPWA.value) { + // Initialize Workbox wb = new Workbox("/sw.js"); - wb.addEventListener("waiting", () => { - checkForUpdate(); - }); + navigator.serviceWorker + .register("/sw.js") + .then((registration) => { + // Native Service Worker API for update detection + registration.addEventListener("updatefound", () => { + const newWorker = registration.installing; + if (newWorker) { + newWorker.addEventListener("statechange", () => { + if (newWorker.state === "installed") { + // Only show prompt if there's a controller (not first install) + if (navigator.serviceWorker.controller) { + checkForUpdate(); + } + } + }); + } + }); - wb.register() - .then((registration: any) => { + // Workbox events for consistency + wb?.addEventListener("waiting", () => { + checkForUpdate(); + }); + + // Check if there's already a waiting worker if (registration.waiting) { checkForUpdate(); } + + // Periodic update checks (optional) + setInterval(() => { + registration.update().catch((err) => { + console.debug( + "Service worker update check failed:", + err + ); + }); + }, 60 * 60 * 1000); // Check every hour }) - .catch((error) => { - console.error("Service worker registration failed:", error); + .catch((err) => { + console.error("Service worker registration failed:", err); }); + + // Register Workbox + wb.register().catch((error) => { + console.error("Workbox registration failed:", error); + }); } const checkForUpdate = () => { - updateAvailable.value = true; + if (!updateAvailable.value) { + updateAvailable.value = true; + } }; + const handleUpdate = () => { if (wb) { + // Send skip waiting message wb.messageSW({ type: "SKIP_WAITING" }).then(() => { + // Notify all tabs to reload + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: "CLIENT_RELOAD", + }); + } window.location.reload(); }); } diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 3b750c8..52b01b0 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -3,33 +3,36 @@ import { precacheAndRoute } from "workbox-precaching"; // Precaching configuration for PWA assets precacheAndRoute(self.__WB_MANIFEST); -// Version - const VERSION = "1.0.4"; -// Service Worker Installation +// Only enable skipWaiting and claim in production +const isProduction = process.env.NODE_ENV === "production"; + self.addEventListener("install", (event) => { - self.skipWaiting(); + if (isProduction) { + self.skipWaiting(); + } }); -// Service Worker Activation self.addEventListener("activate", (event) => { event.waitUntil( (async () => { - const clients = await self.clients.matchAll({ type: "window" }); - - // Notify all open clients about the version - clients.forEach((client) => - client.postMessage({ type: "VERSION_CHECK", version: VERSION }) - ); - - self.clients.claim(); + if (isProduction) { + const clients = await self.clients.matchAll({ type: "window" }); + clients.forEach((client) => + client.postMessage({ + type: "VERSION_CHECK", + version: VERSION, + }) + ); + self.clients.claim(); + } console.log("Service Worker Activated (Version: " + VERSION + ")"); })() ); }); -// Push Notification Handler for Django Web Push +// Rest of your existing handlers remain the same... self.addEventListener("push", (event) => { try { const payload = event.data?.json() || { @@ -51,7 +54,6 @@ self.addEventListener("push", (event) => { } }); -// Notification Click Handler self.addEventListener("notificationclick", (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data?.url || "/"));