sojorn/sojorn_docs/features/fcm-implementation.md

222 lines
6.8 KiB
Markdown

# FCM (Firebase Cloud Messaging) Implementation Guide
## Overview
This document describes the complete FCM push notification implementation for Sojorn, covering both the Go backend and Flutter client. The system supports Android, iOS, and Web platforms.
---
## Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Flutter App │────▶│ Go Backend │────▶│ Firebase FCM │
│ (Android/Web) │ │ Push Service │ │ Cloud Messaging │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ POST /notifications/device │
│ (Register FCM Token) │
│ │ │
│ │ SendPush() │
│ │──────────────────────▶│
│ │ │
│◀──────────────────────│◀──────────────────────│
│ Push Notification │ │
```
---
## Backend Configuration
### Required Environment Variables
```env
# Firebase Cloud Messaging (FCM)
FIREBASE_CREDENTIALS_FILE=/opt/sojorn/firebase-service-account.json
FIREBASE_WEB_VAPID_KEY=BNxS7_your_actual_vapid_key_here
```
### Firebase Service Account JSON
Download from Firebase Console > Project Settings > Service Accounts > Generate New Private Key
The JSON file should contain:
- `project_id`
- `private_key`
- `client_email`
---
## Database Schema
### `user_fcm_tokens` Table
```sql
CREATE TABLE IF NOT EXISTS user_fcm_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
token TEXT NOT NULL,
device_type TEXT, -- 'web', 'android', 'ios', 'desktop'
last_updated TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, token) -- Prevents duplicate tokens per device
);
```
**Key Features:**
- Composite unique constraint on `(user_id, token)` prevents duplicate registrations
- `device_type` column supports platform-specific logic
- `ON DELETE CASCADE` ensures tokens are cleaned up when user is deleted
---
## Push Notification Triggers
| Event | Handler | Recipient | Data Payload |
|-------|---------|-----------|--------------|
| New Follower | `user_handler.go:Follow` | Followed user | `type`, `follower_id` |
| Follow Request | `user_handler.go:Follow` | Target user | `type`, `follower_id` |
| Follow Accepted | `user_handler.go:AcceptFollowRequest` | Requester | `type`, `follower_id` |
| New Chat Message | `chat_handler.go:SendMessage` | Receiver | `type`, `conversation_id`, `encrypted` |
| Comment on Post | `post_handler.go:CreateComment` | Post author | `type`, `post_id`, `post_type`, `target` |
| Post Saved | `post_handler.go:SavePost` | Post author | `type`, `post_id`, `post_type`, `target` |
| Beacon Vouched | `post_handler.go:VouchBeacon` | Beacon author | `type`, `beacon_id`, `target` |
| Beacon Reported | `post_handler.go:ReportBeacon` | Beacon author | `type`, `beacon_id`, `target` |
---
## Data Payload Structure
All push notifications include a `data` payload for deep linking:
```json
{
"type": "comment|save|reply|chat|new_follower|follow_request|beacon_vouch",
"post_id": "uuid", // For post-related notifications
"conversation_id": "uuid", // For chat notifications
"follower_id": "uuid", // For follow notifications
"beacon_id": "uuid", // For beacon notifications
"target": "main_feed|quip_feed|beacon_map|secure_chat|profile|thread_view"
}
```
---
## Flutter Client Implementation
### Initialization Flow
1. **Android 13+ Permission Check**: Explicitly request `POST_NOTIFICATIONS` permission
2. **Firebase Permission Request**: Request FCM permissions via SDK
3. **Token Retrieval**: Get FCM token (with VAPID key for web)
4. **Backend Registration**: POST token to `/notifications/device`
5. **Set up Listeners**: Handle token refresh and message callbacks
### Deep Linking (Message Open Handling)
The Flutter client handles these notification types:
| Type | Navigation Target |
|------|-------------------|
| `chat`, `new_message` | SecureChatScreen with conversation |
| `save`, `comment`, `reply` | Based on `target` field (home/quips/beacon) |
| `new_follower`, `follow_request` | Profile screen of follower |
| `beacon_vouch`, `beacon_report` | Beacon map |
### Logout Flow
On logout, the client:
1. Calls `DELETE /notifications/device` with the current token
2. Deletes the token from Firebase locally
3. Resets initialization state
---
## API Endpoints
### Register Device Token
```http
POST /api/v1/notifications/device
Authorization: Bearer <JWT>
Content-Type: application/json
{
"fcm_token": "device_token_here",
"platform": "android|ios|web|desktop"
}
```
### Unregister Device Token
```http
DELETE /api/v1/notifications/device
Authorization: Bearer <JWT>
Content-Type: application/json
{
"fcm_token": "device_token_here"
}
```
---
## Troubleshooting
### Token Not Registering
1. Check Firebase is initialized properly
2. Verify `FIREBASE_CREDENTIALS_FILE` path is correct
3. For web, ensure `FIREBASE_WEB_VAPID_KEY` is set
4. Check network connectivity to Go backend
### Notifications Not Arriving
1. Verify token exists in `user_fcm_tokens` table
2. Check Firebase Console for delivery reports
3. Ensure app hasn't restricted background data
4. On Android 13+, verify POST_NOTIFICATIONS permission
### Invalid Token Cleanup
The `PushService` automatically removes invalid tokens when FCM returns `messaging.IsRegistrationTokenNotRegistered` error.
---
## Testing
### Manual Token Verification
```sql
SELECT user_id, token, device_type, last_updated
FROM user_fcm_tokens
WHERE user_id = 'your-user-uuid';
```
### Send Test Notification
Use Firebase Console > Cloud Messaging > Send test message with the device token.
---
## Platform-Specific Notes
### Android
- Target SDK 33+ requires `POST_NOTIFICATIONS` runtime permission
- Add to `AndroidManifest.xml`:
```xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
```
### Web
- Requires valid VAPID key from Firebase Console
- Service worker must be properly configured
- HTTPS required (except localhost)
### iOS
- Requires APNs configuration in Firebase
- Provisioning profile must include push notification capability