152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Bell, BellOff, Loader2, AlertTriangle } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { subscribeUser, unsubscribeUser } 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)
|
|
|
|
useEffect(() => {
|
|
const checks = []
|
|
if (!('serviceWorker' in navigator)) checks.push("No Service Worker support")
|
|
if (!('PushManager' in window)) checks.push("No PushManager support")
|
|
if (!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) checks.push("Missing VAPID Key")
|
|
|
|
if (checks.length === 0) {
|
|
setIsSupported(true)
|
|
registerServiceWorker()
|
|
} else {
|
|
console.warn("Push not supported:", checks.join(", "))
|
|
setDebugInfo(checks.join(", "))
|
|
}
|
|
}, [])
|
|
|
|
async function registerServiceWorker() {
|
|
try {
|
|
const registration = 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() {
|
|
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(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!)
|
|
})
|
|
|
|
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>
|
|
)
|
|
}
|