Files
cat-sitting-planner/components/push-manager.tsx

175 lines
6.2 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { Bell, BellOff, Loader2, AlertTriangle } from "lucide-react"
import { toast } from "sonner"
import { subscribeUser, unsubscribeUser, getVapidPublicKey } from "@/app/actions/subscription"
import { Button } from "@/components/ui/button"
function urlBase64ToUint8Array(base64String: string) {
try {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
} catch (e) {
console.error("VAPID Key conversion failed", e)
throw new Error("Invalid VAPID Key format")
}
}
export function PushSubscriptionSettings({ planId }: { planId: string }) {
const [isSupported, setIsSupported] = useState(false)
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
const [loading, setLoading] = useState(false)
const [debugInfo, setDebugInfo] = useState<string | null>(null)
const [vapidKey, setVapidKey] = useState<string | null>(null)
useEffect(() => {
async function checkSupport() {
const checks = []
if (!('serviceWorker' in navigator)) checks.push("No Service Worker support")
if (!('PushManager' in window)) checks.push("No PushManager support")
try {
const key = await getVapidPublicKey()
if (!key) {
checks.push("Missing VAPID Key (Server)")
} else {
setVapidKey(key)
}
} catch (e) {
checks.push("Failed to fetch VAPID Key")
}
if (checks.length === 0) {
setIsSupported(true)
registerServiceWorker()
} else {
console.warn("Push not supported:", checks.join(", "))
setDebugInfo(checks.join(", "))
}
}
checkSupport()
}, [])
async function registerServiceWorker() {
try {
// Explicitly register the service worker
const registration = await navigator.serviceWorker.register('/push-sw.js')
// Wait for it to be ready
await navigator.serviceWorker.ready
const sub = await registration.pushManager.getSubscription()
setSubscription(sub)
} catch (e: any) {
console.error("SW registration error", e)
setDebugInfo(`SW Error: ${e.message}`)
}
}
async function subscribe() {
if (!vapidKey) {
toast.error("VAPID Key missing")
return
}
setLoading(true)
setDebugInfo(null)
try {
console.log("Requesting permission...")
const permission = await Notification.requestPermission()
console.log("Permission result:", permission)
if (permission === 'denied') {
toast.error("Notifications are blocked in your browser settings.")
setLoading(false)
return
}
if (permission !== 'granted') {
toast.error("Notifications permission not granted.")
setLoading(false)
return
}
console.log("Waiting for SW ready...")
const registration = await navigator.serviceWorker.ready
console.log("SW Ready. Subscribing...")
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidKey)
})
console.log("Subscribed locally:", sub)
setSubscription(sub)
await subscribeUser(planId, sub.toJSON())
toast.success("Push Notifications enabled")
} catch (error: any) {
console.error("Subscription failed", error)
setDebugInfo(`Error: ${error.message || "Unknown error"}`)
toast.error("Failed to enable notifications. See debug info.")
} finally {
setLoading(false)
}
}
async function unsubscribe() {
setLoading(true)
try {
if (subscription) {
await subscription.unsubscribe()
await unsubscribeUser(subscription.endpoint)
setSubscription(null)
toast.success("Notifications disabled")
}
} catch (error) {
console.error(error)
toast.error("Failed to disable.")
} finally {
setLoading(false)
}
}
if (!isSupported && !debugInfo) return null
return (
<div className="flex flex-col gap-2 p-4 border rounded-md mb-4 bg-muted/50">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<h3 className="font-medium text-sm">Device Notifications</h3>
<p className="text-xs text-muted-foreground">
Receive updates on this device
</p>
</div>
{subscription ? (
<Button variant="outline" size="sm" onClick={unsubscribe} disabled={loading}>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <BellOff className="w-4 h-4 mr-2" />}
Disable
</Button>
) : (
<Button size="sm" onClick={subscribe} disabled={loading || !isSupported} variant="secondary">
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Bell className="w-4 h-4 mr-2" />}
Enable
</Button>
)}
</div>
{debugInfo && (
<div className="text-xs text-destructive flex items-center gap-1 bg-destructive/10 p-2 rounded">
<AlertTriangle className="w-3 h-3" />
{debugInfo}
</div>
)}
</div>
)
}