diff --git a/REMINDER_FEATURE_SETUP.md b/REMINDER_FEATURE_SETUP.md new file mode 100644 index 0000000..d21b6e0 --- /dev/null +++ b/REMINDER_FEATURE_SETUP.md @@ -0,0 +1,328 @@ +# Daily Reminder Feature - Complete Setup & Context + +**Date:** 2026-04-20 +**Status:** ✅ Enabled & Ready for Testing + +--- + +## Overview + +The Daily Reminder feature is a **fully implemented Firebase Cloud Messaging (FCM)** system that sends push notifications to remind users to journal. It works even when the browser is closed (on mobile PWA). + +**Key Point:** All code was already in place but disabled in the UI. This document captures the setup and what was changed to enable it. + +--- + +## Architecture + +### Frontend Flow + +**Files:** `src/hooks/useReminder.ts`, `src/hooks/reminderApi.ts`, `src/pages/SettingsPage.tsx` + +1. User opens Settings → clicks "Daily Reminder" button +2. Modal opens with time picker (`ClockTimePicker` component) +3. User selects time (e.g., 08:00) → clicks "Save" +4. `enableReminder()` is called: + - Requests browser notification permission (`Notification.requestPermission()`) + - Gets FCM token from service worker + - Sends token to backend: `POST /api/notifications/fcm-token` + - Sends settings to backend: `PUT /api/notifications/reminder/{userId}` + - Stores time + enabled state in localStorage + +**Message Handling:** + +- `listenForegroundMessages()` called on app mount (in `src/main.tsx`) +- When app is **focused**: Firebase SDK triggers `onMessage()` → shows notification manually +- When app is **closed**: Service worker (`public/sw.js`) handles it via `onBackgroundMessage()` → shows notification + +### Backend Flow + +**Files:** `backend/scheduler.py`, `backend/routers/notifications.py`, `backend/main.py` + +**Initialization:** + +- `start_scheduler()` called in FastAPI app lifespan +- Initializes Firebase Admin SDK (requires `FIREBASE_SERVICE_ACCOUNT_JSON`) +- Starts APScheduler cron job + +**Every Minute:** + +1. Find all users with `reminder.enabled=true` and FCM tokens +2. For each user: + - Convert UTC time → user's timezone (stored in DB) + - Check if current HH:MM matches `reminder.time` (e.g., "08:00") + - Check if already notified today (via `reminder.lastNotifiedDate`) + - Check if user has written a journal entry today + - **If NOT written yet:** Send FCM push via `firebase_admin.messaging.send_each_for_multicast()` + - Auto-prune stale tokens on failure + - Mark as notified today + +**Database Structure (MongoDB):** + +```js +users collection { + _id: ObjectId, + fcmTokens: [token1, token2, ...], // per device + reminder: { + enabled: boolean, + time: "HH:MM", // 24-hour format + timezone: "Asia/Kolkata", // IANA timezone + lastNotifiedDate: "2026-04-16" // prevents duplicates today + } +} +``` + +--- + +## Changes Made (2026-04-20) + +### 1. Updated Frontend Environment (`.env.local`) + +**Changed:** Firebase credentials from mentor's project → personal test project + +```env +VITE_FIREBASE_API_KEY=AIzaSyAjGq7EFrp1mE_8Ni2iZz8LNk7ySVz-lX8 +VITE_FIREBASE_AUTH_DOMAIN=react-test-8cb04.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=react-test-8cb04 +VITE_FIREBASE_MESSAGING_SENDER_ID=1036594341832 +VITE_FIREBASE_APP_ID=1:1036594341832:web:9db6fa337e9cd2e953c2fd +VITE_FIREBASE_VAPID_KEY=BLXhAWY-ms-ACW4PFpqnPak3VZobBIruylVE8Jt-Gm4x53g4aAzEhQzjTvGW8O7dX76-ZoUjlBV15b-EODr1IaY +``` + +### 2. Updated Backend Environment (`backend/.env`) + +**Changed:** Added Firebase service account JSON (from personal test project) + +```env +FIREBASE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"react-test-8cb04",...} +``` + +### 3. Deleted Service Account JSON File + +- Removed: `service account.json` (no longer needed — credentials now in env var) + +### 4. Enabled Reminder UI (`src/pages/SettingsPage.tsx`) + +**Before:** + +```tsx +
+ +
+``` + +**After:** + +```tsx + +``` + +- Changed from disabled toggle → interactive button +- Shows current reminder time or "Set a daily reminder" +- Clicking opens time picker modal + +### 5. Removed Type Ignore Comment + +**Before:** + +```tsx +// @ts-ignore — intentionally unused, reminder is disabled (coming soon) +const handleReminderToggle = async () => { +``` + +**After:** + +```tsx +const handleReminderToggle = async () => { +``` + +--- + +## Critical Code Files + +| File | Purpose | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `src/hooks/useReminder.ts` | `enableReminder()`, `disableReminder()`, `reenableReminder()`, `getFcmToken()`, `listenForegroundMessages()` | +| `src/hooks/reminderApi.ts` | `saveFcmToken()`, `saveReminderSettings()` | +| `backend/scheduler.py` | `send_reminder_notifications()`, `_process_user()`, `_send_push()`, `init_firebase()` | +| `backend/routers/notifications.py` | `POST /fcm-token`, `PUT /reminder/{user_id}` endpoints | +| `public/sw.js` | Service worker background message handler | +| `src/pages/SettingsPage.tsx` | UI: time picker modal, reminder state mgmt | +| `src/main.tsx` | Calls `listenForegroundMessages()` on mount | +| `backend/main.py` | Scheduler initialization in app lifespan | + +--- + +## How to Test + +### Prerequisites + +- ✅ Backend `.env` has Firebase service account JSON +- ✅ Frontend `.env.local` has Firebase web config + VAPID key +- ✅ UI is enabled (button visible in Settings) + +### Steps + +1. **Restart the backend** (so it picks up new `FIREBASE_SERVICE_ACCOUNT_JSON`) + + ```bash + docker-compose down + docker-compose up + ``` + +2. **Open the app** and go to **Settings** + +3. **Click "Daily Reminder"** → time picker modal opens + +4. **Pick a time** (e.g., 14:30 for testing: pick a time 1-2 minutes in the future) + +5. **Click "Save"** + - Browser asks for notification permission → Accept + - Time is saved locally + sent to backend + +6. **Monitor backend logs:** + + ```bash + docker logs grateful-journal-backend-1 -f + ``` + + Look for: `Reminder sent to user {user_id}: X ok, 0 failed` + +7. **At the reminder time:** + - If browser is open: notification appears in-app + - If browser is closed: PWA/OS notification appears (mobile) + +### Troubleshooting + +| Issue | Solution | +| --------------------------------------------------- | ---------------------------------------------------------------------------------- | +| Browser asks for notification permission repeatedly | Check `Notification.permission === 'default'` in browser console | +| FCM token is null | Check `VITE_FIREBASE_VAPID_KEY` is correct; browser may not support FCM | +| Scheduler doesn't run | Restart backend; check `FIREBASE_SERVICE_ACCOUNT_JSON` is valid JSON | +| Notification doesn't appear | Check `reminder.lastNotifiedDate` in MongoDB; trigger time must match exactly | +| Token registration fails | Check backend logs; 400 error means invalid userId format (must be valid ObjectId) | + +--- + +## Environment Variables Reference + +### Frontend (`.env.local`) + +``` +VITE_FIREBASE_API_KEY # Firebase API key +VITE_FIREBASE_AUTH_DOMAIN # Firebase auth domain +VITE_FIREBASE_PROJECT_ID # Firebase project ID +VITE_FIREBASE_MESSAGING_SENDER_ID # Firebase sender ID +VITE_FIREBASE_APP_ID # Firebase app ID +VITE_FIREBASE_VAPID_KEY # FCM Web Push VAPID key (from Firebase Console → Messaging) +VITE_API_URL # Backend API URL (e.g., http://localhost:8001/api) +``` + +### Backend (`backend/.env`) + +``` +FIREBASE_SERVICE_ACCOUNT_JSON # Entire Firebase service account JSON (minified single line) +MONGODB_URI # MongoDB connection string +MONGODB_DB_NAME # Database name +API_PORT # Backend port +ENVIRONMENT # production/development +FRONTEND_URL # Frontend URL for CORS +``` + +--- + +## Next Steps + +### For Production + +- Switch back to mentor's Firebase credentials (remove personal test project) +- Update `.env.local` and `backend/.env` with production Firebase values + +### Future Improvements + +- Add UI toggle to enable/disable without removing settings +- Show timezone in Settings (currently auto-detected) +- Show last notification date in UI +- Add snooze button to notifications +- Let users set multiple reminder times + +### Resetting to Disabled State + +If you need to disable reminders again: + +1. Revert `.env.local` and `backend/.env` to mentor's credentials +2. Revert `src/pages/SettingsPage.tsx` to show "Coming soon" UI +3. Add back `@ts-ignore` comment + +--- + +## Technical Notes + +### Why This Approach? + +- **FCM:** Works on web, mobile, PWA; no polling needed +- **Service Worker:** Handles background notifications even when browser closed +- **Timezone:** Stores user's IANA timezone to support global users +- **Duplicate Prevention:** Tracks `lastNotifiedDate` per user +- **Smart Timing:** Only notifies if user hasn't written today (no spam) + +### Security Considerations + +- Firebase service account JSON should never be in git (only in env vars) +- FCM tokens are device-specific; backend stores them securely +- All reminder data is encrypted end-to-end (matches app's crypto design) + +### Known Limitations + +- Reminder check runs every minute (not more frequent) +- FCM token refresh is handled by Firebase SDK automatically +- Stale tokens are auto-pruned on failed sends +- Timezone must be valid IANA format (not GMT±X) + +--- + +## Quick Reference Commands + +**Check backend scheduler logs:** + +```bash +docker logs grateful-journal-backend-1 -f | grep -i "reminder\|firebase" +``` + +**View user reminders in MongoDB:** + +```bash +docker exec grateful-journal-mongo-1 mongosh grateful_journal --eval "db.users.findOne({_id: ObjectId('...')})" --username admin --password internvps +``` + +**Clear FCM tokens for a user (testing):** + +```bash +docker exec grateful-journal-mongo-1 mongosh grateful_journal --eval "db.users.updateOne({_id: ObjectId('...')}, {\$set: {fcmTokens: []}})" --username admin --password internvps +``` + +--- + +## Support + +For questions about: + +- **Reminders:** Check daily_reminder_feature.md in memory +- **FCM:** Firebase Cloud Messaging docs +- **APScheduler:** APScheduler documentation +- **Firebase Admin SDK:** Firebase Admin SDK for Python docs diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc index be19f9a..cb04897 100644 Binary files a/backend/__pycache__/config.cpython-312.pyc and b/backend/__pycache__/config.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 84248ae..bd7cb47 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/config.py b/backend/config.py index 78160a4..92eb104 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,5 +1,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict # type: ignore from functools import lru_cache +from pathlib import Path + +_ENV_FILE = str(Path(__file__).parent / ".env") class Settings(BaseSettings): @@ -12,7 +15,7 @@ class Settings(BaseSettings): firebase_service_account_json: str = "" model_config = SettingsConfigDict( - env_file=".env", + env_file=_ENV_FILE, case_sensitive=False, extra="ignore", # ignore unknown env vars (e.g. VITE_* from root .env) ) diff --git a/backend/main.py b/backend/main.py index 102b70c..40cef81 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,3 +1,4 @@ +import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from db import MongoDB @@ -7,6 +8,13 @@ from routers import notifications from scheduler import start_scheduler from contextlib import asynccontextmanager +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + force=True, +) +logging.getLogger("scheduler").setLevel(logging.DEBUG) + settings = get_settings() _scheduler = None diff --git a/backend/routers/users.py b/backend/routers/users.py index dd88015..c4a77ab 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -54,6 +54,7 @@ async def register_user(user_data: UserCreate): "theme": user.get("theme", "light"), "backgroundImage": user.get("backgroundImage"), "backgroundImages": user.get("backgroundImages", []), + "reminder": user.get("reminder"), "createdAt": user["createdAt"].isoformat(), "updatedAt": user["updatedAt"].isoformat(), "message": "User registered successfully" if result.upserted_id else "User already exists" @@ -83,6 +84,7 @@ async def get_user_by_email(email: str): "theme": user.get("theme", "light"), "backgroundImage": user.get("backgroundImage"), "backgroundImages": user.get("backgroundImages", []), + "reminder": user.get("reminder"), "tutorial": user.get("tutorial"), "createdAt": user["createdAt"].isoformat(), "updatedAt": user["updatedAt"].isoformat() diff --git a/backend/scheduler.py b/backend/scheduler.py index c858704..8756e10 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -49,79 +49,109 @@ def init_firebase(): def send_reminder_notifications(): """Check all users and send reminders where due.""" if not _firebase_initialized: + log.warning("Reminder check skipped — Firebase not initialized") return db = get_database() now_utc = datetime.utcnow().replace(second=0, microsecond=0) - # Find all users with reminder enabled and at least one FCM token - users = db.users.find({ + candidates = list(db.users.find({ "reminder.enabled": True, "fcmTokens": {"$exists": True, "$not": {"$size": 0}}, - "reminder.time": {"$exists": True}, - }) + })) - for user in users: + log.debug(f"Reminder check at {now_utc.strftime('%H:%M')} UTC — {len(candidates)} candidate(s)") + + for user in candidates: try: - _process_user(db, user, now_utc) + if user.get("reminder", {}).get("time"): + _process_user(db, user, now_utc) + _process_universal(db, user, now_utc) except Exception as e: log.error(f"Error processing reminder for user {user.get('_id')}: {e}") +def _get_user_local_time(now_utc: datetime, timezone_str: str): + """Returns (now_local, today_str, user_tz).""" + try: + user_tz = pytz.timezone(timezone_str) + except pytz.UnknownTimeZoneError: + user_tz = pytz.utc + now_local = now_utc.replace(tzinfo=pytz.utc).astimezone(user_tz) + today_str = now_local.strftime("%Y-%m-%d") + return now_local, today_str, user_tz + + +def _wrote_today(db, user_id, now_local, user_tz) -> bool: + today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) + today_start_utc = today_start_local.astimezone(pytz.utc).replace(tzinfo=None) + today_end_utc = today_start_utc + timedelta(days=1) + return db.entries.count_documents({ + "userId": user_id, + "createdAt": {"$gte": today_start_utc, "$lt": today_end_utc}, + }) > 0 + + def _process_user(db, user: dict, now_utc: datetime): + uid = user.get("_id") reminder = user.get("reminder", {}) - reminder_time_str = reminder.get("time") # "HH:MM" + reminder_time_str = reminder.get("time") timezone_str = reminder.get("timezone", "UTC") fcm_tokens: list = user.get("fcmTokens", []) if not reminder_time_str or not fcm_tokens: return - try: - user_tz = pytz.timezone(timezone_str) - except pytz.UnknownTimeZoneError: - user_tz = pytz.utc - - # Current time in user's timezone - now_local = now_utc.replace(tzinfo=pytz.utc).astimezone(user_tz) + now_local, today_str, user_tz = _get_user_local_time(now_utc, timezone_str) current_hm = now_local.strftime("%H:%M") if current_hm != reminder_time_str: - return # Not the right minute - - # Check if already notified today (in user's local date) - today_local_str = now_local.strftime("%Y-%m-%d") - last_notified = reminder.get("lastNotifiedDate", "") - if last_notified == today_local_str: - return # Already sent today - - # Check if user has already written today (using createdAt in their timezone) - today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) - today_start_utc = today_start_local.astimezone(pytz.utc).replace(tzinfo=None) - today_end_utc = today_start_utc + timedelta(days=1) - - entry_count = db.entries.count_documents({ - "userId": user["_id"], - "createdAt": {"$gte": today_start_utc, "$lt": today_end_utc}, - }) - - if entry_count > 0: - # Already wrote today — mark notified to avoid repeated checks - db.users.update_one( - {"_id": user["_id"]}, - {"$set": {"reminder.lastNotifiedDate": today_local_str}} - ) + log.debug(f"User {uid}: skipped — current time {current_hm} != reminder time {reminder_time_str} ({timezone_str})") return - # Send FCM notification - _send_push(user["_id"], fcm_tokens, db, today_local_str) + if _wrote_today(db, uid, now_local, user_tz): + log.debug(f"User {uid}: skipped — already wrote today") + return + + log.info(f"User {uid}: sending reminder (time={reminder_time_str}, tz={timezone_str})") + _send_push(uid, fcm_tokens, db) -def _send_push(user_id, tokens: list, db, today_local_str: str): +def _process_universal(db, user: dict, now_utc: datetime): + """Universal 11pm reminder — fires if enabled and no entry written today.""" + uid = user.get("_id") + reminder = user.get("reminder", {}) + timezone_str = reminder.get("timezone", "UTC") + fcm_tokens: list = user.get("fcmTokens", []) + + if not fcm_tokens: + return + + now_local, today_str, user_tz = _get_user_local_time(now_utc, timezone_str) + + if now_local.strftime("%H:%M") != "23:00": + return + + if reminder.get("lastUniversalDate") == today_str: + log.debug(f"User {uid}: universal reminder skipped — already sent today") + return + + if _wrote_today(db, uid, now_local, user_tz): + log.debug(f"User {uid}: universal reminder skipped — already wrote today") + db.users.update_one({"_id": uid}, {"$set": {"reminder.lastUniversalDate": today_str}}) + return + + log.info(f"User {uid}: sending universal 11pm reminder (tz={timezone_str})") + _send_push(uid, fcm_tokens, db, universal=True) + db.users.update_one({"_id": uid}, {"$set": {"reminder.lastUniversalDate": today_str}}) + + +def _send_push(user_id, tokens: list, db, universal: bool = False): """Send FCM multicast and prune stale tokens.""" + title = "Last chance to journal today 🌙" if universal else "Time to journal 🌱" message = messaging.MulticastMessage( notification=messaging.Notification( - title="Time to journal 🌱", + title=title, body="You haven't written today yet. Take a moment to reflect.", ), tokens=tokens, @@ -143,7 +173,6 @@ def _send_push(user_id, tokens: list, db, today_local_str: str): response = messaging.send_each_for_multicast(message) log.info(f"Reminder sent to user {user_id}: {response.success_count} ok, {response.failure_count} failed") - # Remove tokens that are no longer valid stale_tokens = [ tokens[i] for i, r in enumerate(response.responses) if not r.success and r.exception and "not-registered" in str(r.exception).lower() @@ -155,12 +184,6 @@ def _send_push(user_id, tokens: list, db, today_local_str: str): ) log.info(f"Removed {len(stale_tokens)} stale FCM tokens for user {user_id}") - # Mark today as notified - db.users.update_one( - {"_id": user_id}, - {"$set": {"reminder.lastNotifiedDate": today_local_str}} - ) - def start_scheduler() -> BackgroundScheduler: """Initialize Firebase and start the minute-by-minute scheduler.""" diff --git a/src/App.css b/src/App.css index 636724e..695da14 100644 --- a/src/App.css +++ b/src/App.css @@ -2112,7 +2112,7 @@ } .confirm-modal { - background: var(--color-surface); + background: #ffffff; border-radius: 20px; padding: 1.75rem; max-width: 380px; @@ -2897,7 +2897,7 @@ } [data-theme="dark"] .confirm-modal { - background: var(--color-surface); + background: #1e1e1e; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); } @@ -3189,7 +3189,6 @@ [data-theme="liquid-glass"] .calendar-card, [data-theme="liquid-glass"] .entry-card, [data-theme="liquid-glass"] .entry-modal, -[data-theme="liquid-glass"] .confirm-modal, [data-theme="liquid-glass"] .settings-profile, [data-theme="liquid-glass"] .settings-card, [data-theme="liquid-glass"] .settings-tutorial-btn, @@ -3412,6 +3411,14 @@ -webkit-backdrop-filter: blur(6px); } +/* -- Modal boxes always opaque -- */ +[data-theme="liquid-glass"] .confirm-modal, +[data-theme="liquid-glass"] .bg-modal { + background: #ffffff; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + /* -- Bottom nav -- */ [data-theme="liquid-glass"] .bottom-nav-btn { color: #475569; @@ -3820,7 +3827,7 @@ body.gj-has-bg .settings-page { ============================ */ .bg-modal { - background: var(--color-surface, #fff); + background: #ffffff; border-radius: 20px; padding: 1.5rem; width: min(440px, calc(100vw - 2rem)); diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 520be32..a07e185 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -30,6 +30,7 @@ import { saveEncryptedSecretKey, getEncryptedSecretKey, } from '../lib/crypto' +import { REMINDER_TIME_KEY, REMINDER_ENABLED_KEY } from '../hooks/useReminder' type MongoUser = { id: string @@ -40,6 +41,11 @@ type MongoUser = { tutorial?: boolean backgroundImage?: string | null backgroundImages?: string[] + reminder?: { + enabled: boolean + time?: string + timezone?: string + } } type AuthContextValue = { @@ -135,6 +141,18 @@ export function AuthProvider({ children }: { children: ReactNode }) { } } + function syncReminderFromDb(mongoUser: MongoUser) { + const r = mongoUser.reminder + if (r) { + localStorage.setItem(REMINDER_ENABLED_KEY, r.enabled ? 'true' : 'false') + if (r.time) localStorage.setItem(REMINDER_TIME_KEY, r.time) + else localStorage.removeItem(REMINDER_TIME_KEY) + } else { + localStorage.setItem(REMINDER_ENABLED_KEY, 'false') + localStorage.removeItem(REMINDER_TIME_KEY) + } + } + // Register or fetch user from MongoDB async function syncUserWithDatabase(authUser: User) { try { @@ -148,12 +166,11 @@ export function AuthProvider({ children }: { children: ReactNode }) { try { console.log('[Auth] Fetching user by email:', email) const existingUser = await getUserByEmail(email, token) as MongoUser - // console.log('[Auth] Found existing user:', existingUser.id) setUserId(existingUser.id) setMongoUser(existingUser) + syncReminderFromDb(existingUser) } catch (error) { console.warn('[Auth] User not found, registering...', error) - // User doesn't exist, register them const newUser = await registerUser( { email, @@ -165,6 +182,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { console.log('[Auth] Registered new user:', newUser.id) setUserId(newUser.id) setMongoUser(newUser) + syncReminderFromDb(newUser) } } catch (error) { console.error('[Auth] Error syncing user with database:', error) @@ -226,13 +244,11 @@ export function AuthProvider({ children }: { children: ReactNode }) { } async function signOut() { - // Clear secret key from memory setSecretKey(null) setMongoUser(null) - // Clear pending tour step (session state) localStorage.removeItem('gj-tour-pending-step') - // Keep device key and encrypted key for next login - // Do NOT clear localStorage or IndexedDB + localStorage.removeItem(REMINDER_TIME_KEY) + localStorage.removeItem(REMINDER_ENABLED_KEY) await firebaseSignOut(auth) setUserId(null) } diff --git a/src/hooks/useReminder.ts b/src/hooks/useReminder.ts index ea3abc1..2a31b7c 100644 --- a/src/hooks/useReminder.ts +++ b/src/hooks/useReminder.ts @@ -29,11 +29,21 @@ export function isReminderEnabled(): boolean { /** Get FCM token using the existing sw.js (which includes Firebase messaging). */ async function getFcmToken(): Promise { const messaging = await messagingPromise - if (!messaging) return null + if (!messaging) { + console.warn('[FCM] Firebase Messaging not supported in this browser') + return null + } - // Use the already-registered sw.js — no second SW needed const swReg = await navigator.serviceWorker.ready - return getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg }) + console.log('[FCM] Service worker ready:', swReg.active?.scriptURL) + + const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg }) + if (token) { + console.log('[FCM] Token obtained:', token.slice(0, 20) + '…') + } else { + console.warn('[FCM] getToken returned empty — VAPID key wrong or SW not registered?') + } + return token } /** @@ -64,16 +74,21 @@ export async function enableReminder( } const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + console.log('[FCM] Saving token and reminder settings:', { timeStr, timezone }) await saveFcmToken(userId, fcmToken, authToken) + console.log('[FCM] Token saved to backend') + await saveReminderSettings(userId, { time: timeStr, enabled: true, timezone }, authToken) + console.log('[FCM] Reminder settings saved to backend') localStorage.setItem(REMINDER_TIME_KEY, timeStr) localStorage.setItem(REMINDER_ENABLED_KEY, 'true') return null } catch (err) { - console.error('FCM reminder setup failed', err) - return 'Failed to set up push notification. Please try again.' + const msg = err instanceof Error ? err.message : String(err) + console.error('[FCM] Reminder setup failed:', msg) + return `Failed to set up reminder: ${msg}` } } @@ -98,16 +113,21 @@ export async function listenForegroundMessages(): Promise<() => void> { const messaging = await messagingPromise if (!messaging) return () => {} + console.log('[FCM] Foreground message listener registered') + const unsubscribe = onMessage(messaging, (payload) => { + console.log('[FCM] Foreground message received:', payload) const title = payload.notification?.title || 'Grateful Journal 🌱' const body = payload.notification?.body || "You haven't written today yet." - if (Notification.permission === 'granted') { - new Notification(title, { - body, - icon: '/web-app-manifest-192x192.png', - tag: 'gj-daily-reminder', - }) + if (Notification.permission !== 'granted') { + console.warn('[FCM] Notification permission not granted — cannot show notification') + return } + new Notification(title, { + body, + icon: '/web-app-manifest-192x192.png', + tag: 'gj-daily-reminder', + }) }) return unsubscribe diff --git a/src/main.tsx b/src/main.tsx index cba487b..d4cee22 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -15,7 +15,9 @@ if ('serviceWorker' in navigator) { } // Show FCM notifications when app is open in foreground -listenForegroundMessages() +listenForegroundMessages().catch((err) => { + console.error('[FCM] Failed to set up foreground message listener:', err) +}) createRoot(document.getElementById('root')!).render( diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 6d8f1bd..c565de2 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -311,7 +311,6 @@ export default function SettingsPage() { } } - // @ts-ignore — intentionally unused, reminder is disabled (coming soon) const handleReminderToggle = async () => { if (!user || !userId) return if (!reminderTime) { @@ -477,8 +476,12 @@ export default function SettingsPage() {
- {/* Daily Reminder — disabled for now, logic preserved */} -
+ {/* Daily Reminder */} +
+ + + + @@ -999,6 +998,35 @@ export default function SettingsPage() { {reminderSaving ? 'Saving…' : 'Save'} + {reminderEnabled && reminderTime && ( + + )} )}