"use client"; import { useEffect, useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { subscribeToPushAction, unsubscribeFromPushAction } from "../actions/push-subscriptions"; import { urlBase64ToUint8Array } from "../lib/vapid"; type Status = "checking" | "unsupported" | "subscribed" | "not-subscribed" | "denied"; /** * The one control that ever asks for push permission — deliberately not * rendered anywhere that loads automatically on first visit (see the * Phase 7 brief's "never request permission on first load"). Placed in * Settings, a screen someone only reaches by deliberately navigating * there, with the benefit explained in the paragraph above the button * before anything is requested. */ function isPushSupported(): boolean { return typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window; } export function PushSubscribeToggle() { // Always starts as "checking" (renders nothing) on both server and // client — `isPushSupported`/`Notification.permission` are // client-only reads, so resolving them in a useState initializer would // make the client's hydration render disagree with the server's and // trigger a hydration error. The real status is only ever resolved // client-side, post-mount, in the effect below. const [status, setStatus] = useState("checking"); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); useEffect(() => { if (status !== "checking") return; let cancelled = false; Promise.resolve() .then(async () => { if (!isPushSupported()) return "unsupported" as const; if (Notification.permission === "denied") return "denied" as const; const reg = await navigator.serviceWorker.getRegistration(); const sub = await reg?.pushManager.getSubscription(); return sub ? ("subscribed" as const) : ("not-subscribed" as const); }) .then((resolved) => { if (!cancelled) setStatus(resolved); }) .catch(() => { if (!cancelled) setStatus("not-subscribed"); }); return () => { cancelled = true; }; }, [status]); const subscribe = () => { setError(null); startTransition(async () => { try { const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; if (!vapidPublicKey) { setError("Push notifications aren't configured for this business yet."); return; } const registration = await navigator.serviceWorker.register("/sw.js"); await navigator.serviceWorker.ready; const permission = await Notification.requestPermission(); if (permission !== "granted") { setStatus("denied"); return; } const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as BufferSource, }); const json = subscription.toJSON(); if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) { setError("Couldn't complete the push subscription. Please try again."); return; } const result = await subscribeToPushAction({ endpoint: json.endpoint, p256dhKey: json.keys.p256dh, authKey: json.keys.auth, deviceLabel: navigator.userAgent.slice(0, 100), }); if (!result.success) { setError(result.error ?? "Couldn't save your subscription."); return; } setStatus("subscribed"); } catch { setError("Couldn't enable push notifications. Please try again."); } }); }; const unsubscribe = () => { setError(null); startTransition(async () => { const registration = await navigator.serviceWorker.getRegistration(); const subscription = await registration?.pushManager.getSubscription(); if (subscription) { await unsubscribeFromPushAction(subscription.endpoint); await subscription.unsubscribe(); } setStatus("not-subscribed"); }); }; if (status === "checking") return null; if (status === "unsupported") { return

Push notifications aren't supported in this browser.

; } if (status === "denied") { return (

Push notifications are blocked for this site in your browser settings. Allow notifications for this site to turn them back on.

); } return (
{status === "not-subscribed" && (

Get a push notification on this device for reminders and important updates, in addition to email.

)} {error && (

{error}

)}
); }