Files
grateful-journal/REMINDER_FEATURE_SETUP.md
2026-04-20 15:23:28 +05:30

329 lines
11 KiB
Markdown

# 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
<div className="settings-item" style={{ opacity: 0.5 }}>
<label className="settings-toggle">
<input type="checkbox" checked={false} disabled readOnly />
</label>
</div>
```
**After:**
```tsx
<button
type="button"
className="settings-item settings-item-button"
onClick={handleOpenReminderModal}
>
<div className="settings-item-content">
<h4 className="settings-item-title">Daily Reminder</h4>
<p className="settings-item-subtitle">
{reminderEnabled && reminderTime
? `Set for ${reminderTime}`
: "Set a daily reminder"}
</p>
</div>
</button>
```
- 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