-
Notifications
You must be signed in to change notification settings - Fork 17
feat: implement PushNotificationService with encrypted token registration (Phase 3) #396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Add FCMService to handle Firebase Cloud Messaging integration following MIP-05 approach: - Implement background message handler to wake app when notifications arrive - Add FCM token management with automatic refresh handling - Set up foreground and background message listeners - Integrate with existing flutter_background_service for event processing - Add platform checks to skip Firebase on unsupported platforms (Linux, web) - Include
…gging - Add check to prevent duplicate Firebase initialization - Replace logger with debugPrint for better visibility during initialization - Change _messaging from final field to getter for lazy initialization - Add empty stub files for platform-specific Firebase options - Improve initialization flow logging with step-by-step debug messages
Add PushNotificationService to handle encrypted FCM token registration with custom push server: - Implement ECDH + ChaCha20-Poly1305 encryption following MIP-05 privacy approach - Add server public key fetching and initialization flow - Support token registration/unregistration per trade public key - Include platform detection (Android/iOS) in encrypted payload - Add padded payload format to prevent traffic analysis - Integrate service
WalkthroughThis pull request implements Firebase Cloud Messaging (FCM) and privacy-preserving push notification infrastructure for the Mostro mobile app. It adds FCM service initialization, token management, background message handling, encrypted token registration with the Mostro push server, and corresponding Riverpod providers, along with configuration and documentation updates. Changes
Sequence DiagramssequenceDiagram
participant App as App Startup
participant FCM as FCMService
participant FB as Firebase
participant Prefs as SharedPreferences
participant BGS as Background Service
App->>FCM: _initializeFirebaseMessaging()
FCM->>FB: FirebaseMessaging.instance.initializeApp()
FB-->>FCM: initialized
FCM->>FB: requestNotificationPermission()
FB-->>FCM: permission result
FCM->>FB: getToken()
FB-->>FCM: token
FCM->>Prefs: store fcm_token
Prefs-->>FCM: stored
FCM->>FB: setBackgroundMessageHandler()
FB-->>FCM: handler registered
FCM->>BGS: trigger pending fetch check
BGS-->>FCM: acknowledged
FCM-->>App: initialized
sequenceDiagram
participant App as App
participant PushSvc as PushNotificationService
participant FCM as FCMService
participant Server as Mostro Push Server
participant Crypto as Crypto Utils
App->>PushSvc: initialize()
PushSvc->>Server: GET /api/info
Server-->>PushSvc: server public key
PushSvc-->>App: initialized
App->>PushSvc: registerToken(tradePubkey)
PushSvc->>FCM: getToken()
FCM-->>PushSvc: fcm_token
PushSvc->>Crypto: generate ephemeral key pair
Crypto-->>PushSvc: ephemeral keypair
PushSvc->>Crypto: ECDH(ephemeral, server_pubkey)
Crypto-->>PushSvc: shared secret
PushSvc->>Crypto: HKDF-SHA256(shared secret)
Crypto-->>PushSvc: derived key
PushSvc->>Crypto: ChaCha20-Poly1305.encrypt()
Crypto-->>PushSvc: ciphertext
PushSvc->>Server: POST /api/register with encrypted token
Server-->>PushSvc: registration response
PushSvc-->>App: success
sequenceDiagram
participant FB as Firebase/System
participant BGHandler as Background Message Handler
participant Service as Background Service
participant Prefs as SharedPreferences
FB->>BGHandler: RemoteMessage (background)
BGHandler->>Service: initialize()
Service-->>BGHandler: ready
BGHandler->>Prefs: record wake timestamp
Prefs-->>BGHandler: stored
BGHandler->>Service: sendPort.send(fetchEvents)
Service->>Service: fetch/decrypt/notify events
Service-->>BGHandler: complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🤖 Fix all issues with AI Agents
In @lib/main.dart:
- Around line 44-46: The app currently creates FCM/push services twice: once in
_initializeFirebaseMessaging() called from main() and again via
fcmServiceProvider/pushNotificationServiceProvider, causing discarded instances
and uninitialized providers; fix this by having _initializeFirebaseMessaging
return the initialized FCM and PushNotification service instances, then override
fcmServiceProvider and pushNotificationServiceProvider with those returned
instances (e.g., via ProviderScope overrides) before continuing to run the app
so every consumer uses the single initialized instances; alternatively remove
the separate providers and wire consumers to use the instances returned by
_initializeFirebaseMessaging, but do not keep both approaches.
In @lib/services/fcm_service.dart:
- Around line 83-86: The platform guard in initialize() is inconsistent with the
background handler: background handler uses "if (kIsWeb || Platform.isLinux)
return;" but initialize() currently uses "if (!kIsWeb && Platform.isLinux)".
Update the initialize() function in lib/services/fcm_service.dart to skip
initialization on both web and Linux (use the same condition kIsWeb ||
Platform.isLinux), keep the debugPrint message, and return early so behavior
matches the background handler.
- Around line 182-197: The _setupTokenRefreshListener currently only persists
the new FCM token to prefs and leaves a stale TODO about Phase 3; update it to
notify the PushNotificationService so refreshed tokens are re-registered with
the server: add an injectable PushNotificationService (or expose a
callback/stream like _onTokenRefresh) and invoke it when a newToken arrives
after saving to _prefs, ensuring _tokenRefreshSubscription, _fcmTokenKey, and
error handling remain intact and remove the outdated TODO.
- Around line 209-216: The if (isRunning) block around the
FlutterBackgroundService() / isRunning check is a no-op; remove the empty if
branch and its comment (or replace it with the intended behavior such as
returning early or dispatching an event to the running service). Locate the code
that instantiates FlutterBackgroundService() and the await service.isRunning()
call and either delete the whole if (isRunning) { /* comment */ } block or
implement the desired action (e.g., service.sendData(...) or return) so there is
no dead code left.
In @lib/services/push_notification_service.dart:
- Around line 114-116: The debugPrint call uses tradePubkey.substring(0, 16)
which can throw a RangeError for malformed short pubkeys; update the logging to
defensively trim the pubkey (e.g., use a safe truncation that takes
min(tradePubkey.length, 16) or returns the whole string when shorter) and/or
introduce a small helper like shortenPubkey(tradePubkey, 16) and use that in
this debugPrint (and other similar log sites) to avoid crashes while preserving
readable output.
- Around line 262-280: The _createPaddedPayload function can overflow when
tokenBytes.length exceeds available space; add a guard after computing
tokenBytes (and before setRange) that checks tokenBytes.length <=
_paddedPayloadSize - 3 and if not, either throw a clear ArgumentError/RangeError
mentioning deviceToken length or safely truncate the tokenBytes to that maximum
length; ensure the error message references _createPaddedPayload and includes
the offending length and allowed max so callers can handle it.
🧹 Nitpick comments (2)
lib/services/push_notification_service.dart (1)
69-80: Consider validating server public key format before accepting it.The code checks for null/empty but doesn't validate that the public key is a valid 66-character hex string (33-byte compressed EC point). A malformed key will cause a cryptic error later in
_encryptToken. Adding upfront validation improves debuggability.🔎 Suggested validation
if (response.statusCode == 200) { final data = jsonDecode(response.body); _serverPubkey = data['server_pubkey']; - if (_serverPubkey != null && _serverPubkey!.isNotEmpty) { + if (_serverPubkey != null && + _serverPubkey!.length == 66 && + RegExp(r'^[0-9a-fA-F]+$').hasMatch(_serverPubkey!)) { _isInitialized = true; debugPrint('PushService: Initialized successfully'); return true; } else { - _logger.e('Server pubkey is null or empty'); + _logger.e('Server pubkey is invalid: $_serverPubkey'); return false; }lib/services/fcm_service.dart (1)
11-12: Consider standardizing logging approach.The file mixes
debugPrint()(e.g., lines 84, 88, 95) withLoggermethods (e.g., lines 126, 148, 152). Consider using one consistently for easier log filtering and level management.Also applies to: 88-89, 126-127
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
docs/FCM_IMPLEMENTATION.mdlib/core/config.dartlib/firebase_options_platform.dartlib/firebase_options_stub.dartlib/main.dartlib/services/fcm_service.dartlib/services/push_notification_service.dartlib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dart
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{dart,flutter}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{dart,flutter}: Runflutter analyzeafter any code change - Mandatory before commits to ensure zero linting issues
Runflutter testafter any code change - Mandatory before commits to ensure all unit tests pass
Files:
lib/core/config.dartlib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/services/fcm_service.dartlib/services/push_notification_service.dartlib/main.dart
**/*.dart
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.dart: Use Riverpod for all state management - encapsulate business logic in Notifiers and access data only through repository classes
All code comments must be in English - use clear, concise English for variable names, function names, and comments
Always checkmountedbefore using BuildContext after async operations to prevent errors on disposed widgets
Useconstconstructors where possible for better performance and immutability
Remove unused imports and dependencies to maintain code cleanliness and reduce build size
**/*.dart: Application code should be organized underlib/, grouped by domain withlib/features/<feature>/structure, shared utilities inlib/shared/, dependency wiring inlib/core/, and services inlib/services/
Persistence, APIs, and background jobs should live inlib/data/andlib/background/; generated localization output must be inlib/generated/and must stay untouched
Applyflutter format .to enforce canonical Dart formatting (two-space indentation, trailing commas) before committing
Resolve every analyzer warning in Dart code
Name Riverpod providers using the<Feature>Provideror<Feature>Notifierconvention
Localize all user-facing strings via ARB files and access them withS.of(context)rather than hard-coded literals
Files:
lib/core/config.dartlib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/services/fcm_service.dartlib/services/push_notification_service.dartlib/main.dart
lib/shared/**/*.dart
📄 CodeRabbit inference engine (CLAUDE.md)
Follow existing feature patterns when adding new shared utilities - refer to order, chat, and auth features as implementation examples
Files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dart
lib/services/**/*.dart
📄 CodeRabbit inference engine (CLAUDE.md)
Access NostrService for all Nostr protocol interactions - NostrService manages relay connections and messaging
Files:
lib/services/fcm_service.dartlib/services/push_notification_service.dart
lib/main.dart
📄 CodeRabbit inference engine (CLAUDE.md)
Configure timeago package locales in app initialization for proper relative time formatting (e.g., 'hace X horas' vs 'hours ago')
Files:
lib/main.dart
🧠 Learnings (19)
📚 Learning: 2025-10-14T21:12:06.887Z
Learnt from: Catrya
Repo: MostroP2P/mobile PR: 327
File: lib/features/order/notfiers/abstract_mostro_notifier.dart:141-154
Timestamp: 2025-10-14T21:12:06.887Z
Learning: In the MostroP2P mobile codebase, the notification system uses a two-layer localization pattern: providers/notifiers (without BuildContext access) call `showCustomMessage()` with string keys (e.g., 'orderTimeoutMaker', 'orderCanceled'), and the UI layer's `NotificationListenerWidget` has a switch statement that maps these keys to localized strings using `S.of(context)`. This architectural pattern properly separates concerns while maintaining full localization support for all user-facing messages.
Applied to files:
lib/core/config.dartlib/shared/providers/push_notification_service_provider.dart
📚 Learning: 2025-05-06T15:49:26.443Z
Learnt from: chebizarro
Repo: MostroP2P/mobile PR: 74
File: lib/services/mostro_service.dart:70-76
Timestamp: 2025-05-06T15:49:26.443Z
Learning: In the Mostro Mobile codebase, Riverpod code generation is used with `Riverpod` annotations. Providers like `eventStorageProvider` are generated in `.g.dart` files from annotated functions in the main provider files. These providers are accessible by importing the main provider file (e.g., `mostro_service_provider.dart`), not by importing a separate provider file.
Applied to files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:26.407Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T12:10:26.407Z
Learning: Applies to **/*.dart : Name Riverpod providers using the `<Feature>Provider` or `<Feature>Notifier` convention
Applied to files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/**/providers/**/*.dart : Organize Riverpod providers by feature in `features/{feature}/providers/` using Notifier pattern for complex state logic
Applied to files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-05-06T15:49:26.443Z
Learnt from: chebizarro
Repo: MostroP2P/mobile PR: 74
File: lib/services/mostro_service.dart:70-76
Timestamp: 2025-05-06T15:49:26.443Z
Learning: In the Mostro Mobile codebase, `eventStorageProvider` is exported from `package:mostro_mobile/shared/providers/mostro_service_provider.dart` and not from a separate `event_storage_provider.dart` file.
Applied to files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to **/*.dart : Use Riverpod for all state management - encapsulate business logic in Notifiers and access data only through repository classes
Applied to files:
lib/shared/providers/fcm_service_provider.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/**/providers/**/*.dart : Use Notifier pattern instead of simple StateNotifier for complex state logic requiring business rule encapsulation
Applied to files:
lib/shared/providers/providers.dartlib/shared/providers/push_notification_service_provider.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/**/notifiers/**/*.dart : Encapsulate business logic in Notifiers - Notifiers should expose state via providers and handle all complex state transitions
Applied to files:
lib/shared/providers/providers.dartlib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/shared/**/*.dart : Follow existing feature patterns when adding new shared utilities - refer to order, chat, and auth features as implementation examples
Applied to files:
lib/shared/providers/providers.dartlib/main.dart
📚 Learning: 2025-08-21T14:45:43.974Z
Learnt from: Catrya
Repo: MostroP2P/mobile PR: 272
File: lib/features/relays/widgets/relay_selector.dart:13-15
Timestamp: 2025-08-21T14:45:43.974Z
Learning: In the Mostro mobile app's RelaySelector widget (lib/features/relays/widgets/relay_selector.dart), watching relaysProvider.notifier correctly triggers rebuilds because the relaysProvider itself depends on settingsProvider (line 8 in relays_provider.dart). When blacklist changes via toggleMostroRelayBlacklist(), the settingsProvider updates, causing relaysProvider to rebuild, which then notifies widgets watching the notifier. The UI correctly reflects active/inactive states in real-time through this dependency chain.
Applied to files:
lib/shared/providers/push_notification_service_provider.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/subscriptions/subscription_manager.dart : Use SubscriptionManager with `fireImmediately: false` during SessionNotifier initialization to prevent premature execution
Applied to files:
lib/services/fcm_service.dartlib/main.dart
📚 Learning: 2025-05-08T16:29:52.154Z
Learnt from: chebizarro
Repo: MostroP2P/mobile PR: 74
File: lib/background/background.dart:74-77
Timestamp: 2025-05-08T16:29:52.154Z
Learning: In the Mostro Mobile background service architecture, events aren't stored by the background process. Instead, the background service only checks if events exist in the eventStore and sends notifications for new ones, while the foreground process is responsible for storing and processing events. This is an intentional design decision to separate concerns.
Applied to files:
lib/services/fcm_service.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/services/nostr_service.dart : Manage all relay connections and Nostr messaging through NostrService - automatically reconnect when relay list updates
Applied to files:
lib/services/fcm_service.dart
📚 Learning: 2025-11-27T12:10:12.081Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.081Z
Learning: Run `flutter test integration_test/` only for significant changes affecting core services and main user flows
Applied to files:
lib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to **/*.dart : Always check `mounted` before using BuildContext after async operations to prevent errors on disposed widgets
Applied to files:
lib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/**/screens/**/*.dart : Use `S.of(context)!.yourKey` for all user-facing strings instead of hardcoded text
Applied to files:
lib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/features/**/screens/**/*.dart : Keep UI code declarative and side-effect free - use post-frame callbacks for side effects like SnackBars/dialogs
Applied to files:
lib/main.dart
📚 Learning: 2025-08-15T01:37:12.243Z
Learnt from: Catrya
Repo: MostroP2P/mobile PR: 270
File: lib/shared/widgets/order_filter.dart:133-135
Timestamp: 2025-08-15T01:37:12.243Z
Learning: The MostroP2P/mobile project requires Flutter >=3.27.0 as specified in pubspec.yaml, which supports the Color.withValues() method, so usage of withValues() throughout the codebase is valid and should not be flagged as a compatibility issue.
Applied to files:
lib/main.dart
📚 Learning: 2025-11-27T12:10:12.082Z
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-27T12:10:12.082Z
Learning: Applies to lib/data/repositories/**/*.dart : Implement Repository pattern for all data access - all data operations must go through repository classes
Applied to files:
lib/main.dart
🔇 Additional comments (20)
lib/core/config.dart (1)
46-51: LGTM! Push server URL configuration follows established patterns.The new
pushServerUrlconstant correctly usesString.fromEnvironmentfor compile-time configuration, matching the existing pattern formostroPubKey. The default production URL is sensible.lib/services/push_notification_service.dart (5)
23-43: LGTM! Clean service structure with proper dependency injection.The class follows good practices: required FCMService dependency, optional URL override with sensible default fallback to
Config.pushServerUrl, and well-documented constants for the MIP-05 encryption scheme.
153-185: LGTM! Unregister flow is appropriately simpler.The unregistration flow correctly skips initialization checks since it doesn't require encryption—only the trade pubkey is sent. The timeout and error handling are consistent with
registerToken.
195-260: LGTM! Solid MIP-05 encryption implementation.The cryptographic flow correctly implements ECDH key derivation with ephemeral keypairs and ChaCha20-Poly1305 authenticated encryption. The Fortuna PRNG seeding with secure random bytes, proper nonce generation, and standard 128-bit auth tag are all appropriate choices.
282-308: LGTM! HKDF implementation is correct for 32-byte output.The extract-then-expand HKDF construction with SHA256 is properly implemented. The single-iteration expand is sufficient since the requested key length (32 bytes) equals the hash output size.
310-345: LGTM! Helper methods are correctly implemented.The byte manipulation utilities (
_hexToBytes,_randomBytes,_bigIntToBytes,_compressPublicKey) follow standard implementations for cryptographic operations.docs/FCM_IMPLEMENTATION.md (1)
107-108: LGTM! Documentation correctly updated to reflect implementation status.The phase completion markers accurately reflect the implemented functionality in this PR.
lib/shared/providers/providers.dart (1)
3-4: LGTM! Provider exports correctly added to barrel file.The new provider exports follow the existing pattern, making
fcmServiceProviderandpushNotificationServiceProvideraccessible via the shared providers barrel import.lib/shared/providers/fcm_service_provider.dart (1)
5-8: LGTM! Provider follows established patterns.The provider correctly uses
ref.watchto obtainSharedPreferencesAsyncand constructsFCMService. SincesharedPreferencesProvideris overridden at app startup and remains stable, theFCMServiceinstance will be cached by Riverpod. Based on learnings, this follows the<Feature>Providernaming convention.lib/main.dart (1)
93-113: Platform check ordering is correct.Good defensive check:
!kIsWebis evaluated first beforePlatform.isLinux, avoiding the runtime error that would occur ifdart:ioPlatform is accessed on web. The try-catch ensures FCM failures don't crash the app.lib/shared/providers/push_notification_service_provider.dart (1)
5-9: No changes needed. Riverpod'sProvider<T>caches instances by default and only recreates them when watched dependencies change. SincefcmServiceProviderdepends on the stablesharedPreferencesProvider(app singleton),pushNotificationServiceProviderwill maintain a singlePushNotificationServiceinstance throughout the app lifetime. The internal state (_isInitialized,_serverPubkey) is preserved correctly.lib/services/fcm_service.dart (9)
1-9: LGTM!Imports are appropriate for the FCM service functionality.
13-60: LGTM!The background handler properly guards against unsupported platforms, initializes Firebase only when needed, records timestamps for debugging, and sets a pending flag when the background service isn't reachable. Good defensive error handling.
62-76: LGTM!Clean class structure with proper dependency injection for
SharedPreferencesAsync, tracked subscriptions for cleanup, and reasonable timeout/key constants.
132-155: LGTM!Permission request correctly handles both
authorizedandprovisionalstatuses, with proper error handling.
157-180: LGTM!Good timeout handling and graceful degradation when token retrieval fails.
227-237: LGTM!Correctly clears the pending fetch flag set by the background handler. Based on learnings, the background service handles fetching/processing while this just coordinates the wake-up signal.
239-253: LGTM!Good caching strategy - retrieves stored token before making a network call.
255-273: LGTM!Good defensive error handling - ensures local storage cleanup even when the Firebase token deletion fails.
275-282: LGTM!Proper cleanup of subscriptions and state reset.
| // Initialize FCM (skip on Linux) | ||
| await _initializeFirebaseMessaging(sharedPreferences); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FCM/Push services are instantiated twice: here and via providers.
The services are created directly in _initializeFirebaseMessaging and separately via fcmServiceProvider/pushNotificationServiceProvider. This means:
- The initialized instances here are discarded after
main()completes - Provider instances are uninitialized when first accessed elsewhere in the app
Consider either:
- Using the providers consistently (override with initialized instances), or
- Removing the providers and using a different DI approach
🔎 Suggested fix: Override providers with initialized instances
// Initialize FCM (skip on Linux)
- await _initializeFirebaseMessaging(sharedPreferences);
+ FCMService? fcmService;
+ PushNotificationService? pushService;
+ if (!kIsWeb && !Platform.isLinux) {
+ try {
+ fcmService = FCMService(sharedPreferences);
+ await fcmService.initialize();
+ pushService = PushNotificationService(fcmService: fcmService);
+ await pushService.initialize();
+ } catch (e) {
+ debugPrint('Failed to initialize Firebase Cloud Messaging: $e');
+ }
+ }
final container = ProviderContainer(
overrides: [
settingsProvider.overrideWith((b) => settings),
backgroundServiceProvider.overrideWithValue(backgroundService),
biometricsHelperProvider.overrideWithValue(biometricsHelper),
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
secureStorageProvider.overrideWithValue(secureStorage),
mostroDatabaseProvider.overrideWithValue(mostroDatabase),
eventDatabaseProvider.overrideWithValue(eventsDatabase),
+ if (fcmService != null)
+ fcmServiceProvider.overrideWithValue(fcmService),
+ if (pushService != null)
+ pushNotificationServiceProvider.overrideWithValue(pushService),
],
);This ensures the same initialized instances are used throughout the app.
🤖 Prompt for AI Agents
In @lib/main.dart around lines 44-46, The app currently creates FCM/push
services twice: once in _initializeFirebaseMessaging() called from main() and
again via fcmServiceProvider/pushNotificationServiceProvider, causing discarded
instances and uninitialized providers; fix this by having
_initializeFirebaseMessaging return the initialized FCM and PushNotification
service instances, then override fcmServiceProvider and
pushNotificationServiceProvider with those returned instances (e.g., via
ProviderScope overrides) before continuing to run the app so every consumer uses
the single initialized instances; alternatively remove the separate providers
and wire consumers to use the instances returned by
_initializeFirebaseMessaging, but do not keep both approaches.
| if (!kIsWeb && Platform.isLinux) { | ||
| debugPrint('FCM: Skipping initialization on Linux (not supported)'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Platform check inconsistent with background handler.
The background handler (line 24) skips both web and Linux: if (kIsWeb || Platform.isLinux) return;
But initialize() only skips Linux: if (!kIsWeb && Platform.isLinux). If web is unsupported, add the same guard here for consistency.
🔎 Proposed fix
- if (!kIsWeb && Platform.isLinux) {
- debugPrint('FCM: Skipping initialization on Linux (not supported)');
+ if (kIsWeb || Platform.isLinux) {
+ debugPrint('FCM: Skipping initialization (platform not supported)');
return;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!kIsWeb && Platform.isLinux) { | |
| debugPrint('FCM: Skipping initialization on Linux (not supported)'); | |
| return; | |
| } | |
| if (kIsWeb || Platform.isLinux) { | |
| debugPrint('FCM: Skipping initialization (platform not supported)'); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In @lib/services/fcm_service.dart around lines 83-86, The platform guard in
initialize() is inconsistent with the background handler: background handler
uses "if (kIsWeb || Platform.isLinux) return;" but initialize() currently uses
"if (!kIsWeb && Platform.isLinux)". Update the initialize() function in
lib/services/fcm_service.dart to skip initialization on both web and Linux (use
the same condition kIsWeb || Platform.isLinux), keep the debugPrint message, and
return early so behavior matches the background handler.
| void _setupTokenRefreshListener() { | ||
| _tokenRefreshSubscription?.cancel(); | ||
|
|
||
| _tokenRefreshSubscription = _messaging.onTokenRefresh.listen( | ||
| (newToken) async { | ||
| debugPrint('FCM: Token refreshed'); | ||
| await _prefs.setString(_fcmTokenKey, newToken); | ||
|
|
||
| // TODO: In Phase 3, send updated encrypted token to notification server | ||
| // This will be implemented in PushNotificationService | ||
| }, | ||
| onError: (error) { | ||
| _logger.e('Error on token refresh: $error'); | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale TODO and missing integration with PushNotificationService.
This PR implements Phase 3, but the TODO still references Phase 3 as future work. More importantly, when the token refreshes, the new token should be re-registered with the push notification server via PushNotificationService. Currently, refreshed tokens are only stored locally.
🔎 Proposed approach
Consider injecting PushNotificationService or exposing a callback/stream for token changes that the service provider can consume:
void _setupTokenRefreshListener() {
_tokenRefreshSubscription?.cancel();
_tokenRefreshSubscription = _messaging.onTokenRefresh.listen(
(newToken) async {
debugPrint('FCM: Token refreshed');
await _prefs.setString(_fcmTokenKey, newToken);
// Notify listeners that token changed so PushNotificationService
// can re-register active trades with the new token
_onTokenRefresh?.call(newToken);
},
onError: (error) {
_logger.e('Error on token refresh: $error');
},
);
}🤖 Prompt for AI Agents
In @lib/services/fcm_service.dart around lines 182-197, The
_setupTokenRefreshListener currently only persists the new FCM token to prefs
and leaves a stale TODO about Phase 3; update it to notify the
PushNotificationService so refreshed tokens are re-registered with the server:
add an injectable PushNotificationService (or expose a callback/stream like
_onTokenRefresh) and invoke it when a newToken arrives after saving to _prefs,
ensuring _tokenRefreshSubscription, _fcmTokenKey, and error handling remain
intact and remove the outdated TODO.
| try { | ||
| final service = FlutterBackgroundService(); | ||
| final isRunning = await service.isRunning(); | ||
|
|
||
| if (isRunning) { | ||
| // The background service will pick up new events through its | ||
| // existing subscription mechanism | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Empty if block is a no-op.
The if (isRunning) block contains only a comment and performs no action. Either implement the intended behavior or remove the dead code.
🔎 Proposed fix - remove no-op code
try {
final service = FlutterBackgroundService();
final isRunning = await service.isRunning();
- if (isRunning) {
- // The background service will pick up new events through its
- // existing subscription mechanism
- }
+ // Background service (if running) will pick up new events through
+ // its existing subscription mechanism - no explicit trigger needed
+ debugPrint('FCM: Foreground message received, service running: $isRunning');
} catch (e) {
_logger.e('Error triggering background service: $e');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| final service = FlutterBackgroundService(); | |
| final isRunning = await service.isRunning(); | |
| if (isRunning) { | |
| // The background service will pick up new events through its | |
| // existing subscription mechanism | |
| } | |
| try { | |
| final service = FlutterBackgroundService(); | |
| final isRunning = await service.isRunning(); | |
| // Background service (if running) will pick up new events through | |
| // its existing subscription mechanism - no explicit trigger needed | |
| debugPrint('FCM: Foreground message received, service running: $isRunning'); | |
| } catch (e) { | |
| _logger.e('Error triggering background service: $e'); | |
| } |
🤖 Prompt for AI Agents
In @lib/services/fcm_service.dart around lines 209-216, The if (isRunning) block
around the FlutterBackgroundService() / isRunning check is a no-op; remove the
empty if branch and its comment (or replace it with the intended behavior such
as returning early or dispatching an event to the running service). Locate the
code that instantiates FlutterBackgroundService() and the await
service.isRunning() call and either delete the whole if (isRunning) { /* comment
*/ } block or implement the desired action (e.g., service.sendData(...) or
return) so there is no dead code left.
| debugPrint( | ||
| 'PushService: Registering token for trade ${tradePubkey.substring(0, 16)}...'); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential RangeError if tradePubkey is shorter than 16 characters.
The substring(0, 16) call will throw if the input is shorter than 16 characters. While trade pubkeys should be 64-char hex strings, defensive coding prevents crashes from malformed inputs.
🔎 Suggested defensive approach
debugPrint(
- 'PushService: Registering token for trade ${tradePubkey.substring(0, 16)}...');
+ 'PushService: Registering token for trade ${tradePubkey.length >= 16 ? tradePubkey.substring(0, 16) : tradePubkey}...');Or create a helper method to avoid repetition across multiple log statements.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| debugPrint( | |
| 'PushService: Registering token for trade ${tradePubkey.substring(0, 16)}...'); | |
| debugPrint( | |
| 'PushService: Registering token for trade ${tradePubkey.length >= 16 ? tradePubkey.substring(0, 16) : tradePubkey}...'); |
🤖 Prompt for AI Agents
In @lib/services/push_notification_service.dart around lines 114-116, The
debugPrint call uses tradePubkey.substring(0, 16) which can throw a RangeError
for malformed short pubkeys; update the logging to defensively trim the pubkey
(e.g., use a safe truncation that takes min(tradePubkey.length, 16) or returns
the whole string when shorter) and/or introduce a small helper like
shortenPubkey(tradePubkey, 16) and use that in this debugPrint (and other
similar log sites) to avoid crashes while preserving readable output.
| /// Create padded payload: platform_byte || token_length (2 bytes BE) || token || random_padding | ||
| Uint8List _createPaddedPayload(String deviceToken, Random random) { | ||
| final tokenBytes = utf8.encode(deviceToken); | ||
| final platformByte = Platform.isIOS ? _platformIos : _platformAndroid; | ||
|
|
||
| final payload = Uint8List(_paddedPayloadSize); | ||
| payload[0] = platformByte; | ||
| payload[1] = (tokenBytes.length >> 8) & 0xFF; | ||
| payload[2] = tokenBytes.length & 0xFF; | ||
| payload.setRange(3, 3 + tokenBytes.length, tokenBytes); | ||
|
|
||
| // Fill rest with random padding | ||
| final paddingStart = 3 + tokenBytes.length; | ||
| for (var i = paddingStart; i < _paddedPayloadSize; i++) { | ||
| payload[i] = random.nextInt(256); | ||
| } | ||
|
|
||
| return payload; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing bounds check could cause buffer overflow with unusually long tokens.
If tokenBytes.length > 217 (220 - 3 header bytes), setRange at line 271 will throw. While FCM tokens are typically ~163 characters, adding a guard prevents unexpected crashes.
🔎 Suggested bounds check
Uint8List _createPaddedPayload(String deviceToken, Random random) {
final tokenBytes = utf8.encode(deviceToken);
+ if (tokenBytes.length > _paddedPayloadSize - 3) {
+ throw ArgumentError('Device token too long: ${tokenBytes.length} bytes');
+ }
final platformByte = Platform.isIOS ? _platformIos : _platformAndroid;🤖 Prompt for AI Agents
In @lib/services/push_notification_service.dart around lines 262-280, The
_createPaddedPayload function can overflow when tokenBytes.length exceeds
available space; add a guard after computing tokenBytes (and before setRange)
that checks tokenBytes.length <= _paddedPayloadSize - 3 and if not, either throw
a clear ArgumentError/RangeError mentioning deviceToken length or safely
truncate the tokenBytes to that maximum length; ensure the error message
references _createPaddedPayload and includes the offending length and allowed
max so callers can handle it.
Description
Implements Phase 3 of FCM integration - encrypted token registration with the Mostro push notification server following MIP-05 privacy approach. The PushNotificationService encrypts FCM device tokens using ECDH key derivation with the server's public key and ChaCha20-Poly1305 encryption, ensuring that tokens are protected in transit and only the notification server can decrypt them. The service fetches the server's public key on initialization and provides methods to register/unregister tokens for specific trades.
Changes
How to Test
Expected output:
Note: Token registration with trades will be tested when integrated with the trade flow. The push server must be running for full testing.
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.