From 90619cc3d5e8823653705923889d8ad33d0b3160 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 12 Jun 2025 13:21:52 -0700 Subject: [PATCH 01/28] Feat: add subscription pooling to MostroService --- integration_test/test_helpers.dart | 181 ++++++++++++------ .../order/notfiers/add_order_notifier.dart | 2 +- .../order/notfiers/order_notifier.dart | 4 +- lib/services/lifecycle_manager.dart | 15 +- lib/services/mostro_service.dart | 119 ++++++------ lib/services/nostr_service.dart | 85 +------- test/mocks.mocks.dart | 4 +- 7 files changed, 190 insertions(+), 220 deletions(-) diff --git a/integration_test/test_helpers.dart b/integration_test/test_helpers.dart index e5567776..0f0b675a 100644 --- a/integration_test/test_helpers.dart +++ b/integration_test/test_helpers.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -37,6 +38,7 @@ class FakeSharedPreferencesAsync implements SharedPreferencesAsync { _store[key] = value; return true; } + @override Future getInt(String key) async => _store[key] as int?; @override @@ -44,151 +46,199 @@ class FakeSharedPreferencesAsync implements SharedPreferencesAsync { _store[key] = value; return true; } - + @override Future clear({Set? allowList}) async { _store.clear(); } - + @override Future containsKey(String key) async { return _store.containsKey(key); } - + @override Future getBool(String key) async { return _store[key] as bool?; } - + @override Future getDouble(String key) async { return _store[key] as double?; } - + @override Future> getKeys({Set? allowList}) async { return _store.keys.toSet(); } - + @override Future?> getStringList(String key) async { return _store[key] as List?; } - + @override Future remove(String key) async { _store.remove(key); return true; } - + @override Future setBool(String key, bool value) async { _store[key] = value; return true; } - + @override Future setDouble(String key, double value) async { _store[key] = value; return true; } - + @override Future setStringList(String key, List value) async { _store[key] = value; return true; } - + @override Future> getAll({Set? allowList}) async { - return Map.fromEntries(_store.entries.where((e) => e.value != null).map((e) => MapEntry(e.key, e.value!))); + return Map.fromEntries(_store.entries + .where((e) => e.value != null) + .map((e) => MapEntry(e.key, e.value!))); } } class FakeSecureStorage implements FlutterSecureStorage { final Map _store = {}; @override - Future deleteAll({AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + Future deleteAll( + {AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + WebOptions? webOptions}) async { _store.clear(); } @override - Future read({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + Future read( + {required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + WebOptions? webOptions}) async { return _store[key]; } @override - Future write({required String key, required String? value, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + Future write( + {required String key, + required String? value, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + WebOptions? webOptions}) async { _store[key] = value; } // Unused methods @override - Future delete({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + Future delete( + {required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + WebOptions? webOptions}) async { _store.remove(key); } @override - Future> readAll({AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { - return Map.fromEntries(_store.entries.where((e) => e.value != null).map((e) => MapEntry(e.key, e.value!))); + Future> readAll( + {AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions, + WebOptions? webOptions}) async { + return Map.fromEntries(_store.entries + .where((e) => e.value != null) + .map((e) => MapEntry(e.key, e.value!))); } @override // TODO: implement aOptions AndroidOptions get aOptions => throw UnimplementedError(); - + @override - Future containsKey({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, WebOptions? webOptions, AppleOptions? mOptions, WindowsOptions? wOptions}) { + Future containsKey( + {required String key, + AppleOptions? iOptions, + AndroidOptions? aOptions, + LinuxOptions? lOptions, + WebOptions? webOptions, + AppleOptions? mOptions, + WindowsOptions? wOptions}) { // TODO: implement containsKey throw UnimplementedError(); } - + @override // TODO: implement iOptions IOSOptions get iOptions => throw UnimplementedError(); - + @override Future isCupertinoProtectedDataAvailable() { // TODO: implement isCupertinoProtectedDataAvailable throw UnimplementedError(); } - + @override // TODO: implement lOptions LinuxOptions get lOptions => throw UnimplementedError(); - + @override // TODO: implement mOptions AppleOptions get mOptions => throw UnimplementedError(); - + @override // TODO: implement onCupertinoProtectedDataAvailabilityChanged - Stream? get onCupertinoProtectedDataAvailabilityChanged => throw UnimplementedError(); - + Stream? get onCupertinoProtectedDataAvailabilityChanged => + throw UnimplementedError(); + @override - void registerListener({required String key, required ValueChanged listener}) { + void registerListener( + {required String key, required ValueChanged listener}) { // TODO: implement registerListener } - + @override void unregisterAllListeners() { // TODO: implement unregisterAllListeners } - + @override void unregisterAllListenersForKey({required String key}) { // TODO: implement unregisterAllListenersForKey } - + @override - void unregisterListener({required String key, required ValueChanged listener}) { + void unregisterListener( + {required String key, required ValueChanged listener}) { // TODO: implement unregisterListener } - + @override // TODO: implement wOptions WindowsOptions get wOptions => throw UnimplementedError(); - + @override // TODO: implement webOptions WebOptions get webOptions => throw UnimplementedError(); @@ -219,13 +269,10 @@ class FakeMostroService implements MostroService { final Ref ref; @override - void init() {} + void init({List? keys}) {} @override - void subscribe(Session session) {} - - @override - Session? getSessionByOrderId(String orderId) => null; + void subscribe(NostrKeyPairs keyPair) {} @override Future submitOrder(MostroMessage order) async { @@ -243,7 +290,8 @@ class FakeMostroService implements MostroService { Future takeBuyOrder(String orderId, int? amount) async {} @override - Future takeSellOrder(String orderId, int? amount, String? lnAddress) async {} + Future takeSellOrder( + String orderId, int? amount, String? lnAddress) async {} @override Future sendInvoice(String orderId, String invoice, int? amount) async {} @@ -264,10 +312,19 @@ class FakeMostroService implements MostroService { Future submitRating(String orderId, int rating) async {} @override - Future publishOrder(MostroMessage order) => throw UnimplementedError(); + Future publishOrder(MostroMessage order) => + throw UnimplementedError(); @override void updateSettings(Settings settings) {} + + @override + NostrRequest? currentRequest; + + @override + void unsubscribe(String pubKey) { + // TODO: implement unsubscribe + } } Future pumpTestApp(WidgetTester tester) async { @@ -284,27 +341,27 @@ Future pumpTestApp(WidgetTester tester) async { overrides: [ settingsProvider.overrideWith((ref) => settingsNotifier), currencyCodesProvider.overrideWith((ref) async => { - 'VES': Currency( - symbol: 'Bs', - name: 'Venezuelan Bolívar', - symbolNative: 'Bs', - code: 'VES', - emoji: '🇻🇪', - decimalDigits: 2, - namePlural: 'Venezuelan bolívars', - price: false, - ), - 'USD': Currency( - symbol: '\$', - name: 'US Dollar', - symbolNative: '\$', - code: 'USD', - emoji: '🇺🇸', - decimalDigits: 2, - namePlural: 'US dollars', - price: false, - ), - }), + 'VES': Currency( + symbol: 'Bs', + name: 'Venezuelan Bolívar', + symbolNative: 'Bs', + code: 'VES', + emoji: '🇻🇪', + decimalDigits: 2, + namePlural: 'Venezuelan bolívars', + price: false, + ), + 'USD': Currency( + symbol: '\$', + name: 'US Dollar', + symbolNative: '\$', + code: 'USD', + emoji: '🇺🇸', + decimalDigits: 2, + namePlural: 'US dollars', + price: false, + ), + }), sharedPreferencesProvider.overrideWithValue(prefs), secureStorageProvider.overrideWithValue(secure), mostroDatabaseProvider.overrideWithValue(db), diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 781d1de2..3ce6ced4 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -78,7 +78,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); - mostroService.subscribe(session); + mostroService.subscribe(session.tradeKey); await mostroService.submitOrder(message); state = OrderState( action: message.action, diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 2fc5bb7b..cf596c70 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -57,7 +57,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.buyer, ); - mostroService.subscribe(session); + mostroService.subscribe(session.tradeKey); await mostroService.takeSellOrder( orderId, amount, @@ -71,7 +71,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.seller, ); - mostroService.subscribe(session); + mostroService.subscribe(session.tradeKey); await mostroService.takeBuyOrder( orderId, amount, diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 82f91bff..29eb37b9 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -86,14 +86,17 @@ class LifecycleManager extends WidgetsBindingObserver { Future _switchToBackground() async { try { - _isInBackground = true; - _logger.i("Switching to background"); - - // Transfer active subscriptions to background service - final backgroundService = ref.read(backgroundServiceProvider); - await backgroundService.setForegroundStatus(false); + ref.read(mostroServiceProvider).currentRequest?.filters.forEach( + (f) => _activeSubscriptions.add(f), + ); if (_activeSubscriptions.isNotEmpty) { + _isInBackground = true; + _logger.i("Switching to background"); + + // Transfer active subscriptions to background service + final backgroundService = ref.read(backgroundServiceProvider); + await backgroundService.setForegroundStatus(false); _logger.i( "Transferring ${_activeSubscriptions.length} active subscriptions to background service"); backgroundService.subscribe(_activeSubscriptions); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index f132df8c..523c17c4 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,11 +1,11 @@ import 'dart:convert'; +import 'package:dart_nostr/nostr/core/key_pairs.dart'; import 'package:dart_nostr/nostr/model/export.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/services/lifecycle_manager.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; @@ -16,88 +16,79 @@ class MostroService { final Ref ref; final SessionNotifier _sessionNotifier; static final Logger _logger = Logger(); - Settings _settings; + final Map _subscriptions = {}; + NostrRequest? currentRequest; + MostroService( this._sessionNotifier, this.ref, ) : _settings = ref.read(settingsProvider).copyWith() { - init(); + //init(); } - void init() async { - final now = DateTime.now(); - final cutoff = now.subtract(const Duration(hours: 24)); - final sessions = _sessionNotifier.sessions; - final messageStorage = ref.read(mostroStorageProvider); - // Set of terminal statuses - const terminalStatuses = { - Status.canceled, - Status.cooperativelyCanceled, - Status.success, - Status.expired, - Status.canceledByAdmin, - Status.settledByAdmin, - Status.completedByAdmin, - }; - for (final session in sessions) { - if (session.startTime.isAfter(cutoff)) { - if (session.orderId != null) { - final latestOrderMsg = await messageStorage - .getLatestMessageOfTypeById(session.orderId!); - final status = latestOrderMsg?.payload is Order - ? (latestOrderMsg!.payload as Order).status - : null; - if (status != null && terminalStatuses.contains(status)) { - continue; - } - } - subscribe(session); - } - } + void init({List? keys}) { + keys?.forEach((kp) => _subscriptions[kp.public] = kp); + _subscribe(); } - void subscribe(Session session) { - final filter = NostrFilter( - kinds: [1059], - p: [session.tradeKey.public], - ); - - final request = NostrRequest(filters: [filter]); + void subscribe(NostrKeyPairs keyPair) { + if (_subscriptions.containsKey(keyPair.public)) return; + _subscriptions[keyPair.public] = keyPair; + _subscribe(); + } - ref.read(lifecycleManagerProvider).addSubscription(filter); + void unsubscribe(String pubKey) { + _subscriptions.remove(pubKey); + _subscribe(); + } + void _subscribe() { final nostrService = ref.read(nostrServiceProvider); - nostrService.subscribeToEvents(request).listen((event) async { - final eventStore = ref.read(eventStorageProvider); - - if (await eventStore.hasItem(event.id!)) return; - await eventStore.putItem( - event.id!, - event, - ); - - final decryptedEvent = await event.unWrap( - session.tradeKey.private, + if (currentRequest != null) { + nostrService.unsubscribe( + currentRequest!.subscriptionId!, ); - if (decryptedEvent.content == null) return; + currentRequest = null; + } - final result = jsonDecode(decryptedEvent.content!); - if (result is! List) return; + if (_subscriptions.isEmpty) return; - final msg = MostroMessage.fromJson(result[0]); - final messageStorage = ref.read(mostroStorageProvider); - await messageStorage.addMessage(decryptedEvent.id!, msg); - _logger.i( - 'Received message of type ${msg.action} with order id ${msg.id}', - ); - }); + final filter = NostrFilter( + kinds: [1059], + p: [..._subscriptions.keys], + ); + currentRequest = NostrRequest(filters: [filter]); + nostrService.subscribeToEvents(currentRequest!).listen(_onData); } - Session? getSessionByOrderId(String orderId) { - return _sessionNotifier.getSessionByOrderId(orderId); + Future _onData(NostrEvent event) async { + if (!_subscriptions.containsKey(event.recipient)) return; + + final eventStore = ref.read(eventStorageProvider); + + if (await eventStore.hasItem(event.id!)) return; + await eventStore.putItem( + event.id!, + event, + ); + + final decryptedEvent = await event.unWrap( + _subscriptions[event.recipient]!.private, + ); + if (decryptedEvent.content == null) return; + + final result = jsonDecode(decryptedEvent.content!); + if (result is! List) return; + + final msg = MostroMessage.fromJson(result[0]); + final messageStorage = ref.read(mostroStorageProvider); + await messageStorage.addMessage(decryptedEvent.id!, msg); + _logger.i( + 'Received message of type ${msg.action} with order id ${msg.id}', + ); } Future submitOrder(MostroMessage order) async { diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index fc0fd826..12aa6214 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -6,17 +6,15 @@ import 'package:dart_nostr/nostr/model/relay_informations.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; -import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class NostrService { late Settings settings; final Nostr _nostr = Nostr.instance; - - NostrService(); - final Logger _logger = Logger(); bool _isInitialized = false; + NostrService(); + Future init(Settings settings) async { this.settings = settings; try { @@ -86,18 +84,6 @@ class NostrService { } } - Future> fecthEvents(NostrFilter filter) async { - if (!_isInitialized) { - throw Exception('Nostr is not initialized. Call init() first.'); - } - - final request = NostrRequest(filters: [filter]); - return await _nostr.services.relays.startEventsSubscriptionAsync( - request: request, - timeout: Config.nostrConnectionTimeout, - ); - } - Stream subscribeToEvents(NostrRequest request) { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); @@ -119,73 +105,6 @@ class NostrService { bool get isInitialized => _isInitialized; - Future generateKeyPair() async { - final keyPair = NostrUtils.generateKeyPair(); - return keyPair; - } - - NostrKeyPairs generateKeyPairFromPrivateKey(String privateKey) { - return NostrUtils.generateKeyPairFromPrivateKey(privateKey); - } - - String getMostroPubKey() { - return settings.mostroPublicKey; - } - - Future createNIP59Event( - String content, String recipientPubKey, String senderPrivateKey) async { - if (!_isInitialized) { - throw Exception('Nostr is not initialized. Call init() first.'); - } - - return NostrUtils.createNIP59Event( - content, - recipientPubKey, - senderPrivateKey, - ); - } - - Future decryptNIP59Event( - NostrEvent event, String privateKey) async { - if (!_isInitialized) { - throw Exception('Nostr is not initialized. Call init() first.'); - } - - return NostrUtils.decryptNIP59Event( - event, - privateKey, - ); - } - - Future createRumor(NostrKeyPairs senderKeyPair, String wrapperKey, - String recipientPubKey, String content) async { - return NostrUtils.createRumor( - senderKeyPair, - wrapperKey, - recipientPubKey, - content, - ); - } - - Future createSeal(NostrKeyPairs senderKeyPair, String wrapperKey, - String recipientPubKey, String encryptedContent) async { - return NostrUtils.createSeal( - senderKeyPair, - wrapperKey, - recipientPubKey, - encryptedContent, - ); - } - - Future createWrap(NostrKeyPairs wrapperKeyPair, - String sealedContent, String recipientPubKey) async { - return NostrUtils.createWrap( - wrapperKeyPair, - sealedContent, - recipientPubKey, - ); - } - void unsubscribe(String id) { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index fa54b693..740cb812 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -67,10 +67,10 @@ class MockMostroService extends _i1.Mock implements _i3.MostroService { ); @override - void subscribe(_i4.Session? session) => super.noSuchMethod( + void subscribe(_i4.Session? keyPair) => super.noSuchMethod( Invocation.method( #subscribe, - [session], + [keyPair], ), returnValueForMissingStub: null, ); From bcc024893317efad3d01226c9f2376b220a640c8 Mon Sep 17 00:00:00 2001 From: Biz Date: Fri, 13 Jun 2025 14:29:03 -0700 Subject: [PATCH 02/28] refactor: simplify order state management and remove unused order action notifier --- .../notfiers/abstract_mostro_notifier.dart | 2 +- .../order/notfiers/add_order_notifier.dart | 13 ++-------- .../order/notfiers/order_notifier.dart | 25 ++----------------- lib/services/mostro_service.dart | 21 +++++----------- .../notifiers/order_action_notifier.dart | 16 ------------ lib/shared/providers/app_init_provider.dart | 6 ----- .../providers/mostro_service_provider.dart | 7 +----- 7 files changed, 12 insertions(+), 78 deletions(-) delete mode 100644 lib/shared/notifiers/order_action_notifier.dart diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index babe929f..627ae0b2 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -1,5 +1,4 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; @@ -7,6 +6,7 @@ import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; +import 'package:logger/logger.dart'; class AbstractMostroNotifier extends StateNotifier { final String orderId; diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 3ce6ced4..d70b8530 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; -import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; @@ -20,10 +19,8 @@ class AddOrderNotifier extends AbstractMostroNotifier { int _requestIdFromOrderId(String orderId) { final uuid = orderId.replaceAll('-', ''); - // Use more bits from UUID to reduce collision probability final uuidPart1 = int.parse(uuid.substring(0, 8), radix: 16); final uuidPart2 = int.parse(uuid.substring(8, 16), radix: 16); - // Combine both parts for better uniqueness return ((uuidPart1 ^ uuidPart2) & 0x7FFFFFFF); } @@ -54,9 +51,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { } Future _confirmOrder(MostroMessage message) async { - final order = message.getPayload(); - - state = OrderState(status: order!.status, action: message.action, order: order); + state = state.updateWith(message); session.orderId = message.id; ref.read(sessionNotifierProvider.notifier).saveSession(session); ref.read(orderNotifierProvider(message.id!).notifier).subscribe(); @@ -80,10 +75,6 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); mostroService.subscribe(session.tradeKey); await mostroService.submitOrder(message); - state = OrderState( - action: message.action, - status: Status.pending, - order: order, - ); + state = state.updateWith(message); } } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index cf596c70..51fc4915 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -1,8 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:mostro_mobile/data/enums.dart'; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; @@ -20,26 +17,8 @@ class OrderNotifier extends AbstractMostroNotifier { try { final storage = ref.read(mostroStorageProvider); final messages = await storage.getAllMessagesForOrderId(orderId); - if (messages.isEmpty) { - return; - } - final msg = messages.firstWhereOrNull((m) => m.action != Action.cantDo); - if (msg?.payload is Order) { - state = OrderState( - status: msg!.getPayload()!.status, - action: msg.action, - order: msg.getPayload()!, - ); - } else { - final orderMsg = - await storage.getLatestMessageOfTypeById(orderId); - if (orderMsg != null) { - state = OrderState( - status: orderMsg.getPayload()!.status, - action: orderMsg.action, - order: orderMsg.getPayload()!, - ); - } + for (final msg in messages) { + state = state.updateWith(msg); } } catch (e, stack) { logger.e( diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 523c17c4..c3414f83 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,32 +1,22 @@ import 'dart:convert'; -import 'package:dart_nostr/nostr/core/key_pairs.dart'; -import 'package:dart_nostr/nostr/model/export.dart'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; -import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/shared/providers.dart'; import 'package:logger/logger.dart'; class MostroService { final Ref ref; - final SessionNotifier _sessionNotifier; static final Logger _logger = Logger(); Settings _settings; final Map _subscriptions = {}; NostrRequest? currentRequest; - MostroService( - this._sessionNotifier, - this.ref, - ) : _settings = ref.read(settingsProvider).copyWith() { - //init(); - } + MostroService(this.ref) : _settings = ref.read(settingsProvider).copyWith(); void init({List? keys}) { keys?.forEach((kp) => _subscriptions[kp.public] = kp); @@ -201,14 +191,15 @@ class MostroService { } Future _getSession(MostroMessage order) async { + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); if (order.requestId != null) { - final session = _sessionNotifier.getSessionByRequestId(order.requestId!); + final session = sessionNotifier.getSessionByRequestId(order.requestId!); if (session == null) { throw Exception('No session found for requestId: ${order.requestId}'); } return session; } else if (order.id != null) { - final session = _sessionNotifier.getSessionByOrderId(order.id!); + final session = sessionNotifier.getSessionByOrderId(order.id!); if (session == null) { throw Exception('No session found for orderId: ${order.id}'); } diff --git a/lib/shared/notifiers/order_action_notifier.dart b/lib/shared/notifiers/order_action_notifier.dart deleted file mode 100644 index bdaf1a2f..00000000 --- a/lib/shared/notifiers/order_action_notifier.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; - -class OrderActionNotifier extends StateNotifier { - OrderActionNotifier({required this.orderId}) : super(Action.newOrder); - - final String orderId; - - void set(Action action) { - state = action; - } -} - -final orderActionNotifierProvider = StateNotifierProvider.family( - (ref, orderId) => OrderActionNotifier(orderId: orderId), -); diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index ae2ba6a0..6a61e47e 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -6,7 +6,6 @@ import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/shared/notifiers/order_action_notifier.dart'; import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; @@ -74,11 +73,6 @@ final appInitializerProvider = FutureProvider((ref) async { if (session.orderId != null) { final order = await mostroStorage.getLatestMessageById(session.orderId!); if (order != null) { - // Set the order action - ref.read(orderActionNotifierProvider(session.orderId!).notifier).set( - order.action, - ); - // Explicitly initialize order notifier // to ensure it's all properly set up for this orderId ref.read(orderNotifierProvider(session.orderId!).notifier).sync(); diff --git a/lib/shared/providers/mostro_service_provider.dart b/lib/shared/providers/mostro_service_provider.dart index d89bab29..b480a5a4 100644 --- a/lib/shared/providers/mostro_service_provider.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -2,7 +2,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/repositories/event_storage.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'mostro_service_provider.g.dart'; @@ -14,10 +13,6 @@ EventStorage eventStorage(Ref ref) { @Riverpod(keepAlive: true) MostroService mostroService(Ref ref) { - final sessionNotifier = ref.read(sessionNotifierProvider.notifier); - final mostroService = MostroService( - sessionNotifier, - ref, - ); + final mostroService = MostroService(ref); return mostroService; } From b78a077a2ed7051c6046e92384254dd1c272f66c Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 17 Jun 2025 00:46:00 -0700 Subject: [PATCH 03/28] feat: implement subscription manager for centralized Nostr event handling --- .../order/notfiers/add_order_notifier.dart | 2 +- .../order/notfiers/order_notifier.dart | 4 +- lib/services/lifecycle_manager.dart | 11 +- lib/services/mostro_service.dart | 158 +++++++++++++----- lib/services/subscription_manager.dart | 65 +++++++ lib/shared/providers/app_init_provider.dart | 65 +------ .../providers/mostro_service_provider.dart | 12 ++ .../subscription_manager_provider.dart | 7 + test/services/mostro_service_test.dart | 126 ++++++++++++-- 9 files changed, 330 insertions(+), 120 deletions(-) create mode 100644 lib/services/subscription_manager.dart create mode 100644 lib/shared/providers/subscription_manager_provider.dart diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index d70b8530..c2980c68 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -73,7 +73,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); - mostroService.subscribe(session.tradeKey); + mostroService.subscribe(session.tradeKey.public); await mostroService.submitOrder(message); state = state.updateWith(message); } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 51fc4915..870269e9 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -36,7 +36,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.buyer, ); - mostroService.subscribe(session.tradeKey); + mostroService.subscribe(session.tradeKey.public); await mostroService.takeSellOrder( orderId, amount, @@ -50,7 +50,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.seller, ); - mostroService.subscribe(session.tradeKey); + mostroService.subscribe(session.tradeKey.public); await mostroService.takeBuyOrder( orderId, amount, diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 29eb37b9..31e1c9ab 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -9,6 +9,7 @@ import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; class LifecycleManager extends WidgetsBindingObserver { final Ref ref; @@ -86,9 +87,13 @@ class LifecycleManager extends WidgetsBindingObserver { Future _switchToBackground() async { try { - ref.read(mostroServiceProvider).currentRequest?.filters.forEach( - (f) => _activeSubscriptions.add(f), - ); + // Get the current subscription filter from the subscription manager + final subscriptionManager = ref.read(subscriptionManagerProvider); + final currentFilter = subscriptionManager.request.filters; + + if (currentFilter.isNotEmpty) { + _activeSubscriptions.addAll(currentFilter); + } if (_activeSubscriptions.isNotEmpty) { _isInBackground = true; diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index c3414f83..2be04fbd 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,61 +1,118 @@ +import 'dart:async'; import 'dart:convert'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; -import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; +import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; class MostroService { final Ref ref; static final Logger _logger = Logger(); + + final Set _subscribedPubKeys = {}; + StreamSubscription? _subscription; Settings _settings; + + MostroService(this.ref) : _settings = ref.read(settingsProvider); - final Map _subscriptions = {}; - NostrRequest? currentRequest; - - MostroService(this.ref) : _settings = ref.read(settingsProvider).copyWith(); + void init() { + ref.listen>(sessionNotifierProvider, (previous, next) { + if (next.isNotEmpty) { + for (final session in next) { + subscribe(session.tradeKey.public); + } + } else { + _clearSubscriptions(); + } + }); + } - void init({List? keys}) { - keys?.forEach((kp) => _subscriptions[kp.public] = kp); - _subscribe(); + void dispose() { + _clearSubscriptions(); + _subscription?.cancel(); } - void subscribe(NostrKeyPairs keyPair) { - if (_subscriptions.containsKey(keyPair.public)) return; - _subscriptions[keyPair.public] = keyPair; - _subscribe(); + /// Subscribes to events for a specific public key. + /// + /// This method adds the public key to the internal set of subscribed keys + /// and updates the subscription if the key was not already being tracked. + /// + /// Throws an [ArgumentError] if [pubKey] is empty or invalid. + /// + /// [pubKey] The public key to subscribe to (must be a valid Nostr public key) + void subscribe(String pubKey) { + if (pubKey.isEmpty) { + _logger.w('Attempted to subscribe to empty pubKey'); + throw ArgumentError('pubKey cannot be empty'); + } + + try { + if (_subscribedPubKeys.add(pubKey)) { + _logger.i('Added subscription for pubKey: $pubKey'); + _updateSubscription(); + } else { + _logger.d('Already subscribed to pubKey: $pubKey'); + } + } catch (e, stackTrace) { + _logger.e('Invalid public key: $pubKey', error: e, stackTrace: stackTrace); + rethrow; + } } void unsubscribe(String pubKey) { - _subscriptions.remove(pubKey); - _subscribe(); + _subscribedPubKeys.remove(pubKey); + _updateSubscription(); } - void _subscribe() { - final nostrService = ref.read(nostrServiceProvider); + void _clearSubscriptions() { + _subscription?.cancel(); + _subscription = null; + _subscribedPubKeys.clear(); + + final subscriptionManager = ref.read(subscriptionManagerProvider.notifier); + subscriptionManager.subscribe(NostrFilter()); + } - if (currentRequest != null) { - nostrService.unsubscribe( - currentRequest!.subscriptionId!, + /// Updates the current subscription with the latest set of public keys. + /// + /// This method creates a new subscription with the current set of public keys + /// and cancels any existing subscription. If there are no public keys to + /// subscribe to, it clears all subscriptions. + void _updateSubscription() { + _subscription?.cancel(); + + if (_subscribedPubKeys.isEmpty) { + _clearSubscriptions(); + return; + } + + try { + final filter = NostrFilter( + kinds: [1059], + p: _subscribedPubKeys.toList(), + ); + + final subscriptionManager = ref.read(subscriptionManagerProvider.notifier); + _subscription = subscriptionManager.subscribe(filter).listen( + _onData, + onError: (error, stackTrace) { + _logger.e('Error in subscription', error: error, stackTrace: stackTrace); + }, + cancelOnError: false, ); - currentRequest = null; + } catch (e, stackTrace) { + _logger.e('Error updating subscription', error: e, stackTrace: stackTrace); + rethrow; } - - if (_subscriptions.isEmpty) return; - - final filter = NostrFilter( - kinds: [1059], - p: [..._subscriptions.keys], - ); - currentRequest = NostrRequest(filters: [filter]); - nostrService.subscribeToEvents(currentRequest!).listen(_onData); } Future _onData(NostrEvent event) async { - if (!_subscriptions.containsKey(event.recipient)) return; + if (event.recipient == null || !_subscribedPubKeys.contains(event.recipient)) return; final eventStore = ref.read(eventStorageProvider); @@ -65,20 +122,35 @@ class MostroService { event, ); - final decryptedEvent = await event.unWrap( - _subscriptions[event.recipient]!.private, - ); - if (decryptedEvent.content == null) return; + final sessions = ref.read(sessionNotifierProvider); + Session? matchingSession; + + try { + matchingSession = sessions.firstWhere( + (s) => s.tradeKey.public == event.recipient, + ); + } catch (e) { + _logger.w('No matching session found for recipient: ${event.recipient}'); + return; + } + final privateKey = matchingSession.tradeKey.private; - final result = jsonDecode(decryptedEvent.content!); - if (result is! List) return; + try { + final decryptedEvent = await event.unWrap(privateKey); + if (decryptedEvent.content == null) return; + + final result = jsonDecode(decryptedEvent.content!); + if (result is! List) return; - final msg = MostroMessage.fromJson(result[0]); - final messageStorage = ref.read(mostroStorageProvider); - await messageStorage.addMessage(decryptedEvent.id!, msg); - _logger.i( - 'Received message of type ${msg.action} with order id ${msg.id}', - ); + final msg = MostroMessage.fromJson(result[0]); + final messageStorage = ref.read(mostroStorageProvider); + await messageStorage.addMessage(decryptedEvent.id!, msg); + _logger.i( + 'Received message of type ${msg.action} with order id ${msg.id}', + ); + } catch (e) { + _logger.e('Error processing event', error: e); + } } Future submitOrder(MostroMessage order) async { diff --git a/lib/services/subscription_manager.dart b/lib/services/subscription_manager.dart new file mode 100644 index 00000000..a99bc34c --- /dev/null +++ b/lib/services/subscription_manager.dart @@ -0,0 +1,65 @@ +import 'dart:async'; + +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; + +class SubscriptionManager extends StateNotifier { + final Ref ref; + StreamSubscription? _subscription; + + SubscriptionManager(this.ref) : super( + Subscription( + id: DateTime.now().millisecondsSinceEpoch.toString(), + request: NostrRequest(filters: []), + ), + ); + + Stream subscribe(NostrFilter filter) { + // Cancel any existing subscription + _subscription?.cancel(); + + // Create a new subscription + final nostrService = ref.read(nostrServiceProvider); + final controller = StreamController(); + + // Update the state with the new filter + final newRequest = NostrRequest(filters: [filter]); + state = state.copyWith(request: newRequest); + + // Start listening to events + _subscription = nostrService.subscribeToEvents(newRequest).listen( + controller.add, + onError: controller.addError, + onDone: controller.close, + cancelOnError: false, + ); + + return controller.stream; + } + + void unsubscribe() { + _subscription?.cancel(); + state = state.copyWith(request: NostrRequest(filters: [])); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} + +class Subscription { + final String id; + final NostrRequest request; + + Subscription({required this.id, required this.request}); + + Subscription copyWith({NostrRequest? request}) { + return Subscription( + id: id, + request: request ?? this.request, + ); + } +} \ No newline at end of file diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index 6a61e47e..aa4264cf 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -4,11 +4,7 @@ import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; @@ -22,71 +18,20 @@ final appInitializerProvider = FutureProvider((ref) async { final sessionManager = ref.read(sessionNotifierProvider.notifier); await sessionManager.init(); - // --- Custom logic for initializing notifiers and chats --- - final now = DateTime.now(); - final cutoff = now.subtract(const Duration(hours: 24)); - final sessions = sessionManager.sessions; - final messageStorage = ref.read(mostroStorageProvider); - final terminalStatuses = { - Status.canceled, - Status.cooperativelyCanceled, - Status.success, - Status.expired, - Status.canceledByAdmin, - Status.settledByAdmin, - Status.completedByAdmin, - }; - for (final session in sessions) { - if (session.startTime.isAfter(cutoff)) { - bool isActive = true; - if (session.orderId != null) { - final latestOrderMsg = await messageStorage.getLatestMessageOfTypeById(session.orderId!); - final status = latestOrderMsg?.payload is Order - ? (latestOrderMsg!.payload as Order).status - : null; - if (status != null && terminalStatuses.contains(status)) { - isActive = false; - } - } - if (isActive) { - // Initialize order notifier if needed - ref.read(orderNotifierProvider(session.orderId!).notifier); - // Initialize chat notifier if needed - if (session.peer != null) { - ref.read(chatRoomsProvider(session.orderId!).notifier); - } - } - } - } - - final mostroService = ref.read(mostroServiceProvider); - ref.listen(settingsProvider, (previous, next) { sessionManager.updateSettings(next); - mostroService.updateSettings(next); ref.read(backgroundServiceProvider).updateSettings(next); }); - final mostroStorage = ref.read(mostroStorageProvider); + final cutoff = DateTime.now().subtract(const Duration(hours: 24)); for (final session in sessionManager.sessions) { - if (session.orderId != null) { - final order = await mostroStorage.getLatestMessageById(session.orderId!); - if (order != null) { - // Explicitly initialize order notifier - // to ensure it's all properly set up for this orderId - ref.read(orderNotifierProvider(session.orderId!).notifier).sync(); - } - - // Read the order notifier provider last, which will watch all the above - ref.read(orderNotifierProvider(session.orderId!)); + if (session.orderId != null && session.startTime.isAfter(cutoff)) { + ref.read(orderNotifierProvider(session.orderId!).notifier); } - if (session.peer != null) { - final chat = ref.read( - chatRoomsProvider(session.orderId!).notifier, - ); - chat.subscribe(); + if (session.peer != null && session.startTime.isAfter(cutoff)) { + ref.read(chatRoomsProvider(session.orderId!).notifier).subscribe(); } } }); diff --git a/lib/shared/providers/mostro_service_provider.dart b/lib/shared/providers/mostro_service_provider.dart index b480a5a4..f92efa45 100644 --- a/lib/shared/providers/mostro_service_provider.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -1,8 +1,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/repositories/event_storage.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; + part 'mostro_service_provider.g.dart'; @Riverpod(keepAlive: true) @@ -14,5 +16,15 @@ EventStorage eventStorage(Ref ref) { @Riverpod(keepAlive: true) MostroService mostroService(Ref ref) { final mostroService = MostroService(ref); + mostroService.init(); + + ref.listen(settingsProvider, (previous, next) { + mostroService.updateSettings(next); + }); + + ref.onDispose(() { + mostroService.dispose(); + }); + return mostroService; } diff --git a/lib/shared/providers/subscription_manager_provider.dart b/lib/shared/providers/subscription_manager_provider.dart new file mode 100644 index 00000000..9621db6f --- /dev/null +++ b/lib/shared/providers/subscription_manager_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/services/subscription_manager.dart'; + +final subscriptionManagerProvider = + StateNotifierProvider( + (ref) => SubscriptionManager(ref), +); diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 8cc8033d..a0335757 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -3,35 +3,128 @@ import 'package:convert/convert.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'dart:async'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:mostro_mobile/core/config.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/services/subscription_manager.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'mostro_service_test.mocks.dart'; import 'mostro_service_helper_functions.dart'; -@GenerateMocks([NostrService, SessionNotifier, Ref]) +// Create a mock SubscriptionManager class for testing +class MockSubscriptionManager extends StateNotifier + implements SubscriptionManager { + MockSubscriptionManager({required this.ref}) : super(Subscription(id: 'test-sub', request: NostrRequest(filters: []))); + + @override + final Ref ref; + + @override + Stream subscribe(NostrFilter filter) { + // Store the filter for verification + _lastFilter = filter; + // Return a new stream to avoid multiple subscriptions to the same controller + return _controller.stream; + } + + NostrFilter? _lastFilter; + NostrFilter? get lastFilter => _lastFilter; + + // Helper to create a mock filter for verification + static NostrFilter createMockFilter() { + return NostrFilter( + kinds: [1059], + limit: 1, + ); + } + + @override + Future cancel() async { + await _controller.close(); + } + + @override + void dispose() { + _controller.close(); + super.dispose(); + } + + // Helper to add events to the stream + void addEvent(NostrEvent event) { + if (!_controller.isClosed) { + _controller.add(event); + } + } + + final StreamController _controller = StreamController.broadcast(); +} + +@GenerateMocks([ + NostrService, + SessionNotifier, + Ref, + StateNotifierProviderRef, +]) void main() { + late MockRef mockRef; + late MockSessionNotifier mockSessionNotifier; + late MockSubscriptionManager mockSubscriptionManager; + late MockNostrService mockNostrService; late MostroService mostroService; late KeyDerivator keyDerivator; - late MockNostrService mockNostrService; - late MockSessionNotifier mockSessionNotifier; - late MockRef mockRef; - - final mockServerTradeIndex = MockServerTradeIndex(); + late MockServerTradeIndex mockServerTradeIndex; + + setUpAll(() { + provideDummy(MockSessionNotifier()); + }); setUp(() { mockNostrService = MockNostrService(); mockSessionNotifier = MockSessionNotifier(); mockRef = MockRef(); - mostroService = MostroService(mockSessionNotifier, mockRef); + mockSubscriptionManager = MockSubscriptionManager(ref: mockRef); + mockServerTradeIndex = MockServerTradeIndex(); + + // Reset mocks before each test + reset(mockNostrService); + reset(mockSessionNotifier); + reset(mockRef); + + // Set up mock behavior + when(mockRef.read(mostroServiceProvider)).thenAnswer((_) => mostroService); + when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); + when(mockRef.read(sessionNotifierProvider.notifier)) + .thenReturn(mockSessionNotifier); + when(mockRef.read(subscriptionManagerProvider.notifier)) + .thenReturn(mockSubscriptionManager); + + // Initialize MostroService with mocks + mostroService = MostroService(mockRef); + + // Initialize MostroService + mostroService = MostroService(mockRef); keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); + + // Reset mock server trade index for each test + mockServerTradeIndex.userTradeIndices.clear(); + }); + + tearDown(() { + // Clean up resources + mostroService.dispose(); + mockSubscriptionManager.dispose(); }); // Helper function to verify signatures as server would @@ -352,10 +445,21 @@ void main() { await mostroService.takeSellOrder(orderId, 400, 'lnbc121314invoice'); // Assert - // Capture the published event - final captured = verify(mockNostrService.publishEvent(captureAny)) - .captured - .single as NostrEvent; + // Verify the subscription was set up correctly + // The subscription should have been set up with a filter for kind 1059 + final capturedFilter = mockSubscriptionManager.lastFilter; + expect(capturedFilter, isNotNull); + expect(capturedFilter!.kinds, contains(1059)); + + // Verify the published event + final capturedEvents = verify( + mockNostrService.publishEvent(captureAny), + ).captured.cast(); + + expect(capturedEvents, hasLength(1)); + final publishedEvent = capturedEvents.first; + expect(publishedEvent.kind, equals(1059)); + expect(publishedEvent.content, isNotEmpty); // Simulate server-side verification final isValid = serverVerifyMessage( From eda3d92060efe0d33dc12e68d1ce111472f64e90 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 19 Jun 2025 15:40:30 -0700 Subject: [PATCH 04/28] Bug when selecting the premium or discount for an order Fixes #120 --- lib/features/order/widgets/premium_section.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/order/widgets/premium_section.dart b/lib/features/order/widgets/premium_section.dart index ac9f2092..c4a6be5f 100644 --- a/lib/features/order/widgets/premium_section.dart +++ b/lib/features/order/widgets/premium_section.dart @@ -35,7 +35,7 @@ class PremiumSection extends StatelessWidget { activeTrackColor: AppTheme.purpleAccent, inactiveTrackColor: AppTheme.backgroundInactive, thumbColor: AppTheme.textPrimary, - overlayColor: AppTheme.purpleAccent.withOpacity(0.2), + overlayColor: AppTheme.purpleAccent.withValues(alpha: 0.2), trackHeight: 4, ), child: Slider( @@ -43,7 +43,7 @@ class PremiumSection extends StatelessWidget { value: value, min: -10, max: 10, - divisions: 200, + divisions: 20, onChanged: onChanged, ), ), From 87e95ee5d40edaebe95b0ec14c1c49096de867f8 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 24 Jun 2025 16:33:20 -0700 Subject: [PATCH 05/28] refactor: revamp subscription management with session-based subscriptions and cleanup --- lib/core/config.dart | 7 +- .../chat/notifiers/chat_rooms_notifier.dart | 22 ++++-- .../chat/providers/chat_room_providers.dart | 4 +- .../chat/screens/chat_rooms_list.dart | 6 +- lib/features/order/models/order_state.dart | 18 ++++- .../notfiers/abstract_mostro_notifier.dart | 4 +- .../order/notfiers/add_order_notifier.dart | 2 +- .../order/notfiers/order_notifier.dart | 4 +- .../widgets/mostro_message_detail_widget.dart | 15 ---- lib/main.dart | 6 +- lib/services/lifecycle_manager.dart | 4 +- lib/services/mostro_service.dart | 51 ++++++------- lib/services/subscription_manager.dart | 75 +++++++++++-------- lib/shared/notifiers/session_notifier.dart | 48 ++++++++---- lib/shared/providers/app_init_provider.dart | 4 +- .../providers/session_notifier_provider.dart | 14 +++- .../subscription_manager_provider.dart | 2 +- .../utils/notification_permission_helper.dart | 9 ++- 18 files changed, 171 insertions(+), 124 deletions(-) diff --git a/lib/core/config.dart b/lib/core/config.dart index 7ffd74e8..8a1a3f4e 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -28,8 +28,11 @@ class Config { // Versión de Mostro static int mostroVersion = 1; - static int expirationSeconds = 900; - static int expirationHours = 24; + static const int expirationSeconds = 900; + static const int expirationHours = 24; + static const int cleanupIntervalMinutes = 30; + static const int sessionExpirationHours = 36; + // Configuración de notificaciones static String notificationChannelId = 'mostro_mobile'; diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart index 2d77ced3..e5159238 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -1,19 +1,31 @@ +import 'dart:async'; + +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; +import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class ChatRoomsNotifier extends StateNotifier> { - final SessionNotifier sessionNotifier; final Ref ref; final _logger = Logger(); - ChatRoomsNotifier(this.ref, this.sessionNotifier) : super(const []) { + StreamSubscription? _subscription; + + ChatRoomsNotifier(this.ref) : super(const []) { loadChats(); } + void subscribe(Session session) { + if (session.sharedKey == null) { + _logger.e('Shared key is null'); + return; + } + + } + /// Reload all chat rooms by triggering their notifiers to resubscribe to events. void reloadAllChats() { for (final chat in state) { @@ -40,8 +52,8 @@ class ChatRoomsNotifier extends StateNotifier> { try { final chats = sessions .where( - (s) => s.peer != null && s.startTime.isAfter(cutoff), - ) + (s) => s.peer != null && s.startTime.isAfter(cutoff), + ) .map((s) { final chat = ref.read(chatRoomsProvider(s.orderId!)); return chat; diff --git a/lib/features/chat/providers/chat_room_providers.dart b/lib/features/chat/providers/chat_room_providers.dart index 05929ecf..8d195771 100644 --- a/lib/features/chat/providers/chat_room_providers.dart +++ b/lib/features/chat/providers/chat_room_providers.dart @@ -2,13 +2,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/features/chat/notifiers/chat_room_notifier.dart'; import 'package:mostro_mobile/features/chat/notifiers/chat_rooms_notifier.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final chatRoomsNotifierProvider = StateNotifierProvider>( (ref) { - final sessionNotifier = ref.watch(sessionNotifierProvider.notifier); - return ChatRoomsNotifier(ref, sessionNotifier); + return ChatRoomsNotifier(ref); }, ); diff --git a/lib/features/chat/screens/chat_rooms_list.dart b/lib/features/chat/screens/chat_rooms_list.dart index 8bc3c7bd..ed5e247a 100644 --- a/lib/features/chat/screens/chat_rooms_list.dart +++ b/lib/features/chat/screens/chat_rooms_list.dart @@ -5,9 +5,9 @@ import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_drawer.dart'; @@ -73,8 +73,8 @@ class ChatListItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider(orderId)); - final pubkey = session!.peer!.publicKey; + final orderState = ref.watch(orderNotifierProvider(orderId)); + final pubkey = orderState.peer!.publicKey; final handle = ref.read(nickNameProvider(pubkey)); return GestureDetector( onTap: () { diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 0a6df8cc..d70b0baa 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -105,6 +105,22 @@ class OrderState { newPaymentRequest = paymentRequest; // Preserve existing } + Peer? newPeer; + if (message.payload is Peer && + message.getPayload()!.publicKey.isNotEmpty) { + newPeer = message.getPayload(); + _logger.i('👤 New Peer found in message'); + } else if (message.payload is Order) { + if (message.getPayload()!.buyerTradePubkey != null) { + newPeer = Peer(publicKey: message.getPayload()!.buyerTradePubkey!); + } else if (message.getPayload()!.sellerTradePubkey != null) { + newPeer = Peer(publicKey: message.getPayload()!.sellerTradePubkey!); + } + _logger.i('👤 New Peer found in message'); + } else { + newPeer = peer; // Preserve existing + } + final newState = copyWith( status: newStatus, action: message.action, @@ -116,7 +132,7 @@ class OrderState { paymentRequest: newPaymentRequest, cantDo: message.getPayload() ?? cantDo, dispute: message.getPayload() ?? dispute, - peer: message.getPayload() ?? peer, + peer: newPeer, ); _logger.i('✅ New state: ${newState.status} - ${newState.action}'); diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index ad16dac8..0013d77e 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -132,7 +132,9 @@ class AbstractMostroNotifier extends StateNotifier { // add seller tradekey to session // open chat final sessionProvider = ref.read(sessionNotifierProvider.notifier); - final peer = Peer(publicKey: order!.sellerTradePubkey!); + final peer = order!.sellerTradePubkey != null + ? Peer(publicKey: order.sellerTradePubkey!) + : null; sessionProvider.updateSession( orderId, (s) => s.peer = peer, diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index c2980c68..dc8b431f 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -73,7 +73,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); - mostroService.subscribe(session.tradeKey.public); + mostroService.subscribe(session); await mostroService.submitOrder(message); state = state.updateWith(message); } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 18210e88..841ec219 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -34,7 +34,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.buyer, ); - mostroService.subscribe(session.tradeKey.public); + mostroService.subscribe(session); await mostroService.takeSellOrder( orderId, amount, @@ -48,7 +48,7 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.seller, ); - mostroService.subscribe(session.tradeKey.public); + mostroService.subscribe(session); await mostroService.takeBuyOrder( orderId, amount, diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 17e81bca..a37d293a 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -13,21 +13,6 @@ class MostroMessageDetail extends ConsumerWidget { final String orderId; const MostroMessageDetail({super.key, required this.orderId}); - /// Helper function to format payment methods for display - /// Returns "method1 (+X más)" if multiple methods, or just "method1" if single - String _formatPaymentMethods(List paymentMethods) { - if (paymentMethods.isEmpty) { - return 'No payment method'; - } - - if (paymentMethods.length == 1) { - return paymentMethods.first; - } - - final additionalCount = paymentMethods.length - 1; - return '${paymentMethods.first} (+$additionalCount más)'; - } - @override Widget build(BuildContext context, WidgetRef ref) { final orderState = ref.watch(orderNotifierProvider(orderId)); diff --git a/lib/main.dart b/lib/main.dart index 83882abd..fe4bf22e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -18,9 +16,7 @@ import 'package:shared_preferences/shared_preferences.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - if (Platform.isAndroid && !Platform.environment.containsKey('FLUTTER_TEST')) { - await requestNotificationPermissionIfNeeded(); - } + await requestNotificationPermissionIfNeeded(); final biometricsHelper = BiometricsHelper(); final sharedPreferences = SharedPreferencesAsync(); diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 31e1c9ab..7d7f3216 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -89,10 +89,10 @@ class LifecycleManager extends WidgetsBindingObserver { try { // Get the current subscription filter from the subscription manager final subscriptionManager = ref.read(subscriptionManagerProvider); - final currentFilter = subscriptionManager.request.filters; + final currentFilter = subscriptionManager.subscriptions.values.map((s) => s.request).toList(); if (currentFilter.isNotEmpty) { - _activeSubscriptions.addAll(currentFilter); + _activeSubscriptions.addAll(currentFilter.expand((s) => s.filters)); } if (_activeSubscriptions.isNotEmpty) { diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 2be04fbd..54d0df92 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -6,30 +6,27 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/services/subscription_manager.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; class MostroService { final Ref ref; - static final Logger _logger = Logger(); + final _logger = Logger(); - final Set _subscribedPubKeys = {}; + final Set _sessions = {}; StreamSubscription? _subscription; Settings _settings; MostroService(this.ref) : _settings = ref.read(settingsProvider); void init() { - ref.listen>(sessionNotifierProvider, (previous, next) { - if (next.isNotEmpty) { - for (final session in next) { - subscribe(session.tradeKey.public); - } - } else { - _clearSubscriptions(); - } - }); + final sessions = ref.read(sessionNotifierProvider); + for (final session in sessions) { + _sessions.add(session); + } + _updateSubscription(); } void dispose() { @@ -45,37 +42,37 @@ class MostroService { /// Throws an [ArgumentError] if [pubKey] is empty or invalid. /// /// [pubKey] The public key to subscribe to (must be a valid Nostr public key) - void subscribe(String pubKey) { - if (pubKey.isEmpty) { + void subscribe(Session session) { + if (session.tradeKey.public.isEmpty) { _logger.w('Attempted to subscribe to empty pubKey'); throw ArgumentError('pubKey cannot be empty'); } try { - if (_subscribedPubKeys.add(pubKey)) { - _logger.i('Added subscription for pubKey: $pubKey'); + if (_sessions.add(session)) { + _logger.i('Added subscription for pubKey: ${session.tradeKey.public}'); _updateSubscription(); } else { - _logger.d('Already subscribed to pubKey: $pubKey'); + _logger.d('Already subscribed to pubKey: ${session.tradeKey.public}'); } } catch (e, stackTrace) { - _logger.e('Invalid public key: $pubKey', error: e, stackTrace: stackTrace); + _logger.e('Invalid public key: ${session.tradeKey.public}', error: e, stackTrace: stackTrace); rethrow; } } - void unsubscribe(String pubKey) { - _subscribedPubKeys.remove(pubKey); + void unsubscribe(Session session) { + _sessions.remove(session); _updateSubscription(); } void _clearSubscriptions() { _subscription?.cancel(); _subscription = null; - _subscribedPubKeys.clear(); + _sessions.clear(); - final subscriptionManager = ref.read(subscriptionManagerProvider.notifier); - subscriptionManager.subscribe(NostrFilter()); + final subscriptionManager = ref.read(subscriptionManagerProvider); + subscriptionManager.unsubscribe(SubscriptionType.orders); } /// Updates the current subscription with the latest set of public keys. @@ -86,7 +83,7 @@ class MostroService { void _updateSubscription() { _subscription?.cancel(); - if (_subscribedPubKeys.isEmpty) { + if (_sessions.isEmpty) { _clearSubscriptions(); return; } @@ -94,11 +91,11 @@ class MostroService { try { final filter = NostrFilter( kinds: [1059], - p: _subscribedPubKeys.toList(), + p: _sessions.map((s) => s.tradeKey.public).toList(), ); - final subscriptionManager = ref.read(subscriptionManagerProvider.notifier); - _subscription = subscriptionManager.subscribe(filter).listen( + final subscriptionManager = ref.read(subscriptionManagerProvider); + _subscription = subscriptionManager.subscribe(SubscriptionType.orders, filter).listen( _onData, onError: (error, stackTrace) { _logger.e('Error in subscription', error: error, stackTrace: stackTrace); @@ -112,8 +109,6 @@ class MostroService { } Future _onData(NostrEvent event) async { - if (event.recipient == null || !_subscribedPubKeys.contains(event.recipient)) return; - final eventStore = ref.read(eventStorageProvider); if (await eventStore.hasItem(event.id!)) return; diff --git a/lib/services/subscription_manager.dart b/lib/services/subscription_manager.dart index a99bc34c..4d182f57 100644 --- a/lib/services/subscription_manager.dart +++ b/lib/services/subscription_manager.dart @@ -4,62 +4,73 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; -class SubscriptionManager extends StateNotifier { +class SubscriptionManager { final Ref ref; - StreamSubscription? _subscription; + final Map subscriptions = {}; - SubscriptionManager(this.ref) : super( - Subscription( - id: DateTime.now().millisecondsSinceEpoch.toString(), - request: NostrRequest(filters: []), - ), - ); + SubscriptionManager(this.ref); - Stream subscribe(NostrFilter filter) { + Stream subscribe(SubscriptionType type, NostrFilter filter) { // Cancel any existing subscription - _subscription?.cancel(); - + subscriptions[type]?.cancel(); + // Create a new subscription final nostrService = ref.read(nostrServiceProvider); final controller = StreamController(); - + // Update the state with the new filter final newRequest = NostrRequest(filters: [filter]); - state = state.copyWith(request: newRequest); - + // Start listening to events - _subscription = nostrService.subscribeToEvents(newRequest).listen( - controller.add, - onError: controller.addError, - onDone: controller.close, - cancelOnError: false, + subscriptions[type] = Subscription( + id: type.toString(), + request: newRequest, + streamSubscription: nostrService.subscribeToEvents(newRequest).listen( + controller.add, + onError: controller.addError, + onDone: controller.close, + cancelOnError: false, + ), ); - + return controller.stream; } - void unsubscribe() { - _subscription?.cancel(); - state = state.copyWith(request: NostrRequest(filters: [])); - } - - @override - void dispose() { - _subscription?.cancel(); - super.dispose(); + void unsubscribe(SubscriptionType type) { + subscriptions[type]?.cancel(); + subscriptions.remove(type); } } +enum SubscriptionType { + chat, + trades, + orders, +} + class Subscription { final String id; final NostrRequest request; + final StreamSubscription streamSubscription; - Subscription({required this.id, required this.request}); + Subscription({ + required this.id, + required this.request, + required this.streamSubscription, + }); - Subscription copyWith({NostrRequest? request}) { + void cancel() { + streamSubscription.cancel(); + } + + Subscription copyWith({ + NostrRequest? request, + StreamSubscription? streamSubscription, + }) { return Subscription( id: id, request: request ?? this.request, + streamSubscription: streamSubscription ?? this.streamSubscription, ); } -} \ No newline at end of file +} diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index faf74a81..2197a74d 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -1,40 +1,61 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/data/repositories/session_storage.dart'; -import 'package:mostro_mobile/features/key_manager/key_manager.dart'; +import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; class SessionNotifier extends StateNotifier> { - final KeyManager _keyManager; + final Ref ref; final SessionStorage _storage; - Settings _settings; - final Map _sessions = {}; final Map _requestIdToSession = {}; Timer? _cleanupTimer; - static const int sessionExpirationHours = 36; - static const int cleanupIntervalMinutes = 30; - static const int maxBatchSize = 100; List get sessions => _sessions.values.toList(); SessionNotifier( - this._keyManager, + this.ref, this._storage, this._settings, ) : super([]); Future init() async { final allSessions = await _storage.getAllSessions(); - final now = DateTime.now(); - final cutoff = now.subtract(const Duration(hours: 48)); + final cutoff = DateTime.now() + .subtract(const Duration(hours: Config.sessionExpirationHours)); for (final session in allSessions) { if (session.startTime.isAfter(cutoff)) { _sessions[session.orderId!] = session; + } else { + await _storage.deleteSession(session.orderId!); + _sessions.remove(session.orderId!); + } + } + state = sessions; + _scheduleCleanup(); + } + + void _scheduleCleanup() { + _cleanupTimer?.cancel(); + _cleanupTimer = Timer.periodic( + const Duration(minutes: Config.cleanupIntervalMinutes), + (timer) => _cleanup(), + ); + } + + void _cleanup() async { + final cutoff = DateTime.now() + .subtract(const Duration(hours: Config.sessionExpirationHours)); + final expiredSessions = await _storage.getAllSessions(); + for (final session in expiredSessions) { + if (session.startTime.isBefore(cutoff)) { + await _storage.deleteSession(session.orderId!); + _sessions.remove(session.orderId!); } } state = sessions; @@ -46,9 +67,9 @@ class SessionNotifier extends StateNotifier> { Future newSession( {String? orderId, int? requestId, Role? role}) async { - final masterKey = _keyManager.masterKeyPair!; - final keyIndex = await _keyManager.getCurrentKeyIndex(); - final tradeKey = await _keyManager.deriveTradeKey(); + final masterKey = ref.read(keyManagerProvider).masterKeyPair!; + final keyIndex = await ref.read(keyManagerProvider).getCurrentKeyIndex(); + final tradeKey = await ref.read(keyManagerProvider).deriveTradeKey(); final session = Session( startTime: DateTime.now(), @@ -76,7 +97,6 @@ class SessionNotifier extends StateNotifier> { state = sessions; } - /// Generic session update and persist method Future updateSession( String orderId, void Function(Session) update) async { final session = _sessions[orderId]; diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index aa4264cf..286fc5e5 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; @@ -19,11 +20,10 @@ final appInitializerProvider = FutureProvider((ref) async { await sessionManager.init(); ref.listen(settingsProvider, (previous, next) { - sessionManager.updateSettings(next); ref.read(backgroundServiceProvider).updateSettings(next); }); - final cutoff = DateTime.now().subtract(const Duration(hours: 24)); + final cutoff = DateTime.now().subtract(const Duration(hours: Config.sessionExpirationHours)); for (final session in sessionManager.sessions) { if (session.orderId != null && session.startTime.isAfter(cutoff)) { diff --git a/lib/shared/providers/session_notifier_provider.dart b/lib/shared/providers/session_notifier_provider.dart index da7bd83a..b45175e1 100644 --- a/lib/shared/providers/session_notifier_provider.dart +++ b/lib/shared/providers/session_notifier_provider.dart @@ -1,6 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/session.dart'; -import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:mostro_mobile/shared/providers/session_storage_provider.dart'; @@ -8,14 +8,20 @@ import 'package:mostro_mobile/shared/providers/session_storage_provider.dart'; final sessionNotifierProvider = StateNotifierProvider>((ref) { - final keyManager = ref.read(keyManagerProvider); final sessionStorage = ref.read(sessionStorageProvider); final settings = ref.read(settingsProvider); - return SessionNotifier( - keyManager, + + final sessionNotifier = SessionNotifier( + ref, sessionStorage, settings.copyWith(), ); + + ref.listen(settingsProvider, (previous, next) { + sessionNotifier.updateSettings(next); + }); + + return sessionNotifier; }); final sessionProvider = StateProvider.family((ref, id) { diff --git a/lib/shared/providers/subscription_manager_provider.dart b/lib/shared/providers/subscription_manager_provider.dart index 9621db6f..6c6a9e08 100644 --- a/lib/shared/providers/subscription_manager_provider.dart +++ b/lib/shared/providers/subscription_manager_provider.dart @@ -2,6 +2,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/services/subscription_manager.dart'; final subscriptionManagerProvider = - StateNotifierProvider( + Provider( (ref) => SubscriptionManager(ref), ); diff --git a/lib/shared/utils/notification_permission_helper.dart b/lib/shared/utils/notification_permission_helper.dart index 867d494a..b29e3933 100644 --- a/lib/shared/utils/notification_permission_helper.dart +++ b/lib/shared/utils/notification_permission_helper.dart @@ -1,9 +1,12 @@ +import 'dart:io'; import 'package:permission_handler/permission_handler.dart'; /// Requests notification permission at runtime (Android 13+/API 33+). Future requestNotificationPermissionIfNeeded() async { - final status = await Permission.notification.status; - if (status.isDenied || status.isRestricted) { - await Permission.notification.request(); + if (Platform.isAndroid && !Platform.environment.containsKey('FLUTTER_TEST')) { + final status = await Permission.notification.status; + if (status.isDenied || status.isRestricted) { + await Permission.notification.request(); + } } } From f95e669fcc069dc2d883c8db16ba0db3f6a0687d Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 25 Jun 2025 08:07:34 -0700 Subject: [PATCH 06/28] refactor: migrate to centralized subscription management system using SubscriptionManager --- integration_test/test_helpers.dart | 13 +- .../chat/notifiers/chat_room_notifier.dart | 78 ++--- .../chat/notifiers/chat_rooms_notifier.dart | 83 ++++- lib/features/subscriptions/subscription.dart | 26 ++ .../subscriptions/subscription_manager.dart | 285 ++++++++++++++++++ .../subscription_manager_provider.dart | 2 +- .../subscriptions/subscription_type.dart | 5 + lib/services/lifecycle_manager.dart | 30 +- lib/services/mostro_service.dart | 108 ++----- lib/services/subscription_manager.dart | 76 ----- lib/shared/providers/app_init_provider.dart | 11 +- test/services/mostro_service_test.dart | 186 +++++++++--- 12 files changed, 643 insertions(+), 260 deletions(-) create mode 100644 lib/features/subscriptions/subscription.dart create mode 100644 lib/features/subscriptions/subscription_manager.dart rename lib/{shared/providers => features/subscriptions}/subscription_manager_provider.dart (67%) create mode 100644 lib/features/subscriptions/subscription_type.dart delete mode 100644 lib/services/subscription_manager.dart diff --git a/integration_test/test_helpers.dart b/integration_test/test_helpers.dart index 0f0b675a..d950debb 100644 --- a/integration_test/test_helpers.dart +++ b/integration_test/test_helpers.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:dart_nostr/dart_nostr.dart'; -import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -272,7 +271,7 @@ class FakeMostroService implements MostroService { void init({List? keys}) {} @override - void subscribe(NostrKeyPairs keyPair) {} + void subscribe(Session session) {} @override Future submitOrder(MostroMessage order) async { @@ -317,13 +316,15 @@ class FakeMostroService implements MostroService { @override void updateSettings(Settings settings) {} - + @override - NostrRequest? currentRequest; + void unsubscribe(Session session) { + // TODO: implement unsubscribe + } @override - void unsubscribe(String pubKey) { - // TODO: implement unsubscribe + void dispose() { + // TODO: implement dispose } } diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index 04fb666b..9bf5ad5d 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/services/lifecycle_manager.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; @@ -13,14 +13,15 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class ChatRoomNotifier extends StateNotifier { /// Reload the chat room by re-subscribing to events. void reload() { - subscription.cancel(); + // Cancel the current subscription if it exists + _subscription?.cancel(); subscribe(); } final _logger = Logger(); final String orderId; final Ref ref; - late StreamSubscription subscription; + StreamSubscription? _subscription; ChatRoomNotifier( super.state, @@ -38,42 +39,46 @@ class ChatRoomNotifier extends StateNotifier { _logger.e('Shared key is null'); return; } - final filter = NostrFilter( - kinds: [1059], - p: [session.sharedKey!.public], - ); - final request = NostrRequest( - filters: [filter], - ); - ref.read(lifecycleManagerProvider).addSubscription(filter); + // Use SubscriptionManager to create a subscription for this specific chat room + final subscriptionManager = ref.read(subscriptionManagerProvider); - subscription = - ref.read(nostrServiceProvider).subscribeToEvents(request).listen( - (event) async { - try { - final eventStore = ref.read(eventStorageProvider); + subscriptionManager.chat.listen(_onChatEvent); + } - await eventStore.putItem( - event.id!, - event, - ); + void _onChatEvent(NostrEvent event) async { + try { + final session = ref.read(sessionProvider(orderId)); + if (session == null || session.sharedKey == null) { + _logger.e('Session or shared key is null when processing chat event'); + return; + } - final chat = await event.mostroUnWrap(session.sharedKey!); - // Deduplicate by message ID and always sort by createdAt - final allMessages = [ - ...state.messages, - chat, - ]; - // Use a map to deduplicate by event id - final deduped = {for (var m in allMessages) m.id: m}.values.toList(); - deduped.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); - state = state.copy(messages: deduped); - } catch (e) { - _logger.e(e); - } - }, - ); + if (session.sharedKey?.public != event.recipient) { + return; + } + + final eventStore = ref.read(eventStorageProvider); + + await eventStore.putItem( + event.id!, + event, + ); + + final chat = await event.mostroUnWrap(session.sharedKey!); + // Deduplicate by message ID and always sort by createdAt + final allMessages = [ + ...state.messages, + chat, + ]; + // Use a map to deduplicate by event id + final deduped = {for (var m in allMessages) m.id: m}.values.toList(); + deduped.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); + state = state.copy(messages: deduped); + } catch (e, stackTrace) { + _logger.e('Error processing chat event', + error: e, stackTrace: stackTrace); + } } Future sendMessage(String text) async { @@ -98,7 +103,8 @@ class ChatRoomNotifier extends StateNotifier { @override void dispose() { - subscription.cancel(); + _subscription?.cancel(); + _logger.i('Disposed chat room notifier for orderId: $orderId'); super.dispose(); } } diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart index e5159238..b88efcc5 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -1,32 +1,78 @@ import 'dart:async'; -import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; -import 'package:mostro_mobile/data/models/session.dart'; + import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; class ChatRoomsNotifier extends StateNotifier> { final Ref ref; final _logger = Logger(); - StreamSubscription? _subscription; + StreamSubscription? _chatSubscription; ChatRoomsNotifier(this.ref) : super(const []) { loadChats(); + //_setupChatSubscription(); } + + void _setupChatSubscription() { + final subscriptionManager = ref.read(subscriptionManagerProvider); + + // Subscribe to the chat stream from SubscriptionManager + // The SubscriptionManager will automatically manage subscriptions based on session changes + _chatSubscription = subscriptionManager.chat.listen( + _onChatEvent, + onError: (error, stackTrace) { + _logger.e('Error in chat subscription', error: error, stackTrace: stackTrace); + }, + cancelOnError: false, + ); + + _logger.i('Chat subscription set up'); + } + /// Handle incoming chat events + void _onChatEvent(NostrEvent event) { + try { + // Find the chat room this event belongs to + final orderId = _findOrderIdForEvent(event); + if (orderId == null) { + _logger.w('Could not determine orderId for chat event: ${event.id}'); + return; + } - void subscribe(Session session) { - if (session.sharedKey == null) { - _logger.e('Shared key is null'); - return; + // Store the event in the event store so it can be processed by the chat room notifier + final eventStore = ref.read(eventStorageProvider); + eventStore.putItem(event.id!, event).then((_) { + // Trigger a reload of the chat room to process the new event + final chatRoomNotifier = ref.read(chatRoomsProvider(orderId).notifier); + if (chatRoomNotifier.mounted) { + chatRoomNotifier.reload(); + } + }).catchError((error, stackTrace) { + _logger.e('Error storing chat event', error: error, stackTrace: stackTrace); + }); + } catch (e, stackTrace) { + _logger.e('Error processing chat event', error: e, stackTrace: stackTrace); } - } - /// Reload all chat rooms by triggering their notifiers to resubscribe to events. + String? _findOrderIdForEvent(NostrEvent event) { + final sessions = ref.read(sessionNotifierProvider); + for (final session in sessions) { + if (session.peer?.publicKey == event.pubkey) { + return session.orderId; + } + } + + return null; + } + void reloadAllChats() { for (final chat in state) { try { @@ -38,10 +84,12 @@ class ChatRoomsNotifier extends StateNotifier> { _logger.e('Failed to reload chat for orderId ${chat.orderId}: $e'); } } + + _refreshAllSubscriptions(); } Future loadChats() async { - final sessions = ref.read(sessionNotifierProvider.notifier).sessions; + final sessions = ref.read(sessionNotifierProvider); if (sessions.isEmpty) { _logger.i("No sessions yet, skipping chat load."); return; @@ -67,4 +115,19 @@ class ChatRoomsNotifier extends StateNotifier> { _logger.e(e); } } + + void _refreshAllSubscriptions() { + // No need to manually refresh subscriptions + // SubscriptionManager now handles this automatically based on SessionNotifier changes + _logger.i('Subscription management is now handled by SubscriptionManager'); + + // Just reload the chat rooms from the current sessions + //loadChats(); + } + + @override + void dispose() { + _chatSubscription?.cancel(); + super.dispose(); + } } diff --git a/lib/features/subscriptions/subscription.dart b/lib/features/subscriptions/subscription.dart new file mode 100644 index 00000000..4e1a2581 --- /dev/null +++ b/lib/features/subscriptions/subscription.dart @@ -0,0 +1,26 @@ +import 'dart:async'; +import 'package:dart_nostr/dart_nostr.dart'; + +class Subscription { + final NostrRequest request; + final StreamSubscription streamSubscription; + + Subscription({ + required this.request, + required this.streamSubscription, + }); + + void cancel() { + streamSubscription.cancel(); + } + + Subscription copyWith({ + NostrRequest? request, + StreamSubscription? streamSubscription, + }) { + return Subscription( + request: request ?? this.request, + streamSubscription: streamSubscription ?? this.streamSubscription, + ); + } +} diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart new file mode 100644 index 00000000..e6d27608 --- /dev/null +++ b/lib/features/subscriptions/subscription_manager.dart @@ -0,0 +1,285 @@ +import 'dart:async'; + +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/data/models/session.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; + +/// Manages Nostr subscriptions across different parts of the application. +/// +/// This class provides a centralized way to handle subscriptions to Nostr events, +/// supporting different subscription types (chat, orders, trades) and automatically +/// managing subscriptions based on session changes in the SessionNotifier. +class SubscriptionManager { + final Ref ref; + final Map> _subscriptions = { + SubscriptionType.chat: {}, + SubscriptionType.orders: {}, + SubscriptionType.trades: {}, + }; + final _logger = Logger(); + ProviderSubscription? _sessionListener; + + // Controllers for each subscription type to expose streams to consumers + final _ordersController = StreamController.broadcast(); + final _tradesController = StreamController.broadcast(); + final _chatController = StreamController.broadcast(); + + // Public streams that consumers can listen to + Stream get orders => _ordersController.stream; + Stream get trades => _tradesController.stream; + Stream get chat => _chatController.stream; + + SubscriptionManager(this.ref) { + _initSessionListener(); + } + + /// Initialize the session listener to automatically update subscriptions + /// when sessions change in the SessionNotifier + void _initSessionListener() { + _sessionListener = ref.listen>( + sessionNotifierProvider, + (previous, current) { + _logger.i('Sessions changed, updating subscriptions'); + _updateAllSubscriptions(current); + }, + onError: (error, stackTrace) { + _logger.e('Error in session listener', error: error, stackTrace: stackTrace); + }, + ); + + // Initialize subscriptions with current sessions + final currentSessions = ref.read(sessionNotifierProvider); + _updateAllSubscriptions(currentSessions); + } + + /// Update all subscription types based on the current sessions + void _updateAllSubscriptions(List sessions) { + if (sessions.isEmpty) { + _logger.i('No sessions available, clearing all subscriptions'); + _clearAllSubscriptions(); + return; + } + + // Update each subscription type + for (final type in SubscriptionType.values) { + _updateSubscription(type, sessions); + } + } + + /// Clear all active subscriptions + void _clearAllSubscriptions() { + for (final type in SubscriptionType.values) { + unsubscribeByType(type); + } + } + + /// Update a specific subscription type with the current sessions + void _updateSubscription(SubscriptionType type, List sessions) { + // Cancel existing subscriptions for this type + unsubscribeByType(type); + + if (sessions.isEmpty) { + _logger.i('No sessions for $type subscription'); + return; + } + + try { + final filter = _createFilterForType(type, sessions); + + // Create a subscription for this type + subscribe( + type: type, + filter: filter, + id: type.toString(), + ); + + _logger.i('Subscription created for $type with ${sessions.length} sessions'); + } catch (e, stackTrace) { + _logger.e('Failed to create $type subscription', + error: e, stackTrace: stackTrace); + } + } + + /// Create a NostrFilter based on the subscription type and sessions + NostrFilter _createFilterForType(SubscriptionType type, List sessions) { + switch (type) { + case SubscriptionType.orders: + return NostrFilter( + kinds: [1059], + p: sessions.map((s) => s.tradeKey.public).toList(), + ); + case SubscriptionType.trades: + return NostrFilter( + kinds: [1059], + p: sessions.map((s) => s.tradeKey.public).toList(), + ); + case SubscriptionType.chat: + return NostrFilter( + kinds: [1059], + p: sessions + .where((s) => s.peer?.publicKey != null) + .map((s) => s.sharedKey?.public) + .whereType() + .toList(), + ); + } + } + + /// Handle incoming events based on their subscription type + void _handleEvent(SubscriptionType type, NostrEvent event) { + try { + switch (type) { + case SubscriptionType.orders: + _ordersController.add(event); + break; + case SubscriptionType.trades: + _tradesController.add(event); + break; + case SubscriptionType.chat: + _chatController.add(event); + break; + } + } catch (e, stackTrace) { + _logger.e('Error handling $type event', + error: e, stackTrace: stackTrace); + } + } + + /// Subscribe to Nostr events with a specific filter and subscription type. + Stream subscribe({ + required SubscriptionType type, + required NostrFilter filter, + String? id, + }) { + final subscriptionId = id ?? type.toString(); + final nostrService = ref.read(nostrServiceProvider); + + final request = NostrRequest( + subscriptionId: subscriptionId, + filters: [filter], + ); + + final stream = nostrService.subscribeToEvents(request); + final streamSubscription = stream.listen( + (event) => _handleEvent(type, event), + onError: (error, stackTrace) { + _logger.e('Error in $type subscription', + error: error, stackTrace: stackTrace); + }, + cancelOnError: false, + ); + + final subscription = Subscription( + request: request, + streamSubscription: streamSubscription, + ); + + _subscriptions[type]![subscriptionId] = subscription; + + switch (type) { + case SubscriptionType.orders: + return orders; + case SubscriptionType.trades: + return trades; + case SubscriptionType.chat: + return chat; + } + } + + /// Subscribe to Nostr events for a specific session. + Stream subscribeSession({ + required SubscriptionType type, + required Session session, + required NostrFilter Function(Session) createFilter, + }) { + final filter = createFilter(session); + final sessionId = session.orderId ?? session.tradeKey.public; + return subscribe( + type: type, + filter: filter, + id: '${type.toString()}_$sessionId', + ); + } + + /// Unsubscribe from a specific subscription by ID. + void unsubscribeById(SubscriptionType type, String id) { + final subscription = _subscriptions[type]?[id]; + if (subscription != null) { + subscription.cancel(); + _subscriptions[type]?.remove(id); + _logger.d('Canceled subscription for $type with id $id'); + } + } + + /// Unsubscribe from all subscriptions of a specific type. + void unsubscribeByType(SubscriptionType type) { + final subscriptions = _subscriptions[type]; + if (subscriptions != null) { + for (final subscription in subscriptions.values) { + subscription.cancel(); + } + subscriptions.clear(); + _logger.d('Canceled all subscriptions for $type'); + } + } + + /// Unsubscribe from a session-based subscription. + void unsubscribeSession(SubscriptionType type, Session session) { + final sessionId = session.orderId ?? session.tradeKey.public; + unsubscribeById(type, '${type.toString()}_$sessionId'); + } + + /// Check if there's an active subscription of a specific type. + bool hasActiveSubscription(SubscriptionType type, {String? id}) { + if (id != null) { + return _subscriptions[type]?.containsKey(id) ?? false; + } + return (_subscriptions[type]?.isNotEmpty ?? false); + } + + /// Get all active filters for a specific subscription type + /// Returns an empty list if no active subscriptions exist for the type + List getActiveFilters(SubscriptionType type) { + final filters = []; + final subscriptions = _subscriptions[type] ?? {}; + + for (final subscription in subscriptions.values) { + if (subscription.request.filters.isNotEmpty) { + filters.add(subscription.request.filters.first); + } + } + + _logger.d('Retrieved ${filters.length} active filters for $type'); + return filters; + } + + /// Unsubscribe from all subscription types + void unsubscribeAll() { + _logger.i('Unsubscribing from all subscriptions'); + for (final type in SubscriptionType.values) { + unsubscribeByType(type); + } + } + + /// Dispose all subscriptions and listeners + void dispose() { + _logger.i('Disposing SubscriptionManager'); + if (_sessionListener != null) { + _sessionListener!.close(); + _sessionListener = null; + } + + unsubscribeAll(); + + _ordersController.close(); + _tradesController.close(); + _chatController.close(); + + _logger.i('SubscriptionManager disposed'); + } +} diff --git a/lib/shared/providers/subscription_manager_provider.dart b/lib/features/subscriptions/subscription_manager_provider.dart similarity index 67% rename from lib/shared/providers/subscription_manager_provider.dart rename to lib/features/subscriptions/subscription_manager_provider.dart index 6c6a9e08..64645576 100644 --- a/lib/shared/providers/subscription_manager_provider.dart +++ b/lib/features/subscriptions/subscription_manager_provider.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/services/subscription_manager.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; final subscriptionManagerProvider = Provider( diff --git a/lib/features/subscriptions/subscription_type.dart b/lib/features/subscriptions/subscription_type.dart new file mode 100644 index 00000000..2ff704d2 --- /dev/null +++ b/lib/features/subscriptions/subscription_type.dart @@ -0,0 +1,5 @@ +enum SubscriptionType { + chat, + trades, + orders, +} diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 7d7f3216..303756a2 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -5,16 +5,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; class LifecycleManager extends WidgetsBindingObserver { final Ref ref; bool _isInBackground = false; - final List _activeSubscriptions = []; final _logger = Logger(); LifecycleManager(this.ref) { @@ -50,9 +50,6 @@ class LifecycleManager extends WidgetsBindingObserver { _isInBackground = false; _logger.i("Switching to foreground"); - // Clear active subscriptions - _activeSubscriptions.clear(); - // Stop background service final backgroundService = ref.read(backgroundServiceProvider); await backgroundService.setForegroundStatus(true); @@ -87,15 +84,20 @@ class LifecycleManager extends WidgetsBindingObserver { Future _switchToBackground() async { try { - // Get the current subscription filter from the subscription manager + // Get the subscription manager final subscriptionManager = ref.read(subscriptionManagerProvider); - final currentFilter = subscriptionManager.subscriptions.values.map((s) => s.request).toList(); + final activeFilters = []; - if (currentFilter.isNotEmpty) { - _activeSubscriptions.addAll(currentFilter.expand((s) => s.filters)); + // Get actual filters for each subscription type + for (final type in SubscriptionType.values) { + final filters = subscriptionManager.getActiveFilters(type); + if (filters.isNotEmpty) { + _logger.d('Found ${filters.length} active filters for $type'); + activeFilters.addAll(filters); + } } - if (_activeSubscriptions.isNotEmpty) { + if (activeFilters.isNotEmpty) { _isInBackground = true; _logger.i("Switching to background"); @@ -103,8 +105,8 @@ class LifecycleManager extends WidgetsBindingObserver { final backgroundService = ref.read(backgroundServiceProvider); await backgroundService.setForegroundStatus(false); _logger.i( - "Transferring ${_activeSubscriptions.length} active subscriptions to background service"); - backgroundService.subscribe(_activeSubscriptions); + "Transferring ${activeFilters.length} active filters to background service"); + backgroundService.subscribe(activeFilters); } else { _logger.w("No active subscriptions to transfer to background service"); } @@ -115,8 +117,10 @@ class LifecycleManager extends WidgetsBindingObserver { } } + @Deprecated('Use SubscriptionManager instead.') void addSubscription(NostrFilter filter) { - _activeSubscriptions.add(filter); + _logger.w('LifecycleManager.addSubscription is deprecated. Use SubscriptionManager instead.'); + // No-op - subscriptions are now tracked by SubscriptionManager } void dispose() { diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 54d0df92..a535097a 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -6,108 +6,64 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; -import 'package:mostro_mobile/services/subscription_manager.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; class MostroService { final Ref ref; final _logger = Logger(); - final Set _sessions = {}; - StreamSubscription? _subscription; Settings _settings; + StreamSubscription? _ordersSubscription; MostroService(this.ref) : _settings = ref.read(settingsProvider); void init() { - final sessions = ref.read(sessionNotifierProvider); - for (final session in sessions) { - _sessions.add(session); - } - _updateSubscription(); + // Subscribe to the orders stream from SubscriptionManager + // The SubscriptionManager will automatically manage subscriptions based on SessionNotifier changes + _ordersSubscription = ref.read(subscriptionManagerProvider).orders.listen( + _onData, + onError: (error, stackTrace) { + _logger.e('Error in orders subscription', error: error, stackTrace: stackTrace); + }, + cancelOnError: false, + ); } void dispose() { - _clearSubscriptions(); - _subscription?.cancel(); + _ordersSubscription?.cancel(); + _logger.i('MostroService disposed'); } - /// Subscribes to events for a specific public key. - /// - /// This method adds the public key to the internal set of subscribed keys - /// and updates the subscription if the key was not already being tracked. + /// No need to manually subscribe to sessions anymore. + /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. /// - /// Throws an [ArgumentError] if [pubKey] is empty or invalid. + /// This method is kept for backward compatibility but doesn't perform manual subscription. /// - /// [pubKey] The public key to subscribe to (must be a valid Nostr public key) + /// [session] The session that would have been subscribed to void subscribe(Session session) { - if (session.tradeKey.public.isEmpty) { - _logger.w('Attempted to subscribe to empty pubKey'); - throw ArgumentError('pubKey cannot be empty'); - } - - try { - if (_sessions.add(session)) { - _logger.i('Added subscription for pubKey: ${session.tradeKey.public}'); - _updateSubscription(); - } else { - _logger.d('Already subscribed to pubKey: ${session.tradeKey.public}'); - } - } catch (e, stackTrace) { - _logger.e('Invalid public key: ${session.tradeKey.public}', error: e, stackTrace: stackTrace); - rethrow; - } + _logger.i('Manual subscription not needed for: ${session.tradeKey.public}'); + // No action needed - SubscriptionManager handles subscriptions automatically } + /// No need to manually unsubscribe from sessions anymore. + /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. + /// + /// This method is kept for backward compatibility but doesn't perform manual unsubscription. + /// + /// [session] The session that would have been unsubscribed from void unsubscribe(Session session) { - _sessions.remove(session); - _updateSubscription(); + _logger.i('Manual unsubscription not needed for: ${session.tradeKey.public}'); + // No action needed - SubscriptionManager handles subscriptions automatically } - void _clearSubscriptions() { - _subscription?.cancel(); - _subscription = null; - _sessions.clear(); - - final subscriptionManager = ref.read(subscriptionManagerProvider); - subscriptionManager.unsubscribe(SubscriptionType.orders); - } - - /// Updates the current subscription with the latest set of public keys. - /// - /// This method creates a new subscription with the current set of public keys - /// and cancels any existing subscription. If there are no public keys to - /// subscribe to, it clears all subscriptions. - void _updateSubscription() { - _subscription?.cancel(); - - if (_sessions.isEmpty) { - _clearSubscriptions(); - return; - } - - try { - final filter = NostrFilter( - kinds: [1059], - p: _sessions.map((s) => s.tradeKey.public).toList(), - ); - - final subscriptionManager = ref.read(subscriptionManagerProvider); - _subscription = subscriptionManager.subscribe(SubscriptionType.orders, filter).listen( - _onData, - onError: (error, stackTrace) { - _logger.e('Error in subscription', error: error, stackTrace: stackTrace); - }, - cancelOnError: false, - ); - } catch (e, stackTrace) { - _logger.e('Error updating subscription', error: e, stackTrace: stackTrace); - rethrow; - } - } + // No need to manually clear subscriptions anymore. + // SubscriptionManager now handles this automatically when needed. + // No need to manually update subscriptions anymore. + // SubscriptionManager now handles this automatically based on SessionNotifier changes. + Future _onData(NostrEvent event) async { final eventStore = ref.read(eventStorageProvider); diff --git a/lib/services/subscription_manager.dart b/lib/services/subscription_manager.dart deleted file mode 100644 index 4d182f57..00000000 --- a/lib/services/subscription_manager.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:async'; - -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; - -class SubscriptionManager { - final Ref ref; - final Map subscriptions = {}; - - SubscriptionManager(this.ref); - - Stream subscribe(SubscriptionType type, NostrFilter filter) { - // Cancel any existing subscription - subscriptions[type]?.cancel(); - - // Create a new subscription - final nostrService = ref.read(nostrServiceProvider); - final controller = StreamController(); - - // Update the state with the new filter - final newRequest = NostrRequest(filters: [filter]); - - // Start listening to events - subscriptions[type] = Subscription( - id: type.toString(), - request: newRequest, - streamSubscription: nostrService.subscribeToEvents(newRequest).listen( - controller.add, - onError: controller.addError, - onDone: controller.close, - cancelOnError: false, - ), - ); - - return controller.stream; - } - - void unsubscribe(SubscriptionType type) { - subscriptions[type]?.cancel(); - subscriptions.remove(type); - } -} - -enum SubscriptionType { - chat, - trades, - orders, -} - -class Subscription { - final String id; - final NostrRequest request; - final StreamSubscription streamSubscription; - - Subscription({ - required this.id, - required this.request, - required this.streamSubscription, - }); - - void cancel() { - streamSubscription.cancel(); - } - - Subscription copyWith({ - NostrRequest? request, - StreamSubscription? streamSubscription, - }) { - return Subscription( - id: id, - request: request ?? this.request, - streamSubscription: streamSubscription ?? this.streamSubscription, - ); - } -} diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index 286fc5e5..a07322a3 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -8,6 +8,7 @@ import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; final appInitializerProvider = FutureProvider((ref) async { final nostrService = ref.read(nostrServiceProvider); @@ -18,6 +19,8 @@ final appInitializerProvider = FutureProvider((ref) async { final sessionManager = ref.read(sessionNotifierProvider.notifier); await sessionManager.init(); + + ref.read(subscriptionManagerProvider); ref.listen(settingsProvider, (previous, next) { ref.read(backgroundServiceProvider).updateSettings(next); @@ -26,11 +29,11 @@ final appInitializerProvider = FutureProvider((ref) async { final cutoff = DateTime.now().subtract(const Duration(hours: Config.sessionExpirationHours)); for (final session in sessionManager.sessions) { - if (session.orderId != null && session.startTime.isAfter(cutoff)) { - ref.read(orderNotifierProvider(session.orderId!).notifier); - } + if(session.orderId == null || session.startTime.isBefore(cutoff)) continue; + + ref.read(orderNotifierProvider(session.orderId!).notifier); - if (session.peer != null && session.startTime.isAfter(cutoff)) { + if (session.peer != null) { ref.read(chatRoomsProvider(session.orderId!).notifier).subscribe(); } } diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index a0335757..c8342fc2 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -6,59 +6,146 @@ import 'package:flutter_test/flutter_test.dart'; import 'dart:async'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:mostro_mobile/core/config.dart'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +// Removed unused import import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; -import 'package:mostro_mobile/services/subscription_manager.dart'; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -import 'package:mostro_mobile/shared/providers/subscription_manager_provider.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'mostro_service_test.mocks.dart'; import 'mostro_service_helper_functions.dart'; // Create a mock SubscriptionManager class for testing -class MockSubscriptionManager extends StateNotifier - implements SubscriptionManager { - MockSubscriptionManager({required this.ref}) : super(Subscription(id: 'test-sub', request: NostrRequest(filters: []))); +class MockSubscriptionManager implements SubscriptionManager { + MockSubscriptionManager({required this.ref}); @override final Ref ref; + final Map> _subscriptions = { + SubscriptionType.chat: {}, + SubscriptionType.orders: {}, + SubscriptionType.trades: {}, + }; + + NostrFilter? _lastFilter; + NostrFilter? get lastFilter => _lastFilter; + String? _lastSubscriptionId; + + // Implement the stream getters required by SubscriptionManager + @override + Stream get chat => _controller.stream; + + @override + Stream get orders => _controller.stream; + @override - Stream subscribe(NostrFilter filter) { + Stream get trades => _controller.stream; + + @override + Stream subscribe({ + required SubscriptionType type, + required NostrFilter filter, + String? id, + }) { // Store the filter for verification _lastFilter = filter; + _lastSubscriptionId = id ?? type.toString(); + + // Create a subscription + final subscription = Subscription( + request: NostrRequest(filters: [filter]), + streamSubscription: _controller.stream.listen((_) {}), + ); + + _subscriptions[type]![_lastSubscriptionId!] = subscription; + // Return a new stream to avoid multiple subscriptions to the same controller return _controller.stream; } - NostrFilter? _lastFilter; - NostrFilter? get lastFilter => _lastFilter; - - // Helper to create a mock filter for verification - static NostrFilter createMockFilter() { - return NostrFilter( - kinds: [1059], - limit: 1, + @override + Stream subscribeSession({ + required SubscriptionType type, + required Session session, + required NostrFilter Function(Session session) createFilter, + }) { + final filter = createFilter(session); + final sessionId = session.orderId ?? session.tradeKey.public; + return subscribe( + type: type, + filter: filter, + id: '${type.toString()}_$sessionId', ); } @override - Future cancel() async { - await _controller.close(); + void unsubscribeById(SubscriptionType type, String id) { + _subscriptions[type]?.remove(id); + } + + @override + void unsubscribeByType(SubscriptionType type) { + _subscriptions[type]?.clear(); + } + + @override + void unsubscribeAll() { + for (final type in SubscriptionType.values) { + unsubscribeByType(type); + } + } + + @override + List getActiveFilters(SubscriptionType type) { + final filters = []; + final subscriptions = _subscriptions[type] ?? {}; + + for (final subscription in subscriptions.values) { + if (subscription.request.filters.isNotEmpty) { + filters.add(subscription.request.filters.first); + } + } + + return filters; + } + + @override + void unsubscribeSession(SubscriptionType type, Session session) { + final sessionId = session.orderId ?? session.tradeKey.public; + unsubscribeById(type, '${type.toString()}_$sessionId'); + } + + @override + bool hasActiveSubscription(SubscriptionType type, {String? id}) { + if (id != null) { + return _subscriptions[type]?.containsKey(id) ?? false; + } + return (_subscriptions[type]?.isNotEmpty ?? false); } @override void dispose() { _controller.close(); - super.dispose(); + } + + // Helper to create a mock filter for verification + static NostrFilter createMockFilter() { + return NostrFilter( + kinds: [1059], + limit: 1, + ); } // Helper to add events to the stream @@ -87,38 +174,61 @@ void main() { late MockServerTradeIndex mockServerTradeIndex; setUpAll(() { + // Create a dummy Settings object that will be used by MockRef + final dummySettings = Settings( + relays: ['wss://relay.damus.io'], + fullPrivacyMode: false, + mostroPublicKey: 'npub1mostro', + defaultFiatCode: 'USD', + ); + + // Provide dummy values for Mockito provideDummy(MockSessionNotifier()); + provideDummy(dummySettings); + provideDummy(MockNostrService()); + + // Create a mock ref for the SubscriptionManager dummy + final mockRefForSubscriptionManager = MockRef(); + provideDummy(MockSubscriptionManager(ref: mockRefForSubscriptionManager)); + + // Create a mock ref that returns the dummy settings + final mockRefForDummy = MockRef(); + when(mockRefForDummy.read(settingsProvider)).thenReturn(dummySettings); + + // Provide a dummy MostroService that uses our properly configured mock ref + provideDummy(MostroService(mockRefForDummy)); }); setUp(() { mockNostrService = MockNostrService(); - mockSessionNotifier = MockSessionNotifier(); mockRef = MockRef(); + mockSessionNotifier = MockSessionNotifier(); mockSubscriptionManager = MockSubscriptionManager(ref: mockRef); + mockNostrService = MockNostrService(); mockServerTradeIndex = MockServerTradeIndex(); - // Reset mocks before each test - reset(mockNostrService); - reset(mockSessionNotifier); - reset(mockRef); + // Setup key derivator + keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); + + // Setup mock session notifier + when(mockSessionNotifier.sessions).thenReturn([]); - // Set up mock behavior - when(mockRef.read(mostroServiceProvider)).thenAnswer((_) => mostroService); + // Setup mock ref with Settings + final settings = Settings( + relays: ['wss://relay.damus.io'], + fullPrivacyMode: false, + mostroPublicKey: 'npub1mostro', + defaultFiatCode: 'USD', + ); + when(mockRef.read(settingsProvider)).thenReturn(settings); + when(mockRef.read(sessionNotifierProvider.notifier)).thenReturn(mockSessionNotifier); when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); - when(mockRef.read(sessionNotifierProvider.notifier)) - .thenReturn(mockSessionNotifier); - when(mockRef.read(subscriptionManagerProvider.notifier)) - .thenReturn(mockSubscriptionManager); - - // Initialize MostroService with mocks - mostroService = MostroService(mockRef); + when(mockRef.read(subscriptionManagerProvider)).thenReturn(mockSubscriptionManager); + // Mock server trade index is used in the service + // but we don't need to mock the provider - // Initialize MostroService + // Create the service under test mostroService = MostroService(mockRef); - keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); - - // Reset mock server trade index for each test - mockServerTradeIndex.userTradeIndices.clear(); }); tearDown(() { From 197ca9614e7cb061c877b766a21eb2088ca1eb56 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 25 Jun 2025 20:13:32 -0700 Subject: [PATCH 07/28] refactor: streamline subscription management and improve navigation flow --- lib/data/repositories/mostro_storage.dart | 15 +- lib/features/order/models/order_state.dart | 6 +- .../notfiers/abstract_mostro_notifier.dart | 34 ++-- .../order/notfiers/add_order_notifier.dart | 8 +- .../order/screens/add_order_screen.dart | 19 ++- lib/features/subscriptions/subscription.dart | 10 ++ .../subscriptions/subscription_manager.dart | 147 ++++++++---------- .../trades/screens/trade_detail_screen.dart | 2 +- lib/services/lifecycle_manager.dart | 3 + lib/services/mostro_service.dart | 26 ++-- 10 files changed, 139 insertions(+), 131 deletions(-) diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart index 9975fa36..1cbfd432 100644 --- a/lib/data/repositories/mostro_storage.dart +++ b/lib/data/repositories/mostro_storage.dart @@ -14,6 +14,7 @@ class MostroStorage extends BaseStorage { Future addMessage(String key, MostroMessage message) async { final id = key; try { + if (await hasItem(id)) return; // Add metadata for easier querying final Map dbMap = message.toJson(); if (message.timestamp == null) { @@ -122,7 +123,6 @@ class MostroStorage extends BaseStorage { /// Stream of the latest message for an order Stream watchLatestMessage(String orderId) { - // We want to watch ALL messages for this orderId, not just a specific key final query = store.query( finder: Finder( filter: Filter.equals('id', orderId), @@ -166,14 +166,17 @@ class MostroStorage extends BaseStorage { ); } - /// Stream of all messages for an order Stream watchByRequestId(int requestId) { final query = store.query( - finder: Finder(filter: Filter.equals('request_id', requestId)), + finder: Finder( + filter: Filter.equals('request_id', requestId), + sortOrders: _getDefaultSort(), + limit: 1, + ), ); - return query - .onSnapshot(db) - .map((snap) => snap == null ? null : fromDbMap('', snap.value)); + + return query.onSnapshots(db).map((snapshots) => + snapshots.isNotEmpty ? MostroMessage.fromJson(snapshots.first.value) : null); } Future> getAllMessagesForOrderId(String orderId) async { diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index d70b0baa..3efde078 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -86,7 +86,7 @@ class OrderState { // Preserve the current state entirely for cantDo messages - they are informational only if (message.action == Action.cantDo) { - return this; + return copyWith(cantDo: message.getPayload()); } // Determine the new status based on the action received @@ -135,10 +135,6 @@ class OrderState { peer: newPeer, ); - _logger.i('✅ New state: ${newState.status} - ${newState.action}'); - _logger - .i('💳 PaymentRequest preserved: ${newState.paymentRequest != null}'); - return newState; } diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 0013d77e..b12a643c 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -42,10 +42,21 @@ class AbstractMostroNotifier extends StateNotifier { data: (MostroMessage? msg) { logger.i('Received message: ${msg?.toJson()}'); if (msg != null) { - handleEvent(msg); + if (mounted) { + state = state.updateWith(msg); + } + if (msg.timestamp != null && + msg.timestamp! > + DateTime.now() + .subtract(const Duration(seconds: 60)) + .millisecondsSinceEpoch) { + handleEvent(msg); + } } }, - error: (error, stack) => handleError(error, stack), + error: (error, stack) { + handleError(error, stack); + }, loading: () {}, ); }, @@ -61,10 +72,6 @@ class AbstractMostroNotifier extends StateNotifier { final notifProvider = ref.read(notificationProvider.notifier); final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; - if (mounted) { - state = state.updateWith(event); - } - switch (event.action) { case Action.newOrder: break; @@ -87,7 +94,7 @@ class AbstractMostroNotifier extends StateNotifier { case Action.canceled: ref.read(mostroStorageProvider).deleteAllMessagesByOrderId(orderId); ref.read(sessionNotifierProvider.notifier).deleteSession(orderId); - navProvider.go('/'); + navProvider.go('/order_book'); notifProvider.showInformation(event.action, values: {'id': orderId}); ref.invalidateSelf(); break; @@ -149,9 +156,10 @@ class AbstractMostroNotifier extends StateNotifier { notifProvider.showInformation(event.action, values: { 'buyer_npub': state.order?.buyerTradePubkey ?? 'Unknown', }); + navProvider.go('/trade_detail/$orderId'); break; case Action.waitingSellerToPay: - navProvider.go('/'); + navProvider.go('/trade_detail/$orderId'); notifProvider.showInformation(event.action, values: { 'id': event.id, 'expiration_seconds': @@ -163,6 +171,7 @@ class AbstractMostroNotifier extends StateNotifier { 'expiration_seconds': mostroInstance?.expirationSeconds ?? Config.expirationSeconds, }); + navProvider.go('/trade_detail/$orderId'); break; case Action.addInvoice: final sessionNotifier = ref.read(sessionNotifierProvider.notifier); @@ -176,14 +185,6 @@ class AbstractMostroNotifier extends StateNotifier { logger.e('Buyer took order, but order is null'); break; } - notifProvider.showInformation(event.action, values: { - 'buyer_npub': order.buyerTradePubkey ?? 'Unknown', - 'fiat_code': order.fiatCode, - 'fiat_amount': order.fiatAmount, - 'payment_method': order.paymentMethod, - }); - // add seller tradekey to session - // open chat final sessionProvider = ref.read(sessionNotifierProvider.notifier); final peer = order.buyerTradePubkey != null ? Peer(publicKey: order.buyerTradePubkey!) @@ -197,6 +198,7 @@ class AbstractMostroNotifier extends StateNotifier { ); final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); + navProvider.go('/trade_detail/$orderId'); break; case Action.cantDo: final cantDo = event.getPayload(); diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index dc8b431f..02ab3246 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -19,9 +19,9 @@ class AddOrderNotifier extends AbstractMostroNotifier { int _requestIdFromOrderId(String orderId) { final uuid = orderId.replaceAll('-', ''); - final uuidPart1 = int.parse(uuid.substring(0, 8), radix: 16); - final uuidPart2 = int.parse(uuid.substring(8, 16), radix: 16); - return ((uuidPart1 ^ uuidPart2) & 0x7FFFFFFF); + final timestamp = DateTime.now().microsecondsSinceEpoch; + return (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & + 0x7FFFFFFF; } @override @@ -75,6 +75,6 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); mostroService.subscribe(session); await mostroService.submitOrder(message); - state = state.updateWith(message); + //state = state.updateWith(message); } } diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index 9409a90d..d1d66e1a 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -280,20 +280,25 @@ class _AddOrderScreenState extends ConsumerState { final satsAmount = int.tryParse(_satsAmountController.text) ?? 0; - // Preparar la lista de métodos de pago para cumplir con NIP-69 List paymentMethods = List.from(_selectedPaymentMethods); if (_showCustomPaymentMethod && _customPaymentMethodController.text.isNotEmpty) { - // Eliminar "Other" de la lista si existe para evitar duplicación paymentMethods.remove("Other"); - // Agregar el método de pago personalizado - paymentMethods.add(_customPaymentMethodController.text); + + String sanitizedPaymentMethod = _customPaymentMethodController.text; + + final problematicChars = RegExp(r'[,"\\\[\]{}]'); + sanitizedPaymentMethod = sanitizedPaymentMethod + .replaceAll(problematicChars, ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + + if (sanitizedPaymentMethod.isNotEmpty) { + paymentMethods.add(sanitizedPaymentMethod); + } } - // Cada método de pago se mantiene como un elemento separado en la lista - // en lugar de concatenarlos en una cadena - final buyerInvoice = _orderType == OrderType.buy && _lightningAddressController.text.isNotEmpty ? _lightningAddressController.text diff --git a/lib/features/subscriptions/subscription.dart b/lib/features/subscriptions/subscription.dart index 4e1a2581..fffd38dc 100644 --- a/lib/features/subscriptions/subscription.dart +++ b/lib/features/subscriptions/subscription.dart @@ -4,23 +4,33 @@ import 'package:dart_nostr/dart_nostr.dart'; class Subscription { final NostrRequest request; final StreamSubscription streamSubscription; + final Function() onCancel; Subscription({ required this.request, required this.streamSubscription, + required this.onCancel, }); void cancel() { streamSubscription.cancel(); + onCancel(); } Subscription copyWith({ NostrRequest? request, StreamSubscription? streamSubscription, + Function()? onCancel, }) { return Subscription( request: request ?? this.request, streamSubscription: streamSubscription ?? this.streamSubscription, + onCancel: onCancel ?? this.onCancel, ); } + + @override + String toString() { + return 'Subscription(request: $request, streamSubscription: $streamSubscription)'; + } } diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index e6d27608..b2857ecf 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -16,19 +16,15 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; /// managing subscriptions based on session changes in the SessionNotifier. class SubscriptionManager { final Ref ref; - final Map> _subscriptions = { - SubscriptionType.chat: {}, - SubscriptionType.orders: {}, - SubscriptionType.trades: {}, - }; + final Map _subscriptions = {}; final _logger = Logger(); ProviderSubscription? _sessionListener; - + // Controllers for each subscription type to expose streams to consumers final _ordersController = StreamController.broadcast(); final _tradesController = StreamController.broadcast(); final _chatController = StreamController.broadcast(); - + // Public streams that consumers can listen to Stream get orders => _ordersController.stream; Stream get trades => _tradesController.stream; @@ -37,26 +33,27 @@ class SubscriptionManager { SubscriptionManager(this.ref) { _initSessionListener(); } - + /// Initialize the session listener to automatically update subscriptions /// when sessions change in the SessionNotifier void _initSessionListener() { _sessionListener = ref.listen>( - sessionNotifierProvider, + sessionNotifierProvider, (previous, current) { _logger.i('Sessions changed, updating subscriptions'); _updateAllSubscriptions(current); }, onError: (error, stackTrace) { - _logger.e('Error in session listener', error: error, stackTrace: stackTrace); + _logger.e('Error in session listener', + error: error, stackTrace: stackTrace); }, ); - + // Initialize subscriptions with current sessions final currentSessions = ref.read(sessionNotifierProvider); _updateAllSubscriptions(currentSessions); } - + /// Update all subscription types based on the current sessions void _updateAllSubscriptions(List sessions) { if (sessions.isEmpty) { @@ -64,49 +61,51 @@ class SubscriptionManager { _clearAllSubscriptions(); return; } - + // Update each subscription type for (final type in SubscriptionType.values) { _updateSubscription(type, sessions); } } - + /// Clear all active subscriptions void _clearAllSubscriptions() { for (final type in SubscriptionType.values) { unsubscribeByType(type); } } - + /// Update a specific subscription type with the current sessions void _updateSubscription(SubscriptionType type, List sessions) { // Cancel existing subscriptions for this type unsubscribeByType(type); - + if (sessions.isEmpty) { _logger.i('No sessions for $type subscription'); return; } - + try { final filter = _createFilterForType(type, sessions); - + // Create a subscription for this type subscribe( type: type, filter: filter, - id: type.toString(), ); - - _logger.i('Subscription created for $type with ${sessions.length} sessions'); + + _logger + .i('Subscription created for $type with ${sessions.length} sessions'); } catch (e, stackTrace) { - _logger.e('Failed to create $type subscription', + _logger.e('Failed to create $type subscription', error: e, stackTrace: stackTrace); } + _logger.i('Updated $type subscription with $_subscriptions'); } - + /// Create a NostrFilter based on the subscription type and sessions - NostrFilter _createFilterForType(SubscriptionType type, List sessions) { + NostrFilter _createFilterForType( + SubscriptionType type, List sessions) { switch (type) { case SubscriptionType.orders: return NostrFilter( @@ -129,7 +128,7 @@ class SubscriptionManager { ); } } - + /// Handle incoming events based on their subscription type void _handleEvent(SubscriptionType type, NostrEvent event) { try { @@ -145,42 +144,41 @@ class SubscriptionManager { break; } } catch (e, stackTrace) { - _logger.e('Error handling $type event', - error: e, stackTrace: stackTrace); + _logger.e('Error handling $type event', error: e, stackTrace: stackTrace); } } - + /// Subscribe to Nostr events with a specific filter and subscription type. Stream subscribe({ required SubscriptionType type, required NostrFilter filter, - String? id, }) { - final subscriptionId = id ?? type.toString(); final nostrService = ref.read(nostrServiceProvider); - + final request = NostrRequest( - subscriptionId: subscriptionId, filters: [filter], ); - + final stream = nostrService.subscribeToEvents(request); final streamSubscription = stream.listen( (event) => _handleEvent(type, event), onError: (error, stackTrace) { - _logger.e('Error in $type subscription', + _logger.e('Error in $type subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, ); - + final subscription = Subscription( request: request, streamSubscription: streamSubscription, + onCancel: () { + ref.read(nostrServiceProvider).unsubscribe(request.subscriptionId!); + }, ); - - _subscriptions[type]![subscriptionId] = subscription; - + + _subscriptions[type] = subscription; + switch (type) { case SubscriptionType.orders: return orders; @@ -190,7 +188,7 @@ class SubscriptionManager { return chat; } } - + /// Subscribe to Nostr events for a specific session. Stream subscribeSession({ required SubscriptionType type, @@ -198,66 +196,55 @@ class SubscriptionManager { required NostrFilter Function(Session) createFilter, }) { final filter = createFilter(session); - final sessionId = session.orderId ?? session.tradeKey.public; return subscribe( type: type, filter: filter, - id: '${type.toString()}_$sessionId', ); } - + /// Unsubscribe from a specific subscription by ID. - void unsubscribeById(SubscriptionType type, String id) { - final subscription = _subscriptions[type]?[id]; + void unsubscribeById(SubscriptionType type) { + final subscription = _subscriptions[type]; if (subscription != null) { subscription.cancel(); - _subscriptions[type]?.remove(id); - _logger.d('Canceled subscription for $type with id $id'); + _subscriptions.remove(type); + _logger.d('Canceled subscription for $type'); } } - + /// Unsubscribe from all subscriptions of a specific type. void unsubscribeByType(SubscriptionType type) { - final subscriptions = _subscriptions[type]; - if (subscriptions != null) { - for (final subscription in subscriptions.values) { - subscription.cancel(); - } - subscriptions.clear(); + final subscription = _subscriptions[type]; + if (subscription != null) { + subscription.cancel(); + _subscriptions.remove(type); _logger.d('Canceled all subscriptions for $type'); } } - + /// Unsubscribe from a session-based subscription. - void unsubscribeSession(SubscriptionType type, Session session) { - final sessionId = session.orderId ?? session.tradeKey.public; - unsubscribeById(type, '${type.toString()}_$sessionId'); + void unsubscribeSession(SubscriptionType type) { + unsubscribeByType(type); } - + /// Check if there's an active subscription of a specific type. - bool hasActiveSubscription(SubscriptionType type, {String? id}) { - if (id != null) { - return _subscriptions[type]?.containsKey(id) ?? false; - } - return (_subscriptions[type]?.isNotEmpty ?? false); + bool hasActiveSubscription(SubscriptionType type) { + return _subscriptions[type] != null; } - + /// Get all active filters for a specific subscription type /// Returns an empty list if no active subscriptions exist for the type List getActiveFilters(SubscriptionType type) { - final filters = []; - final subscriptions = _subscriptions[type] ?? {}; - - for (final subscription in subscriptions.values) { - if (subscription.request.filters.isNotEmpty) { - filters.add(subscription.request.filters.first); - } - } - - _logger.d('Retrieved ${filters.length} active filters for $type'); - return filters; + final subscription = _subscriptions[type]; + return subscription?.request.filters ?? []; + } + + void subscribeAll() { + unsubscribeAll(); + _logger.i('Subscribing to all subscriptions'); + _initSessionListener(); } - + /// Unsubscribe from all subscription types void unsubscribeAll() { _logger.i('Unsubscribing from all subscriptions'); @@ -265,7 +252,7 @@ class SubscriptionManager { unsubscribeByType(type); } } - + /// Dispose all subscriptions and listeners void dispose() { _logger.i('Disposing SubscriptionManager'); @@ -273,13 +260,13 @@ class SubscriptionManager { _sessionListener!.close(); _sessionListener = null; } - + unsubscribeAll(); - + _ordersController.close(); _tradesController.close(); _chatController.close(); - + _logger.i('SubscriptionManager disposed'); } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 709afda5..09a0dcb1 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -457,7 +457,7 @@ class TradeDetailScreen extends ConsumerWidget { /// CLOSE Widget _buildCloseButton(BuildContext context) { return OutlinedButton( - onPressed: () => context.pop(), + onPressed: () => context.go('/order_book'), style: AppTheme.theme.outlinedButtonTheme.style, child: const Text('CLOSE'), ); diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 303756a2..c8f504f4 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -58,6 +58,9 @@ class LifecycleManager extends WidgetsBindingObserver { // Add a small delay to ensure the background service has fully transitioned await Future.delayed(const Duration(milliseconds: 500)); + final subscriptionManager = ref.read(subscriptionManagerProvider); + subscriptionManager.subscribeAll(); + // Reinitialize the mostro service _logger.i("Reinitializing MostroService"); ref.read(mostroServiceProvider).init(); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index a535097a..76e47c6b 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -13,10 +13,10 @@ import 'package:mostro_mobile/features/settings/settings_provider.dart'; class MostroService { final Ref ref; final _logger = Logger(); - + Settings _settings; StreamSubscription? _ordersSubscription; - + MostroService(this.ref) : _settings = ref.read(settingsProvider); void init() { @@ -25,7 +25,8 @@ class MostroService { _ordersSubscription = ref.read(subscriptionManagerProvider).orders.listen( _onData, onError: (error, stackTrace) { - _logger.e('Error in orders subscription', error: error, stackTrace: stackTrace); + _logger.e('Error in orders subscription', + error: error, stackTrace: stackTrace); }, cancelOnError: false, ); @@ -38,9 +39,9 @@ class MostroService { /// No need to manually subscribe to sessions anymore. /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. - /// + /// /// This method is kept for backward compatibility but doesn't perform manual subscription. - /// + /// /// [session] The session that would have been subscribed to void subscribe(Session session) { _logger.i('Manual subscription not needed for: ${session.tradeKey.public}'); @@ -49,12 +50,13 @@ class MostroService { /// No need to manually unsubscribe from sessions anymore. /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. - /// + /// /// This method is kept for backward compatibility but doesn't perform manual unsubscription. - /// + /// /// [session] The session that would have been unsubscribed from void unsubscribe(Session session) { - _logger.i('Manual unsubscription not needed for: ${session.tradeKey.public}'); + _logger + .i('Manual unsubscription not needed for: ${session.tradeKey.public}'); // No action needed - SubscriptionManager handles subscriptions automatically } @@ -63,7 +65,7 @@ class MostroService { // No need to manually update subscriptions anymore. // SubscriptionManager now handles this automatically based on SessionNotifier changes. - + Future _onData(NostrEvent event) async { final eventStore = ref.read(eventStorageProvider); @@ -75,7 +77,7 @@ class MostroService { final sessions = ref.read(sessionNotifierProvider); Session? matchingSession; - + try { matchingSession = sessions.firstWhere( (s) => s.tradeKey.public == event.recipient, @@ -89,7 +91,7 @@ class MostroService { try { final decryptedEvent = await event.unWrap(privateKey); if (decryptedEvent.content == null) return; - + final result = jsonDecode(decryptedEvent.content!); if (result is! List) return; @@ -97,7 +99,7 @@ class MostroService { final messageStorage = ref.read(mostroStorageProvider); await messageStorage.addMessage(decryptedEvent.id!, msg); _logger.i( - 'Received message of type ${msg.action} with order id ${msg.id}', + 'Received DM, Event ID: ${decryptedEvent.id} with payload: ${decryptedEvent.content}', ); } catch (e) { _logger.e('Error processing event', error: e); From de9a0c38a0d440df2f9910777d1ed8ce71442619 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 25 Jun 2025 21:30:55 -0700 Subject: [PATCH 08/28] refactor: remove trades subscription type and related functionality --- lib/features/subscriptions/subscription.dart | 2 +- .../subscriptions/subscription_manager.dart | 19 +++++-------------- .../subscriptions/subscription_type.dart | 1 - 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/features/subscriptions/subscription.dart b/lib/features/subscriptions/subscription.dart index fffd38dc..d1babd6c 100644 --- a/lib/features/subscriptions/subscription.dart +++ b/lib/features/subscriptions/subscription.dart @@ -12,7 +12,7 @@ class Subscription { required this.onCancel, }); - void cancel() { + void cancel( ) { streamSubscription.cancel(); onCancel(); } diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index b2857ecf..ee37f641 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -12,7 +12,7 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; /// Manages Nostr subscriptions across different parts of the application. /// /// This class provides a centralized way to handle subscriptions to Nostr events, -/// supporting different subscription types (chat, orders, trades) and automatically +/// supporting different subscription types (chat, orders) and automatically /// managing subscriptions based on session changes in the SessionNotifier. class SubscriptionManager { final Ref ref; @@ -22,12 +22,10 @@ class SubscriptionManager { // Controllers for each subscription type to expose streams to consumers final _ordersController = StreamController.broadcast(); - final _tradesController = StreamController.broadcast(); final _chatController = StreamController.broadcast(); // Public streams that consumers can listen to Stream get orders => _ordersController.stream; - Stream get trades => _tradesController.stream; Stream get chat => _chatController.stream; SubscriptionManager(this.ref) { @@ -112,11 +110,6 @@ class SubscriptionManager { kinds: [1059], p: sessions.map((s) => s.tradeKey.public).toList(), ); - case SubscriptionType.trades: - return NostrFilter( - kinds: [1059], - p: sessions.map((s) => s.tradeKey.public).toList(), - ); case SubscriptionType.chat: return NostrFilter( kinds: [1059], @@ -136,9 +129,6 @@ class SubscriptionManager { case SubscriptionType.orders: _ordersController.add(event); break; - case SubscriptionType.trades: - _tradesController.add(event); - break; case SubscriptionType.chat: _chatController.add(event); break; @@ -177,13 +167,15 @@ class SubscriptionManager { }, ); + if (_subscriptions.containsKey(type)) { + _subscriptions[type]!.cancel(); + } + _subscriptions[type] = subscription; switch (type) { case SubscriptionType.orders: return orders; - case SubscriptionType.trades: - return trades; case SubscriptionType.chat: return chat; } @@ -264,7 +256,6 @@ class SubscriptionManager { unsubscribeAll(); _ordersController.close(); - _tradesController.close(); _chatController.close(); _logger.i('SubscriptionManager disposed'); diff --git a/lib/features/subscriptions/subscription_type.dart b/lib/features/subscriptions/subscription_type.dart index 2ff704d2..cdf712b8 100644 --- a/lib/features/subscriptions/subscription_type.dart +++ b/lib/features/subscriptions/subscription_type.dart @@ -1,5 +1,4 @@ enum SubscriptionType { chat, - trades, orders, } From 3e985347a7da4180a8a187605234c41d13357c37 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 25 Jun 2025 22:55:23 -0700 Subject: [PATCH 09/28] refactor: update mock implementations and add SubscriptionManager mock for testing --- .../order/notfiers/add_order_notifier.dart | 2 +- .../subscriptions/subscription_manager.dart | 3 +- .../providers/mostro_service_provider.g.dart | 2 +- test/mocks.dart | 96 ++- test/mocks.mocks.dart | 567 ++++++++++++++++-- test/notifiers/add_order_notifier_test.dart | 13 +- test/notifiers/take_order_notifier_test.dart | 19 +- test/services/mostro_service_test.dart | 138 +---- test/services/mostro_service_test.mocks.dart | 415 ++++--------- 9 files changed, 776 insertions(+), 479 deletions(-) diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 02ab3246..b6abec5b 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -75,6 +75,6 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); mostroService.subscribe(session); await mostroService.submitOrder(message); - //state = state.updateWith(message); + state = state.updateWith(message); } } diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index ee37f641..fec6aca6 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -234,7 +234,8 @@ class SubscriptionManager { void subscribeAll() { unsubscribeAll(); _logger.i('Subscribing to all subscriptions'); - _initSessionListener(); + final currentSessions = ref.read(sessionNotifierProvider); + _updateAllSubscriptions(currentSessions); } /// Unsubscribe from all subscription types diff --git a/lib/shared/providers/mostro_service_provider.g.dart b/lib/shared/providers/mostro_service_provider.g.dart index cd970ea4..983ec992 100644 --- a/lib/shared/providers/mostro_service_provider.g.dart +++ b/lib/shared/providers/mostro_service_provider.g.dart @@ -22,7 +22,7 @@ final eventStorageProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef EventStorageRef = ProviderRef; -String _$mostroServiceHash() => r'41bba48eb8dcfb160c783c1f1bde78928c3df1cb'; +String _$mostroServiceHash() => r'f33c395adee013f6c71bbbed4c7cfd2dd6286673'; /// See also [mostroService]. @ProviderFor(mostroService) diff --git a/test/mocks.dart b/test/mocks.dart index 969a30d1..b6d1f6b5 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockito/annotations.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; @@ -8,6 +10,9 @@ import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/features/key_manager/key_manager.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_notifier.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:sembast/sembast.dart'; @@ -23,6 +28,9 @@ import 'mocks.mocks.dart'; SessionStorage, KeyManager, MostroStorage, + Settings, + Ref, + SubscriptionManager, ]) // Custom mock for SettingsNotifier that returns a specific Settings object @@ -37,8 +45,8 @@ class MockSettingsNotifier extends SettingsNotifier { // Custom mock for SessionNotifier that avoids database dependencies class MockSessionNotifier extends SessionNotifier { - MockSessionNotifier(MockKeyManager super.keyManager, - MockSessionStorage super.sessionStorage, super.settings); + MockSessionNotifier(super.ref, MockKeyManager keyManager, + MockSessionStorage super.sessionStorage, MockSettings super.settings); @override Session? getSessionByOrderId(String orderId) => null; @@ -67,4 +75,88 @@ class MockSessionNotifier extends SessionNotifier { } } +// Custom mock for SubscriptionManager +class MockSubscriptionManager extends SubscriptionManager { + final StreamController _ordersController = StreamController.broadcast(); + final StreamController _chatController = StreamController.broadcast(); + final Map _subscriptions = {}; + NostrFilter? _lastFilter; + + MockSubscriptionManager() : super(MockRef()); + + NostrFilter? get lastFilter => _lastFilter; + + @override + Stream get orders => _ordersController.stream; + + @override + Stream get chat => _chatController.stream; + + @override + Stream subscribe({ + required SubscriptionType type, + required NostrFilter filter, + String? id, + }) { + _lastFilter = filter; + + final request = NostrRequest(filters: [filter]); + request.subscriptionId = id ?? type.toString(); + + final subscription = Subscription( + request: request, + streamSubscription: _ordersController.stream.listen((_) {}), + onCancel: () {}, + ); + + _subscriptions[type] = subscription; + + return type == SubscriptionType.orders ? orders : chat; + } + + @override + void unsubscribeByType(SubscriptionType type) { + _subscriptions.remove(type); + } + + @override + void unsubscribeAll() { + _subscriptions.clear(); + } + + @override + List getActiveFilters(SubscriptionType type) { + final subscription = _subscriptions[type]; + if (subscription != null && subscription.request.filters.isNotEmpty) { + return [subscription.request.filters.first]; + } + return []; + } + + @override + bool hasActiveSubscription(SubscriptionType type, {String? id}) { + if (id != null) { + final subscription = _subscriptions[type]; + return subscription != null && subscription.request.subscriptionId == id; + } + return _subscriptions.containsKey(type); + } + + // Helper to add events to the stream + void addEvent(NostrEvent event, SubscriptionType type) { + if (type == SubscriptionType.orders) { + _ordersController.add(event); + } else if (type == SubscriptionType.chat) { + _chatController.add(event); + } + } + + @override + void dispose() { + _ordersController.close(); + _chatController.close(); + super.dispose(); + } +} + void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 480c8d8a..6b8b1547 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -5,21 +5,25 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; -import 'package:dart_nostr/nostr/core/key_pairs.dart' as _i6; -import 'package:dart_nostr/nostr/model/export.dart' as _i10; +import 'package:dart_nostr/dart_nostr.dart' as _i6; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i12; +import 'package:mockito/src/dummies.dart' as _i11; import 'package:mostro_mobile/data/models.dart' as _i5; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i16; +import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i15; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' as _i9; -import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i14; -import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i15; -import 'package:mostro_mobile/features/settings/settings.dart' as _i8; -import 'package:mostro_mobile/services/mostro_service.dart' as _i7; +import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i13; +import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i14; +import 'package:mostro_mobile/features/settings/settings.dart' as _i7; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart' + as _i16; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart' + as _i17; +import 'package:mostro_mobile/services/mostro_service.dart' as _i8; import 'package:sembast/sembast.dart' as _i4; -import 'package:shared_preferences/src/shared_preferences_async.dart' as _i11; +import 'package:sembast/src/api/transaction.dart' as _i12; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i10; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -118,10 +122,52 @@ class _FakeMostroMessage_7 extends _i1.SmartFake ); } +class _FakeSettings_8 extends _i1.SmartFake implements _i7.Settings { + _FakeSettings_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeProviderContainer_9 extends _i1.SmartFake + implements _i2.ProviderContainer { + _FakeProviderContainer_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeKeepAliveLink_10 extends _i1.SmartFake implements _i2.KeepAliveLink { + _FakeKeepAliveLink_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeProviderSubscription_11 extends _i1.SmartFake + implements _i2.ProviderSubscription { + _FakeProviderSubscription_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [MostroService]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroService extends _i1.Mock implements _i7.MostroService { +class MockMostroService extends _i1.Mock implements _i8.MostroService { MockMostroService() { _i1.throwOnMissingStub(this); } @@ -144,21 +190,32 @@ class MockMostroService extends _i1.Mock implements _i7.MostroService { returnValueForMissingStub: null, ); + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + @override void subscribe(_i5.Session? session) => super.noSuchMethod( Invocation.method( #subscribe, - [keyPair], + [session], ), returnValueForMissingStub: null, ); @override - _i5.Session? getSessionByOrderId(String? orderId) => - (super.noSuchMethod(Invocation.method( - #getSessionByOrderId, - [orderId], - )) as _i5.Session?); + void unsubscribe(_i5.Session? session) => super.noSuchMethod( + Invocation.method( + #unsubscribe, + [session], + ), + returnValueForMissingStub: null, + ); @override _i3.Future submitOrder(_i5.MostroMessage<_i5.Payload>? order) => @@ -295,7 +352,7 @@ class MockMostroService extends _i1.Mock implements _i7.MostroService { ) as _i3.Future); @override - void updateSettings(_i8.Settings? settings) => super.noSuchMethod( + void updateSettings(_i7.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -314,10 +371,10 @@ class MockOpenOrdersRepository extends _i1.Mock } @override - _i3.Stream> get eventsStream => (super.noSuchMethod( + _i3.Stream> get eventsStream => (super.noSuchMethod( Invocation.getter(#eventsStream), - returnValue: _i3.Stream>.empty(), - ) as _i3.Stream>); + returnValue: _i3.Stream>.empty(), + ) as _i3.Stream>); @override void dispose() => super.noSuchMethod( @@ -329,17 +386,17 @@ class MockOpenOrdersRepository extends _i1.Mock ); @override - _i3.Future<_i10.NostrEvent?> getOrderById(String? orderId) => + _i3.Future<_i6.NostrEvent?> getOrderById(String? orderId) => (super.noSuchMethod( Invocation.method( #getOrderById, [orderId], ), - returnValue: _i3.Future<_i10.NostrEvent?>.value(), - ) as _i3.Future<_i10.NostrEvent?>); + returnValue: _i3.Future<_i6.NostrEvent?>.value(), + ) as _i3.Future<_i6.NostrEvent?>); @override - _i3.Future addOrder(_i10.NostrEvent? order) => (super.noSuchMethod( + _i3.Future addOrder(_i6.NostrEvent? order) => (super.noSuchMethod( Invocation.method( #addOrder, [order], @@ -359,17 +416,16 @@ class MockOpenOrdersRepository extends _i1.Mock ) as _i3.Future); @override - _i3.Future> getAllOrders() => (super.noSuchMethod( + _i3.Future> getAllOrders() => (super.noSuchMethod( Invocation.method( #getAllOrders, [], ), - returnValue: - _i3.Future>.value(<_i10.NostrEvent>[]), - ) as _i3.Future>); + returnValue: _i3.Future>.value(<_i6.NostrEvent>[]), + ) as _i3.Future>); @override - _i3.Future updateOrder(_i10.NostrEvent? order) => (super.noSuchMethod( + _i3.Future updateOrder(_i6.NostrEvent? order) => (super.noSuchMethod( Invocation.method( #updateOrder, [order], @@ -379,7 +435,7 @@ class MockOpenOrdersRepository extends _i1.Mock ) as _i3.Future); @override - void updateSettings(_i8.Settings? settings) => super.noSuchMethod( + void updateSettings(_i7.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -400,8 +456,9 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock - implements _i11.SharedPreferencesAsync { + implements _i10.SharedPreferencesAsync { MockSharedPreferencesAsync() { _i1.throwOnMissingStub(this); } @@ -607,7 +664,7 @@ class MockDatabase extends _i1.Mock implements _i4.Database { @override String get path => (super.noSuchMethod( Invocation.getter(#path), - returnValue: _i12.dummyValue( + returnValue: _i11.dummyValue( this, Invocation.getter(#path), ), @@ -615,14 +672,14 @@ class MockDatabase extends _i1.Mock implements _i4.Database { @override _i3.Future transaction( - _i3.FutureOr Function(_i4.Transaction)? action) => + _i3.FutureOr Function(_i12.Transaction)? action) => (super.noSuchMethod( Invocation.method( #transaction, [action], ), - returnValue: _i12.ifNotNull( - _i12.dummyValueOrNull( + returnValue: _i11.ifNotNull( + _i11.dummyValueOrNull( this, Invocation.method( #transaction, @@ -653,7 +710,7 @@ class MockDatabase extends _i1.Mock implements _i4.Database { /// A class which mocks [SessionStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionStorage extends _i1.Mock implements _i14.SessionStorage { +class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { MockSessionStorage() { _i1.throwOnMissingStub(this); } @@ -916,7 +973,7 @@ class MockSessionStorage extends _i1.Mock implements _i14.SessionStorage { /// A class which mocks [KeyManager]. /// /// See the documentation for Mockito's code generation for more information. -class MockKeyManager extends _i1.Mock implements _i15.KeyManager { +class MockKeyManager extends _i1.Mock implements _i14.KeyManager { MockKeyManager() { _i1.throwOnMissingStub(this); } @@ -1067,7 +1124,7 @@ class MockKeyManager extends _i1.Mock implements _i15.KeyManager { /// A class which mocks [MostroStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { +class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { MockMostroStorage() { _i1.throwOnMissingStub(this); } @@ -1434,3 +1491,437 @@ class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { ), ) as _i4.Filter); } + +/// A class which mocks [Settings]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSettings extends _i1.Mock implements _i7.Settings { + MockSettings() { + _i1.throwOnMissingStub(this); + } + + @override + bool get fullPrivacyMode => (super.noSuchMethod( + Invocation.getter(#fullPrivacyMode), + returnValue: false, + ) as bool); + + @override + List get relays => (super.noSuchMethod( + Invocation.getter(#relays), + returnValue: [], + ) as List); + + @override + String get mostroPublicKey => (super.noSuchMethod( + Invocation.getter(#mostroPublicKey), + returnValue: _i11.dummyValue( + this, + Invocation.getter(#mostroPublicKey), + ), + ) as String); + + @override + _i7.Settings copyWith({ + List? relays, + bool? privacyModeSetting, + String? mostroInstance, + String? defaultFiatCode, + }) => + (super.noSuchMethod( + Invocation.method( + #copyWith, + [], + { + #relays: relays, + #privacyModeSetting: privacyModeSetting, + #mostroInstance: mostroInstance, + #defaultFiatCode: defaultFiatCode, + }, + ), + returnValue: _FakeSettings_8( + this, + Invocation.method( + #copyWith, + [], + { + #relays: relays, + #privacyModeSetting: privacyModeSetting, + #mostroInstance: mostroInstance, + #defaultFiatCode: defaultFiatCode, + }, + ), + ), + ) as _i7.Settings); + + @override + Map toJson() => (super.noSuchMethod( + Invocation.method( + #toJson, + [], + ), + returnValue: {}, + ) as Map); +} + +/// A class which mocks [Ref]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRef extends _i1.Mock + implements _i2.Ref { + MockRef() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.ProviderContainer get container => (super.noSuchMethod( + Invocation.getter(#container), + returnValue: _FakeProviderContainer_9( + this, + Invocation.getter(#container), + ), + ) as _i2.ProviderContainer); + + @override + T refresh(_i2.Refreshable? provider) => (super.noSuchMethod( + Invocation.method( + #refresh, + [provider], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #refresh, + [provider], + ), + ), + ) as T); + + @override + void invalidate(_i2.ProviderOrFamily? provider) => super.noSuchMethod( + Invocation.method( + #invalidate, + [provider], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void listenSelf( + void Function( + State?, + State, + )? listener, { + void Function( + Object, + StackTrace, + )? onError, + }) => + super.noSuchMethod( + Invocation.method( + #listenSelf, + [listener], + {#onError: onError}, + ), + returnValueForMissingStub: null, + ); + + @override + void invalidateSelf() => super.noSuchMethod( + Invocation.method( + #invalidateSelf, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void onAddListener(void Function()? cb) => super.noSuchMethod( + Invocation.method( + #onAddListener, + [cb], + ), + returnValueForMissingStub: null, + ); + + @override + void onRemoveListener(void Function()? cb) => super.noSuchMethod( + Invocation.method( + #onRemoveListener, + [cb], + ), + returnValueForMissingStub: null, + ); + + @override + void onResume(void Function()? cb) => super.noSuchMethod( + Invocation.method( + #onResume, + [cb], + ), + returnValueForMissingStub: null, + ); + + @override + void onCancel(void Function()? cb) => super.noSuchMethod( + Invocation.method( + #onCancel, + [cb], + ), + returnValueForMissingStub: null, + ); + + @override + void onDispose(void Function()? cb) => super.noSuchMethod( + Invocation.method( + #onDispose, + [cb], + ), + returnValueForMissingStub: null, + ); + + @override + T read(_i2.ProviderListenable? provider) => (super.noSuchMethod( + Invocation.method( + #read, + [provider], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #read, + [provider], + ), + ), + ) as T); + + @override + bool exists(_i2.ProviderBase? provider) => (super.noSuchMethod( + Invocation.method( + #exists, + [provider], + ), + returnValue: false, + ) as bool); + + @override + T watch(_i2.ProviderListenable? provider) => (super.noSuchMethod( + Invocation.method( + #watch, + [provider], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #watch, + [provider], + ), + ), + ) as T); + + @override + _i2.KeepAliveLink keepAlive() => (super.noSuchMethod( + Invocation.method( + #keepAlive, + [], + ), + returnValue: _FakeKeepAliveLink_10( + this, + Invocation.method( + #keepAlive, + [], + ), + ), + ) as _i2.KeepAliveLink); + + @override + _i2.ProviderSubscription listen( + _i2.ProviderListenable? provider, + void Function( + T?, + T, + )? listener, { + void Function( + Object, + StackTrace, + )? onError, + bool? fireImmediately, + }) => + (super.noSuchMethod( + Invocation.method( + #listen, + [ + provider, + listener, + ], + { + #onError: onError, + #fireImmediately: fireImmediately, + }, + ), + returnValue: _FakeProviderSubscription_11( + this, + Invocation.method( + #listen, + [ + provider, + listener, + ], + { + #onError: onError, + #fireImmediately: fireImmediately, + }, + ), + ), + ) as _i2.ProviderSubscription); +} + +/// A class which mocks [SubscriptionManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSubscriptionManager extends _i1.Mock + implements _i16.SubscriptionManager { + MockSubscriptionManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Ref get ref => (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _FakeRef_0( + this, + Invocation.getter(#ref), + ), + ) as _i2.Ref); + + @override + _i3.Stream<_i6.NostrEvent> get orders => (super.noSuchMethod( + Invocation.getter(#orders), + returnValue: _i3.Stream<_i6.NostrEvent>.empty(), + ) as _i3.Stream<_i6.NostrEvent>); + + @override + _i3.Stream<_i6.NostrEvent> get chat => (super.noSuchMethod( + Invocation.getter(#chat), + returnValue: _i3.Stream<_i6.NostrEvent>.empty(), + ) as _i3.Stream<_i6.NostrEvent>); + + @override + _i3.Stream<_i6.NostrEvent> subscribe({ + required _i17.SubscriptionType? type, + required _i6.NostrFilter? filter, + }) => + (super.noSuchMethod( + Invocation.method( + #subscribe, + [], + { + #type: type, + #filter: filter, + }, + ), + returnValue: _i3.Stream<_i6.NostrEvent>.empty(), + ) as _i3.Stream<_i6.NostrEvent>); + + @override + _i3.Stream<_i6.NostrEvent> subscribeSession({ + required _i17.SubscriptionType? type, + required _i5.Session? session, + required _i6.NostrFilter Function(_i5.Session)? createFilter, + }) => + (super.noSuchMethod( + Invocation.method( + #subscribeSession, + [], + { + #type: type, + #session: session, + #createFilter: createFilter, + }, + ), + returnValue: _i3.Stream<_i6.NostrEvent>.empty(), + ) as _i3.Stream<_i6.NostrEvent>); + + @override + void unsubscribeById(_i17.SubscriptionType? type) => super.noSuchMethod( + Invocation.method( + #unsubscribeById, + [type], + ), + returnValueForMissingStub: null, + ); + + @override + void unsubscribeByType(_i17.SubscriptionType? type) => super.noSuchMethod( + Invocation.method( + #unsubscribeByType, + [type], + ), + returnValueForMissingStub: null, + ); + + @override + void unsubscribeSession(_i17.SubscriptionType? type) => super.noSuchMethod( + Invocation.method( + #unsubscribeSession, + [type], + ), + returnValueForMissingStub: null, + ); + + @override + bool hasActiveSubscription(_i17.SubscriptionType? type) => + (super.noSuchMethod( + Invocation.method( + #hasActiveSubscription, + [type], + ), + returnValue: false, + ) as bool); + + @override + List<_i6.NostrFilter> getActiveFilters(_i17.SubscriptionType? type) => + (super.noSuchMethod( + Invocation.method( + #getActiveFilters, + [type], + ), + returnValue: <_i6.NostrFilter>[], + ) as List<_i6.NostrFilter>); + + @override + void subscribeAll() => super.noSuchMethod( + Invocation.method( + #subscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void unsubscribeAll() => super.noSuchMethod( + Invocation.method( + #unsubscribeAll, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/notifiers/add_order_notifier_test.dart b/test/notifiers/add_order_notifier_test.dart index c31a4960..0e5e77c5 100644 --- a/test/notifiers/add_order_notifier_test.dart +++ b/test/notifiers/add_order_notifier_test.dart @@ -8,7 +8,6 @@ import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; -import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; @@ -34,7 +33,7 @@ void main() { late MockKeyManager mockKeyManager; late MockSessionNotifier mockSessionNotifier; late MockMostroStorage mockMostroStorage; - + late MockRef ref; const testUuid = "12345678-1234-1234-1234-123456789abc"; setUp(() { @@ -45,17 +44,13 @@ void main() { mockSessionStorage = MockSessionStorage(); mockKeyManager = MockKeyManager(); mockMostroStorage = MockMostroStorage(); + ref = MockRef(); // Create test settings - final testSettings = Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: 'test_key', - defaultFiatCode: 'USD', - ); + final testSettings = MockSettings(); mockSessionNotifier = - MockSessionNotifier(mockKeyManager, mockSessionStorage, testSettings); + MockSessionNotifier(ref, mockKeyManager, mockSessionStorage, testSettings); // Stub the KeyManager methods when(mockKeyManager.masterKeyPair).thenReturn( diff --git a/test/notifiers/take_order_notifier_test.dart b/test/notifiers/take_order_notifier_test.dart index d4e80ed3..05105766 100644 --- a/test/notifiers/take_order_notifier_test.dart +++ b/test/notifiers/take_order_notifier_test.dart @@ -32,6 +32,7 @@ void main() { late MockKeyManager mockKeyManager; late MockSessionNotifier mockSessionNotifier; late MockMostroStorage mockMostroStorage; + late MockRef ref; const testOrderId = "test_order_id"; setUp(() { @@ -43,16 +44,12 @@ void main() { mockSessionStorage = MockSessionStorage(); mockKeyManager = MockKeyManager(); mockMostroStorage = MockMostroStorage(); + ref = MockRef(); // Create test settings - final testSettings = Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', - defaultFiatCode: 'USD', - ); + final testSettings = MockSettings(); - mockSessionNotifier = MockSessionNotifier(mockKeyManager, mockSessionStorage, testSettings); + mockSessionNotifier = MockSessionNotifier(ref, mockKeyManager, mockSessionStorage, testSettings); // Stub the KeyManager methods when(mockKeyManager.masterKeyPair).thenReturn( @@ -131,7 +128,7 @@ void main() { Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ), mockPreferences @@ -195,7 +192,7 @@ void main() { Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ), mockPreferences @@ -258,7 +255,7 @@ void main() { Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ), mockPreferences @@ -307,7 +304,7 @@ void main() { Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ), mockPreferences diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index ccf6b9b6..48ddc9dd 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -1,16 +1,10 @@ import 'dart:convert'; import 'package:convert/convert.dart'; -import 'package:dart_nostr/dart_nostr.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'dart:async'; -import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:mostro_mobile/features/subscriptions/subscription.dart'; -import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/core/config.dart'; -// Removed unused import import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; @@ -24,18 +18,17 @@ import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; -import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; -import 'mostro_service_test.mocks.dart'; +import '../mocks.mocks.dart'; +import 'mostro_service_test.mocks.dart' hide MockRef; import 'mostro_service_helper_functions.dart'; -@GenerateMocks([NostrService, SessionNotifier, Ref]) void main() { // Provide dummy values for Mockito provideDummy(Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', )); @@ -47,13 +40,17 @@ void main() { late MostroService mostroService; late KeyDerivator keyDerivator; late MockServerTradeIndex mockServerTradeIndex; - + late MockNostrService mockNostrService; + late MockSessionNotifier mockSessionNotifier; + late MockRef mockRef; + late MockSubscriptionManager mockSubscriptionManager; + setUpAll(() { // Create a dummy Settings object that will be used by MockRef final dummySettings = Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: 'npub1mostro', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ); @@ -63,8 +60,7 @@ void main() { provideDummy(MockNostrService()); // Create a mock ref for the SubscriptionManager dummy - final mockRefForSubscriptionManager = MockRef(); - provideDummy(MockSubscriptionManager(ref: mockRefForSubscriptionManager)); + provideDummy(MockSubscriptionManager()); // Create a mock ref that returns the dummy settings final mockRefForDummy = MockRef(); @@ -76,10 +72,11 @@ void main() { setUp(() { mockNostrService = MockNostrService(); - mockRef = MockRef(); mockSessionNotifier = MockSessionNotifier(); mockRef = MockRef(); - + mockSubscriptionManager = MockSubscriptionManager(); + mockServerTradeIndex = MockServerTradeIndex(); + keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); // Generate a valid test key pair for mostro public key final testKeyPair = NostrUtils.generateKeyPair(); @@ -95,12 +92,14 @@ void main() { when(mockRef.read(settingsProvider)).thenReturn(testSettings); when(mockRef.read(mostroStorageProvider)).thenReturn(MockMostroStorage()); when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); + when(mockRef.read(subscriptionManagerProvider)).thenReturn(mockSubscriptionManager); // Stub SessionNotifier methods when(mockSessionNotifier.sessions).thenReturn([]); - mostroService = MostroService(mockSessionNotifier, mockRef); + mostroService = MostroService(mockRef); keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); + mockServerTradeIndex = MockServerTradeIndex(); // Setup mock session notifier when(mockSessionNotifier.sessions).thenReturn([]); @@ -109,7 +108,7 @@ void main() { final settings = Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: 'npub1mostro', + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', ); when(mockRef.read(settingsProvider)).thenReturn(settings); @@ -181,31 +180,9 @@ void main() { when(mockSessionNotifier.getSessionByOrderId(orderId)) .thenReturn(session); - // Mock NostrService's createRumor, createSeal, createWrap, publishEvent - when(mockNostrService.createRumor(any, any, any, any)) - .thenAnswer((_) async => 'encryptedRumorContent'); - - when(mockNostrService.generateKeyPair()) - .thenAnswer((_) async => NostrUtils.generateKeyPair()); - - when(mockNostrService.createSeal(any, any, any, any)) - .thenAnswer((_) async => 'sealedContent'); - - when(mockNostrService.createWrap(any, any, any)) - .thenAnswer((_) async => NostrEvent( - id: 'wrapEventId', - kind: 1059, - pubkey: 'wrapperPubKey', - content: 'sealedContent', - createdAt: DateTime.now(), - tags: [ - ['p', 'mostroPubKey'] - ], - sig: 'wrapSignature', - )); - + // Mock NostrService's publishEvent only when(mockNostrService.publishEvent(any)) - .thenAnswer((_) async => Future.value()); + .thenAnswer((_) async {}); when(mockSessionNotifier.newSession(orderId: orderId)) .thenAnswer((_) async => session); @@ -262,28 +239,9 @@ void main() { when(mockSessionNotifier.getSessionByOrderId(orderId)) .thenReturn(session); - // Mock NostrService's createRumor, createSeal, createWrap, publishEvent - when(mockNostrService.createRumor(any, any, any, any)) - .thenAnswer((_) async => 'encryptedRumorContentInvalid'); - - when(mockNostrService.generateKeyPair()) - .thenAnswer((_) async => NostrUtils.generateKeyPair()); - - when(mockNostrService.createSeal(any, any, any, any)) - .thenAnswer((_) async => 'sealedContentInvalid'); - - when(mockNostrService.createWrap(any, any, any)) - .thenAnswer((_) async => NostrEvent( - id: 'wrapEventIdInvalid', - kind: 1059, - pubkey: 'wrapperPubKeyInvalid', - content: 'sealedContentInvalid', - createdAt: DateTime.now(), - tags: [ - ['p', 'mostroPubKey'] - ], - sig: 'invalidWrapSignature', - )); + // Mock NostrService's publishEvent only - other methods are now static in NostrUtils + when(mockNostrService.publishEvent(any)) + .thenAnswer((_) async => Future.value()); when(mockNostrService.publishEvent(any)) .thenAnswer((_) async => Future.value()); @@ -342,28 +300,9 @@ void main() { // Simulate that tradeIndex=3 has already been used mockServerTradeIndex.userTradeIndices[userPubKey] = 3; - // Mock NostrService's createRumor, createSeal, createWrap, publishEvent - when(mockNostrService.createRumor(any, any, any, any)) - .thenAnswer((_) async => 'encryptedRumorContentReused'); - - when(mockNostrService.generateKeyPair()) - .thenAnswer((_) async => NostrUtils.generateKeyPair()); - - when(mockNostrService.createSeal(any, any, any, any)) - .thenAnswer((_) async => 'sealedContentReused'); - - when(mockNostrService.createWrap(any, any, any)) - .thenAnswer((_) async => NostrEvent( - id: 'wrapEventIdReused', - kind: 1059, - pubkey: 'wrapperPubKeyReused', - content: 'sealedContentReused', - createdAt: DateTime.now(), - tags: [ - ['p', 'mostroPubKey'] - ], - sig: 'wrapSignatureReused', - )); + // Mock NostrService's publishEvent only - other methods are now static in NostrUtils + when(mockNostrService.publishEvent(any)) + .thenAnswer((_) async => Future.value()); when(mockNostrService.publishEvent(any)) .thenAnswer((_) async => Future.value()); @@ -420,28 +359,9 @@ void main() { when(mockSessionNotifier.getSessionByOrderId(orderId)) .thenReturn(session); - // Mock NostrService's createRumor, createSeal, createWrap, publishEvent - when(mockNostrService.createRumor(any, any, any, any)) - .thenAnswer((_) async => 'encryptedRumorContentFullPrivacy'); - - when(mockNostrService.generateKeyPair()) - .thenAnswer((_) async => NostrUtils.generateKeyPair()); - - when(mockNostrService.createSeal(any, any, any, any)) - .thenAnswer((_) async => 'sealedContentFullPrivacy'); - - when(mockNostrService.createWrap(any, any, any)) - .thenAnswer((_) async => NostrEvent( - id: 'wrapEventIdFullPrivacy', - kind: 1059, - pubkey: 'wrapperPubKeyFullPrivacy', - content: 'sealedContentFullPrivacy', - createdAt: DateTime.now(), - tags: [ - ['p', 'mostroPubKey'] - ], - sig: 'wrapSignatureFullPrivacy', - )); + // Mock NostrService's publishEvent only - other methods are now static in NostrUtils + when(mockNostrService.publishEvent(any)) + .thenAnswer((_) async => Future.value()); when(mockNostrService.publishEvent(any)) .thenAnswer((_) async => Future.value()); diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart index c97bf088..d6c1c576 100644 --- a/test/services/mostro_service_test.mocks.dart +++ b/test/services/mostro_service_test.mocks.dart @@ -3,19 +3,19 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i7; +import 'dart:async' as _i6; -import 'package:dart_nostr/dart_nostr.dart' as _i3; -import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i8; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; +import 'package:dart_nostr/dart_nostr.dart' as _i8; +import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i7; +import 'package:flutter_riverpod/flutter_riverpod.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i9; -import 'package:mostro_mobile/data/models/enums/role.dart' as _i11; +import 'package:mockito/src/dummies.dart' as _i12; +import 'package:mostro_mobile/data/models/enums/role.dart' as _i10; import 'package:mostro_mobile/data/models/session.dart' as _i4; import 'package:mostro_mobile/features/settings/settings.dart' as _i2; -import 'package:mostro_mobile/services/nostr_service.dart' as _i6; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i10; -import 'package:state_notifier/state_notifier.dart' as _i12; +import 'package:mostro_mobile/services/nostr_service.dart' as _i5; +import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i9; +import 'package:state_notifier/state_notifier.dart' as _i11; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -41,8 +41,9 @@ class _FakeSettings_0 extends _i1.SmartFake implements _i2.Settings { ); } -class _FakeNostrKeyPairs_1 extends _i1.SmartFake implements _i3.NostrKeyPairs { - _FakeNostrKeyPairs_1( +class _FakeRef_1 extends _i1.SmartFake + implements _i3.Ref { + _FakeRef_1( Object parent, Invocation parentInvocation, ) : super( @@ -51,8 +52,8 @@ class _FakeNostrKeyPairs_1 extends _i1.SmartFake implements _i3.NostrKeyPairs { ); } -class _FakeNostrEvent_2 extends _i1.SmartFake implements _i3.NostrEvent { - _FakeNostrEvent_2( +class _FakeSession_2 extends _i1.SmartFake implements _i4.Session { + _FakeSession_2( Object parent, Invocation parentInvocation, ) : super( @@ -61,8 +62,9 @@ class _FakeNostrEvent_2 extends _i1.SmartFake implements _i3.NostrEvent { ); } -class _FakeSession_3 extends _i1.SmartFake implements _i4.Session { - _FakeSession_3( +class _FakeProviderContainer_3 extends _i1.SmartFake + implements _i3.ProviderContainer { + _FakeProviderContainer_3( Object parent, Invocation parentInvocation, ) : super( @@ -71,9 +73,8 @@ class _FakeSession_3 extends _i1.SmartFake implements _i4.Session { ); } -class _FakeProviderContainer_4 extends _i1.SmartFake - implements _i5.ProviderContainer { - _FakeProviderContainer_4( +class _FakeKeepAliveLink_4 extends _i1.SmartFake implements _i3.KeepAliveLink { + _FakeKeepAliveLink_4( Object parent, Invocation parentInvocation, ) : super( @@ -82,19 +83,9 @@ class _FakeProviderContainer_4 extends _i1.SmartFake ); } -class _FakeKeepAliveLink_5 extends _i1.SmartFake implements _i5.KeepAliveLink { - _FakeKeepAliveLink_5( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProviderSubscription_6 extends _i1.SmartFake - implements _i5.ProviderSubscription { - _FakeProviderSubscription_6( +class _FakeProviderSubscription_5 extends _i1.SmartFake + implements _i3.ProviderSubscription { + _FakeProviderSubscription_5( Object parent, Invocation parentInvocation, ) : super( @@ -106,7 +97,7 @@ class _FakeProviderSubscription_6 extends _i1.SmartFake /// A class which mocks [NostrService]. /// /// See the documentation for Mockito's code generation for more information. -class MockNostrService extends _i1.Mock implements _i6.NostrService { +class MockNostrService extends _i1.Mock implements _i5.NostrService { MockNostrService() { _i1.throwOnMissingStub(this); } @@ -136,264 +127,65 @@ class MockNostrService extends _i1.Mock implements _i6.NostrService { ); @override - _i7.Future init(_i2.Settings? settings) => (super.noSuchMethod( + _i6.Future init(_i2.Settings? settings) => (super.noSuchMethod( Invocation.method( #init, [settings], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i7.Future updateSettings(_i2.Settings? newSettings) => + _i6.Future updateSettings(_i2.Settings? newSettings) => (super.noSuchMethod( Invocation.method( #updateSettings, [newSettings], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i7.Future<_i8.RelayInformations?> getRelayInfo(String? relayUrl) => + _i6.Future<_i7.RelayInformations?> getRelayInfo(String? relayUrl) => (super.noSuchMethod( Invocation.method( #getRelayInfo, [relayUrl], ), - returnValue: _i7.Future<_i8.RelayInformations?>.value(), - ) as _i7.Future<_i8.RelayInformations?>); + returnValue: _i6.Future<_i7.RelayInformations?>.value(), + ) as _i6.Future<_i7.RelayInformations?>); @override - _i7.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod( + _i6.Future publishEvent(_i8.NostrEvent? event) => (super.noSuchMethod( Invocation.method( #publishEvent, [event], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i7.Future> fecthEvents(_i3.NostrFilter? filter) => - (super.noSuchMethod( - Invocation.method( - #fecthEvents, - [filter], - ), - returnValue: _i7.Future>.value(<_i3.NostrEvent>[]), - ) as _i7.Future>); - - @override - _i7.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrRequest? request) => + _i6.Stream<_i8.NostrEvent> subscribeToEvents(_i8.NostrRequest? request) => (super.noSuchMethod( Invocation.method( #subscribeToEvents, [request], ), - returnValue: _i7.Stream<_i3.NostrEvent>.empty(), - ) as _i7.Stream<_i3.NostrEvent>); + returnValue: _i6.Stream<_i8.NostrEvent>.empty(), + ) as _i6.Stream<_i8.NostrEvent>); @override - _i7.Future disconnectFromRelays() => (super.noSuchMethod( + _i6.Future disconnectFromRelays() => (super.noSuchMethod( Invocation.method( #disconnectFromRelays, [], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future<_i3.NostrKeyPairs> generateKeyPair() => (super.noSuchMethod( - Invocation.method( - #generateKeyPair, - [], - ), - returnValue: _i7.Future<_i3.NostrKeyPairs>.value(_FakeNostrKeyPairs_1( - this, - Invocation.method( - #generateKeyPair, - [], - ), - )), - ) as _i7.Future<_i3.NostrKeyPairs>); - - @override - _i3.NostrKeyPairs generateKeyPairFromPrivateKey(String? privateKey) => - (super.noSuchMethod( - Invocation.method( - #generateKeyPairFromPrivateKey, - [privateKey], - ), - returnValue: _FakeNostrKeyPairs_1( - this, - Invocation.method( - #generateKeyPairFromPrivateKey, - [privateKey], - ), - ), - ) as _i3.NostrKeyPairs); - - @override - String getMostroPubKey() => (super.noSuchMethod( - Invocation.method( - #getMostroPubKey, - [], - ), - returnValue: _i9.dummyValue( - this, - Invocation.method( - #getMostroPubKey, - [], - ), - ), - ) as String); - - @override - _i7.Future<_i3.NostrEvent> createNIP59Event( - String? content, - String? recipientPubKey, - String? senderPrivateKey, - ) => - (super.noSuchMethod( - Invocation.method( - #createNIP59Event, - [ - content, - recipientPubKey, - senderPrivateKey, - ], - ), - returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( - this, - Invocation.method( - #createNIP59Event, - [ - content, - recipientPubKey, - senderPrivateKey, - ], - ), - )), - ) as _i7.Future<_i3.NostrEvent>); - - @override - _i7.Future<_i3.NostrEvent> decryptNIP59Event( - _i3.NostrEvent? event, - String? privateKey, - ) => - (super.noSuchMethod( - Invocation.method( - #decryptNIP59Event, - [ - event, - privateKey, - ], - ), - returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( - this, - Invocation.method( - #decryptNIP59Event, - [ - event, - privateKey, - ], - ), - )), - ) as _i7.Future<_i3.NostrEvent>); - - @override - _i7.Future createRumor( - _i3.NostrKeyPairs? senderKeyPair, - String? wrapperKey, - String? recipientPubKey, - String? content, - ) => - (super.noSuchMethod( - Invocation.method( - #createRumor, - [ - senderKeyPair, - wrapperKey, - recipientPubKey, - content, - ], - ), - returnValue: _i7.Future.value(_i9.dummyValue( - this, - Invocation.method( - #createRumor, - [ - senderKeyPair, - wrapperKey, - recipientPubKey, - content, - ], - ), - )), - ) as _i7.Future); - - @override - _i7.Future createSeal( - _i3.NostrKeyPairs? senderKeyPair, - String? wrapperKey, - String? recipientPubKey, - String? encryptedContent, - ) => - (super.noSuchMethod( - Invocation.method( - #createSeal, - [ - senderKeyPair, - wrapperKey, - recipientPubKey, - encryptedContent, - ], - ), - returnValue: _i7.Future.value(_i9.dummyValue( - this, - Invocation.method( - #createSeal, - [ - senderKeyPair, - wrapperKey, - recipientPubKey, - encryptedContent, - ], - ), - )), - ) as _i7.Future); - - @override - _i7.Future<_i3.NostrEvent> createWrap( - _i3.NostrKeyPairs? wrapperKeyPair, - String? sealedContent, - String? recipientPubKey, - ) => - (super.noSuchMethod( - Invocation.method( - #createWrap, - [ - wrapperKeyPair, - sealedContent, - recipientPubKey, - ], - ), - returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( - this, - Invocation.method( - #createWrap, - [ - wrapperKeyPair, - sealedContent, - recipientPubKey, - ], - ), - )), - ) as _i7.Future<_i3.NostrEvent>); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override void unsubscribe(String? id) => super.noSuchMethod( @@ -408,11 +200,20 @@ class MockNostrService extends _i1.Mock implements _i6.NostrService { /// A class which mocks [SessionNotifier]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { +class MockSessionNotifier extends _i1.Mock implements _i9.SessionNotifier { MockSessionNotifier() { _i1.throwOnMissingStub(this); } + @override + _i3.Ref get ref => (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _FakeRef_1( + this, + Invocation.getter(#ref), + ), + ) as _i3.Ref); + @override List<_i4.Session> get sessions => (super.noSuchMethod( Invocation.getter(#sessions), @@ -426,10 +227,10 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { ) as bool); @override - _i7.Stream> get stream => (super.noSuchMethod( + _i6.Stream> get stream => (super.noSuchMethod( Invocation.getter(#stream), - returnValue: _i7.Stream>.empty(), - ) as _i7.Stream>); + returnValue: _i6.Stream>.empty(), + ) as _i6.Stream>); @override List<_i4.Session> get state => (super.noSuchMethod( @@ -450,7 +251,7 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { ) as bool); @override - set onError(_i5.ErrorListener? _onError) => super.noSuchMethod( + set onError(_i3.ErrorListener? _onError) => super.noSuchMethod( Invocation.setter( #onError, _onError, @@ -468,14 +269,14 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { ); @override - _i7.Future init() => (super.noSuchMethod( + _i6.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override void updateSettings(_i2.Settings? settings) => super.noSuchMethod( @@ -487,10 +288,10 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { ); @override - _i7.Future<_i4.Session> newSession({ + _i6.Future<_i4.Session> newSession({ String? orderId, int? requestId, - _i11.Role? role, + _i10.Role? role, }) => (super.noSuchMethod( Invocation.method( @@ -502,7 +303,7 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { #role: role, }, ), - returnValue: _i7.Future<_i4.Session>.value(_FakeSession_3( + returnValue: _i6.Future<_i4.Session>.value(_FakeSession_2( this, Invocation.method( #newSession, @@ -514,20 +315,20 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { }, ), )), - ) as _i7.Future<_i4.Session>); + ) as _i6.Future<_i4.Session>); @override - _i7.Future saveSession(_i4.Session? session) => (super.noSuchMethod( + _i6.Future saveSession(_i4.Session? session) => (super.noSuchMethod( Invocation.method( #saveSession, [session], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i7.Future updateSession( + _i6.Future updateSession( String? orderId, void Function(_i4.Session)? update, ) => @@ -539,9 +340,9 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { update, ], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override _i4.Session? getSessionByRequestId(int? requestId) => @@ -565,33 +366,33 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { )) as _i4.Session?); @override - _i7.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( + _i6.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( Invocation.method( #loadSession, [keyIndex], ), - returnValue: _i7.Future<_i4.Session?>.value(), - ) as _i7.Future<_i4.Session?>); + returnValue: _i6.Future<_i4.Session?>.value(), + ) as _i6.Future<_i4.Session?>); @override - _i7.Future reset() => (super.noSuchMethod( + _i6.Future reset() => (super.noSuchMethod( Invocation.method( #reset, [], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i7.Future deleteSession(String? sessionId) => (super.noSuchMethod( + _i6.Future deleteSession(String? sessionId) => (super.noSuchMethod( Invocation.method( #deleteSession, [sessionId], ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override void dispose() => super.noSuchMethod( @@ -619,8 +420,8 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { ) as bool); @override - _i5.RemoveListener addListener( - _i12.Listener>? listener, { + _i3.RemoveListener addListener( + _i11.Listener>? listener, { bool? fireImmediately = true, }) => (super.noSuchMethod( @@ -630,34 +431,34 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { {#fireImmediately: fireImmediately}, ), returnValue: () {}, - ) as _i5.RemoveListener); + ) as _i3.RemoveListener); } /// A class which mocks [Ref]. /// /// See the documentation for Mockito's code generation for more information. class MockRef extends _i1.Mock - implements _i5.Ref { + implements _i3.Ref { MockRef() { _i1.throwOnMissingStub(this); } @override - _i5.ProviderContainer get container => (super.noSuchMethod( + _i3.ProviderContainer get container => (super.noSuchMethod( Invocation.getter(#container), - returnValue: _FakeProviderContainer_4( + returnValue: _FakeProviderContainer_3( this, Invocation.getter(#container), ), - ) as _i5.ProviderContainer); + ) as _i3.ProviderContainer); @override - T refresh(_i5.Refreshable? provider) => (super.noSuchMethod( + T refresh(_i3.Refreshable? provider) => (super.noSuchMethod( Invocation.method( #refresh, [provider], ), - returnValue: _i9.dummyValue( + returnValue: _i12.dummyValue( this, Invocation.method( #refresh, @@ -667,7 +468,7 @@ class MockRef extends _i1.Mock ) as T); @override - void invalidate(_i5.ProviderOrFamily? provider) => super.noSuchMethod( + void invalidate(_i3.ProviderOrFamily? provider) => super.noSuchMethod( Invocation.method( #invalidate, [provider], @@ -759,12 +560,12 @@ class MockRef extends _i1.Mock ); @override - T read(_i5.ProviderListenable? provider) => (super.noSuchMethod( + T read(_i3.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( #read, [provider], ), - returnValue: _i9.dummyValue( + returnValue: _i12.dummyValue( this, Invocation.method( #read, @@ -774,7 +575,7 @@ class MockRef extends _i1.Mock ) as T); @override - bool exists(_i5.ProviderBase? provider) => (super.noSuchMethod( + bool exists(_i3.ProviderBase? provider) => (super.noSuchMethod( Invocation.method( #exists, [provider], @@ -783,12 +584,12 @@ class MockRef extends _i1.Mock ) as bool); @override - T watch(_i5.ProviderListenable? provider) => (super.noSuchMethod( + T watch(_i3.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( #watch, [provider], ), - returnValue: _i9.dummyValue( + returnValue: _i12.dummyValue( this, Invocation.method( #watch, @@ -798,23 +599,23 @@ class MockRef extends _i1.Mock ) as T); @override - _i5.KeepAliveLink keepAlive() => (super.noSuchMethod( + _i3.KeepAliveLink keepAlive() => (super.noSuchMethod( Invocation.method( #keepAlive, [], ), - returnValue: _FakeKeepAliveLink_5( + returnValue: _FakeKeepAliveLink_4( this, Invocation.method( #keepAlive, [], ), ), - ) as _i5.KeepAliveLink); + ) as _i3.KeepAliveLink); @override - _i5.ProviderSubscription listen( - _i5.ProviderListenable? provider, + _i3.ProviderSubscription listen( + _i3.ProviderListenable? provider, void Function( T?, T, @@ -837,7 +638,7 @@ class MockRef extends _i1.Mock #fireImmediately: fireImmediately, }, ), - returnValue: _FakeProviderSubscription_6( + returnValue: _FakeProviderSubscription_5( this, Invocation.method( #listen, @@ -851,5 +652,5 @@ class MockRef extends _i1.Mock }, ), ), - ) as _i5.ProviderSubscription); + ) as _i3.ProviderSubscription); } From 83450c4a218cbdb7dac05110969c08416132034f Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 25 Jun 2025 23:25:04 -0700 Subject: [PATCH 10/28] feat: add DM action to order states and update trade detail screen actions --- lib/features/order/models/order_state.dart | 3 +++ lib/features/trades/screens/trade_detail_screen.dart | 10 +--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 3efde078..85dfea0f 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -233,6 +233,7 @@ class OrderState { Action.buyerTookOrder: [ Action.cancel, Action.dispute, + Action.sendDm, ], Action.holdInvoicePaymentAccepted: [ Action.cancel, @@ -299,6 +300,7 @@ class OrderState { Action.fiatSent, Action.cancel, Action.dispute, + Action.sendDm, ], Action.holdInvoicePaymentSettled: [ Action.fiatSent, @@ -308,6 +310,7 @@ class OrderState { Action.buyerTookOrder: [ Action.cancel, Action.dispute, + Action.sendDm, ], }, Status.fiatSent: { diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 09a0dcb1..710792c7 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -386,15 +386,11 @@ class TradeDetailScreen extends ConsumerWidget { )); break; - case actions.Action.holdInvoicePaymentSettled: - case actions.Action.holdInvoicePaymentAccepted: + case actions.Action.sendDm: widgets.add(_buildContactButton(context)); break; case actions.Action.holdInvoicePaymentCanceled: - // These are system actions, not user actions, so no button needed - break; - case actions.Action.buyerInvoiceAccepted: case actions.Action.waitingSellerToPay: case actions.Action.waitingBuyerInvoice: @@ -407,15 +403,11 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.adminTookDispute: case actions.Action.paymentFailed: case actions.Action.invoiceUpdated: - case actions.Action.sendDm: case actions.Action.tradePubkey: case actions.Action.cantDo: case actions.Action.released: - // Not user-facing or not relevant as a button break; - default: - // Optionally handle unknown or unimplemented actions break; } } From 2ddcdfbe7771b337948e8000d658bc312180c72f Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 26 Jun 2025 00:18:17 -0700 Subject: [PATCH 11/28] refactor: migrate to automatic subscription management using SubscriptionManager --- .../chat/notifiers/chat_room_notifier.dart | 3 +- .../order/notfiers/add_order_notifier.dart | 1 - .../order/notfiers/order_notifier.dart | 2 - .../subscriptions/subscription_manager.dart | 53 ++----------------- lib/services/mostro_service.dart | 37 ++----------- test/mocks.dart | 6 +-- test/services/mostro_service_test.dart | 19 ------- 7 files changed, 8 insertions(+), 113 deletions(-) diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index 9bf5ad5d..f3034ee8 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -42,8 +42,7 @@ class ChatRoomNotifier extends StateNotifier { // Use SubscriptionManager to create a subscription for this specific chat room final subscriptionManager = ref.read(subscriptionManagerProvider); - - subscriptionManager.chat.listen(_onChatEvent); + _subscription = subscriptionManager.chat.listen(_onChatEvent); } void _onChatEvent(NostrEvent event) async { diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index b6abec5b..458029c9 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -73,7 +73,6 @@ class AddOrderNotifier extends AbstractMostroNotifier { requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); - mostroService.subscribe(session); await mostroService.submitOrder(message); state = state.updateWith(message); } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 841ec219..6b62b227 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -34,7 +34,6 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.buyer, ); - mostroService.subscribe(session); await mostroService.takeSellOrder( orderId, amount, @@ -48,7 +47,6 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.seller, ); - mostroService.subscribe(session); await mostroService.takeBuyOrder( orderId, amount, diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index fec6aca6..045afcc0 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -18,13 +18,11 @@ class SubscriptionManager { final Ref ref; final Map _subscriptions = {}; final _logger = Logger(); - ProviderSubscription? _sessionListener; + late final ProviderSubscription _sessionListener; - // Controllers for each subscription type to expose streams to consumers final _ordersController = StreamController.broadcast(); final _chatController = StreamController.broadcast(); - // Public streams that consumers can listen to Stream get orders => _ordersController.stream; Stream get chat => _chatController.stream; @@ -32,27 +30,20 @@ class SubscriptionManager { _initSessionListener(); } - /// Initialize the session listener to automatically update subscriptions - /// when sessions change in the SessionNotifier void _initSessionListener() { _sessionListener = ref.listen>( sessionNotifierProvider, (previous, current) { - _logger.i('Sessions changed, updating subscriptions'); _updateAllSubscriptions(current); }, + fireImmediately: true, onError: (error, stackTrace) { _logger.e('Error in session listener', error: error, stackTrace: stackTrace); }, ); - - // Initialize subscriptions with current sessions - final currentSessions = ref.read(sessionNotifierProvider); - _updateAllSubscriptions(currentSessions); } - /// Update all subscription types based on the current sessions void _updateAllSubscriptions(List sessions) { if (sessions.isEmpty) { _logger.i('No sessions available, clearing all subscriptions'); @@ -60,22 +51,18 @@ class SubscriptionManager { return; } - // Update each subscription type for (final type in SubscriptionType.values) { _updateSubscription(type, sessions); } } - /// Clear all active subscriptions void _clearAllSubscriptions() { for (final type in SubscriptionType.values) { unsubscribeByType(type); } } - /// Update a specific subscription type with the current sessions void _updateSubscription(SubscriptionType type, List sessions) { - // Cancel existing subscriptions for this type unsubscribeByType(type); if (sessions.isEmpty) { @@ -86,7 +73,6 @@ class SubscriptionManager { try { final filter = _createFilterForType(type, sessions); - // Create a subscription for this type subscribe( type: type, filter: filter, @@ -98,10 +84,8 @@ class SubscriptionManager { _logger.e('Failed to create $type subscription', error: e, stackTrace: stackTrace); } - _logger.i('Updated $type subscription with $_subscriptions'); } - /// Create a NostrFilter based on the subscription type and sessions NostrFilter _createFilterForType( SubscriptionType type, List sessions) { switch (type) { @@ -122,7 +106,6 @@ class SubscriptionManager { } } - /// Handle incoming events based on their subscription type void _handleEvent(SubscriptionType type, NostrEvent event) { try { switch (type) { @@ -138,7 +121,6 @@ class SubscriptionManager { } } - /// Subscribe to Nostr events with a specific filter and subscription type. Stream subscribe({ required SubscriptionType type, required NostrFilter filter, @@ -181,7 +163,6 @@ class SubscriptionManager { } } - /// Subscribe to Nostr events for a specific session. Stream subscribeSession({ required SubscriptionType type, required Session session, @@ -194,38 +175,22 @@ class SubscriptionManager { ); } - /// Unsubscribe from a specific subscription by ID. - void unsubscribeById(SubscriptionType type) { - final subscription = _subscriptions[type]; - if (subscription != null) { - subscription.cancel(); - _subscriptions.remove(type); - _logger.d('Canceled subscription for $type'); - } - } - - /// Unsubscribe from all subscriptions of a specific type. void unsubscribeByType(SubscriptionType type) { final subscription = _subscriptions[type]; if (subscription != null) { subscription.cancel(); _subscriptions.remove(type); - _logger.d('Canceled all subscriptions for $type'); } } - /// Unsubscribe from a session-based subscription. void unsubscribeSession(SubscriptionType type) { unsubscribeByType(type); } - /// Check if there's an active subscription of a specific type. bool hasActiveSubscription(SubscriptionType type) { return _subscriptions[type] != null; } - /// Get all active filters for a specific subscription type - /// Returns an empty list if no active subscriptions exist for the type List getActiveFilters(SubscriptionType type) { final subscription = _subscriptions[type]; return subscription?.request.filters ?? []; @@ -233,32 +198,20 @@ class SubscriptionManager { void subscribeAll() { unsubscribeAll(); - _logger.i('Subscribing to all subscriptions'); final currentSessions = ref.read(sessionNotifierProvider); _updateAllSubscriptions(currentSessions); } - /// Unsubscribe from all subscription types void unsubscribeAll() { - _logger.i('Unsubscribing from all subscriptions'); for (final type in SubscriptionType.values) { unsubscribeByType(type); } } - /// Dispose all subscriptions and listeners void dispose() { - _logger.i('Disposing SubscriptionManager'); - if (_sessionListener != null) { - _sessionListener!.close(); - _sessionListener = null; - } - + _sessionListener.close(); unsubscribeAll(); - _ordersController.close(); _chatController.close(); - - _logger.i('SubscriptionManager disposed'); } } diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 76e47c6b..ba60ba65 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; @@ -37,35 +38,6 @@ class MostroService { _logger.i('MostroService disposed'); } - /// No need to manually subscribe to sessions anymore. - /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. - /// - /// This method is kept for backward compatibility but doesn't perform manual subscription. - /// - /// [session] The session that would have been subscribed to - void subscribe(Session session) { - _logger.i('Manual subscription not needed for: ${session.tradeKey.public}'); - // No action needed - SubscriptionManager handles subscriptions automatically - } - - /// No need to manually unsubscribe from sessions anymore. - /// SubscriptionManager now automatically handles subscriptions based on SessionNotifier changes. - /// - /// This method is kept for backward compatibility but doesn't perform manual unsubscription. - /// - /// [session] The session that would have been unsubscribed from - void unsubscribe(Session session) { - _logger - .i('Manual unsubscription not needed for: ${session.tradeKey.public}'); - // No action needed - SubscriptionManager handles subscriptions automatically - } - - // No need to manually clear subscriptions anymore. - // SubscriptionManager now handles this automatically when needed. - - // No need to manually update subscriptions anymore. - // SubscriptionManager now handles this automatically based on SessionNotifier changes. - Future _onData(NostrEvent event) async { final eventStore = ref.read(eventStorageProvider); @@ -76,13 +48,10 @@ class MostroService { ); final sessions = ref.read(sessionNotifierProvider); - Session? matchingSession; - - try { - matchingSession = sessions.firstWhere( + final matchingSession = sessions.firstWhereOrNull( (s) => s.tradeKey.public == event.recipient, ); - } catch (e) { + if (matchingSession == null) { _logger.w('No matching session found for recipient: ${event.recipient}'); return; } diff --git a/test/mocks.dart b/test/mocks.dart index b6d1f6b5..c486e0be 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -134,11 +134,7 @@ class MockSubscriptionManager extends SubscriptionManager { } @override - bool hasActiveSubscription(SubscriptionType type, {String? id}) { - if (id != null) { - final subscription = _subscriptions[type]; - return subscription != null && subscription.request.subscriptionId == id; - } + bool hasActiveSubscription(SubscriptionType type) { return _subscriptions.containsKey(type); } diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 48ddc9dd..59f02d28 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -97,26 +97,7 @@ void main() { // Stub SessionNotifier methods when(mockSessionNotifier.sessions).thenReturn([]); - mostroService = MostroService(mockRef); - keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); - mockServerTradeIndex = MockServerTradeIndex(); - - // Setup mock session notifier - when(mockSessionNotifier.sessions).thenReturn([]); - - // Setup mock ref with Settings - final settings = Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', - defaultFiatCode: 'USD', - ); - when(mockRef.read(settingsProvider)).thenReturn(settings); when(mockRef.read(sessionNotifierProvider.notifier)).thenReturn(mockSessionNotifier); - when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); - when(mockRef.read(subscriptionManagerProvider)).thenReturn(mockSubscriptionManager); - // Mock server trade index is used in the service - // but we don't need to mock the provider // Create the service under test mostroService = MostroService(mockRef); From 8c2d58af390b42576331ed84eda50fbeb681521a Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 26 Jun 2025 08:04:51 -0700 Subject: [PATCH 12/28] refactor: simplify subscription filter logic and remove unused subscription ID parameter --- .../subscriptions/subscription_manager.dart | 5 ++-- test/mocks.dart | 24 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index 045afcc0..a913439f 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -98,9 +98,8 @@ class SubscriptionManager { return NostrFilter( kinds: [1059], p: sessions - .where((s) => s.peer?.publicKey != null) - .map((s) => s.sharedKey?.public) - .whereType() + .where((s) => s.sharedKey?.public != null) + .map((s) => s.sharedKey!.public) .toList(), ); } diff --git a/test/mocks.dart b/test/mocks.dart index c486e0be..f5b5ec60 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -96,23 +96,21 @@ class MockSubscriptionManager extends SubscriptionManager { Stream subscribe({ required SubscriptionType type, required NostrFilter filter, - String? id, }) { _lastFilter = filter; - + final request = NostrRequest(filters: [filter]); - request.subscriptionId = id ?? type.toString(); - + final subscription = Subscription( - request: request, - streamSubscription: _ordersController.stream.listen((_) {}), - onCancel: () {}, - ); - - _subscriptions[type] = subscription; - - return type == SubscriptionType.orders ? orders : chat; - } + request: request, + streamSubscription: const Stream.empty().listen((_) {}), + onCancel: () {}, + ); + + _subscriptions[type] = subscription; + + return type == SubscriptionType.orders ? orders : chat; +} @override void unsubscribeByType(SubscriptionType type) { From 376400003749ecddb77c22b97a382936372dcb24 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 26 Jun 2025 08:45:52 -0700 Subject: [PATCH 13/28] refactor: update mock subscription manager with proper stream handling and remove unused methods --- integration_test/test_helpers.dart | 8 ------ test/mocks.dart | 45 +++++++++++++++++------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/integration_test/test_helpers.dart b/integration_test/test_helpers.dart index d950debb..e8921fa8 100644 --- a/integration_test/test_helpers.dart +++ b/integration_test/test_helpers.dart @@ -270,9 +270,6 @@ class FakeMostroService implements MostroService { @override void init({List? keys}) {} - @override - void subscribe(Session session) {} - @override Future submitOrder(MostroMessage order) async { final storage = ref.read(mostroStorageProvider); @@ -317,11 +314,6 @@ class FakeMostroService implements MostroService { @override void updateSettings(Settings settings) {} - @override - void unsubscribe(Session session) { - // TODO: implement unsubscribe - } - @override void dispose() { // TODO: implement dispose diff --git a/test/mocks.dart b/test/mocks.dart index f5b5ec60..6dbbda48 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -77,21 +77,23 @@ class MockSessionNotifier extends SessionNotifier { // Custom mock for SubscriptionManager class MockSubscriptionManager extends SubscriptionManager { - final StreamController _ordersController = StreamController.broadcast(); - final StreamController _chatController = StreamController.broadcast(); + final StreamController _ordersController = + StreamController.broadcast(); + final StreamController _chatController = + StreamController.broadcast(); final Map _subscriptions = {}; NostrFilter? _lastFilter; - + MockSubscriptionManager() : super(MockRef()); - + NostrFilter? get lastFilter => _lastFilter; - + @override Stream get orders => _ordersController.stream; - + @override Stream get chat => _chatController.stream; - + @override Stream subscribe({ required SubscriptionType type, @@ -102,26 +104,29 @@ class MockSubscriptionManager extends SubscriptionManager { final request = NostrRequest(filters: [filter]); final subscription = Subscription( - request: request, - streamSubscription: const Stream.empty().listen((_) {}), - onCancel: () {}, - ); + request: request, + streamSubscription: (type == SubscriptionType.orders + ? _ordersController.stream + : _chatController.stream) + .listen((_) {}), + onCancel: () {}, + ); - _subscriptions[type] = subscription; + _subscriptions[type] = subscription; + + return type == SubscriptionType.orders ? orders : chat; + } - return type == SubscriptionType.orders ? orders : chat; -} - @override void unsubscribeByType(SubscriptionType type) { _subscriptions.remove(type); } - + @override void unsubscribeAll() { _subscriptions.clear(); } - + @override List getActiveFilters(SubscriptionType type) { final subscription = _subscriptions[type]; @@ -130,12 +135,12 @@ class MockSubscriptionManager extends SubscriptionManager { } return []; } - + @override bool hasActiveSubscription(SubscriptionType type) { return _subscriptions.containsKey(type); } - + // Helper to add events to the stream void addEvent(NostrEvent event, SubscriptionType type) { if (type == SubscriptionType.orders) { @@ -144,7 +149,7 @@ class MockSubscriptionManager extends SubscriptionManager { _chatController.add(event); } } - + @override void dispose() { _ordersController.close(); From 71834ff8dfb6672a7116b6d60745fb647fdfb243 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 2 Jul 2025 12:10:31 -0700 Subject: [PATCH 14/28] fix: prevent duplicate events in chat and handle unix timestamps correctly (Fixes: Error displaying the date and time of order creation #83) --- lib/features/chat/notifiers/chat_room_notifier.dart | 10 ++++++---- lib/features/chat/screens/chat_room_screen.dart | 6 ++++-- lib/features/key_manager/key_manager.dart | 7 +++++-- lib/features/subscriptions/subscription_manager.dart | 4 +++- lib/features/trades/screens/trade_detail_screen.dart | 8 +++----- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index f3034ee8..f2276a58 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -59,10 +59,12 @@ class ChatRoomNotifier extends StateNotifier { final eventStore = ref.read(eventStorageProvider); - await eventStore.putItem( - event.id!, - event, - ); + if (!await eventStore.hasItem(event.id!)) { + await eventStore.putItem( + event.id!, + event, + ); + } final chat = await event.mostroUnWrap(session.sharedKey!); // Deduplicate by message ID and always sort by createdAt diff --git a/lib/features/chat/screens/chat_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index adda9b2c..8f0248c7 100644 --- a/lib/features/chat/screens/chat_room_screen.dart +++ b/lib/features/chat/screens/chat_room_screen.dart @@ -8,6 +8,7 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; @@ -29,7 +30,8 @@ class _MessagesDetailScreenState extends ConsumerState { Widget build(BuildContext context) { final chatDetailState = ref.watch(chatRoomsProvider(widget.orderId)); final session = ref.read(sessionProvider(widget.orderId)); - final peer = session!.peer!.publicKey; + final orderState = ref.read(orderNotifierProvider(widget.orderId)); + final peer = orderState.peer!.publicKey; return Scaffold( backgroundColor: AppTheme.dark1, @@ -63,7 +65,7 @@ class _MessagesDetailScreenState extends ConsumerState { children: [ const SizedBox(height: 12.0), Text('Order: ${widget.orderId}'), - _buildMessageHeader(peer, session), + _buildMessageHeader(peer, session!), _buildBody(chatDetailState, peer), _buildMessageInput(), const SizedBox(height: 12.0), diff --git a/lib/features/key_manager/key_manager.dart b/lib/features/key_manager/key_manager.dart index 6e487cf8..692d8472 100644 --- a/lib/features/key_manager/key_manager.dart +++ b/lib/features/key_manager/key_manager.dart @@ -16,9 +16,10 @@ class KeyManager { Future init() async { if (!await hasMasterKey()) { await generateAndStoreMasterKey(); + } else { + masterKeyPair = await _getMasterKey(); + tradeKeyIndex = await getCurrentKeyIndex(); } - masterKeyPair = await _getMasterKey(); - tradeKeyIndex = await getCurrentKeyIndex(); } Future hasMasterKey() async { @@ -43,6 +44,8 @@ class KeyManager { await _storage.storeMnemonic(mnemonic); await _storage.storeMasterKey(masterKeyHex); await setCurrentKeyIndex(1); + masterKeyPair = await _getMasterKey(); + tradeKeyIndex = await getCurrentKeyIndex(); } Future importMnemonic(String mnemonic) async { diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index a913439f..fad5eb5e 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -92,7 +92,9 @@ class SubscriptionManager { case SubscriptionType.orders: return NostrFilter( kinds: [1059], - p: sessions.map((s) => s.tradeKey.public).toList(), + p: sessions + .map((s) => s.tradeKey.public) + .toList(), ); case SubscriptionType.chat: return NostrFilter( diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 710792c7..08b097ac 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -53,7 +53,7 @@ class TradeDetailScreen extends ConsumerWidget { // Detailed info: includes the last Mostro message action text MostroMessageDetail(orderId: orderId), const SizedBox(height: 24), - _buildCountDownTime(orderPayload.expiresAt), + _buildCountDownTime(orderPayload.expiresAt != null ? orderPayload.expiresAt!*1000 : null), const SizedBox(height: 36), Wrap( alignment: WrapAlignment.center, @@ -104,10 +104,8 @@ class TradeDetailScreen extends ConsumerWidget { final method = tradeState.order!.paymentMethod; final timestamp = formatDateTime( tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 - ? DateTime.fromMillisecondsSinceEpoch(tradeState.order!.createdAt!) - : DateTime.fromMillisecondsSinceEpoch( - tradeState.order!.createdAt ?? 0, - ), + ? DateTime.fromMillisecondsSinceEpoch(tradeState.order!.createdAt!*1000) + : session.startTime, ); return CustomCard( padding: const EdgeInsets.all(16), From b285968725b913a8ae87713495ace602f63a8c53 Mon Sep 17 00:00:00 2001 From: Biz Date: Fri, 4 Jul 2025 10:06:00 -0700 Subject: [PATCH 15/28] refactor: add session-based action validation to reactive button widget --- lib/shared/widgets/mostro_reactive_button.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/shared/widgets/mostro_reactive_button.dart b/lib/shared/widgets/mostro_reactive_button.dart index 51e59798..dec0dc83 100644 --- a/lib/shared/widgets/mostro_reactive_button.dart +++ b/lib/shared/widgets/mostro_reactive_button.dart @@ -5,11 +5,10 @@ import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; enum ButtonStyleType { raised, outlined, text } -/// A button specially designed for reactive operations that shows loading state -/// and handles the unique event-based nature of the mostro protocol. class MostroReactiveButton extends ConsumerStatefulWidget { final String label; final ButtonStyleType buttonStyle; @@ -29,7 +28,7 @@ class MostroReactiveButton extends ConsumerStatefulWidget { required this.onPressed, required this.orderId, required this.action, - this.timeout = const Duration(seconds: 30), + this.timeout = const Duration(seconds: 5), this.showSuccessIndicator = false, this.backgroundColor, }); @@ -43,7 +42,7 @@ class _MostroReactiveButtonState extends ConsumerState { bool _loading = false; bool _showSuccess = false; Timer? _timeoutTimer; - dynamic _lastSeenAction; + actions.Action? _lastSeenAction; @override void dispose() { @@ -73,9 +72,14 @@ class _MostroReactiveButtonState extends ConsumerState { @override Widget build(BuildContext context) { final orderState = ref.watch(orderNotifierProvider(widget.orderId)); - //if (orderState.action != widget.action) { - //return const SizedBox.shrink(); - //} + final session = ref.watch(sessionProvider(widget.orderId)); + + if (session != null) { + final nextStates = orderState.getActions(session.role!); + if (!nextStates.contains(widget.action)) { + return const SizedBox.shrink(); + } + } ref.listen( mostroMessageStreamProvider(widget.orderId), From 7b65edef144dd0537a3402d2c0785a4040698850 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 16:33:16 -0700 Subject: [PATCH 16/28] refactor: clean up notification service by removing unused code and comments --- lib/notifications/notification_service.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/notifications/notification_service.dart b/lib/notifications/notification_service.dart index a133838c..978fe4fb 100644 --- a/lib/notifications/notification_service.dart +++ b/lib/notifications/notification_service.dart @@ -38,19 +38,16 @@ Future showLocalNotification(NostrEvent event) async { playSound: true, enableVibration: true, ticker: 'ticker', - // Uncomment for heads-up notification, use with care: - // fullScreenIntent: true, ), iOS: DarwinNotificationDetails( presentAlert: true, presentBadge: true, presentSound: true, - // Optionally set interruption level for iOS 15+: interruptionLevel: InterruptionLevel.critical, ), ); await flutterLocalNotificationsPlugin.show( - event.id.hashCode, // Use unique ID for each event + event.id.hashCode, 'New Mostro Event', 'You have received a new message from Mostro', details, @@ -78,10 +75,4 @@ Future retryNotification(NostrEvent event, {int maxAttempts = 3}) async { await Future.delayed(Duration(seconds: backoffSeconds)); } } - - // Optionally store failed notifications for later retry when app returns to foreground - if (!success) { - // Store the event ID in a persistent queue for later retry - // await failedNotificationsQueue.add(event.id!); - } } From 3b0873d7aa65c02daa72c32981364fef53e94940 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 16:54:07 -0700 Subject: [PATCH 17/28] refactor: enhance model validation and error handling across data models --- lib/data/models/amount.dart | 53 ++++++++++- lib/data/models/cant_do.dart | 52 ++++++++--- lib/data/models/chat_model.dart | 14 --- lib/data/models/chat_room.dart | 25 ++++++ lib/data/models/currency.dart | 122 ++++++++++++++++++++++--- lib/data/models/dispute.dart | 49 ++++++++-- lib/data/models/order.dart | 130 +++++++++++++++++++++------ lib/data/models/payment_request.dart | 84 +++++++++++++---- lib/data/models/peer.dart | 43 +++++++-- lib/data/models/range_amount.dart | 104 ++++++++++++++++++--- lib/data/models/rating_user.dart | 52 ++++++++++- lib/data/models/session.dart | 94 ++++++++++++++++--- lib/data/models/text_message.dart | 36 +++++++- 13 files changed, 737 insertions(+), 121 deletions(-) diff --git a/lib/data/models/amount.dart b/lib/data/models/amount.dart index 6f335af6..494305c2 100644 --- a/lib/data/models/amount.dart +++ b/lib/data/models/amount.dart @@ -3,7 +3,11 @@ import 'package:mostro_mobile/data/models/payload.dart'; class Amount implements Payload { final int amount; - Amount({required this.amount}); + Amount({required this.amount}) { + if (amount < 0) { + throw ArgumentError('Amount cannot be negative: $amount'); + } + } @override Map toJson() { @@ -12,6 +16,53 @@ class Amount implements Payload { }; } + factory Amount.fromJson(dynamic json) { + try { + if (json == null) { + throw FormatException('Amount JSON cannot be null'); + } + + int amountValue; + if (json is Map) { + if (!json.containsKey('amount')) { + throw FormatException('Missing required field: amount'); + } + final value = json['amount']; + if (value is int) { + amountValue = value; + } else if (value is String) { + amountValue = int.tryParse(value) ?? + (throw FormatException('Invalid amount format: $value')); + } else { + throw FormatException('Invalid amount type: ${value.runtimeType}'); + } + } else if (json is int) { + amountValue = json; + } else if (json is String) { + amountValue = int.tryParse(json) ?? + (throw FormatException('Invalid amount format: $json')); + } else { + throw FormatException('Invalid JSON type for Amount: ${json.runtimeType}'); + } + + return Amount(amount: amountValue); + } catch (e) { + throw FormatException('Failed to parse Amount from JSON: $e'); + } + } + @override String get type => 'amount'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Amount && other.amount == amount; + } + + @override + int get hashCode => amount.hashCode; + + @override + String toString() => 'Amount(amount: $amount)'; } diff --git a/lib/data/models/cant_do.dart b/lib/data/models/cant_do.dart index 051f7f68..0094d95b 100644 --- a/lib/data/models/cant_do.dart +++ b/lib/data/models/cant_do.dart @@ -4,24 +4,40 @@ import 'package:mostro_mobile/data/models/payload.dart'; class CantDo implements Payload { final CantDoReason cantDoReason; + CantDo({required this.cantDoReason}); + factory CantDo.fromJson(Map json) { - if (json['cant_do'] is String) { - return CantDo( - cantDoReason: CantDoReason.fromString( - json['cant_do'], - ), - ); - } else { + try { + final cantDoValue = json['cant_do']; + if (cantDoValue == null) { + throw FormatException('Missing required field: cant_do'); + } + + String reasonString; + if (cantDoValue is String) { + reasonString = cantDoValue; + } else if (cantDoValue is Map) { + final cantDoReason = cantDoValue['cant-do']; + if (cantDoReason == null) { + throw FormatException('Missing required field: cant-do in cant_do object'); + } + reasonString = cantDoReason.toString(); + } else { + throw FormatException('Invalid cant_do type: ${cantDoValue.runtimeType}'); + } + + if (reasonString.isEmpty) { + throw FormatException('CantDo reason cannot be empty'); + } + return CantDo( - cantDoReason: CantDoReason.fromString( - json['cant_do']['cant-do'], - ), + cantDoReason: CantDoReason.fromString(reasonString), ); + } catch (e) { + throw FormatException('Failed to parse CantDo from JSON: $e'); } } - CantDo({required this.cantDoReason}); - @override Map toJson() { return { @@ -33,4 +49,16 @@ class CantDo implements Payload { @override String get type => 'cant_do'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CantDo && other.cantDoReason == cantDoReason; + } + + @override + int get hashCode => cantDoReason.hashCode; + + @override + String toString() => 'CantDo(cantDoReason: $cantDoReason)'; } diff --git a/lib/data/models/chat_model.dart b/lib/data/models/chat_model.dart index fb9aa6a9..8b137891 100644 --- a/lib/data/models/chat_model.dart +++ b/lib/data/models/chat_model.dart @@ -1,15 +1 @@ -class ChatModel { - final String id; - final String username; - final String lastMessage; - final String timeAgo; - final bool isUnread; - ChatModel({ - required this.id, - required this.username, - required this.lastMessage, - required this.timeAgo, - this.isUnread = false, - }); -} diff --git a/lib/data/models/chat_room.dart b/lib/data/models/chat_room.dart index a2f14eee..41ee0b03 100644 --- a/lib/data/models/chat_room.dart +++ b/lib/data/models/chat_room.dart @@ -5,6 +5,9 @@ class ChatRoom { final List messages; ChatRoom({required this.orderId, required this.messages}) { + if (orderId.isEmpty) { + throw ArgumentError('Order ID cannot be empty'); + } messages.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); } @@ -16,4 +19,26 @@ class ChatRoom { messages: messages ?? this.messages, ); } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ChatRoom && + other.orderId == orderId && + _listEquals(other.messages, messages); + } + + bool _listEquals(List a, List b) { + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + @override + int get hashCode => Object.hash(orderId, messages.length); + + @override + String toString() => 'ChatRoom(orderId: $orderId, messages: ${messages.length} messages)'; } diff --git a/lib/data/models/currency.dart b/lib/data/models/currency.dart index 115d1eb5..834474d2 100644 --- a/lib/data/models/currency.dart +++ b/lib/data/models/currency.dart @@ -9,7 +9,6 @@ class Currency { final bool price; String? locale; - Currency({ required this.symbol, required this.name, @@ -20,19 +19,118 @@ class Currency { required this.namePlural, required this.price, this.locale, - }); + }) { + if (symbol.isEmpty) { + throw ArgumentError('Currency symbol cannot be empty'); + } + if (name.isEmpty) { + throw ArgumentError('Currency name cannot be empty'); + } + if (code.isEmpty) { + throw ArgumentError('Currency code cannot be empty'); + } + if (decimalDigits < 0) { + throw ArgumentError('Decimal digits cannot be negative: $decimalDigits'); + } + } factory Currency.fromJson(Map json) { - return Currency( - symbol: json['symbol'], - name: json['name'], - symbolNative: json['symbol_native'], - code: json['code'], - emoji: json['emoji'], - decimalDigits: json['decimal_digits'], - namePlural: json['name_plural'], - price: json['price'] ?? false, - locale: json['locale'], + try { + // Validate required fields + final requiredFields = ['symbol', 'name', 'symbol_native', 'code', 'emoji', 'decimal_digits', 'name_plural']; + for (final field in requiredFields) { + if (!json.containsKey(field) || json[field] == null) { + throw FormatException('Missing required field: $field'); + } + } + + // Parse and validate decimal_digits + final decimalDigitsValue = json['decimal_digits']; + int decimalDigits; + if (decimalDigitsValue is int) { + decimalDigits = decimalDigitsValue; + } else if (decimalDigitsValue is String) { + decimalDigits = int.tryParse(decimalDigitsValue) ?? + (throw FormatException('Invalid decimal_digits format: $decimalDigitsValue')); + } else { + throw FormatException('Invalid decimal_digits type: ${decimalDigitsValue.runtimeType}'); + } + + // Parse price field + final priceValue = json['price']; + bool price; + if (priceValue is bool) { + price = priceValue; + } else if (priceValue is String) { + price = priceValue.toLowerCase() == 'true'; + } else if (priceValue == null) { + price = false; + } else { + throw FormatException('Invalid price type: ${priceValue.runtimeType}'); + } + + return Currency( + symbol: json['symbol'].toString(), + name: json['name'].toString(), + symbolNative: json['symbol_native'].toString(), + code: json['code'].toString(), + emoji: json['emoji'].toString(), + decimalDigits: decimalDigits, + namePlural: json['name_plural'].toString(), + price: price, + locale: json['locale']?.toString(), + ); + } catch (e) { + throw FormatException('Failed to parse Currency from JSON: $e'); + } + } + + Map toJson() { + return { + 'symbol': symbol, + 'name': name, + 'symbol_native': symbolNative, + 'code': code, + 'emoji': emoji, + 'decimal_digits': decimalDigits, + 'name_plural': namePlural, + 'price': price, + if (locale != null) 'locale': locale, + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Currency && + other.symbol == symbol && + other.name == name && + other.symbolNative == symbolNative && + other.decimalDigits == decimalDigits && + other.code == code && + other.emoji == emoji && + other.namePlural == namePlural && + other.price == price && + other.locale == locale; + } + + @override + int get hashCode { + return Object.hash( + symbol, + name, + symbolNative, + decimalDigits, + code, + emoji, + namePlural, + price, + locale, ); } + + @override + String toString() { + return 'Currency(symbol: $symbol, name: $name, code: $code, price: $price)'; + } } diff --git a/lib/data/models/dispute.dart b/lib/data/models/dispute.dart index c25e7b71..22315731 100644 --- a/lib/data/models/dispute.dart +++ b/lib/data/models/dispute.dart @@ -3,7 +3,11 @@ import 'package:mostro_mobile/data/models/payload.dart'; class Dispute implements Payload { final String disputeId; - Dispute({required this.disputeId}); + Dispute({required this.disputeId}) { + if (disputeId.isEmpty) { + throw ArgumentError('Dispute ID cannot be empty'); + } + } @override Map toJson() { @@ -13,13 +17,46 @@ class Dispute implements Payload { } factory Dispute.fromJson(Map json) { - final oid = json['dispute']; - return Dispute( - disputeId: oid is List ? oid[0] : oid, - ); + try { + + final oid = json['dispute']; + if (oid == null) { + throw FormatException('Missing required field: dispute'); + } + + String disputeIdValue; + if (oid is List) { + if (oid.isEmpty) { + throw FormatException('Dispute list cannot be empty'); + } + disputeIdValue = oid[0]?.toString() ?? + (throw FormatException('First element of dispute list is null')); + } else { + disputeIdValue = oid.toString(); + } + + if (disputeIdValue.isEmpty) { + throw FormatException('Dispute ID cannot be empty'); + } + + return Dispute(disputeId: disputeIdValue); + } catch (e) { + throw FormatException('Failed to parse Dispute from JSON: $e'); + } } - @override String get type => 'dispute'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Dispute && other.disputeId == disputeId; + } + + @override + int get hashCode => disputeId.hashCode; + + @override + String toString() => 'Dispute(disputeId: $disputeId)'; } diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 0fa04923..609a1187 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -82,38 +82,110 @@ class Order implements Payload { } factory Order.fromJson(Map json) { - // Validate required fields - void validateField(String field) { - if (!json.containsKey(field)) { - throw FormatException('Missing required field: $field'); + try { + // Validate required fields + void validateField(String field) { + if (!json.containsKey(field) || json[field] == null) { + throw FormatException('Missing required field: $field'); + } } - } - // Validate required fields - ['kind', 'status', 'fiat_code', 'fiat_amount', 'payment_method', 'premium'] - .forEach(validateField); + // Validate required fields + ['kind', 'status', 'fiat_code', 'fiat_amount', 'payment_method'] + .forEach(validateField); - return Order( - id: json['id'], - kind: OrderType.fromString(json['kind'].toString()), - status: Status.fromString(json['status']), - amount: json['amount'], - fiatCode: json['fiat_code'], - minAmount: json['min_amount'], - maxAmount: json['max_amount'], - fiatAmount: json['fiat_amount'], - paymentMethod: json['payment_method'], - premium: json['premium'], - masterBuyerPubkey: json['master_buyer_pubkey'], - masterSellerPubkey: json['master_seller_pubkey'], - buyerTradePubkey: json['buyer_trade_pubkey'], - sellerTradePubkey: json['seller_trade_pubkey'], - buyerInvoice: json['buyer_invoice'], - createdAt: json['created_at'], - expiresAt: json['expires_at'], - buyerToken: json['buyer_token'], - sellerToken: json['seller_token'], - ); + // Parse and validate integer fields with type safety + int parseIntField(String field, {int defaultValue = 0}) { + final value = json[field]; + if (value == null) return defaultValue; + if (value is int) return value; + if (value is String) { + return int.tryParse(value) ?? + (throw FormatException('Invalid $field format: $value')); + } + throw FormatException('Invalid $field type: ${value.runtimeType}'); + } + + // Parse and validate string fields + String parseStringField(String field) { + final value = json[field]; + if (value == null) { + throw FormatException('Missing required field: $field'); + } + final stringValue = value.toString(); + if (stringValue.isEmpty) { + throw FormatException('Field $field cannot be empty'); + } + return stringValue; + } + + // Parse optional string fields + String? parseOptionalStringField(String field) { + final value = json[field]; + return value?.toString(); + } + + // Parse optional integer fields + int? parseOptionalIntField(String field) { + final value = json[field]; + if (value == null) return null; + if (value is int) return value; + if (value is String) { + return int.tryParse(value); + } + return null; + } + + final amount = parseIntField('amount'); + final fiatAmount = parseIntField('fiat_amount'); + final premium = parseIntField('premium'); + + // Validate amounts are not negative + if (amount < 0) { + throw FormatException('Amount cannot be negative: $amount'); + } + if (fiatAmount < 0) { + throw FormatException('Fiat amount cannot be negative: $fiatAmount'); + } + + final minAmount = parseOptionalIntField('min_amount'); + final maxAmount = parseOptionalIntField('max_amount'); + + // Validate min/max amount relationship + if (minAmount != null && minAmount < 0) { + throw FormatException('Min amount cannot be negative: $minAmount'); + } + if (maxAmount != null && maxAmount < 0) { + throw FormatException('Max amount cannot be negative: $maxAmount'); + } + if (minAmount != null && maxAmount != null && minAmount > maxAmount) { + throw FormatException('Min amount ($minAmount) cannot be greater than max amount ($maxAmount)'); + } + + return Order( + id: parseOptionalStringField('id'), + kind: OrderType.fromString(parseStringField('kind')), + status: Status.fromString(parseStringField('status')), + amount: amount, + fiatCode: parseStringField('fiat_code'), + minAmount: minAmount, + maxAmount: maxAmount, + fiatAmount: fiatAmount, + paymentMethod: parseStringField('payment_method'), + premium: premium, + masterBuyerPubkey: parseOptionalStringField('master_buyer_pubkey'), + masterSellerPubkey: parseOptionalStringField('master_seller_pubkey'), + buyerTradePubkey: parseOptionalStringField('buyer_trade_pubkey'), + sellerTradePubkey: parseOptionalStringField('seller_trade_pubkey'), + buyerInvoice: parseOptionalStringField('buyer_invoice'), + createdAt: parseOptionalIntField('created_at'), + expiresAt: parseOptionalIntField('expires_at'), + buyerToken: parseOptionalIntField('buyer_token'), + sellerToken: parseOptionalIntField('seller_token'), + ); + } catch (e) { + throw FormatException('Failed to parse Order from JSON: $e'); + } } factory Order.fromEvent(NostrEvent event) { diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart index de07b79d..b4ba906d 100644 --- a/lib/data/models/payment_request.dart +++ b/lib/data/models/payment_request.dart @@ -35,25 +35,77 @@ class PaymentRequest implements Payload { } factory PaymentRequest.fromJson(List json) { - if (json.length < 2) { - throw FormatException('Invalid JSON format: insufficient elements'); - } - final orderJson = json[0]; - final Order? order = orderJson != null - ? Order.fromJson(orderJson['order'] ?? orderJson) - : null; - final lnInvoice = json[1]; - if (lnInvoice != null && lnInvoice is! String) { - throw FormatException('Invalid type for lnInvoice: expected String'); + try { + if (json.length < 2) { + throw FormatException('Invalid JSON format: insufficient elements (expected at least 2, got ${json.length})'); + } + + // Parse order + final orderJson = json[0]; + Order? order; + if (orderJson != null) { + if (orderJson is Map) { + order = Order.fromJson(orderJson['order'] ?? orderJson); + } else { + throw FormatException('Invalid order type: ${orderJson.runtimeType}'); + } + } + + // Parse lnInvoice + final lnInvoice = json[1]; + if (lnInvoice != null && lnInvoice is! String) { + throw FormatException('Invalid type for lnInvoice: expected String, got ${lnInvoice.runtimeType}'); + } + if (lnInvoice is String && lnInvoice.isEmpty) { + throw FormatException('lnInvoice cannot be empty string'); + } + + // Parse amount (optional) + int? amount; + if (json.length > 2) { + final amountValue = json[2]; + if (amountValue != null) { + if (amountValue is int) { + amount = amountValue; + } else if (amountValue is String) { + amount = int.tryParse(amountValue) ?? + (throw FormatException('Invalid amount format: $amountValue')); + } else { + throw FormatException('Invalid amount type: ${amountValue.runtimeType}'); + } + if (amount < 0) { + throw FormatException('Amount cannot be negative: $amount'); + } + } + } + + return PaymentRequest( + order: order, + lnInvoice: lnInvoice, + amount: amount, + ); + } catch (e) { + throw FormatException('Failed to parse PaymentRequest from JSON: $e'); } - final amount = json.length > 2 ? json[2] as int? : null; - return PaymentRequest( - order: order, - lnInvoice: lnInvoice, - amount: amount, - ); } @override String get type => 'payment_request'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PaymentRequest && + other.order == order && + other.lnInvoice == lnInvoice && + other.amount == amount; + } + + @override + int get hashCode => Object.hash(order, lnInvoice, amount); + + @override + String toString() { + return 'PaymentRequest(order: $order, lnInvoice: $lnInvoice, amount: $amount)'; + } } diff --git a/lib/data/models/peer.dart b/lib/data/models/peer.dart index 24d91d3b..80592f6a 100644 --- a/lib/data/models/peer.dart +++ b/lib/data/models/peer.dart @@ -3,16 +3,33 @@ import 'package:mostro_mobile/data/models/payload.dart'; class Peer implements Payload { final String publicKey; - Peer({required this.publicKey}); + Peer({required this.publicKey}) { + if (publicKey.isEmpty) { + throw ArgumentError('Public key cannot be empty'); + } + // Basic validation for hex string format (64 characters for secp256k1) + if (publicKey.length != 64 || !RegExp(r'^[0-9a-fA-F]+$').hasMatch(publicKey)) { + throw ArgumentError('Invalid public key format: must be 64-character hex string'); + } + } factory Peer.fromJson(Map json) { - final pubkey = json['pubkey']; - if (pubkey == null || pubkey is! String) { - throw FormatException('Invalid or missing pubkey in JSON'); + try { + final pubkey = json['pubkey']; + if (pubkey == null) { + throw FormatException('Missing required field: pubkey'); + } + if (pubkey is! String) { + throw FormatException('Invalid pubkey type: expected String, got ${pubkey.runtimeType}'); + } + if (pubkey.isEmpty) { + throw FormatException('Public key cannot be empty'); + } + + return Peer(publicKey: pubkey); + } catch (e) { + throw FormatException('Failed to parse Peer from JSON: $e'); } - return Peer( - publicKey: pubkey, - ); } @override @@ -26,4 +43,16 @@ class Peer implements Payload { @override String get type => 'peer'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Peer && other.publicKey == publicKey; + } + + @override + int get hashCode => publicKey.hashCode; + + @override + String toString() => 'Peer(publicKey: $publicKey)'; } diff --git a/lib/data/models/range_amount.dart b/lib/data/models/range_amount.dart index f4977852..53de9703 100644 --- a/lib/data/models/range_amount.dart +++ b/lib/data/models/range_amount.dart @@ -2,32 +2,114 @@ class RangeAmount { final int minimum; final int? maximum; - RangeAmount(this.minimum, this.maximum); + RangeAmount(this.minimum, this.maximum) { + if (minimum < 0) { + throw ArgumentError('Minimum amount cannot be negative: $minimum'); + } + if (maximum != null && maximum! < 0) { + throw ArgumentError('Maximum amount cannot be negative: $maximum'); + } + if (maximum != null && maximum! < minimum) { + throw ArgumentError('Maximum amount ($maximum) cannot be less than minimum ($minimum)'); + } + } factory RangeAmount.fromList(List fa) { - if (fa.length < 2) { - throw ArgumentError( - 'List must have at least two elements: a label and a minimum value.'); - } + try { + if (fa.length < 2) { + throw FormatException( + 'List must have at least two elements: a label and a minimum value.'); + } + + final minString = fa[1]; + if (minString.isEmpty) { + throw FormatException('Minimum value string cannot be empty'); + } + + final min = double.tryParse(minString)?.toInt(); + if (min == null) { + throw FormatException('Invalid minimum value format: $minString'); + } - final min = double.tryParse(fa[1])?.toInt() ?? 0; + int? max; + if (fa.length > 2) { + final maxString = fa[2]; + if (maxString.isNotEmpty) { + max = double.tryParse(maxString)?.toInt(); + if (max == null) { + throw FormatException('Invalid maximum value format: $maxString'); + } + } + } - int? max; - if (fa.length > 2) { - max = double.tryParse(fa[2])?.toInt(); + return RangeAmount(min, max); + } catch (e) { + throw FormatException('Failed to parse RangeAmount from list: $e'); } + } - return RangeAmount(min, max); + factory RangeAmount.fromJson(Map json) { + try { + if (!json.containsKey('minimum')) { + throw FormatException('Missing required field: minimum'); + } + + final minValue = json['minimum']; + int minimum; + if (minValue is int) { + minimum = minValue; + } else if (minValue is String) { + minimum = int.tryParse(minValue) ?? + (throw FormatException('Invalid minimum format: $minValue')); + } else { + throw FormatException('Invalid minimum type: ${minValue.runtimeType}'); + } + + int? maximum; + final maxValue = json['maximum']; + if (maxValue != null) { + if (maxValue is int) { + maximum = maxValue; + } else if (maxValue is String) { + maximum = int.tryParse(maxValue) ?? + (throw FormatException('Invalid maximum format: $maxValue')); + } else { + throw FormatException('Invalid maximum type: ${maxValue.runtimeType}'); + } + } + + return RangeAmount(minimum, maximum); + } catch (e) { + throw FormatException('Failed to parse RangeAmount from JSON: $e'); + } } factory RangeAmount.empty() { return RangeAmount(0, null); } + Map toJson() { + return { + 'minimum': minimum, + if (maximum != null) 'maximum': maximum, + }; + } + bool isRange() { - return maximum != null ? true : false; + return maximum != null; } + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is RangeAmount && + other.minimum == minimum && + other.maximum == maximum; + } + + @override + int get hashCode => Object.hash(minimum, maximum); + @override String toString() { if (maximum != null) { diff --git a/lib/data/models/rating_user.dart b/lib/data/models/rating_user.dart index 15e93711..8541b870 100644 --- a/lib/data/models/rating_user.dart +++ b/lib/data/models/rating_user.dart @@ -3,7 +3,11 @@ import 'package:mostro_mobile/data/models/payload.dart'; class RatingUser implements Payload { final int userRating; - RatingUser({required this.userRating}); + RatingUser({required this.userRating}) { + if (userRating < 1 || userRating > 5) { + throw ArgumentError('User rating must be between 1 and 5, got: $userRating'); + } + } @override Map toJson() { @@ -13,9 +17,53 @@ class RatingUser implements Payload { } factory RatingUser.fromJson(dynamic json) { - return RatingUser(userRating: json as int); + try { + int rating; + + if (json is int) { + rating = json; + } else if (json is String) { + rating = int.tryParse(json) ?? + (throw FormatException('Invalid rating format: $json')); + } else if (json is Map) { + final ratingValue = json['user_rating'] ?? json['rating']; + if (ratingValue == null) { + throw FormatException('Missing rating field in JSON object'); + } + if (ratingValue is int) { + rating = ratingValue; + } else if (ratingValue is String) { + rating = int.tryParse(ratingValue) ?? + (throw FormatException('Invalid rating format: $ratingValue')); + } else { + throw FormatException('Invalid rating type: ${ratingValue.runtimeType}'); + } + } else { + throw FormatException('Invalid JSON type for RatingUser: ${json.runtimeType}'); + } + + if (rating < 1 || rating > 5) { + throw FormatException('Rating must be between 1 and 5, got: $rating'); + } + + return RatingUser(userRating: rating); + } catch (e) { + throw FormatException('Failed to parse RatingUser from JSON: $e'); + } } @override String get type => 'rating_user'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is RatingUser && other.userRating == userRating; + } + + @override + int get hashCode => userRating.hashCode; + + @override + String toString() => 'RatingUser(userRating: $userRating)'; } diff --git a/lib/data/models/session.dart b/lib/data/models/session.dart index feba2707..d22639a7 100644 --- a/lib/data/models/session.dart +++ b/lib/data/models/session.dart @@ -47,16 +47,90 @@ class Session { }; factory Session.fromJson(Map json) { - return Session( - masterKey: json['master_key'], - tradeKey: json['trade_key'], - keyIndex: json['key_index'], - fullPrivacy: json['full_privacy'], - startTime: DateTime.parse(json['start_time']), - orderId: json['order_id'], - role: json['role'] != null ? Role.fromString(json['role']) : null, - peer: json['peer'] != null ? Peer(publicKey: json['peer']) : null, - ); + try { + // Validate required fields + final requiredFields = ['master_key', 'trade_key', 'key_index', 'full_privacy', 'start_time']; + for (final field in requiredFields) { + if (!json.containsKey(field) || json[field] == null) { + throw FormatException('Missing required field: $field'); + } + } + + // Parse keyIndex + final keyIndexValue = json['key_index']; + int keyIndex; + if (keyIndexValue is int) { + keyIndex = keyIndexValue; + } else if (keyIndexValue is String) { + keyIndex = int.tryParse(keyIndexValue) ?? + (throw FormatException('Invalid key_index format: $keyIndexValue')); + } else { + throw FormatException('Invalid key_index type: ${keyIndexValue.runtimeType}'); + } + + if (keyIndex < 0) { + throw FormatException('Key index cannot be negative: $keyIndex'); + } + + // Parse fullPrivacy + final fullPrivacyValue = json['full_privacy']; + bool fullPrivacy; + if (fullPrivacyValue is bool) { + fullPrivacy = fullPrivacyValue; + } else if (fullPrivacyValue is String) { + fullPrivacy = fullPrivacyValue.toLowerCase() == 'true'; + } else { + throw FormatException('Invalid full_privacy type: ${fullPrivacyValue.runtimeType}'); + } + + // Parse startTime + final startTimeValue = json['start_time']; + DateTime startTime; + if (startTimeValue is String) { + if (startTimeValue.isEmpty) { + throw FormatException('Start time string cannot be empty'); + } + startTime = DateTime.tryParse(startTimeValue) ?? + (throw FormatException('Invalid start_time format: $startTimeValue')); + } else { + throw FormatException('Invalid start_time type: ${startTimeValue.runtimeType}'); + } + + // Parse optional role + Role? role; + final roleValue = json['role']; + if (roleValue != null) { + if (roleValue is String && roleValue.isNotEmpty) { + role = Role.fromString(roleValue); + } else if (roleValue is! String) { + throw FormatException('Invalid role type: ${roleValue.runtimeType}'); + } + } + + // Parse optional peer + Peer? peer; + final peerValue = json['peer']; + if (peerValue != null) { + if (peerValue is String && peerValue.isNotEmpty) { + peer = Peer(publicKey: peerValue); + } else if (peerValue is! String) { + throw FormatException('Invalid peer type: ${peerValue.runtimeType}'); + } + } + + return Session( + masterKey: json['master_key'], + tradeKey: json['trade_key'], + keyIndex: keyIndex, + fullPrivacy: fullPrivacy, + startTime: startTime, + orderId: json['order_id']?.toString(), + role: role, + peer: peer, + ); + } catch (e) { + throw FormatException('Failed to parse Session from JSON: $e'); + } } NostrKeyPairs? get sharedKey => _sharedKey; diff --git a/lib/data/models/text_message.dart b/lib/data/models/text_message.dart index c1af504d..91c4cef6 100644 --- a/lib/data/models/text_message.dart +++ b/lib/data/models/text_message.dart @@ -3,7 +3,29 @@ import 'package:mostro_mobile/data/models/payload.dart'; class TextMessage implements Payload { final String message; - TextMessage({required this.message}); + TextMessage({required this.message}) { + if (message.isEmpty) { + throw ArgumentError('Text message cannot be empty'); + } + } + + factory TextMessage.fromJson(Map json) { + try { + final messageValue = json['message'] ?? json['text_message']; + if (messageValue == null) { + throw FormatException('Missing required field: message or text_message'); + } + + final messageString = messageValue.toString(); + if (messageString.isEmpty) { + throw FormatException('Text message cannot be empty'); + } + + return TextMessage(message: messageString); + } catch (e) { + throw FormatException('Failed to parse TextMessage from JSON: $e'); + } + } @override Map toJson() { @@ -14,4 +36,16 @@ class TextMessage implements Payload { @override String get type => 'text_message'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TextMessage && other.message == message; + } + + @override + int get hashCode => message.hashCode; + + @override + String toString() => 'TextMessage(message: $message)'; } From cf07cf252c3c7d4f4eb03b0d53615fb1db7c405f Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 21:16:39 -0700 Subject: [PATCH 18/28] chore: upgrade go_router to v16 and share_plus to v10 refactor: change background service settings --- lib/background/background.dart | 10 +- lib/background/mobile_background_service.dart | 8 +- lib/data/models/order.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 10 +- lib/l10n/intl_en.arb | 36 +- lib/l10n/intl_es.arb | 36 +- lib/l10n/intl_it.arb | 36 +- .../widgets/pay_lightning_invoice_widget.dart | 6 +- pubspec.lock | 100 +- pubspec.yaml | 4 +- test/mocks.dart | 86 +- test/mocks.mocks.dart | 954 ++++++++++-------- test/services/mostro_service_test.dart | 78 +- test/services/mostro_service_test.mocks.dart | 656 ------------ 14 files changed, 655 insertions(+), 1367 deletions(-) delete mode 100644 test/services/mostro_service_test.mocks.dart diff --git a/lib/background/background.dart b/lib/background/background.dart index 5960d524..9d3e6343 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -15,7 +15,7 @@ bool isAppForeground = true; Future serviceMain(ServiceInstance service) async { // If on Android, set up a permanent notification so the OS won't kill it. if (service is AndroidServiceInstance) { - service.setAsForegroundService(); + //service.setAsForegroundService(); } final Map> activeSubscriptions = {}; @@ -68,8 +68,12 @@ Future serviceMain(ServiceInstance service) async { }; subscription.listen((event) async { - if (await eventStore.hasItem(event.id!)) return; - await retryNotification(event); + try { + if (await eventStore.hasItem(event.id!)) return; + await retryNotification(event); + } catch (e) { + // ignore + } }); }); diff --git a/lib/background/mobile_background_service.dart b/lib/background/mobile_background_service.dart index 5f290646..f42c1660 100644 --- a/lib/background/mobile_background_service.dart +++ b/lib/background/mobile_background_service.dart @@ -24,16 +24,16 @@ class MobileBackgroundService implements BackgroundService { Future init() async { await service.configure( iosConfiguration: IosConfiguration( - autoStart: true, + autoStart: false, onForeground: serviceMain, onBackground: onIosBackground, ), androidConfiguration: AndroidConfiguration( autoStart: false, onStart: serviceMain, - isForegroundMode: true, - autoStartOnBoot: true, - initialNotificationTitle: "Mostro P2P", + isForegroundMode: false, + autoStartOnBoot: false, + initialNotificationTitle: "Mostro", initialNotificationContent: "Connected to Mostro service", foregroundServiceTypes: [ AndroidForegroundType.dataSync, diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 609a1187..07acb666 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -116,7 +116,7 @@ class Order implements Payload { if (stringValue.isEmpty) { throw FormatException('Field $field cannot be empty'); } - return stringValue; + return stringValue; // ignore: return_of_null } // Parse optional string fields diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 96de4da9..50e5ecce 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -367,7 +367,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.cooperativeCancelInitiatedByYou: // El usuario ya inició cooperative cancel, ahora debe esperar respuesta widgets.add(_buildNostrButton( - S.of(context)!.cancelPending, + S.of(context)!.cancelPendingButton, action: actions.Action.cooperativeCancelInitiatedByYou, backgroundColor: Colors.grey, onPressed: null, @@ -376,7 +376,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.cooperativeCancelInitiatedByPeer: widgets.add(_buildNostrButton( - S.of(context)!.acceptCancel, + S.of(context)!.acceptCancelButton, action: actions.Action.cooperativeCancelAccepted, backgroundColor: AppTheme.red1, onPressed: () => @@ -389,7 +389,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.purchaseCompleted: widgets.add(_buildNostrButton( - S.of(context)!.completePurchase, + S.of(context)!.completePurchaseButton, action: actions.Action.purchaseCompleted, backgroundColor: AppTheme.mostroGreen, onPressed: () => ref @@ -450,7 +450,7 @@ class TradeDetailScreen extends ConsumerWidget { Color? backgroundColor, }) { return MostroReactiveButton( - label: label, + label: label.toUpperCase(), buttonStyle: ButtonStyleType.raised, orderId: orderId, action: action, @@ -469,7 +469,7 @@ class TradeDetailScreen extends ConsumerWidget { style: ElevatedButton.styleFrom( backgroundColor: AppTheme.mostroGreen, ), - child: Text(S.of(context)!.contact), + child: Text(S.of(context)!.contactButton), ); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 121a4019..ab3952e4 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -213,7 +213,6 @@ "holdInvoiceCltvDelta": "Hold Invoice CLTV Delta", "invoiceExpirationWindow": "Invoice Expiration Window", - "@_comment_order_creation": "Order Creation Form Strings", "enterFiatAmountBuy": "Enter the fiat amount you want to pay (you can set a range)", "enterFiatAmountSell": "Enter the fiat amount you want to receive (you can set a range)", "enterAmountHint": "Enter amount (example: 100 or 100-500)", @@ -229,7 +228,6 @@ "premiumTitle": "Premium (%)", "premiumTooltip": "Adjust how much above or below the market price you want your offer. By default, it's set to 0%, with no premium or discount, so if you don't want to change the price, you can leave it as is.", - "@_comment_account_screen": "Account Management Screen Strings", "secretWords": "Secret Words", "toRestoreYourAccount": "To restore your account", "privacy": "Privacy", @@ -244,7 +242,6 @@ "noMnemonicFound": "No mnemonic found", "errorLoadingMnemonic": "Error: {error}", - "@_comment_chat_screens": "Chat Screen Strings", "noMessagesAvailable": "No messages available", "back": "BACK", "typeAMessage": "Type a message...", @@ -252,18 +249,15 @@ "yourHandle": "Your handle: {handle}", "yourSharedKey": "Your shared key:", - "@_comment_trade_actions": "Trade Detail Actions", "payInvoiceButton": "PAY INVOICE", "addInvoiceButton": "ADD INVOICE", "release": "RELEASE", "takeSell": "TAKE SELL", "takeBuy": "TAKE BUY", - "@_comment_currency_errors": "Currency Error Messages", "noExchangeDataAvailable": "No exchange data available", "errorFetchingCurrencies": "Error fetching currencies", - "@_comment_currency_section": "Currency Selection Section Strings", "selectFiatCurrencyPay": "Select the fiat currency you will pay with", "selectFiatCurrencyReceive": "Select the Fiat Currency you want to receive", "loadingCurrencies": "Loading currencies...", @@ -273,18 +267,11 @@ "searchCurrencies": "Search currencies...", "noCurrenciesFound": "No currencies found", - "@_comment_price_type_section": "Price Type Section Strings", "priceType": "Price type", - "marketPrice": "Market price", "fixedPrice": "Fixed price", "market": "Market", "priceTypeTooltip": "• Select Market Price if you want to use the price that Bitcoin has when someone takes your offer.\\n• Select Fixed Price if you want to define the exact amount of Bitcoin you will exchange.", - "@_comment_take_order_screen": "Take Order Screen Strings", - "orderDetails": "ORDER DETAILS", - "selling": "selling", - "buying": "buying", - "atMarketPrice": "at market price", "withPremiumPercent": "with a +{premium}% premium", "@withPremiumPercent": { "placeholders": { @@ -301,7 +288,6 @@ } } }, - "noPaymentMethod": "No payment method", "someoneIsSellingBuying": "Someone is {action}{satAmount} sats for {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -373,31 +359,27 @@ } }, - "@_comment_lightning_invoice": "Lightning Invoice Widget Strings", "pleaseEnterLightningInvoiceFor": "Please enter a Lightning Invoice for: ", "sats": " sats", "lightningInvoice": "Lightning Invoice", "enterInvoiceHere": "Enter invoice here", - "@_comment_trade_detail_screen": "Trade Detail Screen Strings", - "cancelPending": "CANCEL PENDING", - "acceptCancel": "ACCEPT CANCEL", - "completePurchase": "COMPLETE PURCHASE", - "rate": "RATE", - "contact": "CONTACT", + "cancelPendingButton": "CANCEL PENDING", + "acceptCancelButton": "ACCEPT CANCEL", + "completePurchaseButton": "COMPLETE PURCHASE", + "rateButton": "RATE", + "contactButton": "CONTACT", - "@_comment_rate_screen": "Rate Counterpart Screen Strings", "rateCounterpart": "Rate Counterpart", "submitRating": "Submit Rating", - "@_comment_pay_invoice_screen": "Pay Lightning Invoice Screen Strings", "payLightningInvoice": "Pay Lightning Invoice", "payInvoiceToContinue": "Pay this invoice to continue the exchange", "failedToGenerateQR": "Failed to generate QR code", "invoiceCopiedToClipboard": "Invoice copied to clipboard", - "copy": "Copy", - "share": "Share", + "copyButton": "Copy", + "shareButton": "Share", "failedToShareInvoice": "Failed to share invoice. Please try copying instead.", - "openWallet": "OPEN WALLET", - "done": "DONE" + "openWalletButton": "OPEN WALLET", + "doneButton": "DONE" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 71238fc7..0f6314ff 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -213,7 +213,6 @@ "holdInvoiceCltvDelta": "Delta CLTV de Factura de Retención", "invoiceExpirationWindow": "Ventana de Expiración de Factura", - "@_comment_order_creation": "Cadenas del Formulario de Creación de Orden", "enterFiatAmountBuy": "Ingresa la cantidad fiat que quieres pagar (puedes establecer un rango)", "enterFiatAmountSell": "Ingresa la cantidad fiat que quieres recibir (puedes establecer un rango)", "enterAmountHint": "Ingresa cantidad (ejemplo: 100 o 100-500)", @@ -229,7 +228,6 @@ "premiumTitle": "Prima (%)", "premiumTooltip": "Ajusta cuánto por encima o por debajo del precio de mercado quieres tu oferta. Por defecto, está establecido en 0%, sin prima o descuento, así que si no quieres cambiar el precio, puedes dejarlo como está.", - "@_comment_account_screen": "Cadenas de Pantalla de Gestión de Cuenta", "secretWords": "Palabras Secretas", "toRestoreYourAccount": "Para restaurar tu cuenta", "privacy": "Privacidad", @@ -244,7 +242,6 @@ "noMnemonicFound": "No se encontró mnemónico", "errorLoadingMnemonic": "Error: {error}", - "@_comment_chat_screens": "Cadenas de Pantalla de Chat", "noMessagesAvailable": "No hay mensajes disponibles", "back": "ATRÁS", "typeAMessage": "Escribe un mensaje...", @@ -252,18 +249,15 @@ "yourHandle": "Tu nombre: {handle}", "yourSharedKey": "Tu clave compartida:", - "@_comment_trade_actions": "Acciones de Detalle de Intercambio", "payInvoiceButton": "PAGAR FACTURA", "addInvoiceButton": "AGREGAR FACTURA", "release": "LIBERAR", "takeSell": "TOMAR VENTA", "takeBuy": "TOMAR COMPRA", - "@_comment_currency_errors": "Mensajes de Error de Moneda", "noExchangeDataAvailable": "No hay datos de intercambio disponibles", "errorFetchingCurrencies": "Error obteniendo monedas", - "@_comment_currency_section": "Cadenas de Sección de Selección de Moneda", "selectFiatCurrencyPay": "Selecciona la moneda fiat con la que pagarás", "selectFiatCurrencyReceive": "Selecciona la Moneda Fiat que quieres recibir", "loadingCurrencies": "Cargando monedas...", @@ -273,18 +267,11 @@ "searchCurrencies": "Buscar monedas...", "noCurrenciesFound": "No se encontraron monedas", - "@_comment_price_type_section": "Cadenas de Sección de Tipo de Precio", "priceType": "Tipo de precio", - "marketPrice": "Precio de mercado", "fixedPrice": "Precio fijo", "market": "Mercado", "priceTypeTooltip": "• Selecciona Precio de Mercado si quieres usar el precio que tiene Bitcoin cuando alguien tome tu oferta.\\n• Selecciona Precio Fijo si quieres definir la cantidad exacta de Bitcoin que intercambiarás.", - "@_comment_take_order_screen": "Cadenas de Pantalla de Tomar Orden", - "orderDetails": "DETALLES DE LA ORDEN", - "selling": "vendiendo", - "buying": "comprando", - "atMarketPrice": "a precio de mercado", "withPremiumPercent": "con un +{premium}% de premio", "@withPremiumPercent": { "placeholders": { @@ -301,7 +288,6 @@ } } }, - "noPaymentMethod": "Sin método de pago", "someoneIsSellingBuying": "Alguien está {action}{satAmount} sats por {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -373,31 +359,27 @@ } }, - "@_comment_lightning_invoice": "Cadenas de Widget de Factura Lightning", "pleaseEnterLightningInvoiceFor": "Por favor ingresa una Factura Lightning para: ", "sats": " sats", "lightningInvoice": "Factura Lightning", "enterInvoiceHere": "Ingresa la factura aquí", - "@_comment_trade_detail_screen": "Cadenas de Pantalla de Detalle de Intercambio", - "cancelPending": "CANCELACIÓN PENDIENTE", - "acceptCancel": "ACEPTAR CANCELACIÓN", - "completePurchase": "COMPLETAR COMPRA", - "rate": "CALIFICAR", - "contact": "CONTACTAR", + "cancelPendingButton": "CANCELACIÓN PENDIENTE", + "acceptCancelButton": "ACEPTAR CANCELACIÓN", + "completePurchaseButton": "COMPLETAR COMPRA", + "rateButton": "CALIFICAR", + "contactButton": "CONTACTAR", - "@_comment_rate_screen": "Cadenas de Pantalla de Calificar Contraparte", "rateCounterpart": "Calificar Contraparte", "submitRating": "Enviar Calificación", - "@_comment_pay_invoice_screen": "Cadenas de Pantalla de Pago de Factura Lightning", "payLightningInvoice": "Pagar Factura Lightning", "payInvoiceToContinue": "Paga esta factura para continuar el intercambio", "failedToGenerateQR": "Error al generar código QR", "invoiceCopiedToClipboard": "Factura copiada al portapapeles", - "copy": "Copiar", - "share": "Compartir", + "copyButton": "Copiar", + "shareButton": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", - "openWallet": "ABRIR BILLETERA", - "done": "HECHO" + "openWalletButton": "ABRIR BILLETERA", + "doneButton": "HECHO" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index aab46113..fa199f06 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -213,7 +213,6 @@ "holdInvoiceCltvDelta": "Delta CLTV Fattura Hold", "invoiceExpirationWindow": "Finestra Scadenza Fattura", - "@_comment_order_creation": "Stringhe Modulo Creazione Ordine", "enterFiatAmountBuy": "Inserisci l'importo fiat che vuoi pagare (puoi impostare un intervallo)", "enterFiatAmountSell": "Inserisci l'importo fiat che vuoi ricevere (puoi impostare un intervallo)", "enterAmountHint": "Inserisci importo (esempio: 100 o 100-500)", @@ -229,7 +228,6 @@ "premiumTitle": "Premio (%)", "premiumTooltip": "Regola quanto sopra o sotto il prezzo di mercato vuoi la tua offerta. Di default, è impostato al 0%, senza premio o sconto, quindi se non vuoi cambiare il prezzo, puoi lasciarlo così com'è.", - "@_comment_account_screen": "Stringhe Schermata Gestione Account", "secretWords": "Parole Segrete", "toRestoreYourAccount": "Per ripristinare il tuo account", "privacy": "Privacy", @@ -244,7 +242,6 @@ "noMnemonicFound": "Nessun mnemonico trovato", "errorLoadingMnemonic": "Errore: {error}", - "@_comment_chat_screens": "Stringhe Schermata Chat", "noMessagesAvailable": "Nessun messaggio disponibile", "back": "INDIETRO", "typeAMessage": "Scrivi un messaggio...", @@ -252,18 +249,15 @@ "yourHandle": "Il tuo nome: {handle}", "yourSharedKey": "La tua chiave condivisa:", - "@_comment_trade_actions": "Azioni Dettaglio Scambio", "payInvoiceButton": "PAGA FATTURA", "addInvoiceButton": "AGGIUNGI FATTURA", "release": "RILASCIA", "takeSell": "PRENDI VENDITA", "takeBuy": "PRENDI ACQUISTO", - "@_comment_currency_errors": "Messaggi Errore Valuta", "noExchangeDataAvailable": "Nessun dato di cambio disponibile", "errorFetchingCurrencies": "Errore nel recupero delle valute", - "@_comment_currency_section": "Stringhe Sezione Selezione Valuta", "selectFiatCurrencyPay": "Seleziona la valuta fiat con cui pagherai", "selectFiatCurrencyReceive": "Seleziona la Valuta Fiat che vuoi ricevere", "loadingCurrencies": "Caricamento valute...", @@ -273,18 +267,11 @@ "searchCurrencies": "Cerca valute...", "noCurrenciesFound": "Nessuna valuta trovata", - "@_comment_price_type_section": "Stringhe Sezione Tipo di Prezzo", "priceType": "Tipo di prezzo", - "marketPrice": "Prezzo di mercato", "fixedPrice": "Prezzo fisso", "market": "Mercato", "priceTypeTooltip": "• Seleziona Prezzo di Mercato se vuoi usare il prezzo che ha Bitcoin quando qualcuno prende la tua offerta.\\n• Seleziona Prezzo Fisso se vuoi definire l'importo esatto di Bitcoin che scambierai.", - "@_comment_take_order_screen": "Stringhe Schermata Prendi Ordine", - "orderDetails": "DETTAGLI ORDINE", - "selling": "vendendo", - "buying": "comprando", - "atMarketPrice": "a prezzo di mercato", "withPremiumPercent": "con un +{premium}% di premio", "@withPremiumPercent": { "placeholders": { @@ -301,7 +288,6 @@ } } }, - "noPaymentMethod": "Nessun metodo di pagamento", "someoneIsSellingBuying": "Qualcuno sta {action}{satAmount} sats per {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -373,31 +359,27 @@ } }, - "@_comment_lightning_invoice": "Stringhe Widget Fattura Lightning", "pleaseEnterLightningInvoiceFor": "Per favore inserisci una Fattura Lightning per: ", "sats": " sats", "lightningInvoice": "Fattura Lightning", "enterInvoiceHere": "Inserisci la fattura qui", - "@_comment_trade_detail_screen": "Stringhe Schermata Dettaglio Scambio", - "cancelPending": "CANCELLAZIONE IN ATTESA", - "acceptCancel": "ACCETTA CANCELLAZIONE", - "completePurchase": "COMPLETA ACQUISTO", - "rate": "VALUTA", - "contact": "CONTATTA", + "cancelPendingButton": "CANCELLAZIONE IN ATTESA", + "acceptCancelButton": "ACCETTA CANCELLAZIONE", + "completePurchaseButton": "COMPLETA ACQUISTO", + "rateButton": "VALUTA", + "contactButton": "CONTATTA", - "@_comment_rate_screen": "Stringhe Schermata Valuta Controparte", "rateCounterpart": "Valuta Controparte", "submitRating": "Invia Valutazione", - "@_comment_pay_invoice_screen": "Stringhe Schermata Pagamento Fattura Lightning", "payLightningInvoice": "Paga Fattura Lightning", "payInvoiceToContinue": "Paga questa fattura per continuare lo scambio", "failedToGenerateQR": "Errore nella generazione del codice QR", "invoiceCopiedToClipboard": "Fattura copiata negli appunti", - "copy": "Copia", - "share": "Condividi", + "copyButton": "Copia", + "shareButton": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", - "openWallet": "APRI PORTAFOGLIO", - "done": "FATTO" + "openWalletButton": "APRI PORTAFOGLIO", + "doneButton": "FATTO" } diff --git a/lib/shared/widgets/pay_lightning_invoice_widget.dart b/lib/shared/widgets/pay_lightning_invoice_widget.dart index 5ade1f42..1a0663e8 100644 --- a/lib/shared/widgets/pay_lightning_invoice_widget.dart +++ b/lib/shared/widgets/pay_lightning_invoice_widget.dart @@ -72,7 +72,7 @@ class _PayLightningInvoiceWidgetState extends State { ); }, icon: const Icon(Icons.copy), - label: Text(S.of(context)!.copy), + label: Text(S.of(context)!.copyButton), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.mostroGreen, shape: RoundedRectangleBorder( @@ -100,7 +100,7 @@ class _PayLightningInvoiceWidgetState extends State { } }, icon: const Icon(Icons.share), - label: Text(S.of(context)!.share), + label: Text(S.of(context)!.shareButton), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.mostroGreen, shape: RoundedRectangleBorder( @@ -120,7 +120,7 @@ class _PayLightningInvoiceWidgetState extends State { ), ), onPressed: widget.onSubmit, - child: Text(S.of(context)!.openWallet), + child: Text(S.of(context)!.openWalletButton), ), const SizedBox(height: 20), Row( diff --git a/pubspec.lock b/pubspec.lock index 26ce8fb4..47cad89f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_config: dependency: transitive description: @@ -141,26 +141,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -495,10 +495,10 @@ packages: dependency: "direct main" description: name: flutter_launcher_icons - sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -511,10 +511,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: b94a50aabbe56ef254f95f3be75640f99120429f0a153b2dc30143cffc9bfdf3 + sha256: edae0c34573233ab03f5ba1f07465e55c384743893042cb19e010b4ee8541c12 url: "https://pub.dev" source: hosted - version: "19.2.1" + version: "19.3.0" flutter_local_notifications_linux: dependency: transitive description: @@ -527,10 +527,10 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.1.0" flutter_local_notifications_windows: dependency: transitive description: @@ -612,10 +612,10 @@ packages: dependency: transitive description: name: flutter_svg - sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter @@ -630,10 +630,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -659,10 +659,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175 + sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 url: "https://pub.dev" source: hosted - version: "15.1.3" + version: "16.0.0" google_fonts: dependency: "direct main" description: @@ -731,10 +731,10 @@ packages: dependency: transitive description: name: idb_shim - sha256: "109ce57e7ae8a758e806c24669bf7809f0e4c0bc390159589819061ec2ca75c0" + sha256: ee391deb010143823d25db15f8b002945e19dcb5f2dd5b696a98cb6db7644012 url: "https://pub.dev" source: hosted - version: "2.6.6+2" + version: "2.6.7" image: dependency: transitive description: @@ -840,10 +840,10 @@ packages: dependency: transitive description: name: local_auth_darwin - sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" + sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f" url: "https://pub.dev" source: hosted - version: "1.4.3" + version: "1.5.0" local_auth_platform_interface: dependency: transitive description: @@ -864,10 +864,10 @@ packages: dependency: "direct main" description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" logging: dependency: transitive description: @@ -912,10 +912,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mockito: dependency: "direct dev" description: @@ -1017,10 +1017,10 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "12.0.0+1" + version: "12.0.1" permission_handler_android: dependency: transitive description: @@ -1105,10 +1105,10 @@ packages: dependency: transitive description: name: posix - sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" process: dependency: transitive description: @@ -1193,26 +1193,26 @@ packages: dependency: "direct main" description: name: sembast_web - sha256: a3ae64d8b1e87af98fbfcb590496e88dd6374ae362bd960ffa692130ee74b9c5 + sha256: "0362c7c241ad6546d3e27b4cfffaae505e5a9661e238dbcdd176756cc960fe7a" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" share_plus: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.2" shared_preferences: dependency: "direct main" description: @@ -1297,10 +1297,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -1398,10 +1398,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1510,10 +1510,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -1550,18 +1550,18 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.1" web_socket_channel: dependency: transitive description: @@ -1590,10 +1590,10 @@ packages: dependency: transitive description: name: win32 - sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "5.14.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 791838fa..7b33d3e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: intl: ^0.20.2 uuid: ^4.5.1 flutter_secure_storage: ^10.0.0-beta.4 - go_router: ^15.0.0 + go_router: ^16.0.0 bip39: ^1.0.6 flutter_hooks: ^0.21.2 hooks_riverpod: ^2.6.1 @@ -72,7 +72,7 @@ dependencies: url: https://github.com/chebizarro/dart-nip44.git ref: master - share_plus: ^9.0.0 + share_plus: ^10.0.0 flutter_local_notifications: ^19.0.0 flutter_background_service: ^5.1.0 path_provider: ^2.1.5 diff --git a/test/mocks.dart b/test/mocks.dart index 6dbbda48..da865e33 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -11,9 +11,8 @@ import 'package:mostro_mobile/features/key_manager/key_manager.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_notifier.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; -import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; -import 'package:mostro_mobile/features/subscriptions/subscription.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; import 'package:sembast/sembast.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -22,6 +21,7 @@ import 'mocks.mocks.dart'; @GenerateMocks([ MostroService, + NostrService, OpenOrdersRepository, SharedPreferencesAsync, Database, @@ -75,87 +75,5 @@ class MockSessionNotifier extends SessionNotifier { } } -// Custom mock for SubscriptionManager -class MockSubscriptionManager extends SubscriptionManager { - final StreamController _ordersController = - StreamController.broadcast(); - final StreamController _chatController = - StreamController.broadcast(); - final Map _subscriptions = {}; - NostrFilter? _lastFilter; - - MockSubscriptionManager() : super(MockRef()); - - NostrFilter? get lastFilter => _lastFilter; - - @override - Stream get orders => _ordersController.stream; - - @override - Stream get chat => _chatController.stream; - - @override - Stream subscribe({ - required SubscriptionType type, - required NostrFilter filter, - }) { - _lastFilter = filter; - - final request = NostrRequest(filters: [filter]); - - final subscription = Subscription( - request: request, - streamSubscription: (type == SubscriptionType.orders - ? _ordersController.stream - : _chatController.stream) - .listen((_) {}), - onCancel: () {}, - ); - - _subscriptions[type] = subscription; - - return type == SubscriptionType.orders ? orders : chat; - } - - @override - void unsubscribeByType(SubscriptionType type) { - _subscriptions.remove(type); - } - - @override - void unsubscribeAll() { - _subscriptions.clear(); - } - - @override - List getActiveFilters(SubscriptionType type) { - final subscription = _subscriptions[type]; - if (subscription != null && subscription.request.filters.isNotEmpty) { - return [subscription.request.filters.first]; - } - return []; - } - - @override - bool hasActiveSubscription(SubscriptionType type) { - return _subscriptions.containsKey(type); - } - - // Helper to add events to the stream - void addEvent(NostrEvent event, SubscriptionType type) { - if (type == SubscriptionType.orders) { - _ordersController.add(event); - } else if (type == SubscriptionType.chat) { - _chatController.add(event); - } - } - - @override - void dispose() { - _ordersController.close(); - _chatController.close(); - super.dispose(); - } -} void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 02d67bcd..ec460cb2 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -3,27 +3,29 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; +import 'dart:async' as _i4; -import 'package:dart_nostr/dart_nostr.dart' as _i6; +import 'package:dart_nostr/dart_nostr.dart' as _i7; +import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i10; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; -import 'package:mostro_mobile/data/models.dart' as _i5; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i15; +import 'package:mockito/src/dummies.dart' as _i13; +import 'package:mostro_mobile/data/models.dart' as _i6; +import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i17; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' - as _i9; -import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i13; -import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i14; -import 'package:mostro_mobile/features/settings/settings.dart' as _i7; + as _i11; +import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i15; +import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i16; +import 'package:mostro_mobile/features/settings/settings.dart' as _i3; import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart' - as _i16; + as _i18; import 'package:mostro_mobile/features/subscriptions/subscription_type.dart' - as _i17; + as _i19; import 'package:mostro_mobile/services/mostro_service.dart' as _i8; -import 'package:sembast/sembast.dart' as _i4; -import 'package:sembast/src/api/transaction.dart' as _i13; -import 'package:shared_preferences/src/shared_preferences_async.dart' as _i11; +import 'package:mostro_mobile/services/nostr_service.dart' as _i9; +import 'package:sembast/sembast.dart' as _i5; +import 'package:sembast/src/api/transaction.dart' as _i14; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i12; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -50,8 +52,8 @@ class _FakeRef_0 extends _i1.SmartFake ); } -class _FakeFuture_1 extends _i1.SmartFake implements _i3.Future { - _FakeFuture_1( +class _FakeSettings_1 extends _i1.SmartFake implements _i3.Settings { + _FakeSettings_1( Object parent, Invocation parentInvocation, ) : super( @@ -60,8 +62,8 @@ class _FakeFuture_1 extends _i1.SmartFake implements _i3.Future { ); } -class _FakeDatabase_2 extends _i1.SmartFake implements _i4.Database { - _FakeDatabase_2( +class _FakeFuture_2 extends _i1.SmartFake implements _i4.Future { + _FakeFuture_2( Object parent, Invocation parentInvocation, ) : super( @@ -70,9 +72,8 @@ class _FakeDatabase_2 extends _i1.SmartFake implements _i4.Database { ); } -class _FakeStoreRef_3 - extends _i1.SmartFake implements _i4.StoreRef { - _FakeStoreRef_3( +class _FakeDatabase_3 extends _i1.SmartFake implements _i5.Database { + _FakeDatabase_3( Object parent, Invocation parentInvocation, ) : super( @@ -81,8 +82,9 @@ class _FakeStoreRef_3 ); } -class _FakeSession_4 extends _i1.SmartFake implements _i5.Session { - _FakeSession_4( +class _FakeStoreRef_4 + extends _i1.SmartFake implements _i5.StoreRef { + _FakeStoreRef_4( Object parent, Invocation parentInvocation, ) : super( @@ -91,8 +93,8 @@ class _FakeSession_4 extends _i1.SmartFake implements _i5.Session { ); } -class _FakeFilter_5 extends _i1.SmartFake implements _i4.Filter { - _FakeFilter_5( +class _FakeSession_5 extends _i1.SmartFake implements _i6.Session { + _FakeSession_5( Object parent, Invocation parentInvocation, ) : super( @@ -101,8 +103,8 @@ class _FakeFilter_5 extends _i1.SmartFake implements _i4.Filter { ); } -class _FakeNostrKeyPairs_6 extends _i1.SmartFake implements _i6.NostrKeyPairs { - _FakeNostrKeyPairs_6( +class _FakeFilter_6 extends _i1.SmartFake implements _i5.Filter { + _FakeFilter_6( Object parent, Invocation parentInvocation, ) : super( @@ -111,9 +113,8 @@ class _FakeNostrKeyPairs_6 extends _i1.SmartFake implements _i6.NostrKeyPairs { ); } -class _FakeMostroMessage_7 extends _i1.SmartFake - implements _i5.MostroMessage { - _FakeMostroMessage_7( +class _FakeNostrKeyPairs_7 extends _i1.SmartFake implements _i7.NostrKeyPairs { + _FakeNostrKeyPairs_7( Object parent, Invocation parentInvocation, ) : super( @@ -122,8 +123,9 @@ class _FakeMostroMessage_7 extends _i1.SmartFake ); } -class _FakeSettings_8 extends _i1.SmartFake implements _i7.Settings { - _FakeSettings_8( +class _FakeMostroMessage_8 extends _i1.SmartFake + implements _i6.MostroMessage { + _FakeMostroMessage_8( Object parent, Invocation parentInvocation, ) : super( @@ -200,36 +202,18 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { ); @override - void subscribe(_i5.Session? session) => super.noSuchMethod( - Invocation.method( - #subscribe, - [session], - ), - returnValueForMissingStub: null, - ); - - @override - void unsubscribe(_i5.Session? session) => super.noSuchMethod( - Invocation.method( - #unsubscribe, - [session], - ), - returnValueForMissingStub: null, - ); - - @override - _i3.Future submitOrder(_i5.MostroMessage<_i5.Payload>? order) => + _i4.Future submitOrder(_i6.MostroMessage<_i6.Payload>? order) => (super.noSuchMethod( Invocation.method( #submitOrder, [order], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future takeBuyOrder( + _i4.Future takeBuyOrder( String? orderId, int? amount, ) => @@ -241,12 +225,12 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { amount, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future takeSellOrder( + _i4.Future takeSellOrder( String? orderId, int? amount, String? lnAddress, @@ -260,12 +244,12 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { lnAddress, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future sendInvoice( + _i4.Future sendInvoice( String? orderId, String? invoice, int? amount, @@ -279,52 +263,52 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { amount, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future cancelOrder(String? orderId) => (super.noSuchMethod( + _i4.Future cancelOrder(String? orderId) => (super.noSuchMethod( Invocation.method( #cancelOrder, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future sendFiatSent(String? orderId) => (super.noSuchMethod( + _i4.Future sendFiatSent(String? orderId) => (super.noSuchMethod( Invocation.method( #sendFiatSent, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future releaseOrder(String? orderId) => (super.noSuchMethod( + _i4.Future releaseOrder(String? orderId) => (super.noSuchMethod( Invocation.method( #releaseOrder, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future disputeOrder(String? orderId) => (super.noSuchMethod( + _i4.Future disputeOrder(String? orderId) => (super.noSuchMethod( Invocation.method( #disputeOrder, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future submitRating( + _i4.Future submitRating( String? orderId, int? rating, ) => @@ -336,23 +320,23 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { rating, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future publishOrder(_i5.MostroMessage<_i5.Payload>? order) => + _i4.Future publishOrder(_i6.MostroMessage<_i6.Payload>? order) => (super.noSuchMethod( Invocation.method( #publishOrder, [order], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - void updateSettings(_i7.Settings? settings) => super.noSuchMethod( + void updateSettings(_i3.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -361,20 +345,123 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { ); } +/// A class which mocks [NostrService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNostrService extends _i1.Mock implements _i9.NostrService { + MockNostrService() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Settings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeSettings_1( + this, + Invocation.getter(#settings), + ), + ) as _i3.Settings); + + @override + bool get isInitialized => (super.noSuchMethod( + Invocation.getter(#isInitialized), + returnValue: false, + ) as bool); + + @override + set settings(_i3.Settings? _settings) => super.noSuchMethod( + Invocation.setter( + #settings, + _settings, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future init(_i3.Settings? settings) => (super.noSuchMethod( + Invocation.method( + #init, + [settings], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future updateSettings(_i3.Settings? newSettings) => + (super.noSuchMethod( + Invocation.method( + #updateSettings, + [newSettings], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future<_i10.RelayInformations?> getRelayInfo(String? relayUrl) => + (super.noSuchMethod( + Invocation.method( + #getRelayInfo, + [relayUrl], + ), + returnValue: _i4.Future<_i10.RelayInformations?>.value(), + ) as _i4.Future<_i10.RelayInformations?>); + + @override + _i4.Future publishEvent(_i7.NostrEvent? event) => (super.noSuchMethod( + Invocation.method( + #publishEvent, + [event], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Stream<_i7.NostrEvent> subscribeToEvents(_i7.NostrRequest? request) => + (super.noSuchMethod( + Invocation.method( + #subscribeToEvents, + [request], + ), + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); + + @override + _i4.Future disconnectFromRelays() => (super.noSuchMethod( + Invocation.method( + #disconnectFromRelays, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + void unsubscribe(String? id) => super.noSuchMethod( + Invocation.method( + #unsubscribe, + [id], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [OpenOrdersRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockOpenOrdersRepository extends _i1.Mock - implements _i9.OpenOrdersRepository { + implements _i11.OpenOrdersRepository { MockOpenOrdersRepository() { _i1.throwOnMissingStub(this); } @override - _i3.Stream> get eventsStream => (super.noSuchMethod( + _i4.Stream> get eventsStream => (super.noSuchMethod( Invocation.getter(#eventsStream), - returnValue: _i3.Stream>.empty(), - ) as _i3.Stream>); + returnValue: _i4.Stream>.empty(), + ) as _i4.Stream>); @override void dispose() => super.noSuchMethod( @@ -386,56 +473,56 @@ class MockOpenOrdersRepository extends _i1.Mock ); @override - _i3.Future<_i6.NostrEvent?> getOrderById(String? orderId) => + _i4.Future<_i7.NostrEvent?> getOrderById(String? orderId) => (super.noSuchMethod( Invocation.method( #getOrderById, [orderId], ), - returnValue: _i3.Future<_i6.NostrEvent?>.value(), - ) as _i3.Future<_i6.NostrEvent?>); + returnValue: _i4.Future<_i7.NostrEvent?>.value(), + ) as _i4.Future<_i7.NostrEvent?>); @override - _i3.Future addOrder(_i6.NostrEvent? order) => (super.noSuchMethod( + _i4.Future addOrder(_i7.NostrEvent? order) => (super.noSuchMethod( Invocation.method( #addOrder, [order], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteOrder(String? orderId) => (super.noSuchMethod( + _i4.Future deleteOrder(String? orderId) => (super.noSuchMethod( Invocation.method( #deleteOrder, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future> getAllOrders() => (super.noSuchMethod( + _i4.Future> getAllOrders() => (super.noSuchMethod( Invocation.method( #getAllOrders, [], ), - returnValue: _i3.Future>.value(<_i6.NostrEvent>[]), - ) as _i3.Future>); + returnValue: _i4.Future>.value(<_i7.NostrEvent>[]), + ) as _i4.Future>); @override - _i3.Future updateOrder(_i6.NostrEvent? order) => (super.noSuchMethod( + _i4.Future updateOrder(_i7.NostrEvent? order) => (super.noSuchMethod( Invocation.method( #updateOrder, [order], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - void updateSettings(_i7.Settings? settings) => super.noSuchMethod( + void updateSettings(_i3.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -458,24 +545,24 @@ class MockOpenOrdersRepository extends _i1.Mock /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock - implements _i10.SharedPreferencesAsync { + implements _i12.SharedPreferencesAsync { MockSharedPreferencesAsync() { _i1.throwOnMissingStub(this); } @override - _i3.Future> getKeys({Set? allowList}) => + _i4.Future> getKeys({Set? allowList}) => (super.noSuchMethod( Invocation.method( #getKeys, [], {#allowList: allowList}, ), - returnValue: _i3.Future>.value({}), - ) as _i3.Future>); + returnValue: _i4.Future>.value({}), + ) as _i4.Future>); @override - _i3.Future> getAll({Set? allowList}) => + _i4.Future> getAll({Set? allowList}) => (super.noSuchMethod( Invocation.method( #getAll, @@ -483,65 +570,65 @@ class MockSharedPreferencesAsync extends _i1.Mock {#allowList: allowList}, ), returnValue: - _i3.Future>.value({}), - ) as _i3.Future>); + _i4.Future>.value({}), + ) as _i4.Future>); @override - _i3.Future getBool(String? key) => (super.noSuchMethod( + _i4.Future getBool(String? key) => (super.noSuchMethod( Invocation.method( #getBool, [key], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future getInt(String? key) => (super.noSuchMethod( + _i4.Future getInt(String? key) => (super.noSuchMethod( Invocation.method( #getInt, [key], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future getDouble(String? key) => (super.noSuchMethod( + _i4.Future getDouble(String? key) => (super.noSuchMethod( Invocation.method( #getDouble, [key], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future getString(String? key) => (super.noSuchMethod( + _i4.Future getString(String? key) => (super.noSuchMethod( Invocation.method( #getString, [key], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future?> getStringList(String? key) => (super.noSuchMethod( + _i4.Future?> getStringList(String? key) => (super.noSuchMethod( Invocation.method( #getStringList, [key], ), - returnValue: _i3.Future?>.value(), - ) as _i3.Future?>); + returnValue: _i4.Future?>.value(), + ) as _i4.Future?>); @override - _i3.Future containsKey(String? key) => (super.noSuchMethod( + _i4.Future containsKey(String? key) => (super.noSuchMethod( Invocation.method( #containsKey, [key], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future setBool( + _i4.Future setBool( String? key, bool? value, ) => @@ -553,12 +640,12 @@ class MockSharedPreferencesAsync extends _i1.Mock value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future setInt( + _i4.Future setInt( String? key, int? value, ) => @@ -570,12 +657,12 @@ class MockSharedPreferencesAsync extends _i1.Mock value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future setDouble( + _i4.Future setDouble( String? key, double? value, ) => @@ -587,12 +674,12 @@ class MockSharedPreferencesAsync extends _i1.Mock value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future setString( + _i4.Future setString( String? key, String? value, ) => @@ -604,12 +691,12 @@ class MockSharedPreferencesAsync extends _i1.Mock value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future setStringList( + _i4.Future setStringList( String? key, List? value, ) => @@ -621,36 +708,36 @@ class MockSharedPreferencesAsync extends _i1.Mock value, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future remove(String? key) => (super.noSuchMethod( + _i4.Future remove(String? key) => (super.noSuchMethod( Invocation.method( #remove, [key], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future clear({Set? allowList}) => (super.noSuchMethod( + _i4.Future clear({Set? allowList}) => (super.noSuchMethod( Invocation.method( #clear, [], {#allowList: allowList}, ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [Database]. /// /// See the documentation for Mockito's code generation for more information. -class MockDatabase extends _i1.Mock implements _i4.Database { +class MockDatabase extends _i1.Mock implements _i5.Database { MockDatabase() { _i1.throwOnMissingStub(this); } @@ -664,77 +751,77 @@ class MockDatabase extends _i1.Mock implements _i4.Database { @override String get path => (super.noSuchMethod( Invocation.getter(#path), - returnValue: _i11.dummyValue( + returnValue: _i13.dummyValue( this, Invocation.getter(#path), ), ) as String); @override - _i3.Future transaction( - _i3.FutureOr Function(_i13.Transaction)? action) => + _i4.Future transaction( + _i4.FutureOr Function(_i14.Transaction)? action) => (super.noSuchMethod( Invocation.method( #transaction, [action], ), - returnValue: _i11.ifNotNull( - _i11.dummyValueOrNull( + returnValue: _i13.ifNotNull( + _i13.dummyValueOrNull( this, Invocation.method( #transaction, [action], ), ), - (T v) => _i3.Future.value(v), + (T v) => _i4.Future.value(v), ) ?? - _FakeFuture_1( + _FakeFuture_2( this, Invocation.method( #transaction, [action], ), ), - ) as _i3.Future); + ) as _i4.Future); @override - _i3.Future close() => (super.noSuchMethod( + _i4.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [SessionStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { +class MockSessionStorage extends _i1.Mock implements _i15.SessionStorage { MockSessionStorage() { _i1.throwOnMissingStub(this); } @override - _i4.Database get db => (super.noSuchMethod( + _i5.Database get db => (super.noSuchMethod( Invocation.getter(#db), - returnValue: _FakeDatabase_2( + returnValue: _FakeDatabase_3( this, Invocation.getter(#db), ), - ) as _i4.Database); + ) as _i5.Database); @override - _i4.StoreRef> get store => (super.noSuchMethod( + _i5.StoreRef> get store => (super.noSuchMethod( Invocation.getter(#store), - returnValue: _FakeStoreRef_3>( + returnValue: _FakeStoreRef_4>( this, Invocation.getter(#store), ), - ) as _i4.StoreRef>); + ) as _i5.StoreRef>); @override - Map toDbMap(_i5.Session? session) => (super.noSuchMethod( + Map toDbMap(_i6.Session? session) => (super.noSuchMethod( Invocation.method( #toDbMap, [session], @@ -743,7 +830,7 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { ) as Map); @override - _i5.Session fromDbMap( + _i6.Session fromDbMap( String? key, Map? jsonMap, ) => @@ -755,7 +842,7 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { jsonMap, ], ), - returnValue: _FakeSession_4( + returnValue: _FakeSession_5( this, Invocation.method( #fromDbMap, @@ -765,69 +852,69 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { ], ), ), - ) as _i5.Session); + ) as _i6.Session); @override - _i3.Future putSession(_i5.Session? session) => (super.noSuchMethod( + _i4.Future putSession(_i6.Session? session) => (super.noSuchMethod( Invocation.method( #putSession, [session], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i5.Session?> getSession(String? sessionId) => (super.noSuchMethod( + _i4.Future<_i6.Session?> getSession(String? sessionId) => (super.noSuchMethod( Invocation.method( #getSession, [sessionId], ), - returnValue: _i3.Future<_i5.Session?>.value(), - ) as _i3.Future<_i5.Session?>); + returnValue: _i4.Future<_i6.Session?>.value(), + ) as _i4.Future<_i6.Session?>); @override - _i3.Future> getAllSessions() => (super.noSuchMethod( + _i4.Future> getAllSessions() => (super.noSuchMethod( Invocation.method( #getAllSessions, [], ), - returnValue: _i3.Future>.value(<_i5.Session>[]), - ) as _i3.Future>); + returnValue: _i4.Future>.value(<_i6.Session>[]), + ) as _i4.Future>); @override - _i3.Future deleteSession(String? sessionId) => (super.noSuchMethod( + _i4.Future deleteSession(String? sessionId) => (super.noSuchMethod( Invocation.method( #deleteSession, [sessionId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Stream<_i5.Session?> watchSession(String? sessionId) => + _i4.Stream<_i6.Session?> watchSession(String? sessionId) => (super.noSuchMethod( Invocation.method( #watchSession, [sessionId], ), - returnValue: _i3.Stream<_i5.Session?>.empty(), - ) as _i3.Stream<_i5.Session?>); + returnValue: _i4.Stream<_i6.Session?>.empty(), + ) as _i4.Stream<_i6.Session?>); @override - _i3.Stream> watchAllSessions() => (super.noSuchMethod( + _i4.Stream> watchAllSessions() => (super.noSuchMethod( Invocation.method( #watchAllSessions, [], ), - returnValue: _i3.Stream>.empty(), - ) as _i3.Stream>); + returnValue: _i4.Stream>.empty(), + ) as _i4.Stream>); @override - _i3.Future putItem( + _i4.Future putItem( String? id, - _i5.Session? item, + _i6.Session? item, ) => (super.noSuchMethod( Invocation.method( @@ -837,61 +924,61 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { item, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i5.Session?> getItem(String? id) => (super.noSuchMethod( + _i4.Future<_i6.Session?> getItem(String? id) => (super.noSuchMethod( Invocation.method( #getItem, [id], ), - returnValue: _i3.Future<_i5.Session?>.value(), - ) as _i3.Future<_i5.Session?>); + returnValue: _i4.Future<_i6.Session?>.value(), + ) as _i4.Future<_i6.Session?>); @override - _i3.Future hasItem(String? id) => (super.noSuchMethod( + _i4.Future hasItem(String? id) => (super.noSuchMethod( Invocation.method( #hasItem, [id], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future deleteItem(String? id) => (super.noSuchMethod( + _i4.Future deleteItem(String? id) => (super.noSuchMethod( Invocation.method( #deleteItem, [id], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteAll() => (super.noSuchMethod( + _i4.Future deleteAll() => (super.noSuchMethod( Invocation.method( #deleteAll, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteWhere(_i4.Filter? filter) => (super.noSuchMethod( + _i4.Future deleteWhere(_i5.Filter? filter) => (super.noSuchMethod( Invocation.method( #deleteWhere, [filter], ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + returnValue: _i4.Future.value(0), + ) as _i4.Future); @override - _i3.Future> find({ - _i4.Filter? filter, - List<_i4.SortOrder>? sort, + _i4.Future> find({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, int? limit, int? offset, }) => @@ -906,22 +993,22 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { #offset: offset, }, ), - returnValue: _i3.Future>.value(<_i5.Session>[]), - ) as _i3.Future>); + returnValue: _i4.Future>.value(<_i6.Session>[]), + ) as _i4.Future>); @override - _i3.Future> getAll() => (super.noSuchMethod( + _i4.Future> getAll() => (super.noSuchMethod( Invocation.method( #getAll, [], ), - returnValue: _i3.Future>.value(<_i5.Session>[]), - ) as _i3.Future>); + returnValue: _i4.Future>.value(<_i6.Session>[]), + ) as _i4.Future>); @override - _i3.Stream> watch({ - _i4.Filter? filter, - List<_i4.SortOrder>? sort, + _i4.Stream> watch({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, }) => (super.noSuchMethod( Invocation.method( @@ -932,20 +1019,20 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { #sort: sort, }, ), - returnValue: _i3.Stream>.empty(), - ) as _i3.Stream>); + returnValue: _i4.Stream>.empty(), + ) as _i4.Stream>); @override - _i3.Stream<_i5.Session?> watchById(String? id) => (super.noSuchMethod( + _i4.Stream<_i6.Session?> watchById(String? id) => (super.noSuchMethod( Invocation.method( #watchById, [id], ), - returnValue: _i3.Stream<_i5.Session?>.empty(), - ) as _i3.Stream<_i5.Session?>); + returnValue: _i4.Stream<_i6.Session?>.empty(), + ) as _i4.Stream<_i6.Session?>); @override - _i4.Filter eq( + _i5.Filter eq( String? field, Object? value, ) => @@ -957,7 +1044,7 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { value, ], ), - returnValue: _FakeFilter_5( + returnValue: _FakeFilter_6( this, Invocation.method( #eq, @@ -967,19 +1054,19 @@ class MockSessionStorage extends _i1.Mock implements _i13.SessionStorage { ], ), ), - ) as _i4.Filter); + ) as _i5.Filter); } /// A class which mocks [KeyManager]. /// /// See the documentation for Mockito's code generation for more information. -class MockKeyManager extends _i1.Mock implements _i14.KeyManager { +class MockKeyManager extends _i1.Mock implements _i16.KeyManager { MockKeyManager() { _i1.throwOnMissingStub(this); } @override - set masterKeyPair(_i6.NostrKeyPairs? _masterKeyPair) => super.noSuchMethod( + set masterKeyPair(_i7.NostrKeyPairs? _masterKeyPair) => super.noSuchMethod( Invocation.setter( #masterKeyPair, _masterKeyPair, @@ -997,160 +1084,160 @@ class MockKeyManager extends _i1.Mock implements _i14.KeyManager { ); @override - _i3.Future init() => (super.noSuchMethod( + _i4.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future hasMasterKey() => (super.noSuchMethod( + _i4.Future hasMasterKey() => (super.noSuchMethod( Invocation.method( #hasMasterKey, [], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future generateAndStoreMasterKey() => (super.noSuchMethod( + _i4.Future generateAndStoreMasterKey() => (super.noSuchMethod( Invocation.method( #generateAndStoreMasterKey, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future generateAndStoreMasterKeyFromMnemonic(String? mnemonic) => + _i4.Future generateAndStoreMasterKeyFromMnemonic(String? mnemonic) => (super.noSuchMethod( Invocation.method( #generateAndStoreMasterKeyFromMnemonic, [mnemonic], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future importMnemonic(String? mnemonic) => (super.noSuchMethod( + _i4.Future importMnemonic(String? mnemonic) => (super.noSuchMethod( Invocation.method( #importMnemonic, [mnemonic], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future getMnemonic() => (super.noSuchMethod( + _i4.Future getMnemonic() => (super.noSuchMethod( Invocation.method( #getMnemonic, [], ), - returnValue: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i6.NostrKeyPairs> deriveTradeKey() => (super.noSuchMethod( + _i4.Future<_i7.NostrKeyPairs> deriveTradeKey() => (super.noSuchMethod( Invocation.method( #deriveTradeKey, [], ), - returnValue: _i3.Future<_i6.NostrKeyPairs>.value(_FakeNostrKeyPairs_6( + returnValue: _i4.Future<_i7.NostrKeyPairs>.value(_FakeNostrKeyPairs_7( this, Invocation.method( #deriveTradeKey, [], ), )), - ) as _i3.Future<_i6.NostrKeyPairs>); + ) as _i4.Future<_i7.NostrKeyPairs>); @override - _i6.NostrKeyPairs deriveTradeKeyPair(int? index) => (super.noSuchMethod( + _i7.NostrKeyPairs deriveTradeKeyPair(int? index) => (super.noSuchMethod( Invocation.method( #deriveTradeKeyPair, [index], ), - returnValue: _FakeNostrKeyPairs_6( + returnValue: _FakeNostrKeyPairs_7( this, Invocation.method( #deriveTradeKeyPair, [index], ), ), - ) as _i6.NostrKeyPairs); + ) as _i7.NostrKeyPairs); @override - _i3.Future<_i6.NostrKeyPairs> deriveTradeKeyFromIndex(int? index) => + _i4.Future<_i7.NostrKeyPairs> deriveTradeKeyFromIndex(int? index) => (super.noSuchMethod( Invocation.method( #deriveTradeKeyFromIndex, [index], ), - returnValue: _i3.Future<_i6.NostrKeyPairs>.value(_FakeNostrKeyPairs_6( + returnValue: _i4.Future<_i7.NostrKeyPairs>.value(_FakeNostrKeyPairs_7( this, Invocation.method( #deriveTradeKeyFromIndex, [index], ), )), - ) as _i3.Future<_i6.NostrKeyPairs>); + ) as _i4.Future<_i7.NostrKeyPairs>); @override - _i3.Future getCurrentKeyIndex() => (super.noSuchMethod( + _i4.Future getCurrentKeyIndex() => (super.noSuchMethod( Invocation.method( #getCurrentKeyIndex, [], ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + returnValue: _i4.Future.value(0), + ) as _i4.Future); @override - _i3.Future setCurrentKeyIndex(int? index) => (super.noSuchMethod( + _i4.Future setCurrentKeyIndex(int? index) => (super.noSuchMethod( Invocation.method( #setCurrentKeyIndex, [index], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [MostroStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { +class MockMostroStorage extends _i1.Mock implements _i17.MostroStorage { MockMostroStorage() { _i1.throwOnMissingStub(this); } @override - _i4.Database get db => (super.noSuchMethod( + _i5.Database get db => (super.noSuchMethod( Invocation.getter(#db), - returnValue: _FakeDatabase_2( + returnValue: _FakeDatabase_3( this, Invocation.getter(#db), ), - ) as _i4.Database); + ) as _i5.Database); @override - _i4.StoreRef> get store => (super.noSuchMethod( + _i5.StoreRef> get store => (super.noSuchMethod( Invocation.getter(#store), - returnValue: _FakeStoreRef_3>( + returnValue: _FakeStoreRef_4>( this, Invocation.getter(#store), ), - ) as _i4.StoreRef>); + ) as _i5.StoreRef>); @override - _i3.Future addMessage( + _i4.Future addMessage( String? key, - _i5.MostroMessage<_i5.Payload>? message, + _i6.MostroMessage<_i6.Payload>? message, ) => (super.noSuchMethod( Invocation.method( @@ -1160,78 +1247,78 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { message, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future>> getAllMessages() => + _i4.Future>> getAllMessages() => (super.noSuchMethod( Invocation.method( #getAllMessages, [], ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i3.Future deleteAllMessages() => (super.noSuchMethod( + _i4.Future deleteAllMessages() => (super.noSuchMethod( Invocation.method( #deleteAllMessages, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteAllMessagesByOrderId(String? orderId) => + _i4.Future deleteAllMessagesByOrderId(String? orderId) => (super.noSuchMethod( Invocation.method( #deleteAllMessagesByOrderId, [orderId], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future>> - getMessagesOfType() => (super.noSuchMethod( + _i4.Future>> + getMessagesOfType() => (super.noSuchMethod( Invocation.method( #getMessagesOfType, [], ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i3.Future<_i5.MostroMessage<_i5.Payload>?> - getLatestMessageOfTypeById(String? orderId) => + _i4.Future<_i6.MostroMessage<_i6.Payload>?> + getLatestMessageOfTypeById(String? orderId) => (super.noSuchMethod( Invocation.method( #getLatestMessageOfTypeById, [orderId], ), - returnValue: _i3.Future<_i5.MostroMessage<_i5.Payload>?>.value(), - ) as _i3.Future<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Future<_i6.MostroMessage<_i6.Payload>?>.value(), + ) as _i4.Future<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Future>> getMessagesForId( + _i4.Future>> getMessagesForId( String? orderId) => (super.noSuchMethod( Invocation.method( #getMessagesForId, [orderId], ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i5.MostroMessage<_i5.Payload> fromDbMap( + _i6.MostroMessage<_i6.Payload> fromDbMap( String? key, Map? jsonMap, ) => @@ -1243,7 +1330,7 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { jsonMap, ], ), - returnValue: _FakeMostroMessage_7<_i5.Payload>( + returnValue: _FakeMostroMessage_8<_i6.Payload>( this, Invocation.method( #fromDbMap, @@ -1253,10 +1340,10 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { ], ), ), - ) as _i5.MostroMessage<_i5.Payload>); + ) as _i6.MostroMessage<_i6.Payload>); @override - Map toDbMap(_i5.MostroMessage<_i5.Payload>? item) => + Map toDbMap(_i6.MostroMessage<_i6.Payload>? item) => (super.noSuchMethod( Invocation.method( #toDbMap, @@ -1266,85 +1353,85 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { ) as Map); @override - _i3.Future hasMessageByKey(String? key) => (super.noSuchMethod( + _i4.Future hasMessageByKey(String? key) => (super.noSuchMethod( Invocation.method( #hasMessageByKey, [key], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future<_i5.MostroMessage<_i5.Payload>?> getLatestMessageById( + _i4.Future<_i6.MostroMessage<_i6.Payload>?> getLatestMessageById( String? orderId) => (super.noSuchMethod( Invocation.method( #getLatestMessageById, [orderId], ), - returnValue: _i3.Future<_i5.MostroMessage<_i5.Payload>?>.value(), - ) as _i3.Future<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Future<_i6.MostroMessage<_i6.Payload>?>.value(), + ) as _i4.Future<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Stream<_i5.MostroMessage<_i5.Payload>?> watchLatestMessage( + _i4.Stream<_i6.MostroMessage<_i6.Payload>?> watchLatestMessage( String? orderId) => (super.noSuchMethod( Invocation.method( #watchLatestMessage, [orderId], ), - returnValue: _i3.Stream<_i5.MostroMessage<_i5.Payload>?>.empty(), - ) as _i3.Stream<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Stream<_i6.MostroMessage<_i6.Payload>?>.empty(), + ) as _i4.Stream<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Stream<_i5.MostroMessage<_i5.Payload>?> watchLatestMessageOfType( + _i4.Stream<_i6.MostroMessage<_i6.Payload>?> watchLatestMessageOfType( String? orderId) => (super.noSuchMethod( Invocation.method( #watchLatestMessageOfType, [orderId], ), - returnValue: _i3.Stream<_i5.MostroMessage<_i5.Payload>?>.empty(), - ) as _i3.Stream<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Stream<_i6.MostroMessage<_i6.Payload>?>.empty(), + ) as _i4.Stream<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Stream>> watchAllMessages( + _i4.Stream>> watchAllMessages( String? orderId) => (super.noSuchMethod( Invocation.method( #watchAllMessages, [orderId], ), - returnValue: _i3.Stream>>.empty(), - ) as _i3.Stream>>); + returnValue: _i4.Stream>>.empty(), + ) as _i4.Stream>>); @override - _i3.Stream<_i5.MostroMessage<_i5.Payload>?> watchByRequestId( + _i4.Stream<_i6.MostroMessage<_i6.Payload>?> watchByRequestId( int? requestId) => (super.noSuchMethod( Invocation.method( #watchByRequestId, [requestId], ), - returnValue: _i3.Stream<_i5.MostroMessage<_i5.Payload>?>.empty(), - ) as _i3.Stream<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Stream<_i6.MostroMessage<_i6.Payload>?>.empty(), + ) as _i4.Stream<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Future>> getAllMessagesForOrderId( + _i4.Future>> getAllMessagesForOrderId( String? orderId) => (super.noSuchMethod( Invocation.method( #getAllMessagesForOrderId, [orderId], ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i3.Future putItem( + _i4.Future putItem( String? id, - _i5.MostroMessage<_i5.Payload>? item, + _i6.MostroMessage<_i6.Payload>? item, ) => (super.noSuchMethod( Invocation.method( @@ -1354,62 +1441,62 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { item, ], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i5.MostroMessage<_i5.Payload>?> getItem(String? id) => + _i4.Future<_i6.MostroMessage<_i6.Payload>?> getItem(String? id) => (super.noSuchMethod( Invocation.method( #getItem, [id], ), - returnValue: _i3.Future<_i5.MostroMessage<_i5.Payload>?>.value(), - ) as _i3.Future<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Future<_i6.MostroMessage<_i6.Payload>?>.value(), + ) as _i4.Future<_i6.MostroMessage<_i6.Payload>?>); @override - _i3.Future hasItem(String? id) => (super.noSuchMethod( + _i4.Future hasItem(String? id) => (super.noSuchMethod( Invocation.method( #hasItem, [id], ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i3.Future deleteItem(String? id) => (super.noSuchMethod( + _i4.Future deleteItem(String? id) => (super.noSuchMethod( Invocation.method( #deleteItem, [id], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteAll() => (super.noSuchMethod( + _i4.Future deleteAll() => (super.noSuchMethod( Invocation.method( #deleteAll, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future deleteWhere(_i4.Filter? filter) => (super.noSuchMethod( + _i4.Future deleteWhere(_i5.Filter? filter) => (super.noSuchMethod( Invocation.method( #deleteWhere, [filter], ), - returnValue: _i3.Future.value(0), - ) as _i3.Future); + returnValue: _i4.Future.value(0), + ) as _i4.Future); @override - _i3.Future>> find({ - _i4.Filter? filter, - List<_i4.SortOrder>? sort, + _i4.Future>> find({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, int? limit, int? offset, }) => @@ -1424,25 +1511,25 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { #offset: offset, }, ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i3.Future>> getAll() => + _i4.Future>> getAll() => (super.noSuchMethod( Invocation.method( #getAll, [], ), - returnValue: _i3.Future>>.value( - <_i5.MostroMessage<_i5.Payload>>[]), - ) as _i3.Future>>); + returnValue: _i4.Future>>.value( + <_i6.MostroMessage<_i6.Payload>>[]), + ) as _i4.Future>>); @override - _i3.Stream>> watch({ - _i4.Filter? filter, - List<_i4.SortOrder>? sort, + _i4.Stream>> watch({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, }) => (super.noSuchMethod( Invocation.method( @@ -1453,21 +1540,21 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { #sort: sort, }, ), - returnValue: _i3.Stream>>.empty(), - ) as _i3.Stream>>); + returnValue: _i4.Stream>>.empty(), + ) as _i4.Stream>>); @override - _i3.Stream<_i5.MostroMessage<_i5.Payload>?> watchById(String? id) => + _i4.Stream<_i6.MostroMessage<_i6.Payload>?> watchById(String? id) => (super.noSuchMethod( Invocation.method( #watchById, [id], ), - returnValue: _i3.Stream<_i5.MostroMessage<_i5.Payload>?>.empty(), - ) as _i3.Stream<_i5.MostroMessage<_i5.Payload>?>); + returnValue: _i4.Stream<_i6.MostroMessage<_i6.Payload>?>.empty(), + ) as _i4.Stream<_i6.MostroMessage<_i6.Payload>?>); @override - _i4.Filter eq( + _i5.Filter eq( String? field, Object? value, ) => @@ -1479,7 +1566,7 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { value, ], ), - returnValue: _FakeFilter_5( + returnValue: _FakeFilter_6( this, Invocation.method( #eq, @@ -1489,13 +1576,13 @@ class MockMostroStorage extends _i1.Mock implements _i15.MostroStorage { ], ), ), - ) as _i4.Filter); + ) as _i5.Filter); } /// A class which mocks [Settings]. /// /// See the documentation for Mockito's code generation for more information. -class MockSettings extends _i1.Mock implements _i7.Settings { +class MockSettings extends _i1.Mock implements _i3.Settings { MockSettings() { _i1.throwOnMissingStub(this); } @@ -1515,14 +1602,14 @@ class MockSettings extends _i1.Mock implements _i7.Settings { @override String get mostroPublicKey => (super.noSuchMethod( Invocation.getter(#mostroPublicKey), - returnValue: _i11.dummyValue( + returnValue: _i13.dummyValue( this, Invocation.getter(#mostroPublicKey), ), ) as String); @override - _i7.Settings copyWith({ + _i3.Settings copyWith({ List? relays, bool? privacyModeSetting, String? mostroInstance, @@ -1539,7 +1626,7 @@ class MockSettings extends _i1.Mock implements _i7.Settings { #defaultFiatCode: defaultFiatCode, }, ), - returnValue: _FakeSettings_8( + returnValue: _FakeSettings_1( this, Invocation.method( #copyWith, @@ -1552,7 +1639,7 @@ class MockSettings extends _i1.Mock implements _i7.Settings { }, ), ), - ) as _i7.Settings); + ) as _i3.Settings); @override Map toJson() => (super.noSuchMethod( @@ -1588,7 +1675,7 @@ class MockRef extends _i1.Mock #refresh, [provider], ), - returnValue: _i11.dummyValue( + returnValue: _i13.dummyValue( this, Invocation.method( #refresh, @@ -1695,7 +1782,7 @@ class MockRef extends _i1.Mock #read, [provider], ), - returnValue: _i11.dummyValue( + returnValue: _i13.dummyValue( this, Invocation.method( #read, @@ -1719,7 +1806,7 @@ class MockRef extends _i1.Mock #watch, [provider], ), - returnValue: _i11.dummyValue( + returnValue: _i13.dummyValue( this, Invocation.method( #watch, @@ -1789,7 +1876,7 @@ class MockRef extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockSubscriptionManager extends _i1.Mock - implements _i16.SubscriptionManager { + implements _i18.SubscriptionManager { MockSubscriptionManager() { _i1.throwOnMissingStub(this); } @@ -1804,21 +1891,21 @@ class MockSubscriptionManager extends _i1.Mock ) as _i2.Ref); @override - _i3.Stream<_i6.NostrEvent> get orders => (super.noSuchMethod( + _i4.Stream<_i7.NostrEvent> get orders => (super.noSuchMethod( Invocation.getter(#orders), - returnValue: _i3.Stream<_i6.NostrEvent>.empty(), - ) as _i3.Stream<_i6.NostrEvent>); + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); @override - _i3.Stream<_i6.NostrEvent> get chat => (super.noSuchMethod( + _i4.Stream<_i7.NostrEvent> get chat => (super.noSuchMethod( Invocation.getter(#chat), - returnValue: _i3.Stream<_i6.NostrEvent>.empty(), - ) as _i3.Stream<_i6.NostrEvent>); + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); @override - _i3.Stream<_i6.NostrEvent> subscribe({ - required _i17.SubscriptionType? type, - required _i6.NostrFilter? filter, + _i4.Stream<_i7.NostrEvent> subscribe({ + required _i19.SubscriptionType? type, + required _i7.NostrFilter? filter, }) => (super.noSuchMethod( Invocation.method( @@ -1829,14 +1916,14 @@ class MockSubscriptionManager extends _i1.Mock #filter: filter, }, ), - returnValue: _i3.Stream<_i6.NostrEvent>.empty(), - ) as _i3.Stream<_i6.NostrEvent>); + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); @override - _i3.Stream<_i6.NostrEvent> subscribeSession({ - required _i17.SubscriptionType? type, - required _i5.Session? session, - required _i6.NostrFilter Function(_i5.Session)? createFilter, + _i4.Stream<_i7.NostrEvent> subscribeSession({ + required _i19.SubscriptionType? type, + required _i6.Session? session, + required _i7.NostrFilter Function(_i6.Session)? createFilter, }) => (super.noSuchMethod( Invocation.method( @@ -1848,20 +1935,11 @@ class MockSubscriptionManager extends _i1.Mock #createFilter: createFilter, }, ), - returnValue: _i3.Stream<_i6.NostrEvent>.empty(), - ) as _i3.Stream<_i6.NostrEvent>); - - @override - void unsubscribeById(_i17.SubscriptionType? type) => super.noSuchMethod( - Invocation.method( - #unsubscribeById, - [type], - ), - returnValueForMissingStub: null, - ); + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); @override - void unsubscribeByType(_i17.SubscriptionType? type) => super.noSuchMethod( + void unsubscribeByType(_i19.SubscriptionType? type) => super.noSuchMethod( Invocation.method( #unsubscribeByType, [type], @@ -1870,7 +1948,7 @@ class MockSubscriptionManager extends _i1.Mock ); @override - void unsubscribeSession(_i17.SubscriptionType? type) => super.noSuchMethod( + void unsubscribeSession(_i19.SubscriptionType? type) => super.noSuchMethod( Invocation.method( #unsubscribeSession, [type], @@ -1879,7 +1957,7 @@ class MockSubscriptionManager extends _i1.Mock ); @override - bool hasActiveSubscription(_i17.SubscriptionType? type) => + bool hasActiveSubscription(_i19.SubscriptionType? type) => (super.noSuchMethod( Invocation.method( #hasActiveSubscription, @@ -1889,14 +1967,14 @@ class MockSubscriptionManager extends _i1.Mock ) as bool); @override - List<_i6.NostrFilter> getActiveFilters(_i17.SubscriptionType? type) => + List<_i7.NostrFilter> getActiveFilters(_i19.SubscriptionType? type) => (super.noSuchMethod( Invocation.method( #getActiveFilters, [type], ), - returnValue: <_i6.NostrFilter>[], - ) as List<_i6.NostrFilter>); + returnValue: <_i7.NostrFilter>[], + ) as List<_i7.NostrFilter>); @override void subscribeAll() => super.noSuchMethod( diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 59f02d28..bd30322e 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -14,13 +14,12 @@ import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; -import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +import '../mocks.dart'; import '../mocks.mocks.dart'; -import 'mostro_service_test.mocks.dart' hide MockRef; import 'mostro_service_helper_functions.dart'; void main() { @@ -28,13 +27,14 @@ void main() { provideDummy(Settings( relays: ['wss://relay.damus.io'], fullPrivacyMode: false, - mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', + mostroPublicKey: + '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', )); - + // Add dummy for MostroStorage provideDummy(MockMostroStorage()); - + // Add dummy for NostrService provideDummy(MockNostrService()); late MostroService mostroService; @@ -44,65 +44,60 @@ void main() { late MockSessionNotifier mockSessionNotifier; late MockRef mockRef; late MockSubscriptionManager mockSubscriptionManager; + late MockKeyManager mockKeyManager; + late MockSessionStorage mockSessionStorage; setUpAll(() { // Create a dummy Settings object that will be used by MockRef - final dummySettings = Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', - defaultFiatCode: 'USD', - ); - + final dummySettings = MockSettings(); + // Provide dummy values for Mockito - provideDummy(MockSessionNotifier()); + provideDummy(MockSessionNotifier( + mockRef, mockKeyManager, mockSessionStorage, dummySettings)); provideDummy(dummySettings); provideDummy(MockNostrService()); - + // Create a mock ref for the SubscriptionManager dummy provideDummy(MockSubscriptionManager()); - + // Create a mock ref that returns the dummy settings final mockRefForDummy = MockRef(); when(mockRefForDummy.read(settingsProvider)).thenReturn(dummySettings); - + // Provide a dummy MostroService that uses our properly configured mock ref provideDummy(MostroService(mockRefForDummy)); }); setUp(() { mockNostrService = MockNostrService(); - mockSessionNotifier = MockSessionNotifier(); + mockSessionNotifier = MockSessionNotifier( + mockRef, mockKeyManager, mockSessionStorage, MockSettings()); mockRef = MockRef(); mockSubscriptionManager = MockSubscriptionManager(); mockServerTradeIndex = MockServerTradeIndex(); + mockKeyManager = MockKeyManager(); + mockSessionStorage = MockSessionStorage(); keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); - // Generate a valid test key pair for mostro public key - final testKeyPair = NostrUtils.generateKeyPair(); - // Create test settings - final testSettings = Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: testKeyPair.public, - defaultFiatCode: 'USD', - ); - + final testSettings = MockSettings(); + // Stub specific provider reads when(mockRef.read(settingsProvider)).thenReturn(testSettings); when(mockRef.read(mostroStorageProvider)).thenReturn(MockMostroStorage()); when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); - when(mockRef.read(subscriptionManagerProvider)).thenReturn(mockSubscriptionManager); - + when(mockRef.read(subscriptionManagerProvider)) + .thenReturn(mockSubscriptionManager); + // Stub SessionNotifier methods when(mockSessionNotifier.sessions).thenReturn([]); - - when(mockRef.read(sessionNotifierProvider.notifier)).thenReturn(mockSessionNotifier); - + + when(mockRef.read(sessionNotifierProvider.notifier)) + .thenReturn(mockSessionNotifier); + // Create the service under test mostroService = MostroService(mockRef); }); - + tearDown(() { // Clean up resources mostroService.dispose(); @@ -162,8 +157,7 @@ void main() { .thenReturn(session); // Mock NostrService's publishEvent only - when(mockNostrService.publishEvent(any)) - .thenAnswer((_) async {}); + when(mockNostrService.publishEvent(any)).thenAnswer((_) async {}); when(mockSessionNotifier.newSession(orderId: orderId)) .thenAnswer((_) async => session); @@ -202,7 +196,8 @@ void main() { final extendedPrivKey = keyDerivator.extendedKeyFromMnemonic(mnemonic); final userPrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, 0); final userPubKey = keyDerivator.privateToPublicKey(userPrivKey); - final tradePrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); + final tradePrivKey = + keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); // Create key pairs final tradeKeyPair = NostrKeyPairs(private: tradePrivKey); final identityKeyPair = NostrKeyPairs(private: userPrivKey); @@ -245,7 +240,8 @@ void main() { 'trade_index': tradeIndex, }, }, - signatureHex: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + signatureHex: + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', ); expect(isValid, isFalse, @@ -260,7 +256,8 @@ void main() { final extendedPrivKey = keyDerivator.extendedKeyFromMnemonic(mnemonic); final userPrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, 0); final userPubKey = keyDerivator.privateToPublicKey(userPrivKey); - final tradePrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); + final tradePrivKey = + keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); // Create key pairs final tradeKeyPair = NostrKeyPairs(private: tradePrivKey); final identityKeyPair = NostrKeyPairs(private: userPrivKey); @@ -322,7 +319,8 @@ void main() { final extendedPrivKey = keyDerivator.extendedKeyFromMnemonic(mnemonic); final userPrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, 0); final userPubKey = keyDerivator.privateToPublicKey(userPrivKey); - final tradePrivKey = keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); + final tradePrivKey = + keyDerivator.derivePrivateKey(extendedPrivKey, tradeIndex); // Create key pairs final tradeKeyPair = NostrKeyPairs(private: tradePrivKey); final identityKeyPair = NostrKeyPairs(private: userPrivKey); @@ -363,7 +361,7 @@ void main() { 'trade_index': tradeIndex, }, }; - + final isValid = serverVerifyMessage( userPubKey: userPubKey, messageContent: messageContent, diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart deleted file mode 100644 index d6c1c576..00000000 --- a/test/services/mostro_service_test.mocks.dart +++ /dev/null @@ -1,656 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in mostro_mobile/test/services/mostro_service_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; - -import 'package:dart_nostr/dart_nostr.dart' as _i8; -import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i7; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i3; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i12; -import 'package:mostro_mobile/data/models/enums/role.dart' as _i10; -import 'package:mostro_mobile/data/models/session.dart' as _i4; -import 'package:mostro_mobile/features/settings/settings.dart' as _i2; -import 'package:mostro_mobile/services/nostr_service.dart' as _i5; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i9; -import 'package:state_notifier/state_notifier.dart' as _i11; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeSettings_0 extends _i1.SmartFake implements _i2.Settings { - _FakeSettings_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeRef_1 extends _i1.SmartFake - implements _i3.Ref { - _FakeRef_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSession_2 extends _i1.SmartFake implements _i4.Session { - _FakeSession_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProviderContainer_3 extends _i1.SmartFake - implements _i3.ProviderContainer { - _FakeProviderContainer_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeKeepAliveLink_4 extends _i1.SmartFake implements _i3.KeepAliveLink { - _FakeKeepAliveLink_4( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProviderSubscription_5 extends _i1.SmartFake - implements _i3.ProviderSubscription { - _FakeProviderSubscription_5( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [NostrService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockNostrService extends _i1.Mock implements _i5.NostrService { - MockNostrService() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.Settings get settings => (super.noSuchMethod( - Invocation.getter(#settings), - returnValue: _FakeSettings_0( - this, - Invocation.getter(#settings), - ), - ) as _i2.Settings); - - @override - bool get isInitialized => (super.noSuchMethod( - Invocation.getter(#isInitialized), - returnValue: false, - ) as bool); - - @override - set settings(_i2.Settings? _settings) => super.noSuchMethod( - Invocation.setter( - #settings, - _settings, - ), - returnValueForMissingStub: null, - ); - - @override - _i6.Future init(_i2.Settings? settings) => (super.noSuchMethod( - Invocation.method( - #init, - [settings], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future updateSettings(_i2.Settings? newSettings) => - (super.noSuchMethod( - Invocation.method( - #updateSettings, - [newSettings], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future<_i7.RelayInformations?> getRelayInfo(String? relayUrl) => - (super.noSuchMethod( - Invocation.method( - #getRelayInfo, - [relayUrl], - ), - returnValue: _i6.Future<_i7.RelayInformations?>.value(), - ) as _i6.Future<_i7.RelayInformations?>); - - @override - _i6.Future publishEvent(_i8.NostrEvent? event) => (super.noSuchMethod( - Invocation.method( - #publishEvent, - [event], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Stream<_i8.NostrEvent> subscribeToEvents(_i8.NostrRequest? request) => - (super.noSuchMethod( - Invocation.method( - #subscribeToEvents, - [request], - ), - returnValue: _i6.Stream<_i8.NostrEvent>.empty(), - ) as _i6.Stream<_i8.NostrEvent>); - - @override - _i6.Future disconnectFromRelays() => (super.noSuchMethod( - Invocation.method( - #disconnectFromRelays, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - void unsubscribe(String? id) => super.noSuchMethod( - Invocation.method( - #unsubscribe, - [id], - ), - returnValueForMissingStub: null, - ); -} - -/// A class which mocks [SessionNotifier]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockSessionNotifier extends _i1.Mock implements _i9.SessionNotifier { - MockSessionNotifier() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Ref get ref => (super.noSuchMethod( - Invocation.getter(#ref), - returnValue: _FakeRef_1( - this, - Invocation.getter(#ref), - ), - ) as _i3.Ref); - - @override - List<_i4.Session> get sessions => (super.noSuchMethod( - Invocation.getter(#sessions), - returnValue: <_i4.Session>[], - ) as List<_i4.Session>); - - @override - bool get mounted => (super.noSuchMethod( - Invocation.getter(#mounted), - returnValue: false, - ) as bool); - - @override - _i6.Stream> get stream => (super.noSuchMethod( - Invocation.getter(#stream), - returnValue: _i6.Stream>.empty(), - ) as _i6.Stream>); - - @override - List<_i4.Session> get state => (super.noSuchMethod( - Invocation.getter(#state), - returnValue: <_i4.Session>[], - ) as List<_i4.Session>); - - @override - List<_i4.Session> get debugState => (super.noSuchMethod( - Invocation.getter(#debugState), - returnValue: <_i4.Session>[], - ) as List<_i4.Session>); - - @override - bool get hasListeners => (super.noSuchMethod( - Invocation.getter(#hasListeners), - returnValue: false, - ) as bool); - - @override - set onError(_i3.ErrorListener? _onError) => super.noSuchMethod( - Invocation.setter( - #onError, - _onError, - ), - returnValueForMissingStub: null, - ); - - @override - set state(List<_i4.Session>? value) => super.noSuchMethod( - Invocation.setter( - #state, - value, - ), - returnValueForMissingStub: null, - ); - - @override - _i6.Future init() => (super.noSuchMethod( - Invocation.method( - #init, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - void updateSettings(_i2.Settings? settings) => super.noSuchMethod( - Invocation.method( - #updateSettings, - [settings], - ), - returnValueForMissingStub: null, - ); - - @override - _i6.Future<_i4.Session> newSession({ - String? orderId, - int? requestId, - _i10.Role? role, - }) => - (super.noSuchMethod( - Invocation.method( - #newSession, - [], - { - #orderId: orderId, - #requestId: requestId, - #role: role, - }, - ), - returnValue: _i6.Future<_i4.Session>.value(_FakeSession_2( - this, - Invocation.method( - #newSession, - [], - { - #orderId: orderId, - #requestId: requestId, - #role: role, - }, - ), - )), - ) as _i6.Future<_i4.Session>); - - @override - _i6.Future saveSession(_i4.Session? session) => (super.noSuchMethod( - Invocation.method( - #saveSession, - [session], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future updateSession( - String? orderId, - void Function(_i4.Session)? update, - ) => - (super.noSuchMethod( - Invocation.method( - #updateSession, - [ - orderId, - update, - ], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i4.Session? getSessionByRequestId(int? requestId) => - (super.noSuchMethod(Invocation.method( - #getSessionByRequestId, - [requestId], - )) as _i4.Session?); - - @override - _i4.Session? getSessionByOrderId(String? orderId) => - (super.noSuchMethod(Invocation.method( - #getSessionByOrderId, - [orderId], - )) as _i4.Session?); - - @override - _i4.Session? getSessionByTradeKey(String? tradeKey) => - (super.noSuchMethod(Invocation.method( - #getSessionByTradeKey, - [tradeKey], - )) as _i4.Session?); - - @override - _i6.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( - Invocation.method( - #loadSession, - [keyIndex], - ), - returnValue: _i6.Future<_i4.Session?>.value(), - ) as _i6.Future<_i4.Session?>); - - @override - _i6.Future reset() => (super.noSuchMethod( - Invocation.method( - #reset, - [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future deleteSession(String? sessionId) => (super.noSuchMethod( - Invocation.method( - #deleteSession, - [sessionId], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - void dispose() => super.noSuchMethod( - Invocation.method( - #dispose, - [], - ), - returnValueForMissingStub: null, - ); - - @override - bool updateShouldNotify( - List<_i4.Session>? old, - List<_i4.Session>? current, - ) => - (super.noSuchMethod( - Invocation.method( - #updateShouldNotify, - [ - old, - current, - ], - ), - returnValue: false, - ) as bool); - - @override - _i3.RemoveListener addListener( - _i11.Listener>? listener, { - bool? fireImmediately = true, - }) => - (super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - {#fireImmediately: fireImmediately}, - ), - returnValue: () {}, - ) as _i3.RemoveListener); -} - -/// A class which mocks [Ref]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockRef extends _i1.Mock - implements _i3.Ref { - MockRef() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.ProviderContainer get container => (super.noSuchMethod( - Invocation.getter(#container), - returnValue: _FakeProviderContainer_3( - this, - Invocation.getter(#container), - ), - ) as _i3.ProviderContainer); - - @override - T refresh(_i3.Refreshable? provider) => (super.noSuchMethod( - Invocation.method( - #refresh, - [provider], - ), - returnValue: _i12.dummyValue( - this, - Invocation.method( - #refresh, - [provider], - ), - ), - ) as T); - - @override - void invalidate(_i3.ProviderOrFamily? provider) => super.noSuchMethod( - Invocation.method( - #invalidate, - [provider], - ), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method( - #notifyListeners, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void listenSelf( - void Function( - State?, - State, - )? listener, { - void Function( - Object, - StackTrace, - )? onError, - }) => - super.noSuchMethod( - Invocation.method( - #listenSelf, - [listener], - {#onError: onError}, - ), - returnValueForMissingStub: null, - ); - - @override - void invalidateSelf() => super.noSuchMethod( - Invocation.method( - #invalidateSelf, - [], - ), - returnValueForMissingStub: null, - ); - - @override - void onAddListener(void Function()? cb) => super.noSuchMethod( - Invocation.method( - #onAddListener, - [cb], - ), - returnValueForMissingStub: null, - ); - - @override - void onRemoveListener(void Function()? cb) => super.noSuchMethod( - Invocation.method( - #onRemoveListener, - [cb], - ), - returnValueForMissingStub: null, - ); - - @override - void onResume(void Function()? cb) => super.noSuchMethod( - Invocation.method( - #onResume, - [cb], - ), - returnValueForMissingStub: null, - ); - - @override - void onCancel(void Function()? cb) => super.noSuchMethod( - Invocation.method( - #onCancel, - [cb], - ), - returnValueForMissingStub: null, - ); - - @override - void onDispose(void Function()? cb) => super.noSuchMethod( - Invocation.method( - #onDispose, - [cb], - ), - returnValueForMissingStub: null, - ); - - @override - T read(_i3.ProviderListenable? provider) => (super.noSuchMethod( - Invocation.method( - #read, - [provider], - ), - returnValue: _i12.dummyValue( - this, - Invocation.method( - #read, - [provider], - ), - ), - ) as T); - - @override - bool exists(_i3.ProviderBase? provider) => (super.noSuchMethod( - Invocation.method( - #exists, - [provider], - ), - returnValue: false, - ) as bool); - - @override - T watch(_i3.ProviderListenable? provider) => (super.noSuchMethod( - Invocation.method( - #watch, - [provider], - ), - returnValue: _i12.dummyValue( - this, - Invocation.method( - #watch, - [provider], - ), - ), - ) as T); - - @override - _i3.KeepAliveLink keepAlive() => (super.noSuchMethod( - Invocation.method( - #keepAlive, - [], - ), - returnValue: _FakeKeepAliveLink_4( - this, - Invocation.method( - #keepAlive, - [], - ), - ), - ) as _i3.KeepAliveLink); - - @override - _i3.ProviderSubscription listen( - _i3.ProviderListenable? provider, - void Function( - T?, - T, - )? listener, { - void Function( - Object, - StackTrace, - )? onError, - bool? fireImmediately, - }) => - (super.noSuchMethod( - Invocation.method( - #listen, - [ - provider, - listener, - ], - { - #onError: onError, - #fireImmediately: fireImmediately, - }, - ), - returnValue: _FakeProviderSubscription_5( - this, - Invocation.method( - #listen, - [ - provider, - listener, - ], - { - #onError: onError, - #fireImmediately: fireImmediately, - }, - ), - ), - ) as _i3.ProviderSubscription); -} From a218df37ca50d9e611a954fddd7ef770c21ca548 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 7 Jul 2025 23:08:43 -0700 Subject: [PATCH 19/28] refactor: improve session validation and error handling in models and tests --- lib/background/background.dart | 3 +- lib/data/models/chat_room.dart | 2 +- lib/data/models/session.dart | 14 +- .../chat/notifiers/chat_rooms_notifier.dart | 62 --- .../trades/screens/trade_detail_screen.dart | 2 +- test/mocks.dart | 103 +++- test/mocks.mocks.dart | 457 ++++++++---------- test/services/mostro_service_test.dart | 130 ++--- 8 files changed, 375 insertions(+), 398 deletions(-) diff --git a/lib/background/background.dart b/lib/background/background.dart index 9d3e6343..d82f92be 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/nostr_filter.dart'; import 'package:mostro_mobile/data/repositories/event_storage.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; @@ -72,7 +73,7 @@ Future serviceMain(ServiceInstance service) async { if (await eventStore.hasItem(event.id!)) return; await retryNotification(event); } catch (e) { - // ignore + Logger().e('Error processing event', error: e); } }); }); diff --git a/lib/data/models/chat_room.dart b/lib/data/models/chat_room.dart index 41ee0b03..5b303e31 100644 --- a/lib/data/models/chat_room.dart +++ b/lib/data/models/chat_room.dart @@ -37,7 +37,7 @@ class ChatRoom { } @override - int get hashCode => Object.hash(orderId, messages.length); + int get hashCode => Object.hash(orderId, Object.hashAll(messages)); @override String toString() => 'ChatRoom(orderId: $orderId, messages: ${messages.length} messages)'; diff --git a/lib/data/models/session.dart b/lib/data/models/session.dart index d22639a7..98ae7de0 100644 --- a/lib/data/models/session.dart +++ b/lib/data/models/session.dart @@ -72,6 +72,16 @@ class Session { throw FormatException('Key index cannot be negative: $keyIndex'); } + // Validate key pair fields + final masterKeyValue = json['master_key']; + final tradeKeyValue = json['trade_key']; + if (masterKeyValue is! NostrKeyPairs) { + throw FormatException('Invalid master_key type: ${masterKeyValue.runtimeType}'); + } + if (tradeKeyValue is! NostrKeyPairs) { + throw FormatException('Invalid trade_key type: ${tradeKeyValue.runtimeType}'); + } + // Parse fullPrivacy final fullPrivacyValue = json['full_privacy']; bool fullPrivacy; @@ -119,8 +129,8 @@ class Session { } return Session( - masterKey: json['master_key'], - tradeKey: json['trade_key'], + masterKey: masterKeyValue, + tradeKey: tradeKeyValue, keyIndex: keyIndex, fullPrivacy: fullPrivacy, startTime: startTime, diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart index b88efcc5..bf13a206 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -1,77 +1,20 @@ import 'dart:async'; -import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; class ChatRoomsNotifier extends StateNotifier> { final Ref ref; final _logger = Logger(); - StreamSubscription? _chatSubscription; - ChatRoomsNotifier(this.ref) : super(const []) { loadChats(); - //_setupChatSubscription(); } - void _setupChatSubscription() { - final subscriptionManager = ref.read(subscriptionManagerProvider); - - // Subscribe to the chat stream from SubscriptionManager - // The SubscriptionManager will automatically manage subscriptions based on session changes - _chatSubscription = subscriptionManager.chat.listen( - _onChatEvent, - onError: (error, stackTrace) { - _logger.e('Error in chat subscription', error: error, stackTrace: stackTrace); - }, - cancelOnError: false, - ); - - _logger.i('Chat subscription set up'); - } - /// Handle incoming chat events - void _onChatEvent(NostrEvent event) { - try { - // Find the chat room this event belongs to - final orderId = _findOrderIdForEvent(event); - if (orderId == null) { - _logger.w('Could not determine orderId for chat event: ${event.id}'); - return; - } - - // Store the event in the event store so it can be processed by the chat room notifier - final eventStore = ref.read(eventStorageProvider); - eventStore.putItem(event.id!, event).then((_) { - // Trigger a reload of the chat room to process the new event - final chatRoomNotifier = ref.read(chatRoomsProvider(orderId).notifier); - if (chatRoomNotifier.mounted) { - chatRoomNotifier.reload(); - } - }).catchError((error, stackTrace) { - _logger.e('Error storing chat event', error: error, stackTrace: stackTrace); - }); - } catch (e, stackTrace) { - _logger.e('Error processing chat event', error: e, stackTrace: stackTrace); - } - } - - String? _findOrderIdForEvent(NostrEvent event) { - final sessions = ref.read(sessionNotifierProvider); - for (final session in sessions) { - if (session.peer?.publicKey == event.pubkey) { - return session.orderId; - } - } - - return null; - } void reloadAllChats() { for (final chat in state) { @@ -125,9 +68,4 @@ class ChatRoomsNotifier extends StateNotifier> { //loadChats(); } - @override - void dispose() { - _chatSubscription?.cancel(); - super.dispose(); - } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 50e5ecce..d1bfa466 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -406,7 +406,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.rateUser: case actions.Action.rateReceived: widgets.add(_buildNostrButton( - S.of(context)!.rate, + S.of(context)!.rateButton, action: actions.Action.rate, backgroundColor: AppTheme.mostroGreen, onPressed: () => context.push('/rate_user/$orderId'), diff --git a/test/mocks.dart b/test/mocks.dart index da865e33..317f6f70 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -11,6 +11,8 @@ import 'package:mostro_mobile/features/key_manager/key_manager.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_notifier.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; @@ -20,8 +22,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'mocks.mocks.dart'; @GenerateMocks([ - MostroService, NostrService, + MostroService, OpenOrdersRepository, SharedPreferencesAsync, Database, @@ -30,7 +32,7 @@ import 'mocks.mocks.dart'; MostroStorage, Settings, Ref, - SubscriptionManager, + ProviderSubscription, ]) // Custom mock for SettingsNotifier that returns a specific Settings object @@ -45,14 +47,26 @@ class MockSettingsNotifier extends SettingsNotifier { // Custom mock for SessionNotifier that avoids database dependencies class MockSessionNotifier extends SessionNotifier { + Session? _mockSession; + List _mockSessions = []; + MockSessionNotifier(super.ref, MockKeyManager keyManager, MockSessionStorage super.sessionStorage, MockSettings super.settings); + // Allow tests to set mock return values + void setMockSession(Session? session) { + _mockSession = session; + } + + void setMockSessions(List sessions) { + _mockSessions = sessions; + } + @override - Session? getSessionByOrderId(String orderId) => null; + Session? getSessionByOrderId(String orderId) => _mockSession; @override - List get sessions => []; + List get sessions => _mockSessions; @override Future newSession( @@ -75,5 +89,86 @@ class MockSessionNotifier extends SessionNotifier { } } +class MockSubscriptionManager extends SubscriptionManager { + final StreamController _ordersController = + StreamController.broadcast(); + final StreamController _chatController = + StreamController.broadcast(); + final Map _subscriptions = {}; + NostrFilter? _lastFilter; + + MockSubscriptionManager(super.ref); + + NostrFilter? get lastFilter => _lastFilter; + + @override + Stream get orders => _ordersController.stream; + + @override + Stream get chat => _chatController.stream; + + @override + Stream subscribe({ + required SubscriptionType type, + required NostrFilter filter, + }) { + _lastFilter = filter; + + final request = NostrRequest(filters: [filter]); + + final subscription = Subscription( + request: request, + streamSubscription: (type == SubscriptionType.orders + ? _ordersController.stream + : _chatController.stream) + .listen((_) {}), + onCancel: () {}, + ); + + _subscriptions[type] = subscription; + + return type == SubscriptionType.orders ? orders : chat; + } + + @override + void unsubscribeByType(SubscriptionType type) { + _subscriptions.remove(type); + } + + @override + void unsubscribeAll() { + _subscriptions.clear(); + } + + @override + List getActiveFilters(SubscriptionType type) { + final subscription = _subscriptions[type]; + if (subscription != null && subscription.request.filters.isNotEmpty) { + return [subscription.request.filters.first]; + } + return []; + } + + @override + bool hasActiveSubscription(SubscriptionType type) { + return _subscriptions.containsKey(type); + } + + // Helper to add events to the stream + void addEvent(NostrEvent event, SubscriptionType type) { + if (type == SubscriptionType.orders) { + _ordersController.add(event); + } else if (type == SubscriptionType.chat) { + _chatController.add(event); + } + } + + @override + void dispose() { + _ordersController.close(); + _chatController.close(); + super.dispose(); + } +} void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index ec460cb2..ccf683ef 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -7,25 +7,22 @@ import 'dart:async' as _i4; import 'package:dart_nostr/dart_nostr.dart' as _i7; import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i10; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; +import 'package:flutter_riverpod/flutter_riverpod.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i13; +import 'package:mockito/src/dummies.dart' as _i14; import 'package:mostro_mobile/data/models.dart' as _i6; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i17; +import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i18; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' - as _i11; -import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i15; -import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i16; -import 'package:mostro_mobile/features/settings/settings.dart' as _i3; -import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart' - as _i18; -import 'package:mostro_mobile/features/subscriptions/subscription_type.dart' - as _i19; -import 'package:mostro_mobile/services/mostro_service.dart' as _i8; + as _i12; +import 'package:mostro_mobile/data/repositories/session_storage.dart' as _i16; +import 'package:mostro_mobile/features/key_manager/key_manager.dart' as _i17; +import 'package:mostro_mobile/features/settings/settings.dart' as _i2; +import 'package:mostro_mobile/services/mostro_service.dart' as _i11; import 'package:mostro_mobile/services/nostr_service.dart' as _i9; +import 'package:riverpod/src/internals.dart' as _i8; import 'package:sembast/sembast.dart' as _i5; -import 'package:sembast/src/api/transaction.dart' as _i14; -import 'package:shared_preferences/src/shared_preferences_async.dart' as _i12; +import 'package:sembast/src/api/transaction.dart' as _i15; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i13; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -41,9 +38,8 @@ import 'package:shared_preferences/src/shared_preferences_async.dart' as _i12; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeRef_0 extends _i1.SmartFake - implements _i2.Ref { - _FakeRef_0( +class _FakeSettings_0 extends _i1.SmartFake implements _i2.Settings { + _FakeSettings_0( Object parent, Invocation parentInvocation, ) : super( @@ -52,8 +48,9 @@ class _FakeRef_0 extends _i1.SmartFake ); } -class _FakeSettings_1 extends _i1.SmartFake implements _i3.Settings { - _FakeSettings_1( +class _FakeRef_1 extends _i1.SmartFake + implements _i3.Ref { + _FakeRef_1( Object parent, Invocation parentInvocation, ) : super( @@ -135,7 +132,7 @@ class _FakeMostroMessage_8 extends _i1.SmartFake } class _FakeProviderContainer_9 extends _i1.SmartFake - implements _i2.ProviderContainer { + implements _i3.ProviderContainer { _FakeProviderContainer_9( Object parent, Invocation parentInvocation, @@ -145,7 +142,7 @@ class _FakeProviderContainer_9 extends _i1.SmartFake ); } -class _FakeKeepAliveLink_10 extends _i1.SmartFake implements _i2.KeepAliveLink { +class _FakeKeepAliveLink_10 extends _i1.SmartFake implements _i3.KeepAliveLink { _FakeKeepAliveLink_10( Object parent, Invocation parentInvocation, @@ -156,7 +153,7 @@ class _FakeKeepAliveLink_10 extends _i1.SmartFake implements _i2.KeepAliveLink { } class _FakeProviderSubscription_11 extends _i1.SmartFake - implements _i2.ProviderSubscription { + implements _i3.ProviderSubscription { _FakeProviderSubscription_11( Object parent, Invocation parentInvocation, @@ -166,22 +163,135 @@ class _FakeProviderSubscription_11 extends _i1.SmartFake ); } +class _FakeNode_12 extends _i1.SmartFake implements _i8.Node { + _FakeNode_12( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [NostrService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNostrService extends _i1.Mock implements _i9.NostrService { + MockNostrService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Settings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeSettings_0( + this, + Invocation.getter(#settings), + ), + ) as _i2.Settings); + + @override + bool get isInitialized => (super.noSuchMethod( + Invocation.getter(#isInitialized), + returnValue: false, + ) as bool); + + @override + set settings(_i2.Settings? _settings) => super.noSuchMethod( + Invocation.setter( + #settings, + _settings, + ), + returnValueForMissingStub: null, + ); + + @override + _i4.Future init(_i2.Settings? settings) => (super.noSuchMethod( + Invocation.method( + #init, + [settings], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future updateSettings(_i2.Settings? newSettings) => + (super.noSuchMethod( + Invocation.method( + #updateSettings, + [newSettings], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Future<_i10.RelayInformations?> getRelayInfo(String? relayUrl) => + (super.noSuchMethod( + Invocation.method( + #getRelayInfo, + [relayUrl], + ), + returnValue: _i4.Future<_i10.RelayInformations?>.value(), + ) as _i4.Future<_i10.RelayInformations?>); + + @override + _i4.Future publishEvent(_i7.NostrEvent? event) => (super.noSuchMethod( + Invocation.method( + #publishEvent, + [event], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + _i4.Stream<_i7.NostrEvent> subscribeToEvents(_i7.NostrRequest? request) => + (super.noSuchMethod( + Invocation.method( + #subscribeToEvents, + [request], + ), + returnValue: _i4.Stream<_i7.NostrEvent>.empty(), + ) as _i4.Stream<_i7.NostrEvent>); + + @override + _i4.Future disconnectFromRelays() => (super.noSuchMethod( + Invocation.method( + #disconnectFromRelays, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + + @override + void unsubscribe(String? id) => super.noSuchMethod( + Invocation.method( + #unsubscribe, + [id], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [MostroService]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroService extends _i1.Mock implements _i8.MostroService { +class MockMostroService extends _i1.Mock implements _i11.MostroService { MockMostroService() { _i1.throwOnMissingStub(this); } @override - _i2.Ref get ref => (super.noSuchMethod( + _i3.Ref get ref => (super.noSuchMethod( Invocation.getter(#ref), - returnValue: _FakeRef_0( + returnValue: _FakeRef_1( this, Invocation.getter(#ref), ), - ) as _i2.Ref); + ) as _i3.Ref); @override void init() => super.noSuchMethod( @@ -336,7 +446,7 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { ) as _i4.Future); @override - void updateSettings(_i3.Settings? settings) => super.noSuchMethod( + void updateSettings(_i2.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -345,114 +455,11 @@ class MockMostroService extends _i1.Mock implements _i8.MostroService { ); } -/// A class which mocks [NostrService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockNostrService extends _i1.Mock implements _i9.NostrService { - MockNostrService() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Settings get settings => (super.noSuchMethod( - Invocation.getter(#settings), - returnValue: _FakeSettings_1( - this, - Invocation.getter(#settings), - ), - ) as _i3.Settings); - - @override - bool get isInitialized => (super.noSuchMethod( - Invocation.getter(#isInitialized), - returnValue: false, - ) as bool); - - @override - set settings(_i3.Settings? _settings) => super.noSuchMethod( - Invocation.setter( - #settings, - _settings, - ), - returnValueForMissingStub: null, - ); - - @override - _i4.Future init(_i3.Settings? settings) => (super.noSuchMethod( - Invocation.method( - #init, - [settings], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future updateSettings(_i3.Settings? newSettings) => - (super.noSuchMethod( - Invocation.method( - #updateSettings, - [newSettings], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future<_i10.RelayInformations?> getRelayInfo(String? relayUrl) => - (super.noSuchMethod( - Invocation.method( - #getRelayInfo, - [relayUrl], - ), - returnValue: _i4.Future<_i10.RelayInformations?>.value(), - ) as _i4.Future<_i10.RelayInformations?>); - - @override - _i4.Future publishEvent(_i7.NostrEvent? event) => (super.noSuchMethod( - Invocation.method( - #publishEvent, - [event], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Stream<_i7.NostrEvent> subscribeToEvents(_i7.NostrRequest? request) => - (super.noSuchMethod( - Invocation.method( - #subscribeToEvents, - [request], - ), - returnValue: _i4.Stream<_i7.NostrEvent>.empty(), - ) as _i4.Stream<_i7.NostrEvent>); - - @override - _i4.Future disconnectFromRelays() => (super.noSuchMethod( - Invocation.method( - #disconnectFromRelays, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - void unsubscribe(String? id) => super.noSuchMethod( - Invocation.method( - #unsubscribe, - [id], - ), - returnValueForMissingStub: null, - ); -} - /// A class which mocks [OpenOrdersRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockOpenOrdersRepository extends _i1.Mock - implements _i11.OpenOrdersRepository { + implements _i12.OpenOrdersRepository { MockOpenOrdersRepository() { _i1.throwOnMissingStub(this); } @@ -522,7 +529,7 @@ class MockOpenOrdersRepository extends _i1.Mock ) as _i4.Future); @override - void updateSettings(_i3.Settings? settings) => super.noSuchMethod( + void updateSettings(_i2.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -543,9 +550,8 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock - implements _i12.SharedPreferencesAsync { + implements _i13.SharedPreferencesAsync { MockSharedPreferencesAsync() { _i1.throwOnMissingStub(this); } @@ -751,7 +757,7 @@ class MockDatabase extends _i1.Mock implements _i5.Database { @override String get path => (super.noSuchMethod( Invocation.getter(#path), - returnValue: _i13.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.getter(#path), ), @@ -759,14 +765,14 @@ class MockDatabase extends _i1.Mock implements _i5.Database { @override _i4.Future transaction( - _i4.FutureOr Function(_i14.Transaction)? action) => + _i4.FutureOr Function(_i15.Transaction)? action) => (super.noSuchMethod( Invocation.method( #transaction, [action], ), - returnValue: _i13.ifNotNull( - _i13.dummyValueOrNull( + returnValue: _i14.ifNotNull( + _i14.dummyValueOrNull( this, Invocation.method( #transaction, @@ -797,7 +803,7 @@ class MockDatabase extends _i1.Mock implements _i5.Database { /// A class which mocks [SessionStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionStorage extends _i1.Mock implements _i15.SessionStorage { +class MockSessionStorage extends _i1.Mock implements _i16.SessionStorage { MockSessionStorage() { _i1.throwOnMissingStub(this); } @@ -1060,7 +1066,7 @@ class MockSessionStorage extends _i1.Mock implements _i15.SessionStorage { /// A class which mocks [KeyManager]. /// /// See the documentation for Mockito's code generation for more information. -class MockKeyManager extends _i1.Mock implements _i16.KeyManager { +class MockKeyManager extends _i1.Mock implements _i17.KeyManager { MockKeyManager() { _i1.throwOnMissingStub(this); } @@ -1211,7 +1217,7 @@ class MockKeyManager extends _i1.Mock implements _i16.KeyManager { /// A class which mocks [MostroStorage]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroStorage extends _i1.Mock implements _i17.MostroStorage { +class MockMostroStorage extends _i1.Mock implements _i18.MostroStorage { MockMostroStorage() { _i1.throwOnMissingStub(this); } @@ -1582,7 +1588,7 @@ class MockMostroStorage extends _i1.Mock implements _i17.MostroStorage { /// A class which mocks [Settings]. /// /// See the documentation for Mockito's code generation for more information. -class MockSettings extends _i1.Mock implements _i3.Settings { +class MockSettings extends _i1.Mock implements _i2.Settings { MockSettings() { _i1.throwOnMissingStub(this); } @@ -1602,14 +1608,14 @@ class MockSettings extends _i1.Mock implements _i3.Settings { @override String get mostroPublicKey => (super.noSuchMethod( Invocation.getter(#mostroPublicKey), - returnValue: _i13.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.getter(#mostroPublicKey), ), ) as String); @override - _i3.Settings copyWith({ + _i2.Settings copyWith({ List? relays, bool? privacyModeSetting, String? mostroInstance, @@ -1626,7 +1632,7 @@ class MockSettings extends _i1.Mock implements _i3.Settings { #defaultFiatCode: defaultFiatCode, }, ), - returnValue: _FakeSettings_1( + returnValue: _FakeSettings_0( this, Invocation.method( #copyWith, @@ -1639,7 +1645,7 @@ class MockSettings extends _i1.Mock implements _i3.Settings { }, ), ), - ) as _i3.Settings); + ) as _i2.Settings); @override Map toJson() => (super.noSuchMethod( @@ -1655,27 +1661,27 @@ class MockSettings extends _i1.Mock implements _i3.Settings { /// /// See the documentation for Mockito's code generation for more information. class MockRef extends _i1.Mock - implements _i2.Ref { + implements _i3.Ref { MockRef() { _i1.throwOnMissingStub(this); } @override - _i2.ProviderContainer get container => (super.noSuchMethod( + _i3.ProviderContainer get container => (super.noSuchMethod( Invocation.getter(#container), returnValue: _FakeProviderContainer_9( this, Invocation.getter(#container), ), - ) as _i2.ProviderContainer); + ) as _i3.ProviderContainer); @override - T refresh(_i2.Refreshable? provider) => (super.noSuchMethod( + T refresh(_i3.Refreshable? provider) => (super.noSuchMethod( Invocation.method( #refresh, [provider], ), - returnValue: _i13.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.method( #refresh, @@ -1685,7 +1691,7 @@ class MockRef extends _i1.Mock ) as T); @override - void invalidate(_i2.ProviderOrFamily? provider) => super.noSuchMethod( + void invalidate(_i3.ProviderOrFamily? provider) => super.noSuchMethod( Invocation.method( #invalidate, [provider], @@ -1777,12 +1783,12 @@ class MockRef extends _i1.Mock ); @override - T read(_i2.ProviderListenable? provider) => (super.noSuchMethod( + T read(_i3.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( #read, [provider], ), - returnValue: _i13.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.method( #read, @@ -1792,7 +1798,7 @@ class MockRef extends _i1.Mock ) as T); @override - bool exists(_i2.ProviderBase? provider) => (super.noSuchMethod( + bool exists(_i3.ProviderBase? provider) => (super.noSuchMethod( Invocation.method( #exists, [provider], @@ -1801,12 +1807,12 @@ class MockRef extends _i1.Mock ) as bool); @override - T watch(_i2.ProviderListenable? provider) => (super.noSuchMethod( + T watch(_i3.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( #watch, [provider], ), - returnValue: _i13.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.method( #watch, @@ -1816,7 +1822,7 @@ class MockRef extends _i1.Mock ) as T); @override - _i2.KeepAliveLink keepAlive() => (super.noSuchMethod( + _i3.KeepAliveLink keepAlive() => (super.noSuchMethod( Invocation.method( #keepAlive, [], @@ -1828,11 +1834,11 @@ class MockRef extends _i1.Mock [], ), ), - ) as _i2.KeepAliveLink); + ) as _i3.KeepAliveLink); @override - _i2.ProviderSubscription listen( - _i2.ProviderListenable? provider, + _i3.ProviderSubscription listen( + _i3.ProviderListenable? provider, void Function( T?, T, @@ -1869,135 +1875,52 @@ class MockRef extends _i1.Mock }, ), ), - ) as _i2.ProviderSubscription); + ) as _i3.ProviderSubscription); } -/// A class which mocks [SubscriptionManager]. +/// A class which mocks [ProviderSubscription]. /// /// See the documentation for Mockito's code generation for more information. -class MockSubscriptionManager extends _i1.Mock - implements _i18.SubscriptionManager { - MockSubscriptionManager() { +class MockProviderSubscription extends _i1.Mock + implements _i3.ProviderSubscription { + MockProviderSubscription() { _i1.throwOnMissingStub(this); } @override - _i2.Ref get ref => (super.noSuchMethod( - Invocation.getter(#ref), - returnValue: _FakeRef_0( + _i8.Node get source => (super.noSuchMethod( + Invocation.getter(#source), + returnValue: _FakeNode_12( this, - Invocation.getter(#ref), + Invocation.getter(#source), ), - ) as _i2.Ref); + ) as _i8.Node); @override - _i4.Stream<_i7.NostrEvent> get orders => (super.noSuchMethod( - Invocation.getter(#orders), - returnValue: _i4.Stream<_i7.NostrEvent>.empty(), - ) as _i4.Stream<_i7.NostrEvent>); - - @override - _i4.Stream<_i7.NostrEvent> get chat => (super.noSuchMethod( - Invocation.getter(#chat), - returnValue: _i4.Stream<_i7.NostrEvent>.empty(), - ) as _i4.Stream<_i7.NostrEvent>); - - @override - _i4.Stream<_i7.NostrEvent> subscribe({ - required _i19.SubscriptionType? type, - required _i7.NostrFilter? filter, - }) => - (super.noSuchMethod( - Invocation.method( - #subscribe, - [], - { - #type: type, - #filter: filter, - }, - ), - returnValue: _i4.Stream<_i7.NostrEvent>.empty(), - ) as _i4.Stream<_i7.NostrEvent>); - - @override - _i4.Stream<_i7.NostrEvent> subscribeSession({ - required _i19.SubscriptionType? type, - required _i6.Session? session, - required _i7.NostrFilter Function(_i6.Session)? createFilter, - }) => - (super.noSuchMethod( - Invocation.method( - #subscribeSession, - [], - { - #type: type, - #session: session, - #createFilter: createFilter, - }, - ), - returnValue: _i4.Stream<_i7.NostrEvent>.empty(), - ) as _i4.Stream<_i7.NostrEvent>); - - @override - void unsubscribeByType(_i19.SubscriptionType? type) => super.noSuchMethod( - Invocation.method( - #unsubscribeByType, - [type], - ), - returnValueForMissingStub: null, - ); - - @override - void unsubscribeSession(_i19.SubscriptionType? type) => super.noSuchMethod( - Invocation.method( - #unsubscribeSession, - [type], - ), - returnValueForMissingStub: null, - ); - - @override - bool hasActiveSubscription(_i19.SubscriptionType? type) => - (super.noSuchMethod( - Invocation.method( - #hasActiveSubscription, - [type], - ), + bool get closed => (super.noSuchMethod( + Invocation.getter(#closed), returnValue: false, ) as bool); @override - List<_i7.NostrFilter> getActiveFilters(_i19.SubscriptionType? type) => - (super.noSuchMethod( - Invocation.method( - #getActiveFilters, - [type], - ), - returnValue: <_i7.NostrFilter>[], - ) as List<_i7.NostrFilter>); - - @override - void subscribeAll() => super.noSuchMethod( + State read() => (super.noSuchMethod( Invocation.method( - #subscribeAll, + #read, [], ), - returnValueForMissingStub: null, - ); - - @override - void unsubscribeAll() => super.noSuchMethod( - Invocation.method( - #unsubscribeAll, - [], + returnValue: _i14.dummyValue( + this, + Invocation.method( + #read, + [], + ), ), - returnValueForMissingStub: null, - ); + ) as State); @override - void dispose() => super.noSuchMethod( + void close() => super.noSuchMethod( Invocation.method( - #dispose, + #close, [], ), returnValueForMissingStub: null, diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index bd30322e..1f57a0b0 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -2,21 +2,21 @@ import 'dart:convert'; import 'package:convert/convert.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; -import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; -import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; -import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager_provider.dart'; +import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; @@ -31,12 +31,33 @@ void main() { '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', defaultFiatCode: 'USD', )); - - // Add dummy for MostroStorage provideDummy(MockMostroStorage()); - - // Add dummy for NostrService provideDummy(MockNostrService()); + + // Create dummy values for Mockito + final dummyRef = MockRef(); + final dummyKeyManager = MockKeyManager(); + final dummySessionStorage = MockSessionStorage(); + final dummySettings = MockSettings(); + + // Stub listen on dummyRef to prevent errors when creating MockSubscriptionManager + when(dummyRef.listen>( + any, + any, + onError: anyNamed('onError'), + fireImmediately: anyNamed('fireImmediately'), + )).thenReturn(MockProviderSubscription>()); + + // Create and provide dummy values + final dummySessionNotifier = MockSessionNotifier( + dummyRef, dummyKeyManager, dummySessionStorage, dummySettings + ); + provideDummy(dummySessionNotifier); + + // Provide dummy for SubscriptionManager + final dummySubscriptionManagerForMockito = MockSubscriptionManager(dummyRef); + provideDummy(dummySubscriptionManagerForMockito); + late MostroService mostroService; late KeyDerivator keyDerivator; late MockServerTradeIndex mockServerTradeIndex; @@ -47,59 +68,49 @@ void main() { late MockKeyManager mockKeyManager; late MockSessionStorage mockSessionStorage; - setUpAll(() { - // Create a dummy Settings object that will be used by MockRef - final dummySettings = MockSettings(); - - // Provide dummy values for Mockito - provideDummy(MockSessionNotifier( - mockRef, mockKeyManager, mockSessionStorage, dummySettings)); - provideDummy(dummySettings); - provideDummy(MockNostrService()); - - // Create a mock ref for the SubscriptionManager dummy - provideDummy(MockSubscriptionManager()); - - // Create a mock ref that returns the dummy settings - final mockRefForDummy = MockRef(); - when(mockRefForDummy.read(settingsProvider)).thenReturn(dummySettings); - - // Provide a dummy MostroService that uses our properly configured mock ref - provideDummy(MostroService(mockRefForDummy)); - }); - setUp(() { - mockNostrService = MockNostrService(); - mockSessionNotifier = MockSessionNotifier( - mockRef, mockKeyManager, mockSessionStorage, MockSettings()); + // Initialize all mocks first mockRef = MockRef(); - mockSubscriptionManager = MockSubscriptionManager(); - mockServerTradeIndex = MockServerTradeIndex(); mockKeyManager = MockKeyManager(); mockSessionStorage = MockSessionStorage(); + mockNostrService = MockNostrService(); + mockServerTradeIndex = MockServerTradeIndex(); keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); - // Create test settings + + // Setup all stubs before creating any objects that use them final testSettings = MockSettings(); - - // Stub specific provider reads + when(testSettings.mostroPublicKey).thenReturn( + '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980'); when(mockRef.read(settingsProvider)).thenReturn(testSettings); when(mockRef.read(mostroStorageProvider)).thenReturn(MockMostroStorage()); when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); - when(mockRef.read(subscriptionManagerProvider)) - .thenReturn(mockSubscriptionManager); - - // Stub SessionNotifier methods - when(mockSessionNotifier.sessions).thenReturn([]); - - when(mockRef.read(sessionNotifierProvider.notifier)) - .thenReturn(mockSessionNotifier); - - // Create the service under test + + // Stub the listen method before creating SubscriptionManager + when(mockRef.listen>( + any, + any, + onError: anyNamed('onError'), + fireImmediately: anyNamed('fireImmediately'), + )).thenReturn(MockProviderSubscription>()); + + // Create mockSessionNotifier + mockSessionNotifier = MockSessionNotifier( + mockRef, + mockKeyManager, + mockSessionStorage, + testSettings, + ); + when(mockRef.read(sessionNotifierProvider.notifier)).thenReturn(mockSessionNotifier); + + // Create mockSubscriptionManager with the stubbed mockRef + mockSubscriptionManager = MockSubscriptionManager(mockRef); + when(mockRef.read(subscriptionManagerProvider)).thenReturn(mockSubscriptionManager); + + // Finally create the service under test mostroService = MostroService(mockRef); }); tearDown(() { - // Clean up resources mostroService.dispose(); mockSubscriptionManager.dispose(); }); @@ -153,14 +164,13 @@ void main() { fullPrivacy: false, ); - when(mockSessionNotifier.getSessionByOrderId(orderId)) - .thenReturn(session); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); // Mock NostrService's publishEvent only when(mockNostrService.publishEvent(any)).thenAnswer((_) async {}); - when(mockSessionNotifier.newSession(orderId: orderId)) - .thenAnswer((_) async => session); + // Note: newSession is already implemented in MockSessionNotifier // Act await mostroService.takeSellOrder(orderId, 100, 'lnbc1234invoice'); @@ -212,8 +222,8 @@ void main() { fullPrivacy: false, ); - when(mockSessionNotifier.getSessionByOrderId(orderId)) - .thenReturn(session); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); // Replaced when() call // Mock NostrService's publishEvent only - other methods are now static in NostrUtils when(mockNostrService.publishEvent(any)) @@ -272,8 +282,8 @@ void main() { fullPrivacy: false, ); - when(mockSessionNotifier.getSessionByOrderId(orderId)) - .thenReturn(session); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); // Replaced when() call // Simulate that tradeIndex=3 has already been used mockServerTradeIndex.userTradeIndices[userPubKey] = 3; @@ -335,8 +345,8 @@ void main() { fullPrivacy: true, ); - when(mockSessionNotifier.getSessionByOrderId(orderId)) - .thenReturn(session); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); // Replaced when() call // Mock NostrService's publishEvent only - other methods are now static in NostrUtils when(mockNostrService.publishEvent(any)) From b003beec4d0ba17f0786e18385b9124d70f85fcf Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 8 Jul 2025 15:25:51 -0700 Subject: [PATCH 20/28] refactor: standardize button text keys across language files --- lib/l10n/intl_en.arb | 5 +++-- lib/l10n/intl_es.arb | 4 ++-- lib/l10n/intl_it.arb | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 31627f24..cd592a10 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -380,11 +380,12 @@ "copyButton": "Copy", "shareButton": "Share", "failedToShareInvoice": "Failed to share invoice. Please try copying instead.", - "openWallet": "OPEN WALLET", + "openWalletButton": "OPEN WALLET", "language": "Language", "systemDefault": "System Default", "english": "English", "spanish": "Spanish", "italian": "Italian", - "chooseLanguageDescription": "Choose your preferred language or use system default" + "chooseLanguageDescription": "Choose your preferred language or use system default", + "doneButton": "DONE" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 0191ad9b..8b9e4c96 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -380,12 +380,12 @@ "copyButton": "Copiar", "shareButton": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", - "openWallet": "ABRIR BILLETERA", + "openWalletButton": "ABRIR BILLETERA", "language": "Idioma", "systemDefault": "Predeterminado del sistema", "english": "Inglés", "spanish": "Español", "italian": "Italiano", "chooseLanguageDescription": "Elige tu idioma preferido o usa el predeterminado del sistema", - "done": "HECHO" + "doneButton": "HECHO" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index be226c2a..9d7c3714 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -380,12 +380,12 @@ "copyButton": "Copia", "shareButton": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", - "openWallet": "APRI PORTAFOGLIO", + "openWalletButton": "APRI PORTAFOGLIO", "language": "Lingua", "systemDefault": "Predefinito di sistema", "english": "Inglese", "spanish": "Spagnolo", "italian": "Italiano", "chooseLanguageDescription": "Scegli la tua lingua preferita o usa il predefinito di sistema", - "done": "FATTO" + "doneButton": "FATTO" } From c71927df98bb0c84636ebc56ccd59e3e9fe94977 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 8 Jul 2025 18:15:24 -0700 Subject: [PATCH 21/28] refactor: simplify event storage and optimize trade filtering logic --- lib/background/background.dart | 4 - .../desktop_background_service.dart | 8 -- lib/core/config.dart | 2 +- lib/data/models/mostro_message.dart | 16 ++-- lib/data/repositories/event_storage.dart | 75 ++----------------- .../chat/notifiers/chat_room_notifier.dart | 5 +- .../order/notfiers/order_notifier.dart | 2 + .../trades/providers/trades_provider.dart | 9 ++- .../trades/screens/trade_detail_screen.dart | 2 +- lib/main.dart | 1 - lib/services/mostro_service.dart | 5 +- test/mocks.mocks.dart | 4 + 12 files changed, 36 insertions(+), 97 deletions(-) diff --git a/lib/background/background.dart b/lib/background/background.dart index d82f92be..8f38a4b6 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -14,10 +14,6 @@ bool isAppForeground = true; @pragma('vm:entry-point') Future serviceMain(ServiceInstance service) async { - // If on Android, set up a permanent notification so the OS won't kill it. - if (service is AndroidServiceInstance) { - //service.setAsForegroundService(); - } final Map> activeSubscriptions = {}; final nostrService = NostrService(); diff --git a/lib/background/desktop_background_service.dart b/lib/background/desktop_background_service.dart index f74668fb..6e5535aa 100644 --- a/lib/background/desktop_background_service.dart +++ b/lib/background/desktop_background_service.dart @@ -4,10 +4,8 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter/services.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/nostr_filter.dart'; -import 'package:mostro_mobile/data/repositories.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; -import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; import 'abstract_background_service.dart'; class DesktopBackgroundService implements BackgroundService { @@ -30,8 +28,6 @@ class DesktopBackgroundService implements BackgroundService { BackgroundIsolateBinaryMessenger.ensureInitialized(token); final nostrService = NostrService(); - final db = await openMostroDatabase('events.db'); - final backgroundStorage = EventStorage(db: db); final logger = Logger(); bool isAppForeground = true; @@ -67,10 +63,6 @@ class DesktopBackgroundService implements BackgroundService { final subscription = nostrService.subscribeToEvents(request); subscription.listen((event) async { - await backgroundStorage.putItem( - event.id!, - event, - ); mainSendPort.send({ 'event': event.toMap(), }); diff --git a/lib/core/config.dart b/lib/core/config.dart index 8a1a3f4e..2838b872 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; class Config { // Configuración de Nostr static const List nostrRelays = [ - 'wss://relay.mostro.network', + 'wss://relay.mostro.network/', //'ws://127.0.0.1:7000', //'ws://192.168.1.103:7000', //'ws://10.0.2.2:7000', // mobile emulator diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index f908bd44..505ed7c5 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -16,14 +16,14 @@ class MostroMessage { T? _payload; int? timestamp; - MostroMessage( - {required this.action, - this.requestId, - this.id, - T? payload, - this.tradeIndex, - this.timestamp}) - : _payload = payload; + MostroMessage({ + required this.action, + this.requestId, + this.id, + T? payload, + this.tradeIndex, + this.timestamp, + }) : _payload = payload; Map toJson() { Map json = { diff --git a/lib/data/repositories/event_storage.dart b/lib/data/repositories/event_storage.dart index 850391b0..4b38b8e3 100644 --- a/lib/data/repositories/event_storage.dart +++ b/lib/data/repositories/event_storage.dart @@ -1,8 +1,7 @@ -import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/data/repositories/base_storage.dart'; import 'package:sembast/sembast.dart'; -class EventStorage extends BaseStorage { +class EventStorage extends BaseStorage> { EventStorage({ required Database db, }) : super( @@ -11,76 +10,12 @@ class EventStorage extends BaseStorage { ); @override - NostrEvent fromDbMap(String key, Map event) { - return NostrEvent( - id: event['id'] as String, - kind: event['kind'] as int, - content: event['content'] == null ? '' : event['content'] as String, - sig: event['sig'] as String, - pubkey: event['pubkey'] as String, - createdAt: DateTime.fromMillisecondsSinceEpoch( - (event['created_at'] as int) * 1000, - ), - tags: List>.from( - (event['tags'] as List) - .map( - (nestedElem) => (nestedElem as List) - .map( - (nestedElemContent) => nestedElemContent.toString(), - ) - .toList(), - ) - .toList(), - ), - ); + Map fromDbMap(String key, Map event) { + return event; } @override - Map toDbMap(NostrEvent event) { - return event.toMap(); - } - - /// Stream of all events for a query - Stream> watchAll({Filter? filter}) { - final finder = filter != null ? Finder(filter: filter) : null; - - return store - .query(finder: finder) - .onSnapshots(db) - .map((snapshots) => snapshots - .map((snapshot) => fromDbMap(snapshot.key, snapshot.value)) - .toList()); - } - - /// Stream of the latest event matching a query - Stream watchLatest({Filter? filter, List? sortOrders}) { - final finder = Finder( - filter: filter, - sortOrders: sortOrders ?? [SortOrder('created_at', false)], - limit: 1 - ); - - return store - .query(finder: finder) - .onSnapshots(db) - .map((snapshots) => snapshots.isNotEmpty - ? fromDbMap(snapshots.first.key, snapshots.first.value) - : null); - } - - /// Stream of events filtered by event ID - @override - Stream watchById(String eventId) { - final finder = Finder( - filter: Filter.equals('id', eventId), - limit: 1 - ); - - return store - .query(finder: finder) - .onSnapshots(db) - .map((snapshots) => snapshots.isNotEmpty - ? fromDbMap(snapshots.first.key, snapshots.first.value) - : null); + Map toDbMap(Map event) { + return event; } } diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index f2276a58..fae69add 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -62,7 +62,10 @@ class ChatRoomNotifier extends StateNotifier { if (!await eventStore.hasItem(event.id!)) { await eventStore.putItem( event.id!, - event, + { + 'id': event.id, + 'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000, + }, ); } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 6b62b227..fa4d820e 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -6,11 +6,13 @@ import 'package:mostro_mobile/services/mostro_service.dart'; class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; + OrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); sync(); subscribe(); } + Future sync() async { try { final storage = ref.read(mostroStorageProvider); diff --git a/lib/features/trades/providers/trades_provider.dart b/lib/features/trades/providers/trades_provider.dart index 68e1eea7..d00349a7 100644 --- a/lib/features/trades/providers/trades_provider.dart +++ b/lib/features/trades/providers/trades_provider.dart @@ -9,6 +9,12 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final _logger = Logger(); +final _statusFilter = { + Status.canceled, + Status.canceledByAdmin, + Status.expired, +}; + final filteredTradesProvider = Provider>>((ref) { final allOrdersAsync = ref.watch(orderEventsProvider); final sessions = ref.watch(sessionNotifierProvider); @@ -29,8 +35,7 @@ final filteredTradesProvider = Provider>>((ref) { final filtered = sortedOrders.reversed .where((o) => orderIds.contains(o.orderId)) - .where((o) => o.status != Status.canceled) - .where((o) => o.status != Status.canceledByAdmin) + .where((o) => !_statusFilter.contains(o.status)) .toList(); _logger.d('Filtered to ${filtered.length} trades'); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index d1bfa466..7399e984 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -457,7 +457,7 @@ class TradeDetailScreen extends ConsumerWidget { backgroundColor: backgroundColor, onPressed: onPressed ?? () {}, // Provide empty function when null showSuccessIndicator: true, - timeout: const Duration(seconds: 30), + timeout: const Duration(seconds: 10), ); } diff --git a/lib/main.dart b/lib/main.dart index 854453e7..37e91174 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,7 +31,6 @@ Future main() async { await initializeNotifications(); - // Initialize timeago localization _initializeTimeAgoLocalization(); final backgroundService = createBackgroundService(settings.settings); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index ba60ba65..b198c561 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -44,7 +44,10 @@ class MostroService { if (await eventStore.hasItem(event.id!)) return; await eventStore.putItem( event.id!, - event, + { + 'id': event.id, + 'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000, + }, ); final sessions = ref.read(sessionNotifierProvider); diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index ccf683ef..b90b5e45 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -550,6 +550,7 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock implements _i13.SharedPreferencesAsync { MockSharedPreferencesAsync() { @@ -1620,6 +1621,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { bool? privacyModeSetting, String? mostroInstance, String? defaultFiatCode, + String? selectedLanguage, }) => (super.noSuchMethod( Invocation.method( @@ -1630,6 +1632,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { #privacyModeSetting: privacyModeSetting, #mostroInstance: mostroInstance, #defaultFiatCode: defaultFiatCode, + #selectedLanguage: selectedLanguage, }, ), returnValue: _FakeSettings_0( @@ -1642,6 +1645,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { #privacyModeSetting: privacyModeSetting, #mostroInstance: mostroInstance, #defaultFiatCode: defaultFiatCode, + #selectedLanguage: selectedLanguage, }, ), ), From 455f0dec0a89c263f5048d5df187aea84d06493a Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 8 Jul 2025 19:11:18 -0700 Subject: [PATCH 22/28] chore: exclude generated files from analysis and add logging for DM events --- analysis_options.yaml | 8 ++++++++ lib/services/mostro_service.dart | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d290213..0114bc86 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,14 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.mocks.dart" + - "**/*.g.dart" + - "**/*.freezed.dart" + - "**/*.gr.dart" + - "**/*.config.dart" + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index b198c561..afe69638 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -183,7 +183,7 @@ class MostroService { masterKey: session.fullPrivacy ? null : session.masterKey, keyIndex: session.fullPrivacy ? null : session.keyIndex, ); - + _logger.i('Sending DM, Event ID: ${event.id} with payload: ${order.toJson()}'); await ref.read(nostrServiceProvider).publishEvent(event); } From c6cdeb1e9a5257d5d07bf20178ea1a968ecfcd94 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 9 Jul 2025 13:16:00 -0700 Subject: [PATCH 23/28] fix: handle empty sessions and missing shared keys in subscription filter creation --- .../subscriptions/subscription_manager.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index fad5eb5e..d9d1f5e5 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -72,7 +72,9 @@ class SubscriptionManager { try { final filter = _createFilterForType(type, sessions); - + if (filter == null) { + return; + } subscribe( type: type, filter: filter, @@ -86,17 +88,24 @@ class SubscriptionManager { } } - NostrFilter _createFilterForType( + NostrFilter? _createFilterForType( SubscriptionType type, List sessions) { switch (type) { case SubscriptionType.orders: + if (sessions.isEmpty) { + return null; + } return NostrFilter( kinds: [1059], - p: sessions - .map((s) => s.tradeKey.public) - .toList(), + p: sessions.map((s) => s.tradeKey.public).toList(), ); case SubscriptionType.chat: + if (sessions.isEmpty) { + return null; + } + if (sessions.where((s) => s.sharedKey?.public != null).isEmpty) { + return null; + } return NostrFilter( kinds: [1059], p: sessions From efe3e4cc6c7474cc9f8f463ce9bc2de5dad31721 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 10 Jul 2025 13:22:24 -0700 Subject: [PATCH 24/28] feat: unsubscribe all active subscriptions when app enters background state --- lib/services/lifecycle_manager.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index c8f504f4..b5f3ce6c 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -103,7 +103,7 @@ class LifecycleManager extends WidgetsBindingObserver { if (activeFilters.isNotEmpty) { _isInBackground = true; _logger.i("Switching to background"); - + subscriptionManager.unsubscribeAll(); // Transfer active subscriptions to background service final backgroundService = ref.read(backgroundServiceProvider); await backgroundService.setForegroundStatus(false); From c2d6b165c3de3172761f211d801251c0344ce8b0 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 10 Jul 2025 14:12:10 -0700 Subject: [PATCH 25/28] feat: add fetchEvents method to NostrService for async event retrieval --- lib/services/nostr_service.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index 3ed942af..bf289c33 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -88,6 +88,18 @@ class NostrService { } } + Future> fetchEvents(NostrFilter filter) async { + if (!_isInitialized) { + throw Exception('Nostr is not initialized. Call init() first.'); + } + + final request = NostrRequest(filters: [filter]); + return await _nostr.services.relays.startEventsSubscriptionAsync( + request: request, + timeout: Config.nostrConnectionTimeout, + ); + } + Stream subscribeToEvents(NostrRequest request) { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); @@ -208,7 +220,7 @@ class NostrService { events = await _fetchFromSpecificRelays(filter, specificRelays); } else { // Use default relays - events = await fecthEvents(filter); + events = await fetchEvents(filter); } if (events.isEmpty) { @@ -260,7 +272,7 @@ class NostrService { events = await _fetchFromSpecificRelays(filter, specificRelays); } else { // Use default relays - events = await fecthEvents(filter); + events = await fetchEvents(filter); } if (events.isEmpty) { @@ -355,7 +367,7 @@ class NostrService { await updateSettings(tempSettings); // Fetch the events - final events = await fecthEvents(filter); + final events = await fetchEvents(filter); // Restore original relays await updateSettings(settings); @@ -363,7 +375,7 @@ class NostrService { return events; } else { // No new relays to add, use normal fetch - return await fecthEvents(filter); + return await fetchEvents(filter); } } catch (e) { _logger.e('Error fetching from specific relays: $e'); From 77c7d142fbdf5cfd0d08c900c275334e8b6f99fc Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 14 Jul 2025 23:12:51 -0500 Subject: [PATCH 26/28] chore: remove unused order-related translations from English locale file --- lib/l10n/intl_en.arb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fb945f4d..4dee30c1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -187,11 +187,9 @@ "expired": "Expired", "success": "Success", "orderIdCopied": "Order ID copied to clipboard", - "orderDetails": "ORDER DETAILS", "noPaymentMethod": "No payment method", "youAreSellingText": "You are selling{sats} sats for {amount} {price} {premium}", "youAreBuyingText": "You are buying{sats} sats for {amount} {price} {premium}", - "atMarketPrice": "at market price", "withPremium": " with a +{premium}% premium", "withDiscount": " with a {premium}% discount", "createdOn": "Created on", @@ -412,9 +410,6 @@ "lightningInvoice": "Lightning Invoice", "enterInvoiceHere": "Enter invoice here", - "cancelPendingButton": "CANCEL PENDING", - "acceptCancelButton": "ACCEPT CANCEL", - "completePurchaseButton": "COMPLETE PURCHASE", "rateButton": "RATE", "contactButton": "CONTACT", From 150ea822a7b9eb86599bfde735a554431056748a Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 15 Jul 2025 12:01:38 -0500 Subject: [PATCH 27/28] refactor: standardize done button text and remove unused wallet button across locales --- lib/l10n/intl_en.arb | 1 - lib/l10n/intl_es.arb | 4 ++-- lib/l10n/intl_it.arb | 5 ++--- lib/shared/widgets/pay_lightning_invoice_widget.dart | 12 ------------ 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 4dee30c1..7ba8e997 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -423,7 +423,6 @@ "copyButton": "Copy", "shareButton": "Share", "failedToShareInvoice": "Failed to share invoice. Please try copying instead.", - "openWalletButton": "OPEN WALLET", "language": "Language", "systemDefault": "System Default", "english": "English", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 588be924..9c0626cc 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -481,6 +481,6 @@ } } }, - "deepLinkGoHome": "Ir al Inicio" - + "deepLinkGoHome": "Ir al Inicio", + "doneButton": "HECHO" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index c8536653..dec7455c 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -469,7 +469,6 @@ "chooseLanguageDescription": "Scegli la tua lingua preferita o usa il predefinito di sistema", "loadingOrder": "Caricamento ordine...", - "done": "FATTO", "unsupportedLinkFormat": "Formato di link non supportato", "failedToOpenLink": "Impossibile aprire il link", "failedToLoadOrder": "Impossibile caricare l'ordine", @@ -489,6 +488,6 @@ } } }, - "deepLinkGoHome": "Vai alla Home" - + "deepLinkGoHome": "Vai alla Home", + "doneButton": "FATTO" } diff --git a/lib/shared/widgets/pay_lightning_invoice_widget.dart b/lib/shared/widgets/pay_lightning_invoice_widget.dart index d8d90af0..2c7f2a29 100644 --- a/lib/shared/widgets/pay_lightning_invoice_widget.dart +++ b/lib/shared/widgets/pay_lightning_invoice_widget.dart @@ -123,18 +123,6 @@ class _PayLightningInvoiceWidgetState extends State { ], ), const SizedBox(height: 20), - // Open Wallet Button - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - onPressed: widget.onSubmit, - child: Text(S.of(context)!.openWalletButton), - ), - const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ From 950c6c19a7f5ffd27cf3625e0351dc3eb132195f Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 21 Jul 2025 10:04:45 -0700 Subject: [PATCH 28/28] fix: update button labels to use shorter translation keys --- lib/shared/widgets/pay_lightning_invoice_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/shared/widgets/pay_lightning_invoice_widget.dart b/lib/shared/widgets/pay_lightning_invoice_widget.dart index 63e2915e..586d9e71 100644 --- a/lib/shared/widgets/pay_lightning_invoice_widget.dart +++ b/lib/shared/widgets/pay_lightning_invoice_widget.dart @@ -86,7 +86,7 @@ class _PayLightningInvoiceWidgetState extends State { ); }, icon: const Icon(Icons.copy), - label: Text(S.of(context)!.copyButton), + label: Text(S.of(context)!.copy), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.mostroGreen, shape: RoundedRectangleBorder( @@ -125,7 +125,7 @@ class _PayLightningInvoiceWidgetState extends State { } }, icon: const Icon(Icons.share), - label: Text(S.of(context)!.shareButton), + label: Text(S.of(context)!.share), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.mostroGreen, shape: RoundedRectangleBorder(