- 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>
69 lines
2.4 KiB
TypeScript
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}`);
|
|
}
|