Surfaces three events without a full notification center: - service worker (public/sw.js) gains push + notificationclick handlers (RTL, focuses an existing tab or opens the target URL). - usePushNotifications hook: permission + VAPID subscribe/unsubscribe against the backend push endpoints. - opt-in UI both ways: a one-time dismissible post-login banner (PushPermissionBanner, rendered in MyLayout) and a permanent toggle (PushSettingToggle) on buyer profile settings and seller store settings. - useBadgeCounts polls /api/notif/v1/badges/; small red badges on the bottom-nav profile icon (chat + order updates), the profile sidebar (پیامها / سفارشها) and the seller dashboard sidebar. Visiting orders clears its badge. Degrades gracefully: if the backend endpoints are absent the counts fall back to zero and nothing throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
197 lines
5.2 KiB
JavaScript
197 lines
5.2 KiB
JavaScript
const STATIC_CACHE_NAME = "vitrown-static-v2";
|
|
const DYNAMIC_CACHE_NAME = "vitrown-dynamic-v2";
|
|
|
|
const STATIC_ASSETS = [
|
|
"/",
|
|
"/resources/manifest.webmanifest",
|
|
"/favicon.ico",
|
|
"/icon-192x192.png",
|
|
"/icon-512x512.png",
|
|
"/apple-touch-icon.png",
|
|
];
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
|
return cache.addAll(STATIC_ASSETS);
|
|
})
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener("activate", (event) => {
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((cacheName) => {
|
|
return (
|
|
cacheName.startsWith("vitrown-") &&
|
|
cacheName !== STATIC_CACHE_NAME &&
|
|
cacheName !== DYNAMIC_CACHE_NAME
|
|
);
|
|
})
|
|
.map((cacheName) => caches.delete(cacheName))
|
|
);
|
|
})
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Fetch event - network first, cache fallback strategy
|
|
self.addEventListener("fetch", (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== "GET") {
|
|
return;
|
|
}
|
|
|
|
// Skip non-HTTP(S) requests (chrome-extension://, etc.)
|
|
if (!url.protocol.startsWith("http")) {
|
|
return;
|
|
}
|
|
|
|
// Skip WebSocket, SSE, and API requests
|
|
if (
|
|
url.pathname.startsWith("/api/") ||
|
|
url.protocol === "ws:" ||
|
|
url.protocol === "wss:" ||
|
|
request.headers.get("accept")?.includes("text/event-stream")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// For navigation requests, try network first
|
|
if (request.mode === "navigate") {
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
const responseToCache = response.clone();
|
|
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
|
|
cache.put(request, responseToCache);
|
|
});
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
return caches.match(request);
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// For static assets (CSS, JS, images), use cache first
|
|
if (
|
|
request.destination === "style" ||
|
|
request.destination === "script" ||
|
|
request.destination === "image" ||
|
|
request.destination === "font"
|
|
) {
|
|
event.respondWith(
|
|
caches.match(request).then((cachedResponse) => {
|
|
if (cachedResponse) {
|
|
// Update cache in background
|
|
fetch(request).then((response) => {
|
|
if (response.status === 200) {
|
|
const responseToCache = response.clone();
|
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
|
cache.put(request, responseToCache);
|
|
});
|
|
}
|
|
});
|
|
return cachedResponse;
|
|
}
|
|
return fetch(request).then((response) => {
|
|
if (response.status === 200) {
|
|
const responseToCache = response.clone();
|
|
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
|
cache.put(request, responseToCache);
|
|
});
|
|
}
|
|
return response;
|
|
});
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// For other requests, network first
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
const responseToCache = response.clone();
|
|
caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
|
|
cache.put(request, responseToCache);
|
|
});
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
return caches.match(request);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Listen for messages from the client
|
|
self.addEventListener("message", (event) => {
|
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
|
self.skipWaiting();
|
|
}
|
|
});
|
|
|
|
// ---- Web Push ----
|
|
// Show a notification when a push arrives. Payload is JSON:
|
|
// { title, body, url, tag, notification_id }
|
|
self.addEventListener("push", (event) => {
|
|
let data = {};
|
|
try {
|
|
data = event.data ? event.data.json() : {};
|
|
} catch (e) {
|
|
data = { title: "ویترون", body: event.data ? event.data.text() : "" };
|
|
}
|
|
|
|
const title = data.title || "ویترون";
|
|
const options = {
|
|
body: data.body || "",
|
|
icon: "/icon-192x192.png",
|
|
badge: "/icon-96x96.png",
|
|
dir: "rtl",
|
|
lang: "fa",
|
|
tag: data.tag || "vitrown",
|
|
renotify: true,
|
|
data: { url: data.url || "/", notificationId: data.notification_id },
|
|
};
|
|
|
|
event.waitUntil(self.registration.showNotification(title, options));
|
|
});
|
|
|
|
// Focus an existing tab (or open a new one) on the target URL when clicked.
|
|
self.addEventListener("notificationclick", (event) => {
|
|
event.notification.close();
|
|
const targetUrl = (event.notification.data && event.notification.data.url) || "/";
|
|
|
|
event.waitUntil(
|
|
clients
|
|
.matchAll({ type: "window", includeUncontrolled: true })
|
|
.then((clientList) => {
|
|
for (const client of clientList) {
|
|
// If a tab is already open, navigate it and focus.
|
|
if ("focus" in client) {
|
|
client.focus();
|
|
if ("navigate" in client) {
|
|
client.navigate(targetUrl).catch(() => {});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(targetUrl);
|
|
}
|
|
})
|
|
);
|
|
});
|