BigBrainParking/app/src/notifications/localReminders.ts
Hank 9a5c9d445f Add Notifications preferences (session-expiry reminder lead time)
- New Notifications screen (Account > Notifications): enable/disable expiry
  reminders + a 5-minute-increment stepper (5–60 min), default 15 min
- localReminders.scheduleExpiryReminder now reads the persisted lead time + enabled
  flag, so it's ready to fire once the start-session flow is wired

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:33:17 -07:00

69 lines
2.4 KiB
TypeScript

import * as Notifications from 'expo-notifications';
import {
getReminderLeadMinutes,
getRemindersEnabled,
} from '@/features/notifications/reminderPrefs';
/**
* Session-expiry reminders as LOCAL scheduled notifications.
*
* This is the key GrapheneOS win: because the app knows a session's end time,
* we schedule the reminder on-device — no server push, no FCM, no Play Services.
* UnifiedPush (see unifiedPush.ts) is reserved for genuinely server-initiated
* events that we can't predict locally.
*/
export async function ensureNotificationPermission(): Promise<boolean> {
const settings = await Notifications.getPermissionsAsync();
if (settings.granted) return true;
const req = await Notifications.requestPermissionsAsync();
return req.granted;
}
Notifications.setNotificationHandler({
handleNotification: async () => ({
// shouldShowAlert is the legacy field; Banner/List are the newer split.
shouldShowAlert: true,
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export interface ScheduleReminderArgs {
transactionId: string | number;
zoneName: string;
/** When the parking session ends. */
endTime: Date;
/** Override the user's configured lead time (minutes before end). */
leadMinutes?: number;
}
/**
* Schedule an expiry reminder using the user's Notifications preferences
* (lead time + enabled). Returns the notification id, or null if disabled/late.
*/
export async function scheduleExpiryReminder(
args: ScheduleReminderArgs,
): Promise<string | null> {
if (!(await getRemindersEnabled())) return null;
const leadMinutes = args.leadMinutes ?? (await getReminderLeadMinutes());
const lead = leadMinutes * 60 * 1000;
const fireAt = new Date(args.endTime.getTime() - lead);
if (fireAt.getTime() <= Date.now()) return null; // already too late
return Notifications.scheduleNotificationAsync({
identifier: `session-${args.transactionId}`,
content: {
title: 'Parking expiring soon',
body: `${args.zoneName} ends at ${args.endTime.toLocaleTimeString()}. Extend if you need more time.`,
data: { transactionId: String(args.transactionId) },
},
trigger: { type: Notifications.SchedulableTriggerInputTypes.DATE, date: fireAt },
});
}
export async function cancelExpiryReminder(transactionId: string | number): Promise<void> {
await Notifications.cancelScheduledNotificationAsync(`session-${transactionId}`);
}