import AsyncStorage from "@react-native-async-storage/async-storage"; import * as Notifications from "expo-notifications"; import { Platform } from "react-native"; const REMINDER_PREFIX = "invoice-send-reminder:"; const FIRED_PREFIX = "invoice-reminder-fired:"; export type InvoiceSendReminderSource = { id: string; status: string; invoiceNumber: string; invoicePrefix: string | null; sendReminderAt: Date | string | null | undefined; client?: { name: string } | null; }; export function invoiceSendReminderNotificationId(invoiceId: string) { return `${REMINDER_PREFIX}${invoiceId}`; } Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false, shouldShowBanner: true, shouldShowList: true, }), }); async function ensureAndroidChannel() { if (Platform.OS !== "android") return; await Notifications.setNotificationChannelAsync("invoice-reminders", { name: "Invoice reminders", importance: Notifications.AndroidImportance.HIGH, sound: "default", vibrationPattern: [0, 250, 250, 250], }); } export async function ensureNotificationPermissions(): Promise { if (Platform.OS === "web") return false; await ensureAndroidChannel(); const { status: existing } = await Notifications.getPermissionsAsync(); if (existing === "granted") return true; const { status } = await Notifications.requestPermissionsAsync({ ios: { allowAlert: true, allowBadge: false, allowSound: true, }, }); return status === "granted"; } function reminderContent(invoice: InvoiceSendReminderSource): Notifications.NotificationContentInput { const label = `${invoice.invoicePrefix ?? "#"}${invoice.invoiceNumber}`; const clientName = invoice.client?.name ?? "your client"; return { title: "Time to send invoice", body: `${label} for ${clientName} is ready to send.`, data: { invoiceId: invoice.id, type: "invoice-send-reminder", }, sound: true, ...(Platform.OS === "android" ? { channelId: "invoice-reminders" } : {}), }; } export async function syncInvoiceSendReminders(invoices: InvoiceSendReminderSource[]) { if (Platform.OS === "web") return; const granted = await ensureNotificationPermissions(); if (!granted) return; const scheduled = await Notifications.getAllScheduledNotificationsAsync(); const ourScheduled = new Set( scheduled .map((entry) => entry.identifier) .filter((id): id is string => Boolean(id?.startsWith(REMINDER_PREFIX))), ); const wanted = new Set(); const now = Date.now(); for (const invoice of invoices) { if (invoice.status !== "draft" || !invoice.sendReminderAt) continue; const notificationId = invoiceSendReminderNotificationId(invoice.id); wanted.add(notificationId); const reminderAt = new Date(invoice.sendReminderAt); const reminderMs = reminderAt.getTime(); if (Number.isNaN(reminderMs)) continue; const firedKey = `${FIRED_PREFIX}${invoice.id}`; const firedAt = await AsyncStorage.getItem(firedKey); const content = reminderContent(invoice); await Notifications.cancelScheduledNotificationAsync(notificationId).catch(() => {}); if (reminderMs <= now) { const alreadyFiredForThisDate = firedAt === reminderAt.toISOString(); if (alreadyFiredForThisDate) continue; await Notifications.scheduleNotificationAsync({ identifier: notificationId, content, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: 2, }, }); await AsyncStorage.setItem(firedKey, reminderAt.toISOString()); continue; } if (firedAt && firedAt !== reminderAt.toISOString()) { await AsyncStorage.removeItem(firedKey); } await Notifications.scheduleNotificationAsync({ identifier: notificationId, content, trigger: { type: Notifications.SchedulableTriggerInputTypes.DATE, date: reminderAt, }, }); } for (const notificationId of ourScheduled) { if (wanted.has(notificationId)) continue; await Notifications.cancelScheduledNotificationAsync(notificationId).catch(() => {}); const invoiceId = notificationId.slice(REMINDER_PREFIX.length); await AsyncStorage.removeItem(`${FIRED_PREFIX}${invoiceId}`); } } export async function cancelInvoiceSendReminder(invoiceId: string) { if (Platform.OS === "web") return; const notificationId = invoiceSendReminderNotificationId(invoiceId); await Notifications.cancelScheduledNotificationAsync(notificationId).catch(() => {}); await AsyncStorage.removeItem(`${FIRED_PREFIX}${invoiceId}`); }