added in browser notifications for notifications in settings
This commit is contained in:
@@ -2,6 +2,7 @@ import { Suspense } from "react";
|
||||
|
||||
import { AppAuthGate } from "@/components/layout/app-auth-gate";
|
||||
import { DailyReminderProvider } from "@/components/reminders/daily-reminder-provider";
|
||||
import { InAppReminderBanner } from "@/components/reminders/in-app-reminder-banner";
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
@@ -12,6 +13,7 @@ export default function AppLayout({
|
||||
<Suspense fallback={<div className="min-h-svh bg-background" />}>
|
||||
<AppAuthGate>
|
||||
<DailyReminderProvider />
|
||||
<InAppReminderBanner />
|
||||
{children}
|
||||
</AppAuthGate>
|
||||
</Suspense>
|
||||
|
||||
@@ -17,14 +17,20 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
clearDailyReminderSentToday,
|
||||
deliverTestDailyReminder,
|
||||
getNotificationPermissionLabel,
|
||||
getNotificationPermissionState,
|
||||
isNotificationSupported,
|
||||
requestNotificationPermission,
|
||||
showTestDailyReminderNotification,
|
||||
} from "@/lib/daily-reminders";
|
||||
import { getDailyReminderStatus } from "@/lib/daily-reminder-status";
|
||||
import { getTodayDateKey } from "@/lib/habits-storage";
|
||||
import {
|
||||
reminderDeliveryOptions,
|
||||
usesInAppReminders,
|
||||
usesSystemReminders,
|
||||
type ReminderDeliveryMethod,
|
||||
} from "@/lib/reminder-delivery";
|
||||
import {
|
||||
avatarVibeOptions,
|
||||
defaultProfilePreferences,
|
||||
@@ -86,7 +92,8 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
||||
|
||||
if (
|
||||
"dailyReminderEnabled" in updates ||
|
||||
"dailyReminderTime" in updates
|
||||
"dailyReminderTime" in updates ||
|
||||
"dailyReminderDelivery" in updates
|
||||
) {
|
||||
clearDailyReminderSentToday(getTodayDateKey());
|
||||
}
|
||||
@@ -102,37 +109,16 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDailyReminderToggle = async (checked: boolean) => {
|
||||
if (checked) {
|
||||
if (!isNotificationSupported()) {
|
||||
setError("This browser does not support notifications.");
|
||||
return;
|
||||
}
|
||||
|
||||
const permission = await requestNotificationPermission();
|
||||
setNotificationPermission(permission);
|
||||
|
||||
if (permission !== "granted") {
|
||||
setError(
|
||||
permission === "denied"
|
||||
? "Notifications are blocked. Enable them in your browser settings to use daily reminders."
|
||||
: "Notification permission was not granted.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const ensureSystemReminderPermission = async (
|
||||
deliveryMethod: ReminderDeliveryMethod,
|
||||
) => {
|
||||
if (!usesSystemReminders(deliveryMethod)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
await updatePreferences({ dailyReminderEnabled: checked });
|
||||
};
|
||||
|
||||
const handleTestNotification = async () => {
|
||||
setNotificationMessage(null);
|
||||
setError(null);
|
||||
|
||||
if (!isNotificationSupported()) {
|
||||
setError("This browser does not support notifications.");
|
||||
return;
|
||||
setError("This browser does not support browser (OS) notifications.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const permission = await requestNotificationPermission();
|
||||
@@ -141,22 +127,86 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
||||
if (permission !== "granted") {
|
||||
setError(
|
||||
permission === "denied"
|
||||
? "Notifications are blocked. Enable them in your browser settings."
|
||||
: "Notification permission was not granted.",
|
||||
? "Browser notifications are blocked. Enable them in your browser settings or choose In-app delivery."
|
||||
: "Browser notification permission was not granted.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleDailyReminderToggle = async (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allowed = await ensureSystemReminderPermission(
|
||||
preferences.dailyReminderDelivery,
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setError(null);
|
||||
await updatePreferences({ dailyReminderEnabled: checked });
|
||||
};
|
||||
|
||||
const handleDeliveryMethodChange = async (
|
||||
deliveryMethod: ReminderDeliveryMethod,
|
||||
) => {
|
||||
if (
|
||||
preferences.dailyReminderEnabled &&
|
||||
usesSystemReminders(deliveryMethod)
|
||||
) {
|
||||
const allowed = await ensureSystemReminderPermission(deliveryMethod);
|
||||
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setError(null);
|
||||
await updatePreferences({ dailyReminderDelivery: deliveryMethod });
|
||||
};
|
||||
|
||||
const handleTestNotification = async () => {
|
||||
setNotificationMessage(null);
|
||||
setError(null);
|
||||
|
||||
const deliveryMethod = preferences.dailyReminderDelivery;
|
||||
|
||||
if (usesSystemReminders(deliveryMethod)) {
|
||||
const allowed = await ensureSystemReminderPermission(deliveryMethod);
|
||||
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const status = await getDailyReminderStatus();
|
||||
const shown = deliverTestDailyReminder(status, deliveryMethod);
|
||||
|
||||
if (!shown) {
|
||||
setError("Could not show a test reminder.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (deliveryMethod === "in_app") {
|
||||
setNotificationMessage(
|
||||
"In-app test reminder sent. Look for the banner above the bottom navigation.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await getDailyReminderStatus();
|
||||
const shown = showTestDailyReminderNotification(status);
|
||||
|
||||
if (!shown) {
|
||||
setError("Could not show a test notification.");
|
||||
if (deliveryMethod === "system") {
|
||||
setNotificationMessage(
|
||||
"Browser test notification sent. Check your browser or OS notification center.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setNotificationMessage(
|
||||
"Test notification sent. Check the corner of your screen or Windows notification center.",
|
||||
"Test reminder sent in-app and through your browser notification center.",
|
||||
);
|
||||
};
|
||||
|
||||
@@ -239,19 +289,44 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
||||
<Label htmlFor="daily-reminder">Daily reminder</Label>
|
||||
<Checkbox
|
||||
id="daily-reminder"
|
||||
disabled={isSaving || !isNotificationSupported()}
|
||||
disabled={isSaving}
|
||||
checked={preferences.dailyReminderEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
void handleDailyReminderToggle(checked === true)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<span className="text-sm">Browser permission</span>
|
||||
<Badge variant="secondary">
|
||||
{getNotificationPermissionLabel(notificationPermission)}
|
||||
</Badge>
|
||||
<div className="space-y-2">
|
||||
<Label>Reminder delivery</Label>
|
||||
<div className="space-y-2">
|
||||
{reminderDeliveryOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleDeliveryMethodChange(option.value)}
|
||||
className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
|
||||
preferences.dailyReminderDelivery === option.value
|
||||
? "border-primary bg-primary/10"
|
||||
: "hover:bg-muted/40"
|
||||
}`}
|
||||
>
|
||||
<p className="text-sm font-medium">{option.label}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{usesSystemReminders(preferences.dailyReminderDelivery) ? (
|
||||
<div className="flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<span className="text-sm">Browser permission</span>
|
||||
<Badge variant="secondary">
|
||||
{getNotificationPermissionLabel(notificationPermission)}
|
||||
</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="reminder-time">Daily reminder time</Label>
|
||||
<Input
|
||||
@@ -265,21 +340,19 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Reminders use your browser's local time and notify you once
|
||||
per day when the quiz or habits are still incomplete. Keep this app
|
||||
open in a tab for reminders to fire. On Windows, check Focus Assist
|
||||
if notifications are hidden.
|
||||
In-app reminders appear inside Habit Pet while a tab is open.
|
||||
Browser (OS) reminders use the Web Notifications API and may appear
|
||||
in your system notification center. Reminders fire once per day at
|
||||
your chosen local time.
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={
|
||||
isSaving || notificationPermission !== "granted"
|
||||
}
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleTestNotification()}
|
||||
>
|
||||
Send test notification
|
||||
Send test reminder
|
||||
</Button>
|
||||
{notificationMessage ? (
|
||||
<p className="text-xs text-emerald-600">{notificationMessage}</p>
|
||||
|
||||
@@ -5,17 +5,18 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
|
||||
import { getDailyReminderStatus } from "@/lib/daily-reminder-status";
|
||||
import {
|
||||
deliverDailyReminder,
|
||||
getNotificationPermissionState,
|
||||
getReminderCheckIntervalMs,
|
||||
hasReachedReminderTime,
|
||||
markDailyReminderSentToday,
|
||||
showDailyReminderNotification,
|
||||
wasDailyReminderSentToday,
|
||||
} from "@/lib/daily-reminders";
|
||||
import {
|
||||
getProfilePreferences,
|
||||
type ProfilePreferences,
|
||||
} from "@/lib/profile-preferences-storage";
|
||||
import { usesInAppReminders, usesSystemReminders } from "@/lib/reminder-delivery";
|
||||
|
||||
export function DailyReminderProvider() {
|
||||
const [preferences, setPreferences] = useState<ProfilePreferences | null>(
|
||||
@@ -47,7 +48,11 @@ export function DailyReminderProvider() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getNotificationPermissionState() !== "granted") {
|
||||
if (
|
||||
usesSystemReminders(activePreferences.dailyReminderDelivery) &&
|
||||
!usesInAppReminders(activePreferences.dailyReminderDelivery) &&
|
||||
getNotificationPermissionState() !== "granted"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +67,10 @@ export function DailyReminderProvider() {
|
||||
}
|
||||
|
||||
if (status.needsReminder) {
|
||||
showDailyReminderNotification(status);
|
||||
deliverDailyReminder(
|
||||
status,
|
||||
activePreferences.dailyReminderDelivery,
|
||||
);
|
||||
}
|
||||
|
||||
markDailyReminderSentToday(status.dateKey);
|
||||
|
||||
83
components/reminders/in-app-reminder-banner.tsx
Normal file
83
components/reminders/in-app-reminder-banner.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, X } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
IN_APP_REMINDER_EVENT,
|
||||
type InAppReminderPayload,
|
||||
} from "@/lib/in-app-reminders";
|
||||
|
||||
export function InAppReminderBanner() {
|
||||
const router = useRouter();
|
||||
const [reminder, setReminder] = useState<InAppReminderPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleReminder = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<InAppReminderPayload>;
|
||||
setReminder(customEvent.detail);
|
||||
};
|
||||
|
||||
window.addEventListener(IN_APP_REMINDER_EVENT, handleReminder);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(IN_APP_REMINDER_EVENT, handleReminder);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!reminder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed inset-x-0 bottom-16 z-50 px-4"
|
||||
>
|
||||
<div className="mx-auto flex max-w-lg items-start gap-3 rounded-xl border border-primary/30 bg-card p-4 shadow-lg">
|
||||
<div className="rounded-full bg-primary/10 p-2">
|
||||
<Bell aria-hidden="true" className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div>
|
||||
<p className="font-medium leading-tight">{reminder.title}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{reminder.body}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setReminder(null);
|
||||
router.push(reminder.targetUrl);
|
||||
}}
|
||||
>
|
||||
Open Pet tab
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setReminder(null)}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Dismiss reminder"
|
||||
onClick={() => setReminder(null)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -123,6 +123,9 @@ function parseProfilePreferences(
|
||||
dailyReminderTime:
|
||||
parsed.dailyReminderTime ??
|
||||
defaultProfilePreferences.dailyReminderTime,
|
||||
dailyReminderDelivery:
|
||||
parsed.dailyReminderDelivery ??
|
||||
defaultProfilePreferences.dailyReminderDelivery,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -45,6 +45,7 @@ function parseStoredPreferences(
|
||||
avatarVibe: "Calm",
|
||||
dailyReminderEnabled: false,
|
||||
dailyReminderTime: "20:00",
|
||||
dailyReminderDelivery: "in_app",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +61,7 @@ function parseStoredPreferences(
|
||||
avatarVibe: "Calm",
|
||||
dailyReminderEnabled: false,
|
||||
dailyReminderTime: "20:00",
|
||||
dailyReminderDelivery: "in_app",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,5 +70,6 @@ function parseStoredPreferences(
|
||||
avatarVibe: "Calm",
|
||||
dailyReminderEnabled: false,
|
||||
dailyReminderTime: "20:00",
|
||||
dailyReminderDelivery: "in_app",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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;
|
||||
@@ -135,7 +141,7 @@ export function buildDailyReminderNotification(status: DailyReminderStatus) {
|
||||
};
|
||||
}
|
||||
|
||||
export function showDailyReminderNotification(
|
||||
export function showSystemDailyReminderNotification(
|
||||
status: DailyReminderStatus,
|
||||
) {
|
||||
if (!isNotificationSupported() || Notification.permission !== "granted") {
|
||||
@@ -160,7 +166,7 @@ export function showDailyReminderNotification(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function showTestDailyReminderNotification(
|
||||
export function showTestSystemDailyReminderNotification(
|
||||
status: DailyReminderStatus,
|
||||
) {
|
||||
if (!isNotificationSupported() || Notification.permission !== "granted") {
|
||||
@@ -169,7 +175,7 @@ export function showTestDailyReminderNotification(
|
||||
|
||||
const { body, targetUrl } = buildDailyReminderNotification(status);
|
||||
const notification = new Notification("Test: Habit Pet reminder", {
|
||||
body: `This is a test notification. ${body}`,
|
||||
body: `This is a browser test notification. ${body}`,
|
||||
tag: `habit-pet-daily-reminder-test-${Date.now()}`,
|
||||
});
|
||||
|
||||
@@ -185,6 +191,61 @@ export function showTestDailyReminderNotification(
|
||||
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;
|
||||
}
|
||||
|
||||
38
lib/in-app-reminders.ts
Normal file
38
lib/in-app-reminders.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { routes } from "@/lib/routes";
|
||||
|
||||
export const IN_APP_REMINDER_EVENT = "habit-pet-in-app-reminder";
|
||||
|
||||
export type InAppReminderPayload = {
|
||||
title: string;
|
||||
body: string;
|
||||
targetUrl: string;
|
||||
isTest?: boolean;
|
||||
};
|
||||
|
||||
export function dispatchInAppReminder(payload: InAppReminderPayload) {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<InAppReminderPayload>(IN_APP_REMINDER_EVENT, {
|
||||
detail: payload,
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function showInAppDailyReminder(input: {
|
||||
title: string;
|
||||
body: string;
|
||||
targetUrl?: string;
|
||||
isTest?: boolean;
|
||||
}) {
|
||||
return dispatchInAppReminder({
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
targetUrl: input.targetUrl ?? routes.avatar,
|
||||
isTest: input.isTest,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
|
||||
import {
|
||||
normalizeReminderDeliveryMethod,
|
||||
type ReminderDeliveryMethod,
|
||||
} from "@/lib/reminder-delivery";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
|
||||
export type ProfilePreferences = {
|
||||
@@ -6,6 +10,7 @@ export type ProfilePreferences = {
|
||||
avatarVibe: string;
|
||||
dailyReminderEnabled: boolean;
|
||||
dailyReminderTime: string;
|
||||
dailyReminderDelivery: ReminderDeliveryMethod;
|
||||
};
|
||||
|
||||
type StoredProfilePreferences = {
|
||||
@@ -13,6 +18,7 @@ type StoredProfilePreferences = {
|
||||
avatarVibe?: string;
|
||||
dailyReminderEnabled?: boolean;
|
||||
dailyReminderTime?: string;
|
||||
dailyReminderDelivery?: string;
|
||||
};
|
||||
|
||||
type ProfileRow = {
|
||||
@@ -34,6 +40,7 @@ export const defaultProfilePreferences: ProfilePreferences = {
|
||||
avatarVibe: "Calm",
|
||||
dailyReminderEnabled: false,
|
||||
dailyReminderTime: "20:00",
|
||||
dailyReminderDelivery: "in_app",
|
||||
};
|
||||
|
||||
function isFocusTopic(value: string) {
|
||||
@@ -71,6 +78,9 @@ function normalizePreferences(
|
||||
defaultProfilePreferences.dailyReminderEnabled,
|
||||
dailyReminderTime:
|
||||
partial.dailyReminderTime ?? defaultProfilePreferences.dailyReminderTime,
|
||||
dailyReminderDelivery: normalizeReminderDeliveryMethod(
|
||||
partial.dailyReminderDelivery,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,7 +89,7 @@ function parseStoredPreferencesJson(raw: string): Partial<ProfilePreferences> {
|
||||
const parsed = JSON.parse(raw) as StoredProfilePreferences;
|
||||
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
return parsed as Partial<ProfilePreferences>;
|
||||
}
|
||||
} catch {
|
||||
return { focusTopic: raw };
|
||||
@@ -112,6 +122,7 @@ function serializeFocusTopicsField(preferences: ProfilePreferences) {
|
||||
avatarVibe: preferences.avatarVibe,
|
||||
dailyReminderEnabled: preferences.dailyReminderEnabled,
|
||||
dailyReminderTime: preferences.dailyReminderTime,
|
||||
dailyReminderDelivery: preferences.dailyReminderDelivery,
|
||||
};
|
||||
|
||||
return JSON.stringify(payload);
|
||||
|
||||
53
lib/reminder-delivery.ts
Normal file
53
lib/reminder-delivery.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export const reminderDeliveryOptions = [
|
||||
{
|
||||
value: "in_app",
|
||||
label: "In-app",
|
||||
description:
|
||||
"Show reminders inside Habit Pet when you have the site open in a tab.",
|
||||
},
|
||||
{
|
||||
value: "system",
|
||||
label: "Browser (OS)",
|
||||
description:
|
||||
"Use the Web Notifications API. Your browser may show these in the Windows or macOS notification center.",
|
||||
},
|
||||
{
|
||||
value: "both",
|
||||
label: "Both",
|
||||
description: "In-app banners plus browser notifications when permitted.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ReminderDeliveryMethod =
|
||||
(typeof reminderDeliveryOptions)[number]["value"];
|
||||
|
||||
export function isReminderDeliveryMethod(
|
||||
value: string | undefined,
|
||||
): value is ReminderDeliveryMethod {
|
||||
return reminderDeliveryOptions.some((option) => option.value === value);
|
||||
}
|
||||
|
||||
export function normalizeReminderDeliveryMethod(
|
||||
value: string | undefined,
|
||||
): ReminderDeliveryMethod {
|
||||
if (value && isReminderDeliveryMethod(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "in_app";
|
||||
}
|
||||
|
||||
export function usesInAppReminders(method: ReminderDeliveryMethod) {
|
||||
return method === "in_app" || method === "both";
|
||||
}
|
||||
|
||||
export function usesSystemReminders(method: ReminderDeliveryMethod) {
|
||||
return method === "system" || method === "both";
|
||||
}
|
||||
|
||||
export function getReminderDeliveryLabel(method: ReminderDeliveryMethod) {
|
||||
return (
|
||||
reminderDeliveryOptions.find((option) => option.value === method)?.label ??
|
||||
"In-app"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user