-
Notifications
You must be signed in to change notification settings - Fork 28
feat: Mortsom automation contract #650
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
Changes from all commits
d37da23
8d5f8fb
23c6901
eaf9c39
f3ee582
239b758
2bcda9f
954fdf1
12c63f9
44bc34b
1dc0fbc
46252b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||||||||||||||||||||||||||||||||||
| # Automation contract | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| This document is the product contract between the mobile app and black-box | ||||||||||||||||||||||||||||||||||||||||
| UI automation (Mortsom, `../mortsom`). It covers stable semantic identifiers, | ||||||||||||||||||||||||||||||||||||||||
| the test-environment build, and the rules for changing either. Everything | ||||||||||||||||||||||||||||||||||||||||
| here is product code: changes go through normal review, and a change to an | ||||||||||||||||||||||||||||||||||||||||
| identifier, a visible business state or the test-environment behaviour is a | ||||||||||||||||||||||||||||||||||||||||
| **contract change** that requires coordinated review with the automation | ||||||||||||||||||||||||||||||||||||||||
| owners. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ## 1. Semantic identifiers | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Every actionable control and business-critical state carries a stable | ||||||||||||||||||||||||||||||||||||||||
| identifier from `lib/core/automation/automation_ids.dart`, attached with the | ||||||||||||||||||||||||||||||||||||||||
| `.withAutomationId()` extension (`lib/core/automation/automation_id.dart`). | ||||||||||||||||||||||||||||||||||||||||
| Flutter exposes it as `Semantics.identifier`, which Android surfaces as the | ||||||||||||||||||||||||||||||||||||||||
| accessibility `resource-id`; drivers locate it with a UiAutomator | ||||||||||||||||||||||||||||||||||||||||
| `resourceId("<id>")` selector. Identifiers are namespaced | ||||||||||||||||||||||||||||||||||||||||
| `<area>.<screen-or-flow>.<control>` and never localized. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| The extension applies at the end of the expression rather than wrapping it, | ||||||||||||||||||||||||||||||||||||||||
| so naming a control neither re-indents its subtree nor adds a level of | ||||||||||||||||||||||||||||||||||||||||
| nesting to `build()`: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ```dart | ||||||||||||||||||||||||||||||||||||||||
| ElevatedButton( | ||||||||||||||||||||||||||||||||||||||||
| onPressed: _submit, | ||||||||||||||||||||||||||||||||||||||||
| child: Text(S.of(context)!.confirm), | ||||||||||||||||||||||||||||||||||||||||
| ).withAutomationId(AutomationIds.orderConfirm) | ||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| It builds the `AutomationId` widget underneath; reach for that widget | ||||||||||||||||||||||||||||||||||||||||
| directly only where an extension call cannot be expressed. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Two modes exist: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| - **merged** (default): the wrapped subtree collapses into one node, so the | ||||||||||||||||||||||||||||||||||||||||
| identifier travels with the visible label, the enabled flag and the tap | ||||||||||||||||||||||||||||||||||||||||
| action. Use it for buttons, text fields and single-purpose controls. | ||||||||||||||||||||||||||||||||||||||||
| - **container** (`merge: false`): the identifier names a row or card that | ||||||||||||||||||||||||||||||||||||||||
| contains several independent controls; an explicit `label` may describe a | ||||||||||||||||||||||||||||||||||||||||
| business state (`connected` / `disconnected`, the wire status of an order). | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| `test/core/automation/automation_contract_test.dart` fails when an identifier | ||||||||||||||||||||||||||||||||||||||||
| listed below disappears, is renamed, or stops being namespaced. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| | Identifier | Owner | Behavioural contract | | ||||||||||||||||||||||||||||||||||||||||
| |---|---|---| | ||||||||||||||||||||||||||||||||||||||||
| | `env.marker` | app shell | Present on every screen only in the test environment; label `TEST ENVIRONMENT · Mortsom`. | | ||||||||||||||||||||||||||||||||||||||||
| | `appbar.drawer`, `appbar.back` | shell | Open the drawer / go back. | | ||||||||||||||||||||||||||||||||||||||||
| | `nav.order_book`, `nav.trades`, `nav.chat` | shell | Bottom navigation tabs. | | ||||||||||||||||||||||||||||||||||||||||
| | `drawer.account`, `drawer.settings`, `drawer.about` | shell | Drawer destinations. | | ||||||||||||||||||||||||||||||||||||||||
| | `onboarding.walkthrough.{back,skip,next,done}` | onboarding | Walkthrough controls; `done`/`skip` mark first run complete. | | ||||||||||||||||||||||||||||||||||||||||
| | `onboarding.community.card.<pubkey>` | onboarding | Selects that community/node. | | ||||||||||||||||||||||||||||||||||||||||
| | `onboarding.community.notice.accept` | onboarding | Accepts the legal notice raised on first launch. | | ||||||||||||||||||||||||||||||||||||||||
| | `onboarding.community.custom_node` | onboarding | Opens the custom-node dialog. | | ||||||||||||||||||||||||||||||||||||||||
| | `onboarding.community.done`, `onboarding.community.skip` | onboarding | Confirm selection / skip. | | ||||||||||||||||||||||||||||||||||||||||
| | `keys.public_key` | account | Read-only npub of the current account (label = npub). | | ||||||||||||||||||||||||||||||||||||||||
| | `keys.generate`, `keys.generate.confirm`, `keys.generate.cancel` | account | Generate a new identity; confirm or dismiss the dialog. | | ||||||||||||||||||||||||||||||||||||||||
| | `keys.import`, `keys.import.mnemonic`, `keys.import.confirm`, `keys.import.cancel` | account | Import a mnemonic; the field is a secret input and is never logged. | | ||||||||||||||||||||||||||||||||||||||||
| | `keys.seed.reveal`, `keys.seed.text` | account | Reveal / display the seed phrase (sensitive; automation never records it). | | ||||||||||||||||||||||||||||||||||||||||
| | `settings.mostro_node`, `settings.mostro_node.pubkey` | settings | Opens the node selector; read-only pubkey of the selected node. | | ||||||||||||||||||||||||||||||||||||||||
| | `settings.wallet` | settings | Wallet status card; opens wallet settings. | | ||||||||||||||||||||||||||||||||||||||||
| | `settings.relays.add`, `settings.relays.add.url`, `settings.relays.add.confirm`, `settings.relays.add.cancel` | relays | Add a relay through the dialog. | | ||||||||||||||||||||||||||||||||||||||||
| | `settings.relays.item.<url>`, `settings.relays.item.<url>.delete` | relays | Relay row and its delete control. `<url>` is the relay URL with trailing slashes removed, so one relay never carries two identifiers. | | ||||||||||||||||||||||||||||||||||||||||
| | `node.add_custom`, `node.custom.pubkey`, `node.custom.name`, `node.custom.confirm`, `node.custom.cancel`, `node.item.<pubkey>` | mostro node | Custom node dialog and node rows. | | ||||||||||||||||||||||||||||||||||||||||
| | `wallet.nwc.uri`, `wallet.nwc.connect` | wallet | NWC URI input (secret) and connect action. | | ||||||||||||||||||||||||||||||||||||||||
| | `wallet.connection` | wallet | State readout; label is `connected` or `disconnected`. | | ||||||||||||||||||||||||||||||||||||||||
| | `wallet.settings.connect`, `wallet.settings.disconnect` | wallet | Open the connect screen / disconnect the wallet from wallet settings. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.book.tab.buy`, `order.book.tab.sell` | order book | Switch the book between buy and sell offers. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.add.fab`, `order.add.buy`, `order.add.sell` | order book | Open the create-order menu and pick a side. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.book.item.<orderId>` | order book | Opens the order (take screen or trade detail). | | ||||||||||||||||||||||||||||||||||||||||
| | `order.create.currency`, `order.create.currency.<CODE>` | create order | Currency picker and its options. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.create.fiat_amount`, `order.create.fiat_amount_max` | create order | Fiat amount (and range maximum). | | ||||||||||||||||||||||||||||||||||||||||
| | `order.create.payment_method`, `order.create.price_type`, `order.create.sats_amount` | create order | Payment method, market/fixed toggle, sats amount (fixed price). | | ||||||||||||||||||||||||||||||||||||||||
| | `order.create.submit`, `order.create.cancel`, `order.confirm.home` | create order | Submit / cancel; back to home from the confirmation screen. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.take.confirm`, `order.take.close`, `order.take.amount`, `order.take.amount.confirm` | take order | Take the order; range amount dialog. | | ||||||||||||||||||||||||||||||||||||||||
| | `order.id` | trade | Read-only order id (label = id). | | ||||||||||||||||||||||||||||||||||||||||
| | `order.status` | trade | Read-only order status; label is the wire status (`pending`, `waiting-payment`, `active`, `fiat-sent`, `success`, `canceled`, ...). | | ||||||||||||||||||||||||||||||||||||||||
| | `trades.item.<orderId>`, `trades.item.status` | trades | Trade row; status chip whose label is the wire status. | | ||||||||||||||||||||||||||||||||||||||||
| | `trade.<action>` (`trade.payInvoice`, `trade.addInvoice`, `trade.fiatSent`, `trade.release`, `trade.takeSell`, `trade.takeBuy`, `trade.rate`, `trade.cancel`, `trade.dispute`, ...) | trade | Trade action buttons named after the protocol action. | | ||||||||||||||||||||||||||||||||||||||||
| | `trade.release.confirm`, `trade.cancel.confirm`, `trade.dispute.confirm` | trade | Confirmation dialogs. | | ||||||||||||||||||||||||||||||||||||||||
| | `invoice.text`, `invoice.submit`, `invoice.cancel` | invoice | Buyer invoice entry. | | ||||||||||||||||||||||||||||||||||||||||
| | `invoice.nwc.generate`, `invoice.nwc.confirm` | invoice | Generate the buyer invoice with the connected wallet and confirm it. | | ||||||||||||||||||||||||||||||||||||||||
| | `invoice.nwc.text` | invoice | Read-only generated buyer invoice (label = bolt11). | | ||||||||||||||||||||||||||||||||||||||||
| | `pay.invoice.text` | payment | Read-only invoice being paid (label = bolt11); invisible readout for correlation. | | ||||||||||||||||||||||||||||||||||||||||
| | `pay.nwc` | payment | Pay the displayed invoice with the connected wallet. | | ||||||||||||||||||||||||||||||||||||||||
| | `pay.cancel` | payment | Cancel the order from the pay-invoice screen. | | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ## 2. Test-environment build | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| The Mortsom build is the same application with a different Dart entry point: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ```sh | ||||||||||||||||||||||||||||||||||||||||
| flutter build apk -t lib/main_mortsom.dart \ | ||||||||||||||||||||||||||||||||||||||||
| --dart-define=MORTSOM_TEST_ENV=true \ | ||||||||||||||||||||||||||||||||||||||||
| --dart-define=MOSTRO_PUB_KEY=<daemon pubkey> \ | ||||||||||||||||||||||||||||||||||||||||
| --dart-define=MORTSOM_RELAYS=ws://10.0.2.2:7000 | ||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| `lib/core/test_environment.dart` enables the test environment only when | ||||||||||||||||||||||||||||||||||||||||
| **both** the entry point armed it and `MORTSOM_TEST_ENV=true` was compiled | ||||||||||||||||||||||||||||||||||||||||
| in. `lib/main.dart` never arms it, and the release pipeline never passes the | ||||||||||||||||||||||||||||||||||||||||
| define, so a release build cannot enter the test environment by accident. | ||||||||||||||||||||||||||||||||||||||||
| When enabled: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| - the local relay seed list (`MORTSOM_RELAYS`) becomes the user relays on the | ||||||||||||||||||||||||||||||||||||||||
| first launch of a fresh install, before any subscription starts; | ||||||||||||||||||||||||||||||||||||||||
| - relay discovery never falls back to the public bootstrap relays | ||||||||||||||||||||||||||||||||||||||||
| (`Config.bootstrapRelays`): `Config.discoveryRelays` resolves to the seed | ||||||||||||||||||||||||||||||||||||||||
| list instead, so a disconnected local relay produces a test failure, not | ||||||||||||||||||||||||||||||||||||||||
| public-network traffic. A build that arms the test environment without | ||||||||||||||||||||||||||||||||||||||||
| `MORTSOM_RELAYS` fails at startup rather than starting with no relay; | ||||||||||||||||||||||||||||||||||||||||
| - plain `ws://` relays on private addresses are accepted by the add-relay | ||||||||||||||||||||||||||||||||||||||||
| validation; | ||||||||||||||||||||||||||||||||||||||||
| - a red `TEST ENVIRONMENT · Mortsom` banner (`env.marker`) is shown on every | ||||||||||||||||||||||||||||||||||||||||
| screen. | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+107
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Verify the documented test-environment relay behaviour matches the code. The document states the seed list becomes the user relays on first launch, and that discovery never uses 📝 Proposed wording fix-- relay discovery never falls back to the public bootstrap relays
- (`Config.discoveryRelays`); a disconnected local relay produces a test
- failure, not public-network traffic;
+- relay discovery never falls back to the public bootstrap relays
+ (`Config.bootstrapRelays`); `Config.discoveryRelays` resolves to the seed
+ list instead, so a disconnected local relay produces a test failure, not
+ public-network traffic;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| The banner copy is deliberately not localized: it is test-only tooling and | ||||||||||||||||||||||||||||||||||||||||
| follows Mortsom's English-only policy. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Only non-secret values travel through Dart defines (daemon pubkey, relay | ||||||||||||||||||||||||||||||||||||||||
| endpoints). Mnemonics and NWC URIs are entered through the UI by the | ||||||||||||||||||||||||||||||||||||||||
| automation, never compiled in and never logged. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ## 3. Changing the contract | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| 1. Update `AutomationIds`, the call site, and this table, in one change. | ||||||||||||||||||||||||||||||||||||||||
| 2. Keep `flutter test test/core/automation` green; add the identifier to the | ||||||||||||||||||||||||||||||||||||||||
| contract test list. | ||||||||||||||||||||||||||||||||||||||||
| 3. Notify the automation owners; Mortsom's adapter contract tests pin the | ||||||||||||||||||||||||||||||||||||||||
| same identifiers. | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,209 @@ | ||||||||||||||||||||||
| import 'dart:io'; | ||||||||||||||||||||||
| import 'package:flutter/foundation.dart'; | ||||||||||||||||||||||
| import 'package:flutter/material.dart'; | ||||||||||||||||||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||||||||||||||||||
| import 'package:flutter_secure_storage/flutter_secure_storage.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/core/app.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/relays/relay.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/relays/relays_provider.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/settings/settings_notifier.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/settings/settings_provider.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/background/background_service.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/services/fcm_service.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/services/push_notification_service.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/shared/providers/providers.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/shared/utils/biometrics_helper.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/shared/utils/notification_permission_helper.dart'; | ||||||||||||||||||||||
| import 'package:mostro_mobile/services/logger_service.dart'; | ||||||||||||||||||||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||||||||||||||||||||
| import 'package:timeago/timeago.dart' as timeago; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Builds every dependency, seeds optional test relays and runs the app. | ||||||||||||||||||||||
| /// | ||||||||||||||||||||||
| /// `seedRelays` is non-empty only for the Mortsom test entry point | ||||||||||||||||||||||
| /// (`lib/main_mortsom.dart`): on the first launch of a fresh install the | ||||||||||||||||||||||
| /// list becomes the user relays, so relay discovery starts from the local | ||||||||||||||||||||||
| /// test relay instead of the public bootstrap relays. | ||||||||||||||||||||||
| Future<void> bootstrapAndRun({List<String> seedRelays = const []}) async { | ||||||||||||||||||||||
| WidgetsFlutterBinding.ensureInitialized(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| initIsolateLogReceiver(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await requestNotificationPermissionIfNeeded(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| final biometricsHelper = BiometricsHelper(); | ||||||||||||||||||||||
| final sharedPreferences = SharedPreferencesAsync(); | ||||||||||||||||||||||
| final secureStorage = const FlutterSecureStorage(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| final mostroDatabase = await openMostroDatabase('mostro.db'); | ||||||||||||||||||||||
| final eventsDatabase = await openMostroDatabase('events.db'); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| final settings = SettingsNotifier(sharedPreferences); | ||||||||||||||||||||||
| await settings.init(); | ||||||||||||||||||||||
| await seedTestRelays(settings, seedRelays); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await initializeNotifications(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _initializeTimeAgoLocalization(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| final backgroundService = createBackgroundService(settings.settings); | ||||||||||||||||||||||
| await backgroundService.init(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize FCM (skip on Linux) | ||||||||||||||||||||||
| final pushServices = await _initializeFirebaseMessaging(sharedPreferences); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 (pushServices != null) ...[ | ||||||||||||||||||||||
| fcmServiceProvider.overrideWithValue(pushServices.fcmService), | ||||||||||||||||||||||
| pushNotificationServiceProvider | ||||||||||||||||||||||
| .overrideWithValue(pushServices.pushService), | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize relay sync on app start | ||||||||||||||||||||||
| _initializeRelaySynchronization(container); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Connect push notification service with session notifier and settings | ||||||||||||||||||||||
| if (pushServices != null) { | ||||||||||||||||||||||
| _initializePushNotificationIntegration(container, pushServices); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| runApp( | ||||||||||||||||||||||
| UncontrolledProviderScope( | ||||||||||||||||||||||
| container: container, | ||||||||||||||||||||||
| child: const MostroApp(), | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Seeds `relays` as user relays when the settings hold no relay yet. | ||||||||||||||||||||||
| /// Idempotent: a second launch keeps whatever the user configured. | ||||||||||||||||||||||
| Future<void> seedTestRelays( | ||||||||||||||||||||||
| SettingsNotifier settings, List<String> relays) async { | ||||||||||||||||||||||
| if (relays.isEmpty || settings.settings.relays.isNotEmpty) { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| await settings.updateRelays(relays); | ||||||||||||||||||||||
| await settings.updateUserRelays( | ||||||||||||||||||||||
| relays | ||||||||||||||||||||||
| .map((url) => Relay(url: url, source: RelaySource.user).toJson()) | ||||||||||||||||||||||
| .toList(), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Initialize relay synchronization on app startup | ||||||||||||||||||||||
| void _initializeRelaySynchronization(ProviderContainer container) { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| // Read the relays provider to trigger initialization of RelaysNotifier | ||||||||||||||||||||||
| // This will automatically start sync with the configured Mostro instance | ||||||||||||||||||||||
| container.read(relaysProvider); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| // Log error but don't crash app if relay sync initialization fails | ||||||||||||||||||||||
| logger.e('Failed to initialize relay synchronization', error: e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+113
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Use the singleton This file already imports Replace each As per coding guidelines: "Always use the pre-configured singleton ♻️ Proposed change for this segment } catch (e) {
// Log error but don't crash app if relay sync initialization fails
- debugPrint('Failed to initialize relay synchronization: $e');
+ logger.e('Failed to initialize relay synchronization', error: e);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Initialize push notification integration with session notifier and settings | ||||||||||||||||||||||
| void _initializePushNotificationIntegration( | ||||||||||||||||||||||
| ProviderContainer container, | ||||||||||||||||||||||
| _PushServices pushServices, | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| // Connect push service with session notifier for automatic token registration | ||||||||||||||||||||||
| container | ||||||||||||||||||||||
| .read(sessionNotifierProvider.notifier) | ||||||||||||||||||||||
| .setPushNotificationService(pushServices.pushService); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Connect push services with settings notifier for unregistration on disable | ||||||||||||||||||||||
| container | ||||||||||||||||||||||
| .read(settingsProvider.notifier) | ||||||||||||||||||||||
| .setPushServices(pushServices.pushService, pushServices.fcmService); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Set up settings check callback | ||||||||||||||||||||||
| pushServices.pushService.isPushEnabledInSettings = () { | ||||||||||||||||||||||
| return container.read(settingsProvider).pushNotificationsEnabled; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Provide the active Mostro instance pubkey for /api/register | ||||||||||||||||||||||
| pushServices.pushService.getMostroPubkey = () { | ||||||||||||||||||||||
| return container.read(settingsProvider).mostroPublicKey; | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| logger.i('Push notification integration initialized'); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| logger.e('Failed to initialize push notification integration', error: e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Initialize timeago localization for supported languages | ||||||||||||||||||||||
| void _initializeTimeAgoLocalization() { | ||||||||||||||||||||||
| // Set Spanish locale for timeago | ||||||||||||||||||||||
| timeago.setLocaleMessages('es', timeago.EsMessages()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Set Italian locale for timeago | ||||||||||||||||||||||
| timeago.setLocaleMessages('it', timeago.ItMessages()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Set German locale for timeago | ||||||||||||||||||||||
| timeago.setLocaleMessages('de', timeago.DeMessages()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Set French locale for timeago | ||||||||||||||||||||||
| timeago.setLocaleMessages('fr', timeago.FrMessages()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Set Portuguese locale for timeago | ||||||||||||||||||||||
| timeago.setLocaleMessages('pt', timeago.PtBrMessages()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // English is already the default, no need to set it | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Result of Firebase/push notification initialization | ||||||||||||||||||||||
| class _PushServices { | ||||||||||||||||||||||
| final FCMService fcmService; | ||||||||||||||||||||||
| final PushNotificationService pushService; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _PushServices({required this.fcmService, required this.pushService}); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Initialize Firebase Cloud Messaging and Push Notification Service | ||||||||||||||||||||||
| /// Returns the initialized services, or null if not supported/failed | ||||||||||||||||||||||
| Future<_PushServices?> _initializeFirebaseMessaging( | ||||||||||||||||||||||
| SharedPreferencesAsync prefs) async { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| // Skip Firebase initialization on Linux (not supported) | ||||||||||||||||||||||
| if (!kIsWeb && Platform.isLinux) { | ||||||||||||||||||||||
| logger.i('Firebase not supported on Linux - skipping FCM initialization'); | ||||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| final fcmService = FCMService(prefs); | ||||||||||||||||||||||
| await fcmService.initialize(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Initialize Push Notification Service (for encrypted token registration) | ||||||||||||||||||||||
| final pushService = PushNotificationService(fcmService: fcmService); | ||||||||||||||||||||||
| await pushService.initialize(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Wire up token refresh to re-register all trade pubkeys | ||||||||||||||||||||||
| fcmService.onTokenRefresh = (_) => pushService.reRegisterAllTokens(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Note: isPushEnabledInSettings callback will be set after ProviderContainer is created | ||||||||||||||||||||||
| // This is done in the app initialization to have access to settings provider | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return _PushServices(fcmService: fcmService, pushService: pushService); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| // Log error but don't crash app if FCM initialization fails | ||||||||||||||||||||||
| logger.e('Failed to initialize Firebase Cloud Messaging', error: e); | ||||||||||||||||||||||
| return null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add the missing identifiers to the table.
AutomationIdsdeclares two identifiers that this table omits:onboarding.community.notice.accept(communityNoticeAccept)keys.generate.cancel(keysGenerateCancel)Section 3 requires the catalog and this table to change together. Add both rows.
📝 Proposed table additions
🧰 Tools
🪛 LanguageTool
[style] ~45-~45: Consider replacing this word to strengthen your wording.
Context: ...a mnemonic; the field is a secret input and is never logged. | |
keys.seed.reveal...(AND_THAT)
🤖 Prompt for AI Agents