/** * useBackgroundDelivery — Android-only closed-app notifications, **without * Google**. Registers a poll token with our `push-service` and hands the native * `skatespots-background` plugin the base URL + token so its worker can ask * "anything new since N?" and raise a local notification. * * Two modes (chosen in Settings): `standard` = WorkManager ~15 min (best-effort), * `fast` = a foreground service ~5 min (sticky notification). */ import { useCallback, useMemo, useState } from 'react'; import { registerPlugin } from '@capacitor/core'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNotificationPrefs } from '@/hooks/useNotificationPrefs'; import { NOTIFICATION_CATEGORIES } from '@/lib/notificationPrefs'; import { isNativePlatform, getPlatform } from '@/lib/platform'; import { pushServiceBase, requestPushChallenge } from '@/lib/pushService'; type DeliveryMode = 'off' | 'standard' | 'fast'; /** * Registered by NAME (`@capacitor/core`), not imported from the package — the * plugin's TS source lives in `packages/skatespots-background` and is only a * build-time dependency of the Android shell (Capacitor finds it via package.json). */ interface SkateSpotsBackgroundPlugin { configure(options: { mode: DeliveryMode; intervalMinutes?: number }): Promise; seed(options: { baseUrl: string; token: string; since: number }): Promise; clearNotifications(): Promise; } const SkateSpotsBackground = registerPlugin('SkateSpotsBackground'); export function useBackgroundDelivery() { const { user } = useCurrentUser(); const supported = isNativePlatform() && getPlatform() === 'android'; const [mode, setMode] = useState('off'); const [prefs] = useNotificationPrefs(); const categories = useMemo( () => NOTIFICATION_CATEGORIES.map((c) => c.key).filter((k) => k !== 'reminders' && prefs[k]), [prefs], ); const enable = useCallback( async (nextMode: Exclude, intervalMinutes = 5) => { if (!supported) throw new Error('Background delivery is Android-only.'); if (!user?.pubkey || !user.signer) throw new Error('Log in first.'); const challenge = await requestPushChallenge(user.pubkey); const proof = await user.signer.signEvent({ kind: 27235, content: '', created_at: Math.floor(Date.now() / 1000), tags: [['challenge', challenge]], }); const res = await fetch(`${pushServiceBase()}/native/poll/register`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pubkey: user.pubkey, proof, categories }), }); if (!res.ok) throw new Error('Could not register for background delivery.'); const { token, seq } = (await res.json()) as { token: string; seq: number }; await SkateSpotsBackground.seed({ baseUrl: pushServiceBase(), token, since: seq ?? 0 }); await SkateSpotsBackground.configure({ mode: nextMode, intervalMinutes }); setMode(nextMode); }, [supported, user, categories], ); /** Re-register with the current category choices (after a toggle change). */ const resubscribe = useCallback(async () => { if (mode === 'off') return; await enable(mode, mode === 'fast' ? 5 : undefined); }, [mode, enable]); const disable = useCallback(async () => { if (!supported) return; await SkateSpotsBackground.configure({ mode: 'off' }); await SkateSpotsBackground.clearNotifications(); setMode('off'); }, [supported]); return { supported, mode, enable, disable, resubscribe, categories }; }