diff --git a/.gitignore b/.gitignore index e2b5011f..a81e853d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,13 +54,13 @@ web/.packages web/build/ # Windows -windows/flutter/generated_plugin_registrant.cc +windows/flutter/generated_plugin_registrant.* windows/flutter/generated_plugins.cmake windows/.flutter-plugins windows/.flutter-plugins-dependencies # Linux -linux/flutter/generated_plugin_registrant.cc +linux/flutter/generated_plugin_registrant.* linux/flutter/generated_plugins.cmake linux/.flutter-plugins linux/.flutter-plugins-dependencies 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/integration_test/test_helpers.dart b/integration_test/test_helpers.dart index bf3264c0..e8921fa8 100644 --- a/integration_test/test_helpers.dart +++ b/integration_test/test_helpers.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:dart_nostr/nostr/model/request/filter.dart'; +import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -268,13 +268,7 @@ class FakeMostroService implements MostroService { final Ref ref; @override - void init() {} - - @override - void subscribe(Session session) {} - - @override - Session? getSessionByOrderId(String orderId) => null; + void init({List? keys}) {} @override Future submitOrder(MostroMessage order) async { @@ -319,6 +313,11 @@ class FakeMostroService implements MostroService { @override void updateSettings(Settings settings) {} + + @override + void dispose() { + // TODO: implement dispose + } } Future pumpTestApp(WidgetTester tester) async { diff --git a/lib/background/background.dart b/lib/background/background.dart index 5960d524..8f38a4b6 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'; @@ -13,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(); @@ -68,8 +65,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) { + Logger().e('Error processing event', error: e); + } }); }); 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/background/mobile_background_service.dart b/lib/background/mobile_background_service.dart index 67d6abb4..f42c1660 100644 --- a/lib/background/mobile_background_service.dart +++ b/lib/background/mobile_background_service.dart @@ -24,15 +24,15 @@ 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, + isForegroundMode: false, + autoStartOnBoot: false, initialNotificationTitle: "Mostro", initialNotificationContent: "Connected to Mostro service", foregroundServiceTypes: [ diff --git a/lib/core/config.dart b/lib/core/config.dart index 3333f4f0..8b24e96c 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/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..5b303e31 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, Object.hashAll(messages)); + + @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 f0d07784..834474d2 100644 --- a/lib/data/models/currency.dart +++ b/lib/data/models/currency.dart @@ -19,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 632a991b..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,12 +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/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/models/order.dart b/lib/data/models/order.dart index 0fa04923..07acb666 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; // ignore: return_of_null + } + + // 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..98ae7de0 100644 --- a/lib/data/models/session.dart +++ b/lib/data/models/session.dart @@ -47,16 +47,100 @@ 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'); + } + + // 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; + 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: masterKeyValue, + tradeKey: tradeKeyValue, + 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)'; } diff --git a/lib/data/repositories/event_storage.dart b/lib/data/repositories/event_storage.dart index ba81fa77..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,67 +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/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/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index 04fb666b..fae69add 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,50 @@ 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 = subscriptionManager.chat.listen(_onChatEvent); + } - subscription = - ref.read(nostrServiceProvider).subscribeToEvents(request).listen( - (event) async { - try { - final eventStore = ref.read(eventStorageProvider); + 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; + } - await eventStore.putItem( - event.id!, - event, - ); + if (session.sharedKey?.public != event.recipient) { + 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); - } - }, - ); + final eventStore = ref.read(eventStorageProvider); + + if (!await eventStore.hasItem(event.id!)) { + await eventStore.putItem( + event.id!, + { + 'id': event.id, + 'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000, + }, + ); + } + + 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 +107,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 398714ec..bf13a206 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -1,20 +1,21 @@ +import 'dart:async'; + 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/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 []) { + ChatRoomsNotifier(this.ref) : super(const []) { loadChats(); } + - /// Reload all chat rooms by triggering their notifiers to resubscribe to events. void reloadAllChats() { for (final chat in state) { try { @@ -26,10 +27,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; @@ -55,4 +58,14 @@ 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(); + } + } 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_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index 63d296dd..ebc72e1d 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'; @@ -30,7 +31,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, @@ -64,7 +66,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/chat/screens/chat_rooms_list.dart b/lib/features/chat/screens/chat_rooms_list.dart index 5b01e951..5a595814 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/custom_drawer_overlay.dart'; @@ -75,8 +75,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/key_manager/key_manager.dart b/lib/features/key_manager/key_manager.dart index e4c3ebcb..dd53161f 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/order/models/order_state.dart b/lib/features/order/models/order_state.dart index b27e3d6a..9dd7df01 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 @@ -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,13 +132,9 @@ 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}'); - _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 ec9d1abe..b12a643c 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; @@ -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; @@ -132,7 +139,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, @@ -147,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': @@ -161,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); @@ -174,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!) @@ -195,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 08dbedf3..458029c9 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,11 +19,9 @@ 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); + final timestamp = DateTime.now().microsecondsSinceEpoch; + return (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & + 0x7FFFFFFF; } @override @@ -54,10 +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(); @@ -79,12 +73,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); - mostroService.subscribe(session); 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 6994e8e6..fa4d820e 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -1,39 +1,25 @@ import 'dart:async'; import 'package:mostro_mobile/data/enums.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'; 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); final messages = await storage.getAllMessagesForOrderId(orderId); - if (messages.isEmpty) { - logger.w('No messages found for order $orderId'); - return; - } - messages.sort((a, b) { - final timestampA = a.timestamp ?? 0; - final timestampB = b.timestamp ?? 0; - return timestampA.compareTo(timestampB); - }); - OrderState currentState = state; - for (final message in messages) { - if (message.action != Action.cantDo) { - currentState = currentState.updateWith(message); - } + for (final msg in messages) { + state = state.updateWith(msg); } - state = currentState; - logger.i( - 'Synced order $orderId to state: ${state.status} - ${state.action}'); } catch (e, stack) { logger.e( 'Error syncing order state for $orderId', @@ -50,7 +36,6 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.buyer, ); - mostroService.subscribe(session); await mostroService.takeSellOrder( orderId, amount, @@ -64,7 +49,6 @@ class OrderNotifier extends AbstractMostroNotifier { orderId: orderId, role: Role.seller, ); - mostroService.subscribe(session); await mostroService.takeBuyOrder( orderId, amount, diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index d7e5a76e..8eb3de65 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -285,20 +285,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 new file mode 100644 index 00000000..d1babd6c --- /dev/null +++ b/lib/features/subscriptions/subscription.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +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 new file mode 100644 index 00000000..d9d1f5e5 --- /dev/null +++ b/lib/features/subscriptions/subscription_manager.dart @@ -0,0 +1,227 @@ +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) and automatically +/// managing subscriptions based on session changes in the SessionNotifier. +class SubscriptionManager { + final Ref ref; + final Map _subscriptions = {}; + final _logger = Logger(); + late final ProviderSubscription _sessionListener; + + final _ordersController = StreamController.broadcast(); + final _chatController = StreamController.broadcast(); + + Stream get orders => _ordersController.stream; + Stream get chat => _chatController.stream; + + SubscriptionManager(this.ref) { + _initSessionListener(); + } + + void _initSessionListener() { + _sessionListener = ref.listen>( + sessionNotifierProvider, + (previous, current) { + _updateAllSubscriptions(current); + }, + fireImmediately: true, + onError: (error, stackTrace) { + _logger.e('Error in session listener', + error: error, stackTrace: stackTrace); + }, + ); + } + + void _updateAllSubscriptions(List sessions) { + if (sessions.isEmpty) { + _logger.i('No sessions available, clearing all subscriptions'); + _clearAllSubscriptions(); + return; + } + + for (final type in SubscriptionType.values) { + _updateSubscription(type, sessions); + } + } + + void _clearAllSubscriptions() { + for (final type in SubscriptionType.values) { + unsubscribeByType(type); + } + } + + void _updateSubscription(SubscriptionType type, List sessions) { + unsubscribeByType(type); + + if (sessions.isEmpty) { + _logger.i('No sessions for $type subscription'); + return; + } + + try { + final filter = _createFilterForType(type, sessions); + if (filter == null) { + return; + } + subscribe( + type: type, + filter: filter, + ); + + _logger + .i('Subscription created for $type with ${sessions.length} sessions'); + } catch (e, stackTrace) { + _logger.e('Failed to create $type subscription', + error: e, stackTrace: stackTrace); + } + } + + 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(), + ); + 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 + .where((s) => s.sharedKey?.public != null) + .map((s) => s.sharedKey!.public) + .toList(), + ); + } + } + + void _handleEvent(SubscriptionType type, NostrEvent event) { + try { + switch (type) { + case SubscriptionType.orders: + _ordersController.add(event); + break; + case SubscriptionType.chat: + _chatController.add(event); + break; + } + } catch (e, stackTrace) { + _logger.e('Error handling $type event', error: e, stackTrace: stackTrace); + } + } + + Stream subscribe({ + required SubscriptionType type, + required NostrFilter filter, + }) { + final nostrService = ref.read(nostrServiceProvider); + + final request = NostrRequest( + 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, + onCancel: () { + ref.read(nostrServiceProvider).unsubscribe(request.subscriptionId!); + }, + ); + + if (_subscriptions.containsKey(type)) { + _subscriptions[type]!.cancel(); + } + + _subscriptions[type] = subscription; + + switch (type) { + case SubscriptionType.orders: + return orders; + case SubscriptionType.chat: + return chat; + } + } + + Stream subscribeSession({ + required SubscriptionType type, + required Session session, + required NostrFilter Function(Session) createFilter, + }) { + final filter = createFilter(session); + return subscribe( + type: type, + filter: filter, + ); + } + + void unsubscribeByType(SubscriptionType type) { + final subscription = _subscriptions[type]; + if (subscription != null) { + subscription.cancel(); + _subscriptions.remove(type); + } + } + + void unsubscribeSession(SubscriptionType type) { + unsubscribeByType(type); + } + + bool hasActiveSubscription(SubscriptionType type) { + return _subscriptions[type] != null; + } + + List getActiveFilters(SubscriptionType type) { + final subscription = _subscriptions[type]; + return subscription?.request.filters ?? []; + } + + void subscribeAll() { + unsubscribeAll(); + final currentSessions = ref.read(sessionNotifierProvider); + _updateAllSubscriptions(currentSessions); + } + + void unsubscribeAll() { + for (final type in SubscriptionType.values) { + unsubscribeByType(type); + } + } + + void dispose() { + _sessionListener.close(); + unsubscribeAll(); + _ordersController.close(); + _chatController.close(); + } +} diff --git a/lib/features/subscriptions/subscription_manager_provider.dart b/lib/features/subscriptions/subscription_manager_provider.dart new file mode 100644 index 00000000..64645576 --- /dev/null +++ b/lib/features/subscriptions/subscription_manager_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; + +final subscriptionManagerProvider = + Provider( + (ref) => SubscriptionManager(ref), +); diff --git a/lib/features/subscriptions/subscription_type.dart b/lib/features/subscriptions/subscription_type.dart new file mode 100644 index 00000000..cdf712b8 --- /dev/null +++ b/lib/features/subscriptions/subscription_type.dart @@ -0,0 +1,4 @@ +enum SubscriptionType { + chat, + orders, +} 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 99e2fea2..e00f41f2 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -484,7 +484,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), controller: controller, ); } @@ -497,7 +497,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 77ee244b..dfd97df6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -247,11 +247,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", @@ -336,7 +334,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", @@ -351,7 +348,6 @@ "noMnemonicFound": "No mnemonic found", "errorLoadingMnemonic": "Error: {error}", - "@_comment_chat_screens": "Chat Screen Strings", "noMessagesAvailable": "No messages available", "back": "BACK", "typeAMessage": "Type a message...", @@ -359,18 +355,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...", @@ -380,9 +373,7 @@ "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.", @@ -406,7 +397,6 @@ } } }, - "noPaymentMethod": "No payment method", "someoneIsSellingBuying": "Someone is {action}{satAmount} sats for {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -504,18 +494,12 @@ "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", + "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 for {sats} Sats equivalent to {fiat_code} {fiat_amount} to continue the exchange for order {order_id}", "@payInvoiceToContinue": { @@ -551,6 +535,7 @@ "italian": "Italian", "loadingOrder": "Loading order...", "chooseLanguageDescription": "Choose your preferred language or use system default", + "doneButton": "DONE", "unsupportedLinkFormat": "Unsupported link format", "failedToOpenLink": "Failed to open link", "failedToLoadOrder": "Failed to load order", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 0758ec9d..d2d49617 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -301,7 +301,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", @@ -316,7 +315,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...", @@ -324,18 +322,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...", @@ -345,18 +340,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": { @@ -373,7 +361,6 @@ } } }, - "noPaymentMethod": "Sin método de pago", "someoneIsSellingBuying": "Alguien está {action}{satAmount} sats por {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -471,18 +458,15 @@ "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 de {sats} Sats equivalentes a {fiat_amount} {fiat_code} para continuar el intercambio de la orden {order_id}", "@payInvoiceToContinue": { diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index efcbfaeb..4297907b 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -297,7 +297,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", @@ -312,7 +311,6 @@ "noMnemonicFound": "Nessun mnemonico trovato", "errorLoadingMnemonic": "Errore: {error}", - "@_comment_chat_screens": "Stringhe Schermata Chat", "noMessagesAvailable": "Nessun messaggio disponibile", "back": "INDIETRO", "typeAMessage": "Scrivi un messaggio...", @@ -320,18 +318,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...", @@ -341,18 +336,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": { @@ -369,7 +357,6 @@ } } }, - "noPaymentMethod": "Nessun metodo di pagamento", "someoneIsSellingBuying": "Qualcuno sta {action}{satAmount} sats per {amountString} {price} {premiumText}", "@someoneIsSellingBuying": { "placeholders": { @@ -467,18 +454,15 @@ "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 di {sats} Sats equivalenti a {fiat_amount} {fiat_code} per continuare lo scambio dell'ordine {order_id}", "@payInvoiceToContinue": { @@ -588,7 +572,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", diff --git a/lib/main.dart b/lib/main.dart index e83e34e8..6336f12b 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'; @@ -19,9 +17,7 @@ import 'package:timeago/timeago.dart' as timeago; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - if (Platform.isAndroid && !Platform.environment.containsKey('FLUTTER_TEST')) { - await requestNotificationPermissionIfNeeded(); - } + await requestNotificationPermissionIfNeeded(); final biometricsHelper = BiometricsHelper(); final sharedPreferences = SharedPreferencesAsync(); @@ -35,7 +31,6 @@ Future main() async { await initializeNotifications(); - // Initialize timeago localization _initializeTimeAgoLocalization(); final backgroundService = createBackgroundService(settings.settings); diff --git a/lib/notifications/notification_service.dart b/lib/notifications/notification_service.dart index 9d4aca2c..1847b285 100644 --- a/lib/notifications/notification_service.dart +++ b/lib/notifications/notification_service.dart @@ -44,45 +44,36 @@ Future showLocalNotification(NostrEvent event) async { 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, ); } -Future retryNotification(NostrEvent event, {int maxAttempts = 3}) async { - int attempt = 0; - bool success = false; - - while (!success && attempt < maxAttempts) { - try { - await showLocalNotification(event); - success = true; - } catch (e) { - attempt++; - if (attempt >= maxAttempts) { - Logger() - .e('Failed to show notification after $maxAttempts attempts: $e'); - break; - } - - // Exponential backoff: 1s, 2s, 4s, etc. - final backoffSeconds = pow(2, attempt - 1).toInt(); - Logger().e( - 'Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s'); - 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!); - } -} +Future retryNotification(NostrEvent event, {int maxAttempts = 3}) async { + int attempt = 0; + bool success = false; + + while (!success && attempt < maxAttempts) { + try { + await showLocalNotification(event); + success = true; + } catch (e) { + attempt++; + if (attempt >= maxAttempts) { + Logger().e('Failed to show notification after $maxAttempts attempts: $e'); + break; + } + + // Exponential backoff: 1s, 2s, 4s, etc. + final backoffSeconds = pow(2, attempt - 1).toInt(); + Logger().e('Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s'); + await Future.delayed(Duration(seconds: backoffSeconds)); + } + } +} diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index 82f91bff..b5f3ce6c 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -5,15 +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/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) { @@ -49,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); @@ -60,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(); @@ -86,17 +87,29 @@ 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); + // Get the subscription manager + final subscriptionManager = ref.read(subscriptionManagerProvider); + final activeFilters = []; + + // 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"); + subscriptionManager.unsubscribeAll(); + // 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); + "Transferring ${activeFilters.length} active filters to background service"); + backgroundService.subscribe(activeFilters); } else { _logger.w("No active subscriptions to transfer to background service"); } @@ -107,8 +120,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 f132df8c..afe69638 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,87 +1,67 @@ +import 'dart:async'; import 'dart:convert'; -import 'package:dart_nostr/nostr/model/export.dart'; +import 'package:collection/collection.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/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.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/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'; -import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; -import 'package:logger/logger.dart'; class MostroService { final Ref ref; - final SessionNotifier _sessionNotifier; - static final Logger _logger = Logger(); + final _logger = Logger(); Settings _settings; - - MostroService( - this._sessionNotifier, - this.ref, - ) : _settings = ref.read(settingsProvider).copyWith() { - init(); + StreamSubscription? _ordersSubscription; + + MostroService(this.ref) : _settings = ref.read(settingsProvider); + + void init() { + // 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 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 dispose() { + _ordersSubscription?.cancel(); + _logger.i('MostroService disposed'); } - void subscribe(Session session) { - final filter = NostrFilter( - kinds: [1059], - p: [session.tradeKey.public], - ); - - final request = NostrRequest(filters: [filter]); - - ref.read(lifecycleManagerProvider).addSubscription(filter); + Future _onData(NostrEvent event) async { + final eventStore = ref.read(eventStorageProvider); - 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!, + { + 'id': event.id, + 'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000, + }, + ); - if (await eventStore.hasItem(event.id!)) return; - await eventStore.putItem( - event.id!, - event, + final sessions = ref.read(sessionNotifierProvider); + final matchingSession = sessions.firstWhereOrNull( + (s) => s.tradeKey.public == event.recipient, ); + if (matchingSession == null) { + _logger.w('No matching session found for recipient: ${event.recipient}'); + return; + } + final privateKey = matchingSession.tradeKey.private; - final decryptedEvent = await event.unWrap( - session.tradeKey.private, - ); + try { + final decryptedEvent = await event.unWrap(privateKey); if (decryptedEvent.content == null) return; final result = jsonDecode(decryptedEvent.content!); @@ -91,13 +71,11 @@ 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}', ); - }); - } - - Session? getSessionByOrderId(String orderId) { - return _sessionNotifier.getSessionByOrderId(orderId); + } catch (e) { + _logger.e('Error processing event', error: e); + } } Future submitOrder(MostroMessage order) async { @@ -205,19 +183,20 @@ 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); } 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/services/nostr_service.dart b/lib/services/nostr_service.dart index d53e4f3d..b49c71ff 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -14,12 +14,11 @@ 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 { diff --git a/lib/shared/notifiers/order_action_notifier.dart b/lib/shared/notifiers/order_action_notifier.dart deleted file mode 100644 index 9b7eb0e3..00000000 --- a/lib/shared/notifiers/order_action_notifier.dart +++ /dev/null @@ -1,17 +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/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 ea00249c..a07322a3 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -1,17 +1,14 @@ 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'; 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'; 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); @@ -22,78 +19,22 @@ 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.read(subscriptionManagerProvider); 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: Config.sessionExpirationHours)); for (final session in sessionManager.sessions) { - 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(); - } + if(session.orderId == null || session.startTime.isBefore(cutoff)) continue; - // Read the order notifier provider last, which will watch all the above - ref.read(orderNotifierProvider(session.orderId!)); - } + ref.read(orderNotifierProvider(session.orderId!).notifier); if (session.peer != null) { - final chat = ref.read( - chatRoomsProvider(session.orderId!).notifier, - ); - chat.subscribe(); + 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 d89bab29..f92efa45 100644 --- a/lib/shared/providers/mostro_service_provider.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -1,9 +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:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; + part 'mostro_service_provider.g.dart'; @Riverpod(keepAlive: true) @@ -14,10 +15,16 @@ 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); + mostroService.init(); + + ref.listen(settingsProvider, (previous, next) { + mostroService.updateSettings(next); + }); + + ref.onDispose(() { + mostroService.dispose(); + }); + return mostroService; } 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/lib/shared/providers/session_notifier_provider.dart b/lib/shared/providers/session_notifier_provider.dart index a3af0e78..1844c6b4 100644 --- a/lib/shared/providers/session_notifier_provider.dart +++ b/lib/shared/providers/session_notifier_provider.dart @@ -1,20 +1,26 @@ 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'; 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/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(); + } } } diff --git a/lib/shared/widgets/mostro_reactive_button.dart b/lib/shared/widgets/mostro_reactive_button.dart index ad9c7980..b20af7a6 100644 --- a/lib/shared/widgets/mostro_reactive_button.dart +++ b/lib/shared/widgets/mostro_reactive_button.dart @@ -5,6 +5,7 @@ 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 } @@ -25,8 +26,6 @@ class MostroReactiveButtonController { } } -/// 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; @@ -47,7 +46,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, this.controller, @@ -106,7 +105,15 @@ class _MostroReactiveButtonState extends ConsumerState { @override Widget build(BuildContext context) { - ref.watch(orderNotifierProvider(widget.orderId)); + final orderState = ref.watch(orderNotifierProvider(widget.orderId)); + 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), diff --git a/lib/shared/widgets/pay_lightning_invoice_widget.dart b/lib/shared/widgets/pay_lightning_invoice_widget.dart index ed5ec7f3..586d9e71 100644 --- a/lib/shared/widgets/pay_lightning_invoice_widget.dart +++ b/lib/shared/widgets/pay_lightning_invoice_widget.dart @@ -136,7 +136,6 @@ class _PayLightningInvoiceWidgetState extends State { ], ), const SizedBox(height: 20), - const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/pubspec.lock b/pubspec.lock index d5895655..0eb036f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,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: @@ -181,26 +181,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: @@ -535,10 +535,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: @@ -551,10 +551,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: @@ -567,10 +567,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: @@ -652,10 +652,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 @@ -670,10 +670,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: @@ -699,10 +699,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: @@ -779,10 +779,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: @@ -888,10 +888,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: @@ -912,10 +912,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: @@ -960,10 +960,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: @@ -1065,10 +1065,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: @@ -1153,10 +1153,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: @@ -1241,26 +1241,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: @@ -1345,10 +1345,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 @@ -1446,10 +1446,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: @@ -1590,10 +1590,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: @@ -1630,18 +1630,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: @@ -1670,10 +1670,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 f7795fd2..c64c6586 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 @@ -73,7 +73,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 969a30d1..317f6f70 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,7 +10,11 @@ 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/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'; @@ -16,6 +22,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'mocks.mocks.dart'; @GenerateMocks([ + NostrService, MostroService, OpenOrdersRepository, SharedPreferencesAsync, @@ -23,6 +30,9 @@ import 'mocks.mocks.dart'; SessionStorage, KeyManager, MostroStorage, + Settings, + Ref, + ProviderSubscription, ]) // Custom mock for SettingsNotifier that returns a specific Settings object @@ -37,14 +47,26 @@ 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); + 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( @@ -67,4 +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 760793a9..b90b5e45 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -3,24 +3,26 @@ // 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/nostr/core/key_pairs.dart' as _i6; -import 'package:dart_nostr/nostr/model/export.dart' as _i10; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; +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 _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i12; -import 'package:mostro_mobile/data/models.dart' as _i5; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i16; +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 _i18; 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: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; + 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 _i15; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i13; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -36,9 +38,8 @@ import 'package:shared_preferences/src/shared_preferences_async.dart' as _i11; // 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( @@ -47,8 +48,9 @@ class _FakeRef_0 extends _i1.SmartFake ); } -class _FakeFuture_1 extends _i1.SmartFake implements _i3.Future { - _FakeFuture_1( +class _FakeRef_1 extends _i1.SmartFake + implements _i3.Ref { + _FakeRef_1( Object parent, Invocation parentInvocation, ) : super( @@ -57,8 +59,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( @@ -67,9 +69,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( @@ -78,8 +79,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( @@ -88,8 +90,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( @@ -98,8 +100,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( @@ -108,9 +110,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( @@ -119,22 +120,178 @@ class _FakeMostroMessage_7 extends _i1.SmartFake ); } +class _FakeMostroMessage_8 extends _i1.SmartFake + implements _i6.MostroMessage { + _FakeMostroMessage_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeProviderContainer_9 extends _i1.SmartFake + implements _i3.ProviderContainer { + _FakeProviderContainer_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeKeepAliveLink_10 extends _i1.SmartFake implements _i3.KeepAliveLink { + _FakeKeepAliveLink_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeProviderSubscription_11 extends _i1.SmartFake + implements _i3.ProviderSubscription { + _FakeProviderSubscription_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +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 _i7.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( @@ -146,34 +303,27 @@ class MockMostroService extends _i1.Mock implements _i7.MostroService { ); @override - void subscribe(_i5.Session? session) => super.noSuchMethod( + void dispose() => super.noSuchMethod( Invocation.method( - #subscribe, - [session], + #dispose, + [], ), returnValueForMissingStub: null, ); @override - _i5.Session? getSessionByOrderId(String? orderId) => - (super.noSuchMethod(Invocation.method( - #getSessionByOrderId, - [orderId], - )) as _i5.Session?); - - @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, ) => @@ -185,12 +335,12 @@ class MockMostroService extends _i1.Mock implements _i7.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, @@ -204,12 +354,12 @@ class MockMostroService extends _i1.Mock implements _i7.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, @@ -223,52 +373,52 @@ class MockMostroService extends _i1.Mock implements _i7.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, ) => @@ -280,23 +430,23 @@ class MockMostroService extends _i1.Mock implements _i7.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(_i8.Settings? settings) => super.noSuchMethod( + void updateSettings(_i2.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -309,16 +459,16 @@ class MockMostroService extends _i1.Mock implements _i7.MostroService { /// /// See the documentation for Mockito's code generation for more information. class MockOpenOrdersRepository extends _i1.Mock - implements _i9.OpenOrdersRepository { + implements _i12.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( @@ -330,57 +480,56 @@ class MockOpenOrdersRepository extends _i1.Mock ); @override - _i3.Future<_i10.NostrEvent?> getOrderById(String? orderId) => + _i4.Future<_i7.NostrEvent?> getOrderById(String? orderId) => (super.noSuchMethod( Invocation.method( #getOrderById, [orderId], ), - returnValue: _i3.Future<_i10.NostrEvent?>.value(), - ) as _i3.Future<_i10.NostrEvent?>); + returnValue: _i4.Future<_i7.NostrEvent?>.value(), + ) as _i4.Future<_i7.NostrEvent?>); @override - _i3.Future addOrder(_i10.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(<_i10.NostrEvent>[]), - ) as _i3.Future>); + returnValue: _i4.Future>.value(<_i7.NostrEvent>[]), + ) as _i4.Future>); @override - _i3.Future updateOrder(_i10.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(_i8.Settings? settings) => super.noSuchMethod( + void updateSettings(_i2.Settings? settings) => super.noSuchMethod( Invocation.method( #updateSettings, [settings], @@ -401,25 +550,26 @@ 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 _i13.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, @@ -427,65 +577,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, ) => @@ -497,12 +647,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, ) => @@ -514,12 +664,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, ) => @@ -531,12 +681,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, ) => @@ -548,12 +698,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, ) => @@ -565,36 +715,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); } @@ -608,77 +758,77 @@ class MockDatabase extends _i1.Mock implements _i4.Database { @override String get path => (super.noSuchMethod( Invocation.getter(#path), - returnValue: _i12.dummyValue( + returnValue: _i14.dummyValue( this, Invocation.getter(#path), ), ) as String); @override - _i3.Future transaction( - _i3.FutureOr Function(_i13.Transaction)? action) => + _i4.Future transaction( + _i4.FutureOr Function(_i15.Transaction)? action) => (super.noSuchMethod( Invocation.method( #transaction, [action], ), - returnValue: _i12.ifNotNull( - _i12.dummyValueOrNull( + returnValue: _i14.ifNotNull( + _i14.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 _i14.SessionStorage { +class MockSessionStorage extends _i1.Mock implements _i16.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], @@ -687,7 +837,7 @@ class MockSessionStorage extends _i1.Mock implements _i14.SessionStorage { ) as Map); @override - _i5.Session fromDbMap( + _i6.Session fromDbMap( String? key, Map? jsonMap, ) => @@ -699,7 +849,7 @@ class MockSessionStorage extends _i1.Mock implements _i14.SessionStorage { jsonMap, ], ), - returnValue: _FakeSession_4( + returnValue: _FakeSession_5( this, Invocation.method( #fromDbMap, @@ -709,69 +859,69 @@ class MockSessionStorage extends _i1.Mock implements _i14.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( @@ -781,61 +931,61 @@ class MockSessionStorage extends _i1.Mock implements _i14.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, }) => @@ -850,22 +1000,22 @@ class MockSessionStorage extends _i1.Mock implements _i14.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( @@ -876,20 +1026,20 @@ class MockSessionStorage extends _i1.Mock implements _i14.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, ) => @@ -901,7 +1051,7 @@ class MockSessionStorage extends _i1.Mock implements _i14.SessionStorage { value, ], ), - returnValue: _FakeFilter_5( + returnValue: _FakeFilter_6( this, Invocation.method( #eq, @@ -911,19 +1061,19 @@ class MockSessionStorage extends _i1.Mock implements _i14.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 _i15.KeyManager { +class MockKeyManager extends _i1.Mock implements _i17.KeyManager { MockKeyManager() { _i1.throwOnMissingStub(this); } @override - set masterKeyPair(_i6.NostrKeyPairs? _masterKeyPair) => super.noSuchMethod( + set masterKeyPair(_i7.NostrKeyPairs? _masterKeyPair) => super.noSuchMethod( Invocation.setter( #masterKeyPair, _masterKeyPair, @@ -941,160 +1091,160 @@ class MockKeyManager extends _i1.Mock implements _i15.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 _i16.MostroStorage { +class MockMostroStorage extends _i1.Mock implements _i18.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( @@ -1104,78 +1254,78 @@ class MockMostroStorage extends _i1.Mock implements _i16.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, ) => @@ -1187,7 +1337,7 @@ class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { jsonMap, ], ), - returnValue: _FakeMostroMessage_7<_i5.Payload>( + returnValue: _FakeMostroMessage_8<_i6.Payload>( this, Invocation.method( #fromDbMap, @@ -1197,10 +1347,10 @@ class MockMostroStorage extends _i1.Mock implements _i16.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, @@ -1210,85 +1360,85 @@ class MockMostroStorage extends _i1.Mock implements _i16.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( @@ -1298,62 +1448,62 @@ class MockMostroStorage extends _i1.Mock implements _i16.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, }) => @@ -1368,25 +1518,25 @@ class MockMostroStorage extends _i1.Mock implements _i16.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( @@ -1397,21 +1547,21 @@ class MockMostroStorage extends _i1.Mock implements _i16.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, ) => @@ -1423,7 +1573,7 @@ class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { value, ], ), - returnValue: _FakeFilter_5( + returnValue: _FakeFilter_6( this, Invocation.method( #eq, @@ -1433,5 +1583,350 @@ class MockMostroStorage extends _i1.Mock implements _i16.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 _i2.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: _i14.dummyValue( + this, + Invocation.getter(#mostroPublicKey), + ), + ) as String); + + @override + _i2.Settings copyWith({ + List? relays, + bool? privacyModeSetting, + String? mostroInstance, + String? defaultFiatCode, + String? selectedLanguage, + }) => + (super.noSuchMethod( + Invocation.method( + #copyWith, + [], + { + #relays: relays, + #privacyModeSetting: privacyModeSetting, + #mostroInstance: mostroInstance, + #defaultFiatCode: defaultFiatCode, + #selectedLanguage: selectedLanguage, + }, + ), + returnValue: _FakeSettings_0( + this, + Invocation.method( + #copyWith, + [], + { + #relays: relays, + #privacyModeSetting: privacyModeSetting, + #mostroInstance: mostroInstance, + #defaultFiatCode: defaultFiatCode, + #selectedLanguage: selectedLanguage, + }, + ), + ), + ) as _i2.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 _i3.Ref { + MockRef() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.ProviderContainer get container => (super.noSuchMethod( + Invocation.getter(#container), + returnValue: _FakeProviderContainer_9( + this, + Invocation.getter(#container), + ), + ) as _i3.ProviderContainer); + + @override + T refresh(_i3.Refreshable? provider) => (super.noSuchMethod( + Invocation.method( + #refresh, + [provider], + ), + returnValue: _i14.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: _i14.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: _i14.dummyValue( + this, + Invocation.method( + #watch, + [provider], + ), + ), + ) as T); + + @override + _i3.KeepAliveLink keepAlive() => (super.noSuchMethod( + Invocation.method( + #keepAlive, + [], + ), + returnValue: _FakeKeepAliveLink_10( + 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_11( + this, + Invocation.method( + #listen, + [ + provider, + listener, + ], + { + #onError: onError, + #fireImmediately: fireImmediately, + }, + ), + ), + ) as _i3.ProviderSubscription); +} + +/// A class which mocks [ProviderSubscription]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockProviderSubscription extends _i1.Mock + implements _i3.ProviderSubscription { + MockProviderSubscription() { + _i1.throwOnMissingStub(this); + } + + @override + _i8.Node get source => (super.noSuchMethod( + Invocation.getter(#source), + returnValue: _FakeNode_12( + this, + Invocation.getter(#source), + ), + ) as _i8.Node); + + @override + bool get closed => (super.noSuchMethod( + Invocation.getter(#closed), + returnValue: false, + ) as bool); + + @override + State read() => (super.noSuchMethod( + Invocation.method( + #read, + [], + ), + returnValue: _i14.dummyValue( + this, + Invocation.method( + #read, + [], + ), + ), + ) as State); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + 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 e2b4373d..33e6efe4 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,19 +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: - '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', - defaultFiatCode: 'USD', - ); - - mockSessionNotifier = - MockSessionNotifier(mockKeyManager, mockSessionStorage, testSettings); - + final testSettings = MockSettings(); + + mockSessionNotifier = MockSessionNotifier(ref, mockKeyManager, mockSessionStorage, testSettings); + // Stub the KeyManager methods when(mockKeyManager.masterKeyPair).thenReturn( NostrKeyPairs( @@ -126,14 +121,14 @@ void main() { keyManagerProvider.overrideWithValue(mockKeyManager), sessionNotifierProvider.overrideWith((ref) => mockSessionNotifier), settingsProvider.overrideWith((ref) => MockSettingsNotifier( - Settings( - relays: ['wss://relay.damus.io'], - fullPrivacyMode: false, - mostroPublicKey: - '6d5c471d0e88c8c688c85dd8a3d84e3c7c5e8a3b6d7a6b2c9e8c5d9a7b3e6c8a', - defaultFiatCode: 'USD', - ), - mockPreferences)), + Settings( + relays: ['wss://relay.damus.io'], + fullPrivacyMode: false, + mostroPublicKey: '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980', + defaultFiatCode: 'USD', + ), + mockPreferences + )), mostroStorageProvider.overrideWithValue(mockMostroStorage), ]); diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 52991c3e..eb64f4ed 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -1,28 +1,27 @@ 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 'package:mockito/annotations.dart'; import 'package:mockito/mockito.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/features/subscriptions/subscription_manager.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:mostro_mobile/shared/notifiers/session_notifier.dart'; -import 'package:mostro_mobile/shared/utils/nostr_utils.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/nostr_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -import 'mostro_service_test.mocks.dart'; -import 'mostro_service_helper_functions.dart'; +import '../mocks.dart'; import '../mocks.mocks.dart'; +import 'mostro_service_helper_functions.dart'; -@GenerateMocks([NostrService, SessionNotifier, Ref]) void main() { // Provide dummy values for Mockito provideDummy(Settings( @@ -38,40 +37,86 @@ void main() { // 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; late MockNostrService mockNostrService; late MockSessionNotifier mockSessionNotifier; late MockRef mockRef; - - final mockServerTradeIndex = MockServerTradeIndex(); + late MockSubscriptionManager mockSubscriptionManager; + late MockKeyManager mockKeyManager; + late MockSessionStorage mockSessionStorage; setUp(() { - mockNostrService = MockNostrService(); - mockSessionNotifier = MockSessionNotifier(); + // Initialize all mocks first mockRef = MockRef(); - - // 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', - ); - - // Stub specific provider reads + mockKeyManager = MockKeyManager(); + mockSessionStorage = MockSessionStorage(); + mockNostrService = MockNostrService(); + mockServerTradeIndex = MockServerTradeIndex(); + keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); + + // Setup all stubs before creating any objects that use them + final testSettings = MockSettings(); + when(testSettings.mostroPublicKey).thenReturn( + '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980'); when(mockRef.read(settingsProvider)).thenReturn(testSettings); when(mockRef.read(mostroStorageProvider)).thenReturn(MockMostroStorage()); when(mockRef.read(nostrServiceProvider)).thenReturn(mockNostrService); + + // 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); + }); - // Stub SessionNotifier methods - when(mockSessionNotifier.sessions).thenReturn([]); - - mostroService = MostroService(mockSessionNotifier, mockRef); - keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); + tearDown(() { + mostroService.dispose(); + mockSubscriptionManager.dispose(); }); // Helper function to verify signatures as server would @@ -123,37 +168,13 @@ void main() { fullPrivacy: false, ); - when(mockSessionNotifier.getSessionByOrderId(orderId)) - .thenReturn(session); - - // Mock NostrService's createRumor, createSeal, createWrap, publishEvent - when(mockNostrService.createRumor(any, any, any, any)) - .thenAnswer((_) async => 'encryptedRumorContent'); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); - when(mockNostrService.generateKeyPair()) - .thenAnswer((_) async => NostrUtils.generateKeyPair()); + // Mock NostrService's publishEvent only + when(mockNostrService.publishEvent(any)).thenAnswer((_) async {}); - 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', - )); - - when(mockNostrService.publishEvent(any)) - .thenAnswer((_) async => Future.value()); - - when(mockSessionNotifier.newSession(orderId: orderId)) - .thenAnswer((_) async => session); + // Note: newSession is already implemented in MockSessionNotifier // Act await mostroService.takeSellOrder(orderId, 100, 'lnbc1234invoice'); @@ -205,31 +226,12 @@ void main() { fullPrivacy: false, ); - 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'); + // Set mock return value for custom mock + mockSessionNotifier.setMockSession(session); // Replaced when() call - 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()); @@ -284,34 +286,15 @@ 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; - // 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()); @@ -366,31 +349,12 @@ void main() { fullPrivacy: true, ); - 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', - )); + // 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)) + .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 deleted file mode 100644 index da25fcd0..00000000 --- a/test/services/mostro_service_test.mocks.dart +++ /dev/null @@ -1,893 +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 _i7; - -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:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i9; -import 'package:mostro_mobile/data/models/enums/role.dart' as _i13; -import 'package:mostro_mobile/data/models/order.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/deep_link_service.dart' as _i11; -import 'package:mostro_mobile/services/nostr_service.dart' as _i6; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i12; -import 'package:state_notifier/state_notifier.dart' as _i14; - -// 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 _FakeNostrKeyPairs_1 extends _i1.SmartFake implements _i3.NostrKeyPairs { - _FakeNostrKeyPairs_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeNostrEvent_2 extends _i1.SmartFake implements _i3.NostrEvent { - _FakeNostrEvent_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSession_3 extends _i1.SmartFake implements _i4.Session { - _FakeSession_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProviderContainer_4 extends _i1.SmartFake - implements _i5.ProviderContainer { - _FakeProviderContainer_4( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -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( - 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 _i6.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 - _i7.Future init(_i2.Settings? settings) => (super.noSuchMethod( - Invocation.method( - #init, - [settings], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future updateSettings(_i2.Settings? newSettings) => - (super.noSuchMethod( - Invocation.method( - #updateSettings, - [newSettings], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future<_i8.RelayInformations?> getRelayInfo(String? relayUrl) => - (super.noSuchMethod( - Invocation.method( - #getRelayInfo, - [relayUrl], - ), - returnValue: _i7.Future<_i8.RelayInformations?>.value(), - ) as _i7.Future<_i8.RelayInformations?>); - - @override - _i7.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod( - Invocation.method( - #publishEvent, - [event], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future> fetchEvents( - _i3.NostrFilter? filter, { - List? specificRelays, - }) => - (super.noSuchMethod( - Invocation.method( - #fetchEvents, - [filter], - {#specificRelays: specificRelays}, - ), - returnValue: _i7.Future>.value(<_i3.NostrEvent>[]), - ) as _i7.Future>); - - @override - _i7.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrRequest? request) => - (super.noSuchMethod( - Invocation.method( - #subscribeToEvents, - [request], - ), - returnValue: _i7.Stream<_i3.NostrEvent>.empty(), - ) as _i7.Stream<_i3.NostrEvent>); - - @override - _i7.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>); - - @override - void unsubscribe(String? id) => super.noSuchMethod( - Invocation.method( - #unsubscribe, - [id], - ), - returnValueForMissingStub: null, - ); - - @override - _i7.Future<_i10.Order?> fetchEventById( - String? eventId, [ - List? specificRelays, - ]) => - (super.noSuchMethod( - Invocation.method( - #fetchEventById, - [ - eventId, - specificRelays, - ], - ), - returnValue: _i7.Future<_i10.Order?>.value(), - ) as _i7.Future<_i10.Order?>); - - @override - _i7.Future<_i11.OrderInfo?> fetchOrderInfoByEventId( - String? eventId, [ - List? specificRelays, - ]) => - (super.noSuchMethod( - Invocation.method( - #fetchOrderInfoByEventId, - [ - eventId, - specificRelays, - ], - ), - returnValue: _i7.Future<_i11.OrderInfo?>.value(), - ) as _i7.Future<_i11.OrderInfo?>); -} - -/// A class which mocks [SessionNotifier]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { - MockSessionNotifier() { - _i1.throwOnMissingStub(this); - } - - @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 - _i7.Stream> get stream => (super.noSuchMethod( - Invocation.getter(#stream), - returnValue: _i7.Stream>.empty(), - ) as _i7.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(_i5.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 - _i7.Future init() => (super.noSuchMethod( - Invocation.method( - #init, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - void updateSettings(_i2.Settings? settings) => super.noSuchMethod( - Invocation.method( - #updateSettings, - [settings], - ), - returnValueForMissingStub: null, - ); - - @override - _i7.Future<_i4.Session> newSession({ - String? orderId, - int? requestId, - _i13.Role? role, - }) => - (super.noSuchMethod( - Invocation.method( - #newSession, - [], - { - #orderId: orderId, - #requestId: requestId, - #role: role, - }, - ), - returnValue: _i7.Future<_i4.Session>.value(_FakeSession_3( - this, - Invocation.method( - #newSession, - [], - { - #orderId: orderId, - #requestId: requestId, - #role: role, - }, - ), - )), - ) as _i7.Future<_i4.Session>); - - @override - _i7.Future saveSession(_i4.Session? session) => (super.noSuchMethod( - Invocation.method( - #saveSession, - [session], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future updateSession( - String? orderId, - void Function(_i4.Session)? update, - ) => - (super.noSuchMethod( - Invocation.method( - #updateSession, - [ - orderId, - update, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.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 - _i7.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( - Invocation.method( - #loadSession, - [keyIndex], - ), - returnValue: _i7.Future<_i4.Session?>.value(), - ) as _i7.Future<_i4.Session?>); - - @override - _i7.Future reset() => (super.noSuchMethod( - Invocation.method( - #reset, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future deleteSession(String? sessionId) => (super.noSuchMethod( - Invocation.method( - #deleteSession, - [sessionId], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.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 - _i5.RemoveListener addListener( - _i14.Listener>? listener, { - bool? fireImmediately = true, - }) => - (super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - {#fireImmediately: fireImmediately}, - ), - returnValue: () {}, - ) as _i5.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 { - MockRef() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.ProviderContainer get container => (super.noSuchMethod( - Invocation.getter(#container), - returnValue: _FakeProviderContainer_4( - this, - Invocation.getter(#container), - ), - ) as _i5.ProviderContainer); - - @override - T refresh(_i5.Refreshable? provider) => (super.noSuchMethod( - Invocation.method( - #refresh, - [provider], - ), - returnValue: _i9.dummyValue( - this, - Invocation.method( - #refresh, - [provider], - ), - ), - ) as T); - - @override - void invalidate(_i5.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(_i5.ProviderListenable? provider) => (super.noSuchMethod( - Invocation.method( - #read, - [provider], - ), - returnValue: _i9.dummyValue( - this, - Invocation.method( - #read, - [provider], - ), - ), - ) as T); - - @override - bool exists(_i5.ProviderBase? provider) => (super.noSuchMethod( - Invocation.method( - #exists, - [provider], - ), - returnValue: false, - ) as bool); - - @override - T watch(_i5.ProviderListenable? provider) => (super.noSuchMethod( - Invocation.method( - #watch, - [provider], - ), - returnValue: _i9.dummyValue( - this, - Invocation.method( - #watch, - [provider], - ), - ), - ) as T); - - @override - _i5.KeepAliveLink keepAlive() => (super.noSuchMethod( - Invocation.method( - #keepAlive, - [], - ), - returnValue: _FakeKeepAliveLink_5( - this, - Invocation.method( - #keepAlive, - [], - ), - ), - ) as _i5.KeepAliveLink); - - @override - _i5.ProviderSubscription listen( - _i5.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_6( - this, - Invocation.method( - #listen, - [ - provider, - listener, - ], - { - #onError: onError, - #fireImmediately: fireImmediately, - }, - ), - ), - ) as _i5.ProviderSubscription); -}