import * as Notifications from "expo-notifications"; import { Platform } from "react-native"; Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false, shouldShowBanner: true, shouldShowList: true, }), }); export async function ensureNotificationPermissions() { const settings = await Notifications.getPermissionsAsync(); if (settings.granted) return true; const req = await Notifications.requestPermissionsAsync(); return req.granted; } export function androidChannel() { if (Platform.OS === "android") { return Notifications.setNotificationChannelAsync("gishen-reminders", { name: "Gishen reminders", importance: Notifications.AndroidImportance.HIGH, sound: "default", }); } return Promise.resolve(); } export function parseTimeHHmm(value: string): { hour: number; minute: number } | null { const m = value.trim().match(/^(\d{1,2}):(\d{2})$/); if (!m) return null; const hour = Number(m[1]); const minute = Number(m[2]); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null; return { hour, minute }; } /** Seconds until the next occurrence of HH:mm (today or tomorrow). */ export function secondsUntilNextHHmm(value: string): number | null { const parsed = parseTimeHHmm(value); if (!parsed) return null; const now = new Date(); const next = new Date(now); next.setHours(parsed.hour, parsed.minute, 0, 0); if (next.getTime() <= now.getTime()) { next.setDate(next.getDate() + 1); } return Math.max(5, Math.round((next.getTime() - now.getTime()) / 1000)); } export async function scheduleDoseReminder(opts: { title: string; body: string; secondsFromNow: number; data?: Record; }) { await ensureNotificationPermissions(); await androidChannel(); return Notifications.scheduleNotificationAsync({ content: { title: opts.title, body: opts.body, data: opts.data, sound: "default", categoryIdentifier: "dose", }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.max(5, opts.secondsFromNow), repeats: false, }, }); } export async function scheduleEscalationCheck(opts: { title: string; body: string; graceSeconds: number; data?: Record; }) { await ensureNotificationPermissions(); await androidChannel(); return Notifications.scheduleNotificationAsync({ content: { title: opts.title, body: opts.body, data: { ...opts.data, type: "escalation" }, sound: "default", }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.max(10, opts.graceSeconds), repeats: false, }, }); } /** * Schedule repeating daily calendar notifications for each HH:mm time. * Also queues a near-term demo notification for the soonest slot (~15s if within a few minutes). */ export async function scheduleDailyDoseReminders(opts: { prescriptionId: string; medicineName: string; times: string[]; /** Optional prior notification ids to cancel */ previousIds?: string[]; }) { const granted = await ensureNotificationPermissions(); await androidChannel(); if (opts.previousIds?.length) { await Promise.all( opts.previousIds.map((id) => Notifications.cancelScheduledNotificationAsync(id).catch(() => {}) ) ); } const notificationIds: string[] = []; const validTimes: string[] = []; for (const time of opts.times) { const parsed = parseTimeHHmm(time); if (!parsed) continue; validTimes.push( `${String(parsed.hour).padStart(2, "0")}:${String(parsed.minute).padStart(2, "0")}` ); if (granted) { const id = await Notifications.scheduleNotificationAsync({ content: { title: `Time for ${opts.medicineName}`, body: `Take your ${time} dose. Open Gishen to confirm.`, data: { prescriptionId: opts.prescriptionId, type: "dose", time, }, sound: "default", categoryIdentifier: "dose", }, trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: parsed.hour, minute: parsed.minute, }, }); notificationIds.push(id); } } // Demo: fire a near-term push so testers can verify without waiting until clock time if (granted && validTimes.length) { const demoId = await scheduleDoseReminder({ title: `Reminder set ยท ${opts.medicineName}`, body: `Daily alerts at ${validTimes.join(", ")}. Demo ping.`, secondsFromNow: 12, data: { prescriptionId: opts.prescriptionId, type: "dose_demo", }, }); notificationIds.push(demoId); } return { granted, times: validTimes, notificationIds, }; }