Merge remote-tracking branch 'origin/main'

This commit is contained in:
marzban-dev
2025-03-27 15:38:14 +03:30
7 changed files with 207 additions and 107 deletions
@@ -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<PushSubscriptionJSON | null>(
// "push-subscription",
// null
// );
// const vapid = ref<VapidKeys | null>(null);
const isSupported = ref(false);
const permission = usePermission("notifications");
const subscription = useLocalStorage<PushSubscriptionJSON | null>(
"push-subscription",
null
);
const vapid = ref<VapidKeys | null>(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,
};
};
+49 -36
View File
@@ -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
}
}
});
DEBUG: process.env.DEBUG,
},
},
});
+52 -8
View File
@@ -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();
});
}
+17 -15
View File
@@ -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 || "/"));