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 && (
+
+ )}
)}