Skip to content

ASA Go: Send FCM push notifications when fire behaviour advisories are issued#5246

Open
conbrad wants to merge 79 commits intomainfrom
task/send-fcm-notifications
Open

ASA Go: Send FCM push notifications when fire behaviour advisories are issued#5246
conbrad wants to merge 79 commits intomainfrom
task/send-fcm-notifications

Conversation

@conbrad
Copy link
Copy Markdown
Collaborator

@conbrad conbrad commented Mar 24, 2026

Adds backend logic to send FCM push notifications to subscribed devices when forecast
advisories for today are processed.

  • Adds firebase-admin dependency to nats consumer for sending notifications
  • Initializes Firebase Admin SDK at startup using FCM_CREDS env var (JSON service account
    credentials)
  • trigger_notifications looks up zones with active advisories, fetches subscribed device
    tokens, and sends multicast FCM messages
  • Failed tokens are logged; responses with UnregisteredError are considered marked as inactive
    in the DB, successful tokens are left as active

SFMS Notes

  • Currently assumes the legacy SFMS processing pipeline
  • today is today in Vancouver time, as that's what legacy SFMS runs in

FCM registration (mobile client)

Registration is managed entirely within usePushNotifications — the hook owns initialization,
token refresh handling, the registration-on-connect effect, and retry. App only calls
initPushNotifications when authenticated; Settings and FireShapeActionsDrawer call
retryRegistration directly from the hook. Firebase token refresh events flow through the
hook's tokenReceived listener into re-registration without App needing to coordinate between
the hook and Redux.

Invariants for subscription updates (see diagram for visual)

A subscription update can only proceed if all of the following hold:

Invariant Enforced by
Device ID resolved useDeviceId() returns non-null
Network connected networkStatus.connected is true
FCM token registered registeredFcmToken is non-null
Subscriptions initialized subscriptionsInitialized is true (initial fetch complete)

These are composed into selectNotificationSettingsDisabled, which disables the subscribe
button in both components. The guard in updateSubscriptions mirrors the selector as a
last-resort defence, but the button should be unreachable before all four hold.

subscriptionsInitialized as an invariant eliminates a race condition: without it, a user could
toggle a subscription while the initial fetch was still in-flight and have it overwritten by
the server response.


Setup error states (before updates are possible)

The notification setup flows through these states, derived in selectNotificationSetupState:

State Condition UI
permissionDenied User denied push notifications "Enable notifications in Settings"
unregistered + connected + no device error Awaiting FCM token Spinner on subscribe
button
registrationFailed FCM registration threw "Unable to register for notifications"
deviceIdError Device.getId() failed "Unable to identify device"
ready Token registered, device ID resolved Button enabled (subject to remaining
invariants)

Update error states (once updates are possible)

Within the ready state, updates go through:

State Condition UI
Loading subscriptions subscriptionsInitialized is false Subscribe button disabled
Updating toggleSubscription in flight Switch in loading state, disabled
Update failed Server call rejected Snackbar + per-item error on the specific switch that
failed

On failure: the optimistic store update is rolled back to the previous subscription list.
The snackbar (updateError) is shown in both the map drawer and the settings accordion. In the
settings accordion only, the specific LoadingSwitch that failed also shows an inline error —
the map drawer has a single subscribe button so no per-item error is needed there.

Testing

  • Configure your tif_pusher env to point to dev bucket and
    https://wps-pr-5246-e1e498-dev.apps.silver.devops.gov.bc.ca/api
  • Make sure hfi and wind_speed tifs exist for today in sfms/uploads/forecast/ AND that
    they're named with today's date AND that their created_at metadata info is set to today's
    date.
  • Build the dev variant of ASA Go off this branch, login and subscribe to all fire zones.
  • Run tif_pusher

Closes #5165

Test Links:

Landing Page
MoreCast
Percentile
Calculator

C-Haines
FireCalc
FireCalc bookmark
Auto Spatial Advisory
(ASA)

HFI Calculator
SFMS Insights
Fire Watch

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 89.42308% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.10%. Comparing base (10fa7c8) to head (a7f087a).

Files with missing lines Patch % Lines
...api/src/app/auto_spatial_advisory/nats_consumer.py 0.00% 11 Missing ⚠️
mobile/asa-go/src/hooks/usePushNotifications.ts 88.00% 4 Missing and 2 partials ⚠️
...a-go/src/components/map/FireShapeActionsDrawer.tsx 84.61% 1 Missing and 3 partials ⚠️
...kend/packages/wps-api/src/app/fcm/notifications.py 95.08% 2 Missing and 1 partial ⚠️
mobile/asa-go/src/App.tsx 62.50% 2 Missing and 1 partial ⚠️
mobile/asa-go/src/components/settings/Settings.tsx 86.66% 1 Missing and 1 partial ⚠️
.../src/components/settings/SubscriptionAccordion.tsx 80.00% 1 Missing and 1 partial ⚠️
...sa-go/src/components/NotificationErrorSnackbar.tsx 83.33% 0 Missing and 1 partial ⚠️
mobile/asa-go/src/slices/pushNotificationSlice.ts 96.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #5246      +/-   ##
============================================
+ Coverage     67.66%   68.10%   +0.43%     
- Complexity        0       26      +26     
============================================
  Files           357      451      +94     
  Lines         15531    17817    +2286     
  Branches       1728     2108     +380     
============================================
+ Hits          10509    12134    +1625     
- Misses         4493     5068     +575     
- Partials        529      615      +86     
Flag Coverage Δ
android 24.68% <ø> (?)
asa_go 78.97% <91.66%> (?)

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@conbrad conbrad marked this pull request as ready for review March 25, 2026 18:23
@conbrad conbrad requested review from brettedw and dgboss March 25, 2026 18:23
@conbrad conbrad requested review from brettedw and dgboss April 7, 2026 18:23
}
};

export const getUpdatedSubscriptions = (
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUpdatedSubscriptions is no longer used in this slice, it's only called in the useNotificationSettings hook. Perhaps move it into the hook or a utility module?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm no it's still used here, and useNotificationSettings is no longer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by useNotificationSettings is no longer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was on the wrong branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in: ceda0b3

@conbrad conbrad requested a review from dgboss April 9, 2026 21:33
@conbrad conbrad requested a review from dgboss April 9, 2026 21:48
const { networkStatus } = useSelector(selectNetworkStatus);
const isActive = useAppIsActive();

const initPushNotifications = useCallback(async () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should initPushNotifications set a registrationError as well? Currently it only logs errors

Copy link
Copy Markdown
Collaborator Author

@conbrad conbrad Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, if it's for permission denied retrying won't help and registrationError would be misleading.
If it's from getToken failing as a transient failure, it's related to registration, but also tied specifically to FCM, I guess we could retry in that case...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came here to ask the same question.
retryRegistration() has a guard if (!registrationError) return; so if the initial attempt at registration fails retryRegistration will always return due to the guard.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a pretty unlikely situation though.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok if getToken fails, it will set registrationError so it will be retried: 9b8ef97

),
);
dispatch(setRegistrationError(false));
dispatch(resetRegistrationAttempts());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this only resets on a successful registration, so I don't think that will ever let us retry in the same session. Maybe we can we add a test that does something like "initial registration failed, then opening Settings/drawer retries registration"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok since retryRegistration is a deliberate call used in Settings/drawer, I made it reset attempts here: b401ed8

Copy link
Copy Markdown
Collaborator

@dgboss dgboss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this beast!

Copy link
Copy Markdown
Collaborator

@brettedw brettedw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this big one on! You probably already have it in mind but I think just the timing of the notifications needs adjusting before merging

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ASA Go: User receives notifications based on their subscriptions

3 participants