316e5def72
Update Expo and React dependencies to compatible versions. Create a custom Metro config to resolve module conflicts in the workspace and map Jest modules to local node_modules. Add a SharedArrayBuffer polyfill for older runtimes and introduce an adapter for expo-notifications to abstract direct imports.
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import * as Notifications from './expoNotifications';
|
|
import { SchedulableTriggerInputTypes } from './expoNotifications';
|
|
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 targetArrivalTimeMs = event.eventTime.getTime() - arrivalBufferMinutes * 60 * 1000;
|
|
|
|
// If we have journeys, use the earliest non-cancelled real departure minus reminder buffer
|
|
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 = earliest real departure time - reminder buffer
|
|
return new Date(best.rD.getTime() - bufferMinutes * 60 * 1000);
|
|
}
|
|
}
|
|
|
|
// Fallback: event time minus arrival buffer minus reminder (no journey data)
|
|
return new Date(targetArrivalTimeMs - 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,
|
|
}),
|
|
});
|
|
}
|