SilverCare Mobile FCM Flow
This document explains the full Firebase Cloud Messaging (FCM) flow used by the SilverCare mobile app.
It covers the current architecture across these projects:
gofa-silvercare-flutter: native Flutter shellsc-gofa-beta1: WebView frontend loaded inside the Flutter shellgofa-web-nextjs: backend APIs and Firebase Admin sending logic- Firebase Console: manual broadcast and campaign testing
This is the current end-to-end reference for:
- notification permission behavior,
- native token registration,
- topic subscription behavior,
- send-to-user,
- send-to-topic,
- Firebase Console broadcast,
- iOS and Android behavior.
Why the architecture is split this way
SilverCare is not a pure Flutter app and not a pure web app.
The current product architecture is:
- Flutter owns native device capabilities.
- WebView frontend owns authentication, user state, and settings UI.
- Next.js backend owns trusted sending logic through Firebase Admin SDK.
That split matters because FCM behavior is also split:
- Flutter can request native notification permission.
- Flutter can read the native FCM token.
- Flutter can subscribe and unsubscribe native FCM topics.
- The frontend knows which signed-in user is active.
- The backend decides how to send notifications safely.
This is why the app uses a WebView bridge rather than putting all notification logic in one place.
High-level notification model
SilverCare currently uses two delivery models.
1. Send to a specific user
Use this when a notification belongs to one known user.
Examples:
- account-specific reminders,
- admin-triggered user messages,
- future business events tied to a single user.
Current backend endpoint:
POST /api/fcm/send-to-user
This is the preferred production-style API for targeted notifications.
2. Send to a topic
Use this when one message should reach many subscribed devices.
Current SilverCare topic:
silvercare-all
Examples:
- general product announcements,
- broad service messages,
- temporary UAT broadcast testing.
SilverCare currently uses a user preference plus native topic subscription to control this topic.
Current user preference model
SilverCare stores the topic preference in Firestore under the client user document.
Path:
Clients/gofa/ClientUsers/{uid}
Current preference fields:
settings.silverCareFcmTopics.all.enabled
settings.silverCareFcmTopics.all.updatedAt
Important distinction:
- this Firestore preference is the user preference,
- the native operating system permission is the system capability,
- the actual device subscription is the native topic state.
The app now treats the settings UI as the final effective notification state, not just the Firestore preference.
That means the UI must consider:
- Firestore preference,
- native notification permission,
- native topic subscription flow.
Notification permission behavior
SilverCare now treats notification permission as a first-class part of the settings experience.
Expected behavior
Case 1: system permission is allowed
If the user has allowed notifications:
- the settings item can show
On, - the app can register the native token,
- the app can subscribe the device to
silvercare-all, - visible system banners can be displayed.
Case 2: system permission is not determined yet
If the user has never made a decision yet:
- turning the setting on requests native notification permission,
- if the user allows, registration and subscription continue,
- if the user denies, the app does not pretend the setting is enabled.
Case 3: system permission is denied
If the user has already denied notifications:
- the settings item shows that system notifications are off,
- tapping the setting opens the system settings page,
- the UI does not falsely present the state as enabled,
- topic preference alone is not treated as proof that banners can be shown.
Why this matters
Without this distinction, the UI can become misleading.
A user may have:
settings.silverCareFcmTopics.all.enabled = true,- a valid FCM token in Firestore,
- even a topic subscription attempt,
but still receive no visible banner because the operating system permission is denied.
The current SilverCare settings flow was updated specifically to avoid that mismatch.
Native registration flow
The current registration flow is:
- user signs in through the WebView frontend,
- frontend asks Flutter for notification permission,
- frontend asks Flutter for the native FCM token,
- frontend asks Flutter for the installation ID,
- frontend calls the backend registration endpoint,
- backend stores the token under the current user document.
Current backend endpoint
POST /api/fcm/register
See also:
gofa-web-nextjs/src/app/api/fcm/register/route.ts
Current frontend service
sc-gofa-beta1/src/services/nativeFcmRegistrationService.ts
Current Flutter bridge file
gofa-silvercare-flutter/lib/screens/webview_screen.dart
Why installation ID matters
The backend stores FCM records using an installation-based key when possible:
installation:{installationId}
That avoids duplicate device rows for the same app installation.
This is especially important in WebView-based architectures where web storage can vary across origins.
SilverCare now keeps installation identity in the native shell instead of relying on WebView local storage.
Topic subscription flow
The silvercare-all topic is controlled by the settings item labeled:
接收通知訊息
The current behavior is:
- the UI checks native notification permission,
- the UI checks the stored Firestore preference,
- the UI decides the effective display state,
- when the user enables the setting:
- if permission is allowed, the app subscribes natively,
- if permission is not determined, the app requests permission first,
- if permission is denied, the app opens system settings,
- when the user disables the setting:
- the app unsubscribes natively,
- Firestore preference is updated to
false.
Current native topic calls
Flutter currently exposes native handlers for:
SUBSCRIBE_FCM_TOPICUNSUBSCRIBE_FCM_TOPICGET_NOTIFICATION_PERMISSION_STATUSOPEN_APP_NOTIFICATION_SETTINGS
Current web service wrappers
sc-gofa-beta1/src/services/fcmTokenService.ts
Current settings UI component
sc-gofa-beta1/src/components/settings/NotificationPreferenceItem.tsx
This component now owns:
- permission status loading,
- Firestore preference loading,
- toggle mutation,
- system-settings recovery behavior,
- notification-specific error display.
Default subscription behavior
SilverCare currently treats the broad topic as default-on for new users.
That means:
- if
settings.silverCareFcmTopics.all.enabledistrue, the device should subscribe, - if it is
false, the device should unsubscribe, - if it is missing, the app currently treats it as default-on and writes back
true.
This behavior is implemented during the authenticated app flow.
Why this was added
During testing, a real issue appeared:
- a user had
enabled = truein Firestore, - a newly installed or newly logged-in device was not yet subscribed,
- the user had to manually toggle the setting off and on.
That was fixed by adding automatic native topic synchronization after login.
Current logic location:
sc-gofa-beta1/src/components/auth/AuthGuard.tsx
This now ensures that after login the device state is aligned with the stored or defaulted preference.
Token refresh behavior
SilverCare also needs to handle token changes.
What matters in practice
For the product, the important requirement is:
- when the native FCM token changes,
- the backend registration must be refreshed,
- Firestore must reflect the latest token for that installation.
Current implementation
Flutter listens for native token refresh and dispatches a web event:
nativeFcmTokenRefreshed
The WebView frontend listens for that event and re-runs the native registration flow.
Current files:
- Flutter:
gofa-silvercare-flutter/lib/screens/webview_screen.dart - frontend listener:
sc-gofa-beta1/src/components/auth/AuthGuard.tsx
Debug validation
A development-only debug trigger was added to simulate this event.
Current file:
sc-gofa-beta1/src/components/debug/VersionDebugger.tsx
This was used to confirm that:
- the web event is received,
- registration is re-run,
/api/fcm/registeris called again,- Firestore
updatedAtandlastSeenAtare refreshed.
Notification open routing
SilverCare supports routing the app when a user taps a notification.
The current payload convention uses:
{
"data": {
"url": "/settings"
}
}
The current native flow is:
- backend sends
data.url, - user taps the system banner,
- Flutter receives the notification-open event,
- Flutter extracts the route,
- the Flutter shell loads that route inside the WebView.
This was verified in real testing using /settings as the target route.
Sending notifications by code
SilverCare currently has two code-based sending patterns.
1. Send to one raw token
Relevant backend endpoint:
POST /api/fcm/send
Use this for:
- direct device testing,
- payload debugging,
- local QA.
2. Send to one user by UID
Relevant backend endpoint:
POST /api/fcm/send-to-user
Use this for:
- production-style targeted sending,
- multi-device delivery for one user,
- backend-controlled selection and invalid-token cleanup.
This is the preferred targeted-notification API.
Relevant file:
gofa-web-nextjs/src/app/api/fcm/send-to-user/route.ts
3. Send to topic by code
SilverCare also has a local topic send script:
gofa-web-nextjs/send-fcm-topic-message.ts
This script was used in testing to send:
- topic:
silvercare-all - route:
/settings
and it was confirmed to work on both iPhone and Android.
Sending broadcasts from Firebase Console
SilverCare topic broadcast was also verified through Firebase Console.
Current tested flow:
- open Firebase Console,
- select project
gofa-sdk, - open Messaging,
- create a Firebase notification message,
- fill title and body,
- in targeting, switch from user segments to topic,
- choose topic
silvercare-all, - send immediately.
This was verified successfully with real subscribed devices.
When to use Console broadcast
Use Firebase Console when you want:
- manual UAT broadcast,
- operator-driven messaging,
- temporary campaign testing,
- non-code verification.
Do not rely on Console as the primary product-notification mechanism for long-term business flows.
Files that now matter most
Flutter shell
gofa-silvercare-flutter/lib/screens/webview_screen.dart
This file currently owns:
- bridge handlers,
- permission requests,
- FCM token access,
- installation ID access,
- topic subscribe and unsubscribe,
- open-notification routing,
- token refresh event dispatch.
WebView frontend
sc-gofa-beta1/src/services/fcmTokenService.tssc-gofa-beta1/src/services/nativeFcmRegistrationService.tssc-gofa-beta1/src/components/auth/AuthGuard.tsxsc-gofa-beta1/src/components/settings/NotificationPreferenceItem.tsxsc-gofa-beta1/src/components/debug/VersionDebugger.tsxsc-gofa-beta1/src/types/client-user.ts
Backend
gofa-web-nextjs/src/app/api/fcm/register/route.tsgofa-web-nextjs/src/app/api/fcm/send/route.tsgofa-web-nextjs/src/app/api/fcm/send-to-user/route.tsgofa-web-nextjs/src/models/client-user.tsgofa-web-nextjs/send-fcm-topic-message.ts
Real behaviors that were validated
The following were validated on real devices during implementation.
iPhone
Verified:
- APNs + FCM delivery works,
- topic subscription works,
- login-time auto-sync works,
- send-to-user works,
- topic broadcast works,
- Firebase Console broadcast works,
- tapping the notification can route into
/settings, - denied system permission prevents visible banners,
- after enabling notifications in system settings, the app state can re-sync.
Android
Verified:
- fresh install on Android 13 requests notification permission,
- topic subscription works,
- login-time auto-sync works,
- topic broadcast works,
- denied system permission shows the correct settings-state message,
- re-enabling notifications aligns the UI again.
Important development notes
1. Debug startup currently clears WebView data
The Flutter shell currently includes a debug behavior that clears WebView state on startup.
That affects:
- cookies,
- cache,
- web storage,
- login persistence in debug testing.
This is important when testing login persistence after app restarts.
2. Permission state and topic state are not the same thing
A user can have:
- an enabled Firestore preference,
- a native token,
- even a topic subscription attempt,
but still receive no visible banner when system notification permission is denied.
Do not collapse these concepts when debugging delivery issues.
3. Console success does not prove product logic
A Firebase Console broadcast only proves:
- Firebase can deliver to the selected audience.
It does not by itself prove:
- user registration timing is correct,
- send-to-user cleanup logic is correct,
- permission-state handling is correct,
- business-triggered routing is correct.
That is why both Console testing and code-path testing are useful.
Recommended production direction
SilverCare now has a workable foundation. The recommended long-term split is:
- use Firestore preference + native topic sync for broad opt-in topics,
- use send-to-user for targeted or business notifications,
- use Firebase Console for manual broadcast and operational campaigns,
- keep permission-aware UI so the settings page represents the real effective state.
That split is both technically clean and product-correct for the current SilverCare architecture.