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 { // 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, // eventId -> journeys settings: ReminderSettings ): Promise { // 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 { 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, }), }); }