import { useEffect, useRef, useState } from 'react'; import { Bell, Loader2, Radio, ShieldCheck, TriangleAlert } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { useNotificationPrefs } from '@/hooks/useNotificationPrefs'; import { usePushNotifications } from '@/hooks/usePushNotifications'; import { useBackgroundDelivery } from '@/hooks/useBackgroundDelivery'; import { useToast } from '@/hooks/useToast'; import { NOTIFICATION_CATEGORIES } from '@/lib/notificationPrefs'; import { getNotificationPermission, requestNotificationPermission, showOsNotification, type OsPermission, } from '@/lib/osNotify'; /** * Notification settings — the opt-in surface for OS notifications. * * Turns on nothing until the user asks, and explains the one honest limitation * up front: notifications are delivered by the device itself while the app is * running (e.g. a background browser tab). A fully closed app needs a push * server, which SkateSpots deliberately does not run. */ export function NotificationSettingsCard() { const [prefs, setPrefs] = useNotificationPrefs(); const [permission, setPermission] = useState('loading'); const [busy, setBusy] = useState(false); const push = usePushNotifications(); const background = useBackgroundDelivery(); const { toast } = useToast(); // When the category choices change, re-send them to whatever channel is on, so // a closed-app push respects the toggles (the server filters by category). const categoriesKey = push.categories.join(','); const firstCategoriesRun = useRef(true); useEffect(() => { if (firstCategoriesRun.current) { firstCategoriesRun.current = false; return; } if (push.state === 'on') void push.resubscribe(); if (background.mode !== 'off') void background.resubscribe(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [categoriesKey]); useEffect(() => { let cancelled = false; void getNotificationPermission().then((result) => { if (!cancelled) setPermission(result); }); return () => { cancelled = true; }; }, []); const enable = async () => { setBusy(true); try { const result = await requestNotificationPermission(); setPermission(result); if (result === 'granted') { setPrefs((prev) => ({ ...prev, enabled: true })); } } finally { setBusy(false); } }; const toggleEnabled = (next: boolean) => { if (!next) { setPrefs((prev) => ({ ...prev, enabled: false })); // One kill switch: turning the master off also stops push + the native poller. void push.disable(); void background.disable(); return; } if (permission === 'granted') { setPrefs((prev) => ({ ...prev, enabled: true })); return; } void enable(); }; const setCategory = (key: (typeof NOTIFICATION_CATEGORIES)[number]['key'], value: boolean) => { setPrefs((prev) => ({ ...prev, [key]: value })); }; const sendTest = () => { void showOsNotification({ id: `test:${Date.now()}`, title: 'SkateSpots notifications are on 🛹', body: 'This is how a DM, zap or crew invite will look.', path: '/', }); }; const unsupported = permission === 'unsupported'; const denied = permission === 'denied'; const granted = permission === 'granted'; return (
{/* Master switch */}

Enable notifications

Buzz this device when something needs you.

{permission === 'loading' && (

Checking permission…

)} {unsupported && (

This device doesn't support notifications here. Rebuild the Android app to enable them on your phone.

)} {denied && (

Notifications are blocked for this site. Allow them in your browser/OS settings, then try again.

)}
{/* Background push (closed-app) */}

Background notifications

Get pinged for messages and trades even when SkateSpots is closed.

{push.state === 'working' ? ( ) : ( { if (value) void push.enable(); else void push.disable(); }} /> )}
{push.state === 'unsupported' && (

This browser doesn't support background push. On mobile, add SkateSpots to your home screen.

)} {push.state === 'error' && push.error && (

{push.error}

)}

To reach you while closed, a SkateSpots notification server gets a ping from your relay when something arrives for your key and passes it to your phone's push service. It sees only that your key has something — never who sent it or what it says, and it can't decrypt your messages. Opt in per device.

{/* Background delivery (Android only — no Google services). */} {background.supported && (

Background delivery (Android)

Check for new messages, offers and trades while the app is closed — no Google services.

Standard is best-effort — Android may defer it under Doze. Fast shows a persistent notification and uses more battery. Nothing is decrypted on our server.

)} {/* Per-category toggles */}

Notify me about

{NOTIFICATION_CATEGORIES.map(({ key, label, hint }) => (

{label}

{hint}

setCategory(key, value)} />
))}
{/* Test + privacy note */}

Alerts are built on your device from data the app already has, and direct-message alerts never decrypt your messages — so Amber is never asked to open one. With{' '} Background notifications off, a fully closed app can't be reached, so alerts arrive next time you open SkateSpots.

); }