import type { LocalNotificationSchema } from '@capacitor/local-notifications'; import { isNativePlatform } from '@/lib/platform'; import { getNotificationPermission, numericId } from '@/lib/osNotify'; /** * Scheduled, future OS reminders — the only kind of notification that still * fires when the app is **fully closed**, because the OS holds the alarm. No * server, no background process, no network call: the app hands the OS a title, * a time and an in-app path, and the OS does the rest. * * Native (Android/iOS) only — the web has no scheduler. On web every function * here is a quiet no-op. * * We track the ids we handed out in localStorage so a reminder that no longer * applies (RSVP withdrawn, event passed) can be cancelled without disturbing * the one-off notifications fired elsewhere. */ export interface Reminder { /** Stable key. Reusing it reschedules in place; dropping it cancels. */ key: string; title: string; body: string; /** In-app path to open when tapped. */ path: string; /** Fire time, ms since epoch. */ at: number; } const REMINDER_IDS_KEY = 'skatespots:notifications:reminders'; type ReminderIdMap = Record; function loadIds(): ReminderIdMap { try { const raw = localStorage.getItem(REMINDER_IDS_KEY); if (!raw) return {}; const parsed = JSON.parse(raw) as unknown; if (parsed && typeof parsed === 'object') return parsed as ReminderIdMap; } catch { // Corrupt/blocked storage — start clean. } return {}; } function saveIds(map: ReminderIdMap): void { try { localStorage.setItem(REMINDER_IDS_KEY, JSON.stringify(map)); } catch { // Ignore — reminders still work for this session. } } async function cancelNative(ids: number[]): Promise { if (ids.length === 0) return; const { LocalNotifications } = await import('@capacitor/local-notifications'); await LocalNotifications.cancel({ notifications: ids.map((id) => ({ id })) }); } /** Cancel every reminder we scheduled (used when turning notifications off). */ export async function cancelAllScheduledReminders(): Promise { const ids = Object.values(loadIds()); saveIds({}); if (!isNativePlatform() || ids.length === 0) return; try { await cancelNative(ids); } catch (err) { console.warn('[reminders] cancel failed', err); } } /** * Make the OS's pending reminders match `reminders` exactly: cancel the ones we * previously scheduled that are no longer wanted, schedule the new ones, and * leave already-scheduled ones alone (re-scheduling would only churn). */ export async function syncScheduledReminders(reminders: Reminder[]): Promise { if (!isNativePlatform()) return; const now = Date.now(); const desired = new Map(reminders.filter((r) => r.at > now).map((r) => [r.key, r])); // Don't touch the OS at all without permission. const permission = await getNotificationPermission(); if (permission !== 'granted') return; try { const { LocalNotifications } = await import('@capacitor/local-notifications'); const previous = loadIds(); const stale = Object.entries(previous) .filter(([key]) => !desired.has(key)) .map(([, id]) => id); await cancelNative(stale); const next: ReminderIdMap = {}; const toSchedule: LocalNotificationSchema[] = []; for (const [key, reminder] of desired) { const id = previous[key] ?? numericId(`reminder:${key}`); next[key] = id; if (previous[key] === undefined) { toSchedule.push({ id, title: reminder.title, body: reminder.body, schedule: { at: new Date(reminder.at), allowWhileIdle: true }, extra: { path: reminder.path }, }); } } if (toSchedule.length > 0) { await LocalNotifications.schedule({ notifications: toSchedule }); } saveIds(next); } catch (err) { console.warn('[reminders] sync failed', err); } }