Files
time_to_leave/apps/mobile/src/services/notifications.ts
T
fegger 2eeae9a27b Add arrival buffer and transport options to notification settings
Update notification calculation to use arrival buffer
Add advanced settings toggle for arrival buffer and transport options
Fix reverse geocode call in SettingsScreen
2026-05-12 15:05:23 +02:00

158 lines
5.1 KiB
TypeScript

import * as Notifications from 'expo-notifications';
import { SchedulableTriggerInputTypes } from 'expo-notifications';
import type { Event, Journey, ReminderSettings } from '@timetoleave/core';
// Register for push notification permissions
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
}),
});
/**
* Calculate leave-by time from event time and journey data.
* Uses earliest real departure time if journeys exist, otherwise event time minus buffer.
*
* @param event - The event to calculate leave-by time for
* @param journeys - Journey data for this event
* @param arrivalBufferMinutes - How many minutes before the event to arrive
* @param bufferMinutes - How many minutes before leaving to be reminded
*/
export function calculateLeaveByTime(
event: Event,
journeys: Journey[],
arrivalBufferMinutes: number,
bufferMinutes: number
): Date {
// Calculate target arrival time (event time minus arrival buffer)
const targetArrivalTime = new Date(event.eventTime);
targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes);
// If we have journeys, use the earliest non-cancelled real departure
if (journeys.length > 0) {
const best = journeys
.filter((j) => !j.cancelled)
.sort((a, b) => a.rD.getTime() - b.rD.getTime())[0];
if (best) {
// Leave by time = target arrival time - buffer minutes
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
}
}
// Fallback: event time minus arrival buffer minus buffer (no journey data)
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
}
/**
* Schedule notifications for an event
*
* @param event - The event to schedule notifications for
* @param journeys - Journey data for this event (optional)
* @param settings - Notification settings
*/
export async function scheduleNotificationsForEvent(
event: Event,
journeys: Journey[] = [],
settings: ReminderSettings
): Promise<void> {
// Don't schedule if notifications are disabled
if (!settings.enabled) {
return;
}
// Calculate leave-by time (when user should actually leave)
const leaveByTime = calculateLeaveByTime(event, journeys, settings.arrivalBufferMinutes, settings.bufferMinutes);
// Cancel existing notifications for this event - cancel one by one
const existing = await Notifications.getAllScheduledNotificationsAsync();
const toCancel = existing.filter(n => n.content.data?.eventId === event.id);
if (toCancel.length > 0) {
for (const notif of toCancel) {
await Notifications.cancelScheduledNotificationAsync(notif.identifier);
}
}
// Default reminders: 30min, 10min, and at leave-by time
// But respect the buffer time - we want reminders relative to when they should leave
const defaultReminders = [30, 10, 0];
// Schedule notifications
for (const minutesBefore of defaultReminders) {
const triggerTime = new Date(leaveByTime.getTime() - minutesBefore * 60 * 1000);
// Skip if trigger time is in the past
if (triggerTime <= new Date()) {
continue;
}
// Skip if this would be before the event actually starts (add some safety margin)
if (triggerTime < new Date(event.eventTime.getTime() - 2 * 60 * 60 * 1000)) {
continue;
}
// Use Date trigger with time property
await Notifications.scheduleNotificationAsync({
content: {
title: `🚆 ${event.title}`,
body: minutesBefore === 0
? 'Zeit zu gehen!'
: `${minutesBefore} Minuten bis du losmusst`,
data: { eventId: event.id },
},
trigger: { type: SchedulableTriggerInputTypes.DATE, date: triggerTime },
});
}
}
/**
* Reschedule notifications for all events
* Use this when origin station changes or notification settings are updated
*/
export async function rescheduleAllNotifications(
events: Event[],
journeysMap: Record<string, Journey[]>, // eventId -> journeys
settings: ReminderSettings
): Promise<void> {
// Cancel ALL existing notifications first
await Notifications.cancelAllScheduledNotificationsAsync();
// Schedule new notifications for each event
for (const event of events) {
const eventJourneys = journeysMap[event.id] || [];
await scheduleNotificationsForEvent(event, eventJourneys, settings);
}
}
// Request permissions if not already granted
let permissionsRequested = false;
export async function requestNotificationPermissions(): Promise<boolean> {
if (permissionsRequested) {
return true;
}
permissionsRequested = true;
const { status } = await Notifications.requestPermissionsAsync();
return status === 'granted';
}
// Request permissions automatically when app starts (for Android)
// This is called in App.tsx
export function setupNotifications() {
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
shouldShowBanner: true,
shouldShowList: true,
}),
});
}