Skip to main content

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 shell
  • sc-gofa-beta1: WebView frontend loaded inside the Flutter shell
  • gofa-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:

  1. Flutter owns native device capabilities.
  2. WebView frontend owns authentication, user state, and settings UI.
  3. 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:

  1. user signs in through the WebView frontend,
  2. frontend asks Flutter for notification permission,
  3. frontend asks Flutter for the native FCM token,
  4. frontend asks Flutter for the installation ID,
  5. frontend calls the backend registration endpoint,
  6. 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:

  1. the UI checks native notification permission,
  2. the UI checks the stored Firestore preference,
  3. the UI decides the effective display state,
  4. 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,
  5. 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_TOPIC
  • UNSUBSCRIBE_FCM_TOPIC
  • GET_NOTIFICATION_PERMISSION_STATUS
  • OPEN_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.enabled is true, 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 = true in 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:

  1. the web event is received,
  2. registration is re-run,
  3. /api/fcm/register is called again,
  4. Firestore updatedAt and lastSeenAt are 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:

  1. backend sends data.url,
  2. user taps the system banner,
  3. Flutter receives the notification-open event,
  4. Flutter extracts the route,
  5. 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:

  1. open Firebase Console,
  2. select project gofa-sdk,
  3. open Messaging,
  4. create a Firebase notification message,
  5. fill title and body,
  6. in targeting, switch from user segments to topic,
  7. choose topic silvercare-all,
  8. 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.ts
  • sc-gofa-beta1/src/services/nativeFcmRegistrationService.ts
  • sc-gofa-beta1/src/components/auth/AuthGuard.tsx
  • sc-gofa-beta1/src/components/settings/NotificationPreferenceItem.tsx
  • sc-gofa-beta1/src/components/debug/VersionDebugger.tsx
  • sc-gofa-beta1/src/types/client-user.ts

Backend

  • gofa-web-nextjs/src/app/api/fcm/register/route.ts
  • gofa-web-nextjs/src/app/api/fcm/send/route.ts
  • gofa-web-nextjs/src/app/api/fcm/send-to-user/route.ts
  • gofa-web-nextjs/src/models/client-user.ts
  • gofa-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.


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.