/** نوتیف مرورگر بدون Firebase — ترجیحاً با gesture کاربر؛ بعد از لاگین هم تلاش می‌شود. */ export type BrowserNotifState = | 'unsupported' | 'needs-pwa' | 'default' | 'granted' | 'denied' function isStandalonePwa() { if (typeof window === 'undefined') return false return ( window.matchMedia('(display-mode: standalone)').matches || Boolean((window.navigator as Navigator & { standalone?: boolean }).standalone) ) } /** فقط دستگاه‌های واقعی اپل موبایل — نه مک با ترک‌پد */ function isAppleMobileDevice() { if (typeof navigator === 'undefined') return false const ua = navigator.userAgent || '' if (/iPhone|iPod/i.test(ua)) return true if (/iPad/i.test(ua)) return true const isTouchMac = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1 if (!isTouchMac) return false if (/CriOS|FxiOS|EdgiOS/i.test(ua)) return true if (/Chrome|Edg|Firefox|OPR/i.test(ua) && !/Mobile/i.test(ua)) return false return true } export function getBrowserNotificationState(): BrowserNotifState { if (typeof window === 'undefined') return 'unsupported' if (typeof window.isSecureContext === 'boolean' && !window.isSecureContext) { return 'unsupported' } const hasApi = 'Notification' in window if (isAppleMobileDevice() && !isStandalonePwa() && !hasApi) { return 'needs-pwa' } if (!hasApi) { return 'unsupported' } const permission = Notification.permission if (permission === 'granted') return 'granted' if (permission === 'denied') return 'denied' return 'default' } export function canRequestBrowserNotification() { return getBrowserNotificationState() === 'default' } export async function requestBrowserNotificationPermission(): Promise { const before = getBrowserNotificationState() if (before === 'granted' || before === 'denied' || before === 'unsupported' || before === 'needs-pwa') { return before } if (!('Notification' in window)) { return getBrowserNotificationState() } try { const result = await Notification.requestPermission() if (result === 'granted') return 'granted' if (result === 'denied') return 'denied' return 'default' } catch { return getBrowserNotificationState() } } /** لوگوی برند از کانفیگ؛ fallback به آیکون‌های PWA */ function resolveNotifIcon() { try { const avatar = String( (window as Window & { __APP_CONFIG__?: { user?: { avatar?: string } } }).__APP_CONFIG__?.user ?.avatar ?? '', ).trim() if (avatar) { return avatar } } catch { // ignore } try { return new URL('/192.png', window.location.origin).href } catch { try { return new URL('/favicon.png', window.location.origin).href } catch { return undefined } } } export type ShowBrowserNotificationOptions = { title: string body?: string tag?: string url?: string } /** نمایش نوتیف سیستم؛ ترجیح با Service Worker (معتبرتر روی Chrome/Android). */ export async function showBrowserNotification(options: ShowBrowserNotificationOptions): Promise { if (getBrowserNotificationState() !== 'granted') { return false } const title = options.title || 'اعلان جدید' const body = options.body || '' const tag = options.tag || `app-notification-${Date.now()}` const url = options.url || '/appNotifications' const icon = resolveNotifIcon() try { if ('serviceWorker' in navigator) { const reg = await navigator.serviceWorker.ready if (reg?.showNotification) { await reg.showNotification(title, { body, tag, icon, badge: icon, data: { url }, renotify: true, }) return true } } } catch (error) { console.warn('SW showNotification failed, falling back', error) } try { const n = new Notification(title, { body, tag, icon, dir: 'rtl', lang: 'fa', }) n.onclick = () => { window.focus() n.close() if (url) { void import('@/router').then((mod) => { mod.default.push(url).catch(() => undefined) }) } } return true } catch (error) { console.warn('Notification constructor failed', error) return false } }