Skip to content

Conversation

@AndreaDiazCorreia
Copy link
Member

@AndreaDiazCorreia AndreaDiazCorreia commented Jan 6, 2026

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

  • lib/services/push_notification_service.dart - New service with MIP-05 encryption
  • lib/shared/providers/push_notification_service_provider.dart - Riverpod provider
  • lib/core/config.dart - Push server URL configuration
  • lib/main.dart - Initialize PushNotificationService on startup

How to Test

# Run the app
fvm flutter run -d <android-device>

# Monitor logs
adb logcat -s flutter:I | grep -E "(FCM:|PushService:)"

Expected output:

FCM: Starting initialization...
FCM: Firebase initialized
FCM: Notification permissions granted
FCM: Token obtained
FCM: Initialized successfully
PushService: Fetching server public key...
PushService: Initialized successfully

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

    • Added Firebase Cloud Messaging (FCM) support for push notifications on Android and iOS
    • Implemented privacy-preserving push token management with end-to-end encryption
    • Enabled automatic push notification handling and token lifecycle management
  • Documentation

    • Updated implementation progress documentation to reflect completed phases

✏️ Tip: You can customize this high-level summary in your review settings.

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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Documentation
docs/FCM_IMPLEMENTATION.md
Marked Phase 2 and Phase 3 as complete (✅) instead of "TO IMPLEMENT"; minor branch reference formatting update.
Configuration
lib/core/config.dart
Added new pushServerUrl config constant with default value https://push.mostro.network.
Application Startup
lib/main.dart
Added FCM and push notification service imports and initialization routine; new _initializeFirebaseMessaging() function handles service setup with platform guards for Linux.
Core Services
lib/services/fcm_service.dart
New FCMService class providing Firebase initialization, token management (get/delete with timeout), permission requests, token refresh listeners, foreground/background message handlers, and platform-specific guards.
Core Services
lib/services/push_notification_service.dart
New PushNotificationService class implementing encrypted token registration via secp256k1 ECDH, HKDF-SHA256, and ChaCha20-Poly1305; handles token registration/unregistration with Mostro push server; includes crypto utilities.
Providers
lib/shared/providers/fcm_service_provider.dart, push_notification_service_provider.dart
New Riverpod providers exposing FCMService and PushNotificationService instances with dependency resolution.
Providers
lib/shared/providers/providers.dart
Added exports for new FCM and push notification service providers.

Sequence Diagrams

sequenceDiagram
    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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • grunch
  • Catrya

Poem

🐰 Hop, hop! The messages now fly secure,
Encrypted tokens, privacy pure,
Firebase whispers through the night,
Background service springs to flight,
Mostro's push notifications ignite!

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing PushNotificationService with encrypted token registration as Phase 3 of the FCM integration roadmap, which aligns with the detailed implementation across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndreaDiazCorreia
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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) with Logger methods (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

📥 Commits

Reviewing files that changed from the base of the PR and between 71950e9 and b75dc09.

📒 Files selected for processing (10)
  • docs/FCM_IMPLEMENTATION.md
  • lib/core/config.dart
  • lib/firebase_options_platform.dart
  • lib/firebase_options_stub.dart
  • lib/main.dart
  • lib/services/fcm_service.dart
  • lib/services/push_notification_service.dart
  • lib/shared/providers/fcm_service_provider.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{dart,flutter}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{dart,flutter}: Run flutter analyze after any code change - Mandatory before commits to ensure zero linting issues
Run flutter test after any code change - Mandatory before commits to ensure all unit tests pass

Files:

  • lib/core/config.dart
  • lib/shared/providers/fcm_service_provider.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • lib/services/fcm_service.dart
  • lib/services/push_notification_service.dart
  • lib/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 check mounted before using BuildContext after async operations to prevent errors on disposed widgets
Use const constructors 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 under lib/, grouped by domain with lib/features/<feature>/ structure, shared utilities in lib/shared/, dependency wiring in lib/core/, and services in lib/services/
Persistence, APIs, and background jobs should live in lib/data/ and lib/background/; generated localization output must be in lib/generated/ and must stay untouched
Apply flutter 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>Provider or <Feature>Notifier convention
Localize all user-facing strings via ARB files and access them with S.of(context) rather than hard-coded literals

Files:

  • lib/core/config.dart
  • lib/shared/providers/fcm_service_provider.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • lib/services/fcm_service.dart
  • lib/services/push_notification_service.dart
  • lib/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.dart
  • lib/shared/providers/providers.dart
  • lib/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.dart
  • lib/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.dart
  • lib/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.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • lib/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.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • 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/**/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.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • lib/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.dart
  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • 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 : 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.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • 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/**/providers/**/*.dart : Use Notifier pattern instead of simple StateNotifier for complex state logic requiring business rule encapsulation

Applied to files:

  • lib/shared/providers/providers.dart
  • lib/shared/providers/push_notification_service_provider.dart
  • 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/**/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.dart
  • 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/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.dart
  • lib/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.dart
  • lib/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 pushServerUrl constant correctly uses String.fromEnvironment for compile-time configuration, matching the existing pattern for mostroPubKey. 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 fcmServiceProvider and pushNotificationServiceProvider accessible 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.watch to obtain SharedPreferencesAsync and constructs FCMService. Since sharedPreferencesProvider is overridden at app startup and remains stable, the FCMService instance will be cached by Riverpod. Based on learnings, this follows the <Feature>Provider naming convention.

lib/main.dart (1)

93-113: Platform check ordering is correct.

Good defensive check: !kIsWeb is evaluated first before Platform.isLinux, avoiding the runtime error that would occur if dart:io Platform 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's Provider<T> caches instances by default and only recreates them when watched dependencies change. Since fcmServiceProvider depends on the stable sharedPreferencesProvider (app singleton), pushNotificationServiceProvider will maintain a single PushNotificationService instance 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 authorized and provisional statuses, 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.

Comment on lines +44 to +46
// Initialize FCM (skip on Linux)
await _initializeFirebaseMessaging(sharedPreferences);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FCM/Push services are instantiated twice: here and via providers.

The services are created directly in _initializeFirebaseMessaging and separately via fcmServiceProvider/pushNotificationServiceProvider. This means:

  1. The initialized instances here are discarded after main() completes
  2. 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.

Comment on lines +83 to +86
if (!kIsWeb && Platform.isLinux) {
debugPrint('FCM: Skipping initialization on Linux (not supported)');
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +182 to +197
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');
},
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +209 to +216
try {
final service = FlutterBackgroundService();
final isRunning = await service.isRunning();

if (isRunning) {
// The background service will pick up new events through its
// existing subscription mechanism
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +114 to +116
debugPrint(
'PushService: Registering token for trade ${tradePubkey.substring(0, 16)}...');

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +262 to +280
/// 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

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.

2 participants