This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Mob/lib/notifications.ts
kirukib 5391780b9b Ship Rx Care tab, notifications inbox, and guest pickup codes.
Replace the profile tab with prescription management (inline reminders and family invite), add a shared tab top bar, separate notifications from orders, and support any-branch guest checkout with GSH codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:35:51 +03:00

176 lines
4.8 KiB
TypeScript

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<string, string>;
}) {
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<string, string>;
}) {
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,
};
}