fix: push notifications — urlBase64ToUint8Array bug, add PushBanner prompt
This commit is contained in:
73
src/components/PushBanner.tsx
Normal file
73
src/components/PushBanner.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Bell, X } from 'lucide-react'
|
||||
import { isPushSupported, subscribeToPush } from '../lib/push'
|
||||
import { api } from '../lib/api'
|
||||
|
||||
const DISMISSED_KEY = 'hotelsync-push-banner-dismissed'
|
||||
|
||||
export function PushBanner() {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPushSupported()) return
|
||||
if (typeof Notification === 'undefined') return
|
||||
if (Notification.permission !== 'default') return
|
||||
if (localStorage.getItem(DISMISSED_KEY)) return
|
||||
// Show after 3s so the UI settles
|
||||
const t = setTimeout(() => setVisible(true), 3000)
|
||||
return () => clearTimeout(t)
|
||||
}, [])
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
const dismiss = () => {
|
||||
setVisible(false)
|
||||
localStorage.setItem(DISMISSED_KEY, '1')
|
||||
}
|
||||
|
||||
const enable = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await Notification.requestPermission()
|
||||
if (result === 'granted') {
|
||||
const { publicKey } = await api.push.getVapidKey()
|
||||
if (publicKey) {
|
||||
const sub = await subscribeToPush(publicKey)
|
||||
if (sub) {
|
||||
const json = sub.toJSON()
|
||||
if (json.endpoint && json.keys?.p256dh && json.keys?.auth) {
|
||||
await api.push.subscribe({ endpoint: json.endpoint, p256dh: json.keys.p256dh, auth: json.keys.auth })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /**/ }
|
||||
finally {
|
||||
setLoading(false)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-24 left-4 right-24 z-40 max-w-xs">
|
||||
<div className="bg-slate-800 dark:bg-slate-900 text-white rounded-2xl shadow-xl px-4 py-3 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-600 flex items-center justify-center shrink-0">
|
||||
<Bell size={15} />
|
||||
</div>
|
||||
<p className="flex-1 text-sm leading-snug">
|
||||
Включите уведомления, чтобы не пропускать сообщения
|
||||
</p>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={enable} disabled={loading}
|
||||
className="text-xs font-semibold text-brand-300 hover:text-brand-200 disabled:opacity-50 transition-colors whitespace-nowrap">
|
||||
{loading ? '...' : 'Включить'}
|
||||
</button>
|
||||
<button onClick={dismiss} className="text-slate-400 hover:text-white transition-colors">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'
|
||||
import { Sidebar } from '../components/layout/Sidebar'
|
||||
import { Topbar } from '../components/layout/Topbar'
|
||||
import { ChatWidget } from '../components/chat/ChatWidget'
|
||||
import { PushBanner } from '../components/PushBanner'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
export function AppLayout() {
|
||||
@@ -34,6 +35,7 @@ export function AppLayout() {
|
||||
</div>
|
||||
|
||||
{!chatHidden && <ChatWidget />}
|
||||
<PushBanner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
function urlBase64ToUint8Array(base64String: string): ArrayBuffer {
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const rawData = window.atob(base64)
|
||||
const arr = new Uint8Array([...rawData].map(char => char.charCodeAt(0)))
|
||||
return arr.buffer as ArrayBuffer
|
||||
return new Uint8Array([...rawData].map(char => char.charCodeAt(0)))
|
||||
}
|
||||
|
||||
export async function registerSW(): Promise<ServiceWorkerRegistration | null> {
|
||||
@@ -22,7 +21,7 @@ export async function subscribeToPush(vapidPublicKey: string): Promise<PushSubsc
|
||||
if (existing) return existing
|
||||
return await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as unknown as ArrayBuffer,
|
||||
})
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user