206 lines
4.8 KiB
TypeScript
206 lines
4.8 KiB
TypeScript
import type { DailyReminderStatus } from "@/lib/daily-reminder-status";
|
|
import { routes } from "@/lib/routes";
|
|
|
|
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 quiz 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 quiz to keep your pet happy.",
|
|
targetUrl,
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: "Habit reminder",
|
|
body: `You have ${status.incompleteTaskCount} habit${status.incompleteTaskCount === 1 ? "" : "s"} left for today.`,
|
|
targetUrl,
|
|
};
|
|
}
|
|
|
|
export function showDailyReminderNotification(
|
|
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 showTestDailyReminderNotification(
|
|
status: DailyReminderStatus,
|
|
) {
|
|
if (!isNotificationSupported() || Notification.permission !== "granted") {
|
|
return false;
|
|
}
|
|
|
|
const { body, targetUrl } = buildDailyReminderNotification(status);
|
|
const notification = new Notification("Test: Habit Pet reminder", {
|
|
body: `This is a 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 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";
|
|
}
|
|
}
|