Files
HaBit/lib/daily-reminders.ts
Harkamal Randhawa 93af01dec1 Rename daily quiz to check-in and add collapsible journal calendar.
Replace user-facing quiz wording with daily check-in, add journal history on the Bit page with month browsing and read-only entry modals, and keep legacy quiz routes redirecting to the new anchor.
2026-05-24 10:20:03 -06:00

267 lines
6.5 KiB
TypeScript

import type { DailyReminderStatus } from "@/lib/daily-reminder-status";
import { showInAppDailyReminder } from "@/lib/in-app-reminders";
import { routes } from "@/lib/routes";
import {
type ReminderDeliveryMethod,
usesInAppReminders,
usesSystemReminders,
} from "@/lib/reminder-delivery";
const REMINDER_SENT_STORAGE_PREFIX = "habit-pet-daily-reminder-sent";
const REMINDER_CHECK_INTERVAL_MS = 15_000;
export type NotificationPermissionState =
| "default"
| "granted"
| "denied"
| "unsupported";
export function isNotificationSupported() {
return typeof window !== "undefined" && "Notification" in window;
}
export function getNotificationPermissionState(): NotificationPermissionState {
if (!isNotificationSupported()) {
return "unsupported";
}
return Notification.permission;
}
export async function requestNotificationPermission(): Promise<NotificationPermissionState> {
if (!isNotificationSupported()) {
return "unsupported";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission === "denied") {
return "denied";
}
const permission = await Notification.requestPermission();
return permission === "default" ? "default" : permission;
}
export function parseReminderTime(reminderTime: string) {
const match = /^(\d{2}):(\d{2})$/.exec(reminderTime.trim());
if (!match) {
return null;
}
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (
!Number.isInteger(hours) ||
!Number.isInteger(minutes) ||
hours < 0 ||
hours > 23 ||
minutes < 0 ||
minutes > 59
) {
return null;
}
return hours * 60 + minutes;
}
export function hasReachedReminderTime(
reminderTime: string,
now = new Date(),
) {
const reminderMinutes = parseReminderTime(reminderTime);
if (reminderMinutes === null) {
return false;
}
const nowMinutes = now.getHours() * 60 + now.getMinutes();
return nowMinutes >= reminderMinutes;
}
function getReminderSentStorageKey(dateKey: string) {
return `${REMINDER_SENT_STORAGE_PREFIX}:${dateKey}`;
}
export function wasDailyReminderSentToday(dateKey: string) {
if (typeof window === "undefined") {
return false;
}
return (
window.localStorage.getItem(getReminderSentStorageKey(dateKey)) === "1"
);
}
export function markDailyReminderSentToday(dateKey: string) {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(getReminderSentStorageKey(dateKey), "1");
}
export function clearDailyReminderSentToday(dateKey: string) {
if (typeof window === "undefined") {
return;
}
window.localStorage.removeItem(getReminderSentStorageKey(dateKey));
}
export function buildDailyReminderNotification(status: DailyReminderStatus) {
const targetUrl = routes.avatar;
if (!status.quizCompleted && status.incompleteTaskCount > 0) {
return {
title: "Time for your daily check-in",
body: `Complete today's wellness check-in and ${status.incompleteTaskCount} remaining habit${status.incompleteTaskCount === 1 ? "" : "s"}.`,
targetUrl,
};
}
if (!status.quizCompleted) {
return {
title: "Time for your daily check-in",
body: "Complete today's wellness check-in to keep your bit in good spirits.",
targetUrl,
};
}
return {
title: "Habit reminder",
body: `You have ${status.incompleteTaskCount} habit${status.incompleteTaskCount === 1 ? "" : "s"} left for today.`,
targetUrl,
};
}
export function showSystemDailyReminderNotification(
status: DailyReminderStatus,
) {
if (!isNotificationSupported() || Notification.permission !== "granted") {
return false;
}
const { title, body, targetUrl } = buildDailyReminderNotification(status);
const notification = new Notification(title, {
body,
tag: `habit-pet-daily-reminder-${status.dateKey}`,
});
notification.onclick = () => {
window.focus();
notification.close();
if (window.location.pathname !== targetUrl) {
window.location.assign(targetUrl);
}
};
return true;
}
export function showTestSystemDailyReminderNotification(
status: DailyReminderStatus,
) {
if (!isNotificationSupported() || Notification.permission !== "granted") {
return false;
}
const { body, targetUrl } = buildDailyReminderNotification(status);
const notification = new Notification("Test: HaBit reminder", {
body: `This is a browser test notification. ${body}`,
tag: `habit-pet-daily-reminder-test-${Date.now()}`,
});
notification.onclick = () => {
window.focus();
notification.close();
if (window.location.pathname !== targetUrl) {
window.location.assign(targetUrl);
}
};
return true;
}
export function deliverDailyReminder(
status: DailyReminderStatus,
deliveryMethod: ReminderDeliveryMethod,
) {
let delivered = false;
if (usesInAppReminders(deliveryMethod)) {
const { title, body, targetUrl } = buildDailyReminderNotification(status);
delivered =
showInAppDailyReminder({ title, body, targetUrl }) || delivered;
}
if (usesSystemReminders(deliveryMethod)) {
delivered =
showSystemDailyReminderNotification(status) || delivered;
}
return delivered;
}
export function deliverTestDailyReminder(
status: DailyReminderStatus,
deliveryMethod: ReminderDeliveryMethod,
) {
let delivered = false;
if (usesInAppReminders(deliveryMethod)) {
const { title, body, targetUrl } = buildDailyReminderNotification(status);
delivered =
showInAppDailyReminder({
title: `Test: ${title}`,
body: `This is an in-app test reminder. ${body}`,
targetUrl,
isTest: true,
}) || delivered;
}
if (usesSystemReminders(deliveryMethod)) {
delivered =
showTestSystemDailyReminderNotification(status) || delivered;
}
return delivered;
}
/** @deprecated Use showSystemDailyReminderNotification */
export function showDailyReminderNotification(status: DailyReminderStatus) {
return showSystemDailyReminderNotification(status);
}
/** @deprecated Use showTestSystemDailyReminderNotification */
export function showTestDailyReminderNotification(status: DailyReminderStatus) {
return showTestSystemDailyReminderNotification(status);
}
export function getReminderCheckIntervalMs() {
return REMINDER_CHECK_INTERVAL_MS;
}
export function getNotificationPermissionLabel(
permission: NotificationPermissionState,
) {
switch (permission) {
case "granted":
return "Allowed";
case "denied":
return "Blocked";
case "default":
return "Not requested";
default:
return "Unsupported";
}
}