Phase: PUSH-1-FCM-PERMISSION-DEVICE-TOKEN — permission + FCM token only. Backend delivery, deep-linking, and notification-centre UI are tracked in later PUSH-* phases.
This document covers the local engineering setup that is required before
the new push code in src/features/notification/* will produce a usable FCM
token. The Firebase console steps themselves are owned by the
storixbiz@gmail.com account holder — the Android side is captured in Notion;
the iOS side needs a sibling Notion page.
Installed in this phase:
@react-native-firebase/app@react-native-firebase/messaging
Both are configured via Expo config plugins in app.config.ts and are picked
up the next time expo prebuild runs.
Optional overrides — defaults are sensible for a local checkout:
| Var | Default | Purpose |
|---|---|---|
EXPO_IOS_BUNDLE_ID |
kr.storix.app |
iOS bundle identifier |
EXPO_ANDROID_PACKAGE |
kr.storix.app |
Android applicationId |
GOOGLE_SERVICES_JSON |
./google-services.json |
Android Firebase client config |
GOOGLE_SERVICE_INFO_PLIST |
./ios/STORIXFE21/GoogleService-Info.plist |
iOS Firebase client config |
The two
GOOGLE_SERVICE*files are normal Firebase client config — they may live in the repo subject to team policy. Never commit a Firebase Admin SDK service-account JSON or an APNs.p8private key; those belong on the backend only.
- Firebase Console → STORIX project → Add app → Android.
- Package name:
kr.storix.app(must matchEXPO_ANDROID_PACKAGE). - Download
google-services.json. - Place it at the project root as
google-services.json. - Run:
The
npm run prebuild:android npx expo run:android # or `npm run android`@react-native-firebase/appconfig plugin installs thecom.google.gms.google-servicesGradle plugin during prebuild. - The
android.permission.POST_NOTIFICATIONSpermission is added throughapp.config.ts → android.permissions; verify it appears in the generatedandroid/app/src/main/AndroidManifest.xml.
Notion reference: see existing Android Firebase setup page (storixbiz@gmail.com).
- Firebase Console → STORIX project → Add app → iOS.
- Bundle ID:
kr.storix.app(must matchEXPO_IOS_BUNDLE_ID). - Download
GoogleService-Info.plist. - Place it at
ios/STORIXFE21/GoogleService-Info.plist(already linked in the Xcode project — overwrite the placeholder). - Apple Developer portal → App ID → enable Push Notifications.
- Apple Developer portal → Keys → create an APNs Auth Key (
.p8), download once, then in Firebase Console → Project Settings → Cloud Messaging → Apple app config → upload the.p8along with Team ID and Key ID. Do not commit the.p8to the repo. - Push capability + entitlements:
- APNs sandbox vs production: this was the root cause of an early
PUSH-1 delivery failure — make sure the entitlement environment matches
the FCM target. Xcode/dev builds use
aps-environment = development(APNs sandbox); TestFlight/Release builds need a separate provisioning profile withaps-environment = production. The Firebase APNs Auth Key covers both environments, but if the entitlement and the FCM send do not agree, the device silently receives nothing. ios/STORIXFE21/STORIXFE21.entitlementsdeclaresaps-environment = developmentfor the dev workflow.ios/STORIXFE21/Info.plistdeclaresUIBackgroundModes = ["remote-notification"].ios/STORIXFE21/AppDelegate.swiftcallsFirebaseApp.configure()idempotently at launch.
- APNs sandbox vs production: this was the root cause of an early
PUSH-1 delivery failure — make sure the entitlement environment matches
the FCM target. Xcode/dev builds use
- Run:
cd ios && pod install && cd - open ios/STORIXFE21.xcworkspace # then build/run on a real iPhone — APNs is not delivered to the simulator.
Notion reference: create a new iOS Firebase setup page that mirrors the Android one. (Owned by: notification feature lead.)
Implemented under src/features/notification/:
services/pushPermission.ts—requestPushPermission()- iOS:
messaging().requestPermission(),AUTHORIZED|PROVISIONAL⇒ allowed. - Android 13+:
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS. - Android <13: treated as granted.
- Never throws — failures map to
denied.
- iOS:
services/fcmToken.ts—getFcmDeviceToken()andsubscribeFcmTokenRefresh()- Calls
registerDeviceForRemoteMessagesfirst on iOS to avoid theapns-token-not-setrace; retries once after1.5s.
- Calls
api/notification.api.ts—registerDeviceToken(payload)- Endpoint not yet implemented. While
ENDPOINT_AVAILABLE = falsethe function returns a synthetic success envelope without making a network call. Flip the flag (and confirm the path) once PUSH-2-BE lands.
- Endpoint not yet implemented. While
hooks/useRegisterDeviceToken.ts— React Query mutation wrapper.hooks/usePushNotificationBootstrap.ts— orchestrates everything; mounted fromapp/_layout.tsxas<PushNotificationBootstrap />inside theQueryClientProvider. Internal module-level guard prevents re-running the permission flow on re-renders.
The bootstrap only runs when useAuthStore.isAuthenticated === true, so
no permission prompt appears on the login screen.
- Fresh install, log in → OS permission prompt appears (iOS) / Android-13 prompt appears.
- Grant permission → FCM token is retrieved (no crash).
- Use
scripts/send-fcm-test-message.mjs <token>to send a direct FCM push → message arrives on the device (foreground delivery confirmed end-to-end without the Firebase Console UI). - Deny permission → no crash, app remains usable.
- Background the app, send a test push → notification appears in tray (no JS hook required for system display).
- Kill & relaunch the app while logged in → no second permission prompt (the module-level guard keeps it once per session).
- Log out → log back in → flow re-runs once (module guard is per-process, not per-account).
Push diagnostics (the
[PUSH_DIAG]log lines anddumpPushDiagnosticshelper) were removed after the APNs sandbox credential issue was resolved. Usescripts/send-fcm-test-message.mjsfor any future delivery debugging.
scripts/send-fcm-test-message.mjs sends a single notification directly to
an FCM registration token via the Firebase Admin SDK. It bypasses the
Firebase Console UI (which silently accepts the wrong identifier) and is the
fastest way to confirm whether FCM → APNs delivery is healthy.
export GOOGLE_APPLICATION_CREDENTIALS=~/.config/storix/firebase-service-account.json
node scripts/send-fcm-test-message.mjs "<FCM_REGISTRATION_TOKEN>"The service-account JSON has full project privileges — keep it outside
the repo. .gitignore already excludes common filenames; double-check
before any git add.
- iOS home-screen badge is owned by APNs
aps.badgewhile the app is in the background. On foreground, on notification read, and on app resume, the client refreshes/api/v1/notifications/unread-countand serializes native badge writes so an older push count cannot overwrite a newer server count. - Android does not support an app-owned numeric badge through
notifee.setBadgeCount; launchers derive the notification dot/count from displayed system notifications. The client assigns foreground local notifications the servernotificationId, cancels that notification after an individual read, and cancels displayed app notifications after mark-all read. - Background FCM messages containing the
notificationblock are displayed by Android system UI. Exact per-notification cancellation for those messages requires the backend to send a stable Android notification ID/tag and for the client to receive that same value. Without it, only mark-all can safely clear the displayed app notifications. - Validate iOS badge updates in a native development or release build. Expo Go deliberately skips the native push bootstrap.
- PUSH-1-BG-HANDLER — register
setBackgroundMessageHandlerat module entry (cannot live inside a React effect). - PUSH-2-BE — replace the
ENDPOINT_AVAILABLE = falseshort-circuit with the real backend path and platform-casing. - PUSH-3-SETTINGS — manual retry / permission re-request from a settings screen for users who initially declined.
- PUSH-4-UI — notification-centre screen and deep-link routing.