Add PWA infrastructure for iOS notification support

- Add manifest.json for proper PWA installation
- Add service worker (sw.js) for push notification handling
- Add ServiceWorkerProvider to register SW and manage notifications
- Generate PNG icons (192x192, 512x512) for iOS compatibility
- Update settings to show notification status and test button
- Use service worker showNotification for iOS Safari compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-25 00:49:05 +00:00
parent e35545b156
commit ad8b45ee1f
8 changed files with 311 additions and 13 deletions

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { AppShell } from "@/components/AppShell";
import { useAuth } from "@/components/AuthProvider";
import { useServiceWorker } from "@/components/ServiceWorkerProvider";
import { APP_NAME } from "@/lib/constants";
import { LogOut, Users, Bell, Sparkles, Loader2, Check, Key, ChevronDown } from "lucide-react";
import Link from "next/link";
@@ -29,11 +30,13 @@ const PROVIDERS = [
export default function SettingsPage() {
const { user, logout } = useAuth();
const { isSupported: swSupported, isRegistered: swRegistered, showNotification } = useServiceWorker();
const [settings, setSettings] = useState<UserSettings | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [showLlmSetup, setShowLlmSetup] = useState(false);
const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>("default");
const [isPWA, setIsPWA] = useState(false);
// LLM setup state
const [selectedProvider, setSelectedProvider] = useState<string>("");
@@ -68,6 +71,11 @@ export default function SettingsPage() {
if ("Notification" in window) {
setNotificationPermission(Notification.permission);
}
// Detect if running as PWA (standalone mode)
const isStandalone = window.matchMedia("(display-mode: standalone)").matches ||
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
setIsPWA(isStandalone);
}, []);
const updateSetting = async (key: string, value: unknown) => {
@@ -204,27 +212,35 @@ export default function SettingsPage() {
}
};
// Reminder check interval
// Reminder check interval - uses service worker for iOS compatibility
useEffect(() => {
if (!settings?.reminderEnabled || !settings?.reminderTime) return;
const checkReminder = () => {
// Track if we've shown the notification this minute
let lastShownMinute = -1;
const checkReminder = async () => {
const now = new Date();
const [hours, minutes] = settings.reminderTime.split(":").map(Number);
const currentMinute = now.getHours() * 60 + now.getMinutes();
if (now.getHours() === hours && now.getMinutes() === minutes) {
if (now.getHours() === hours && now.getMinutes() === minutes && currentMinute !== lastShownMinute) {
lastShownMinute = currentMinute;
if (Notification.permission === "granted") {
new Notification(APP_NAME, {
await showNotification(APP_NAME, {
body: "Take a moment to reflect on what you're grateful for today.",
icon: "/icon.png",
icon: "/icons/icon.svg",
tag: "daily-reminder",
});
}
}
};
// Check immediately and then every minute
checkReminder();
const interval = setInterval(checkReminder, 60000);
return () => clearInterval(interval);
}, [settings?.reminderEnabled, settings?.reminderTime]);
}, [settings?.reminderEnabled, settings?.reminderTime, showNotification]);
const getProviderName = (id: string) => PROVIDERS.find(p => p.id === id)?.name || id;
@@ -302,9 +318,37 @@ export default function SettingsPage() {
/>
</div>
)}
{settings.reminderEnabled && notificationPermission === "granted" && swRegistered && (
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-green-500">
<Check size={14} />
<span>Notifications ready{isPWA ? " (PWA mode)" : ""}</span>
</div>
<button
onClick={() => showNotification(APP_NAME, {
body: "Test notification - reminders are working!",
icon: "/icons/icon.svg",
tag: "test-notification",
})}
className="text-sm text-accent hover:underline"
>
Test
</button>
</div>
)}
{settings.reminderEnabled && notificationPermission === "granted" && !swRegistered && (
<p className="mt-2 text-sm text-amber-400">
Service worker loading... Notifications may be limited.
</p>
)}
{notificationPermission === "denied" && (
<p className="mt-2 text-sm text-red-400">
Notifications are blocked. Please enable them in your browser settings.
Notifications are blocked. Please enable them in your browser/device settings.
</p>
)}
{settings.reminderEnabled && notificationPermission === "default" && (
<p className="mt-2 text-sm text-amber-400">
Notification permission not yet granted. Toggle reminders off and on to request permission.
</p>
)}
</div>