166 lines
4.3 KiB
TypeScript
166 lines
4.3 KiB
TypeScript
/** نوتیف مرورگر بدون 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<BrowserNotifState> {
|
||
|
|
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<boolean> {
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|