From 1f95c4482866c7d9d22e5781a026b112a10de076 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 13 May 2025 08:29:57 -0700 Subject: [PATCH 01/26] feat: add pay invoice button and update trade state handling for sellers --- .../trades/providers/trade_state_provider.dart | 6 ++---- lib/features/trades/screens/trade_detail_screen.dart | 11 +++++++++++ .../trades/widgets/mostro_message_detail_widget.dart | 4 ++-- lib/shared/utils/nostr_utils.dart | 2 +- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/features/trades/providers/trade_state_provider.dart b/lib/features/trades/providers/trade_state_provider.dart index 5c0292f7..3825a025 100644 --- a/lib/features/trades/providers/trade_state_provider.dart +++ b/lib/features/trades/providers/trade_state_provider.dart @@ -1,10 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; -import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/trades/models/trade_state.dart'; -import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:collection/collection.dart'; @@ -12,11 +10,11 @@ import 'package:collection/collection.dart'; final tradeStateProvider = Provider.family.autoDispose((ref, orderId) { - final statusAsync = ref.watch(eventProvider(orderId)); + final statusAsync = ref.watch(mostroMessageStreamProvider(orderId)); final messagesAsync = ref.watch(mostroMessageHistoryProvider(orderId)); final lastOrderMessageAsync = ref.watch(mostroOrderStreamProvider(orderId)); - final status = statusAsync?.status ?? Status.pending; + final status = statusAsync.value?.getPayload()?.status ?? Status.pending; final messages = messagesAsync.value ?? []; final lastActionMessage = messages.firstWhereOrNull((m) => m.action != actions.Action.cantDo); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 0a9561ad..f4898e9b 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -467,6 +467,16 @@ class TradeDetailScreen extends ConsumerWidget { ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); + if (userRole == Role.seller && + tradeState.lastAction == actions.Action.payInvoice) { + widgets.add(_buildNostrButton( + 'PAY INVOICE', + action: actions.Action.payInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/pay_invoice/$orderId'), + )); + } + return widgets; // Terminal states according to Mostro FSM @@ -499,6 +509,7 @@ class TradeDetailScreen extends ConsumerWidget { timeout: const Duration(seconds: 30), ); } + Widget _buildContactButton(BuildContext context) { return ElevatedButton( onPressed: () { diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 101e0b64..533a7c9e 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -84,9 +84,9 @@ class MostroMessageDetail extends ConsumerWidget { 900; return S.of(context)!.payInvoice( orderPayload?.amount.toString() ?? '', - orderPayload?.fiatCode ?? '', + '${expSecs ~/ 60} minutes', orderPayload?.fiatAmount.toString() ?? '', - expSecs, + orderPayload?.fiatCode ?? '', ); case actions.Action.addInvoice: final expSecs = ref diff --git a/lib/shared/utils/nostr_utils.dart b/lib/shared/utils/nostr_utils.dart index 309eb56e..6e92d790 100644 --- a/lib/shared/utils/nostr_utils.dart +++ b/lib/shared/utils/nostr_utils.dart @@ -191,7 +191,7 @@ class NostrUtils { tags: [ ["p", recipientPubKey] ], - createdAt: DateTime.now(), + createdAt: randomNow(), ); return wrapEvent; From dcb9855346eaed5c78fbc06baa8978744411d236 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 13 May 2025 20:54:18 -0700 Subject: [PATCH 02/26] refactor: rename trade state fields and implement FSM-driven action buttons --- lib/core/mostro_fsm.dart | 29 +- .../order/widgets/action_button_config.dart | 69 ++++ lib/features/order/widgets/mostro_button.dart | 71 ++++ lib/features/trades/models/trade_state.dart | 24 +- .../providers/trade_state_provider.dart | 8 +- .../trades/screens/trade_detail_screen.dart | 386 +++++++----------- .../widgets/mostro_message_detail_widget.dart | 21 +- 7 files changed, 333 insertions(+), 275 deletions(-) create mode 100644 lib/features/order/widgets/action_button_config.dart create mode 100644 lib/features/order/widgets/mostro_button.dart diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart index d213daa8..dace852f 100644 --- a/lib/core/mostro_fsm.dart +++ b/lib/core/mostro_fsm.dart @@ -3,11 +3,10 @@ import 'package:mostro_mobile/data/models/enums/status.dart'; /// Finite-State-Machine helper for Mostro order lifecycles. /// -/// This table was generated directly from the authoritative -/// specification sent by the Mostro team. Only *state–transition → -/// next-state* information is encoded here. All auxiliary / neutral -/// notifications intentionally map to the **same** state so that -/// `nextStatus` always returns a non-null value. +/// This table was generated directly from the authoritative specification. +/// Only *state–transition → next-state* information is encoded here. +/// All auxiliary / neutral notifications intentionally map to +/// the **same** state so that `nextStatus` always returns a non-null value. class MostroFSM { MostroFSM._(); @@ -15,8 +14,8 @@ class MostroFSM { static final Map> _transitions = { // ───────────────────────── MATCHING / TAKING ──────────────────────── Status.pending: { - Action.takeSell: Status.waitingBuyerInvoice, - Action.takeBuy: Status.waitingBuyerInvoice, // invoice presence handled elsewhere + Action.takeSell: Status.waitingBuyerInvoice, // can go to waitingPayment + Action.takeBuy: Status.waitingPayment, Action.cancel: Status.canceled, Action.disputeInitiatedByYou: Status.dispute, Action.disputeInitiatedByPeer: Status.dispute, @@ -24,7 +23,8 @@ class MostroFSM { // ───────────────────────── INVOICING ──────────────────────────────── Status.waitingBuyerInvoice: { - Action.addInvoice: Status.waitingPayment, + Action.waitingBuyerInvoice: Status.active, + Action.addInvoice: Status.waitingPayment, // can go to active Action.cancel: Status.canceled, Action.disputeInitiatedByYou: Status.dispute, Action.disputeInitiatedByPeer: Status.dispute, @@ -32,7 +32,8 @@ class MostroFSM { // ───────────────────────── HOLD INVOICE PAYMENT ──────────────────── Status.waitingPayment: { - Action.payInvoice: Status.active, + Action.payInvoice: Status.waitingBuyerInvoice, // can go to active + Action.waitingSellerToPay: Status.waitingBuyerInvoice, Action.holdInvoicePaymentAccepted: Status.active, Action.holdInvoicePaymentCanceled: Status.canceled, Action.cancel: Status.canceled, @@ -42,6 +43,9 @@ class MostroFSM { // ───────────────────────── ACTIVE TRADE ──────────────────────────── Status.active: { + Action.holdInvoicePaymentAccepted: Status.fiatSent, + Action.buyerTookOrder: Status.fiatSent, + Action.fiatSent: Status.fiatSent, Action.cooperativeCancelInitiatedByYou: Status.cooperativelyCanceled, Action.cooperativeCancelInitiatedByPeer: Status.cooperativelyCanceled, @@ -90,4 +94,11 @@ class MostroFSM { final safeCurrent = current ?? Status.pending; return _transitions[safeCurrent]?[action] ?? safeCurrent; } + + /// Returns a list of all actions that can be applied to [status]. + /// This is used to determine which buttons to show in the UI and + /// to validate user input. + static List actionsForStatus(Status status) { + return _transitions[status]!.keys.toList(); + } } diff --git a/lib/features/order/widgets/action_button_config.dart b/lib/features/order/widgets/action_button_config.dart new file mode 100644 index 00000000..c1eb5272 --- /dev/null +++ b/lib/features/order/widgets/action_button_config.dart @@ -0,0 +1,69 @@ +import 'dart:ui'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; +import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; + +/// Configuration for Mostro action buttons +/// +/// This class encapsulates all the properties needed to render +/// a MostroButton and defines its appearance and behavior. +class MostroButtonConfig { + /// Display label for the button + final String label; + + /// Background color for the button + final Color color; + + /// The Mostro action this button represents + final actions.Action action; + + /// Callback when the button is pressed + final VoidCallback onPressed; + + /// Whether to show success/failure indicator + final bool showSuccessIndicator; + + /// Timeout duration for reactive buttons + final Duration timeout; + + /// Optional order ID associated with this button + final String? orderId; + + /// Button style (raised, outlined, text) + final ButtonStyleType buttonStyle; + + const MostroButtonConfig({ + required this.label, + required this.color, + required this.action, + required this.onPressed, + this.showSuccessIndicator = true, + this.timeout = const Duration(seconds: 30), + this.orderId, + this.buttonStyle = ButtonStyleType.raised, + }); + + /// Creates a copy of this config with specified fields replaced + MostroButtonConfig copyWith({ + String? label, + Color? color, + actions.Action? action, + VoidCallback? onPressed, + bool? showSuccessIndicator, + Duration? timeout, + String? orderId, + ButtonStyleType? buttonStyle, + }) { + return MostroButtonConfig( + label: label ?? this.label, + color: color ?? this.color, + action: action ?? this.action, + onPressed: onPressed ?? this.onPressed, + showSuccessIndicator: showSuccessIndicator ?? this.showSuccessIndicator, + timeout: timeout ?? this.timeout, + orderId: orderId ?? this.orderId, + buttonStyle: buttonStyle ?? this.buttonStyle, + ); + } +} + +// Using ButtonStyleType from mostro_reactive_button.dart \ No newline at end of file diff --git a/lib/features/order/widgets/mostro_button.dart b/lib/features/order/widgets/mostro_button.dart new file mode 100644 index 00000000..b64ea9ba --- /dev/null +++ b/lib/features/order/widgets/mostro_button.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/features/order/widgets/action_button_config.dart'; +import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; + +// Use ButtonStyleType from mostro_reactive_button.dart + +/// A unified button component for Mostro actions +/// +/// This widget combines configuration from MostroButtonConfig with +/// the reactive behavior of MostroReactiveButton, creating a +/// consistent button UI across the app. +class MostroButton extends ConsumerWidget { + final MostroButtonConfig config; + + const MostroButton({ + super.key, + required this.config, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // If we have an orderId and action, we can create a reactive button + if (config.orderId != null) { + return MostroReactiveButton( + label: config.label, + buttonStyle: config.buttonStyle, + orderId: config.orderId!, + action: config.action, + backgroundColor: config.color, + onPressed: config.onPressed, + showSuccessIndicator: config.showSuccessIndicator, + timeout: config.timeout, + ); + } + + // Otherwise create a regular button with the right style + return _buildButton(context); + } + + Widget _buildButton(BuildContext context) { + switch (config.buttonStyle) { + case ButtonStyleType.raised: + return ElevatedButton( + onPressed: config.onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: config.color, + ), + child: Text(config.label), + ); + + case ButtonStyleType.outlined: + return OutlinedButton( + onPressed: config.onPressed, + style: OutlinedButton.styleFrom( + foregroundColor: config.color, + ), + child: Text(config.label), + ); + + case ButtonStyleType.text: + return TextButton( + onPressed: config.onPressed, + style: TextButton.styleFrom( + foregroundColor: config.color, + ), + child: Text(config.label), + ); + } + } +} diff --git a/lib/features/trades/models/trade_state.dart b/lib/features/trades/models/trade_state.dart index bb682698..67e28a7e 100644 --- a/lib/features/trades/models/trade_state.dart +++ b/lib/features/trades/models/trade_state.dart @@ -4,39 +4,39 @@ import 'package:mostro_mobile/data/models/enums/action.dart' as actions; class TradeState { final Status status; - final actions.Action? lastAction; - final Order? orderPayload; + final actions.Action? action; + final Order? order; TradeState({ required this.status, - required this.lastAction, - required this.orderPayload, + required this.action, + required this.order, }); @override String toString() => - 'TradeState(status: $status, lastAction: $lastAction, orderPayload: $orderPayload)'; + 'TradeState(status: $status, action: $action, order: $order)'; @override bool operator ==(Object other) => identical(this, other) || other is TradeState && other.status == status && - other.lastAction == lastAction && - other.orderPayload == orderPayload; + other.action == action && + other.order == order; @override - int get hashCode => Object.hash(status, lastAction, orderPayload); + int get hashCode => Object.hash(status, action, order); TradeState copyWith({ Status? status, - actions.Action? lastAction, - Order? orderPayload, + actions.Action? action, + Order? order, }) { return TradeState( status: status ?? this.status, - lastAction: lastAction ?? this.lastAction, - orderPayload: orderPayload ?? this.orderPayload, + action: action ?? this.action, + order: order ?? this.order, ); } } diff --git a/lib/features/trades/providers/trade_state_provider.dart b/lib/features/trades/providers/trade_state_provider.dart index 3825a025..1f65f29c 100644 --- a/lib/features/trades/providers/trade_state_provider.dart +++ b/lib/features/trades/providers/trade_state_provider.dart @@ -10,19 +10,17 @@ import 'package:collection/collection.dart'; final tradeStateProvider = Provider.family.autoDispose((ref, orderId) { - final statusAsync = ref.watch(mostroMessageStreamProvider(orderId)); final messagesAsync = ref.watch(mostroMessageHistoryProvider(orderId)); final lastOrderMessageAsync = ref.watch(mostroOrderStreamProvider(orderId)); - final status = statusAsync.value?.getPayload()?.status ?? Status.pending; final messages = messagesAsync.value ?? []; final lastActionMessage = messages.firstWhereOrNull((m) => m.action != actions.Action.cantDo); final orderPayload = lastOrderMessageAsync.value?.getPayload(); return TradeState( - status: status, - lastAction: lastActionMessage?.action, - orderPayload: orderPayload, + status: orderPayload?.status ?? Status.pending, + action: lastActionMessage?.action, + order: orderPayload, ); }); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index f4898e9b..16067c68 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/core/mostro_fsm.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; @@ -28,7 +29,7 @@ class TradeDetailScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tradeState = ref.watch(tradeStateProvider(orderId)); // If message is null or doesn't have an Order payload, show loading - final orderPayload = tradeState.orderPayload; + final orderPayload = tradeState.order; if (orderPayload == null) { return const Scaffold( backgroundColor: AppTheme.dark1, @@ -83,18 +84,18 @@ class TradeDetailScreen extends ConsumerWidget { final selling = session!.role == Role.seller ? 'selling' : 'buying'; final currencyFlag = CurrencyUtils.getFlagFromCurrency( - tradeState.orderPayload!.fiatCode, + tradeState.order!.fiatCode, ); final amountString = - '${tradeState.orderPayload!.fiatAmount} ${tradeState.orderPayload!.fiatCode} $currencyFlag'; + '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; // If `orderPayload.amount` is 0, the trade is "at market price" - final isZeroAmount = (tradeState.orderPayload!.amount == 0); - final satText = isZeroAmount ? '' : ' ${tradeState.orderPayload!.amount}'; + final isZeroAmount = (tradeState.order!.amount == 0); + final satText = isZeroAmount ? '' : ' ${tradeState.order!.amount}'; final priceText = isZeroAmount ? 'at market price' : ''; - final premium = tradeState.orderPayload!.premium; + final premium = tradeState.order!.premium; final premiumText = premium == 0 ? '' : (premium > 0) @@ -102,14 +103,12 @@ class TradeDetailScreen extends ConsumerWidget { : 'with a $premium% discount'; // Payment method - final method = tradeState.orderPayload!.paymentMethod; + final method = tradeState.order!.paymentMethod; final timestamp = formatDateTime( - tradeState.orderPayload!.createdAt != null && - tradeState.orderPayload!.createdAt! > 0 - ? DateTime.fromMillisecondsSinceEpoch( - tradeState.orderPayload!.createdAt!) + tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 + ? DateTime.fromMillisecondsSinceEpoch(tradeState.order!.createdAt!) : DateTime.fromMillisecondsSinceEpoch( - tradeState.orderPayload!.createdAt ?? 0, + tradeState.order!.createdAt ?? 0, ), ); return CustomCard( @@ -213,91 +212,49 @@ class TradeDetailScreen extends ConsumerWidget { final session = ref.watch(sessionProvider(orderId)); final userRole = session?.role; - switch (tradeState.status) { - case Status.pending: - // According to Mostro FSM: Pending state - final widgets = []; + final userActions = MostroFSM.actionsForStatus(tradeState.status); + if (userActions.isEmpty) return []; - // FSM: In pending state, seller can cancel - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.cancel, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); + final widgets = []; - return widgets; - - case Status.waitingPayment: - // According to Mostro FSM: waiting-payment state - final widgets = []; - - // FSM: Seller can pay-invoice and cancel - if (userRole == Role.seller) { - widgets.add(_buildNostrButton( - 'PAY INVOICE', - action: actions.Action.waitingBuyerInvoice, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/pay_invoice/$orderId'), - )); - } - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - return widgets; - - case Status.waitingBuyerInvoice: - // According to Mostro FSM: waiting-buyer-invoice state - final widgets = []; - - // FSM: Buyer can add-invoice and cancel - if (userRole == Role.buyer) { + for (final action in userActions) { + // FSM-driven action mapping: ensure all actions are handled + switch (action) { + case actions.Action.cancel: + case actions.Action.canceled: widgets.add(_buildNostrButton( - 'ADD INVOICE', - action: actions.Action.payInvoice, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/add_invoice/$orderId'), + 'CANCEL', + action: action, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); - } - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - - return widgets; - - case Status.settledHoldInvoice: - if (tradeState.lastAction == actions.Action.rate) { - return [ - // Rate button if applicable (common for both roles) - _buildNostrButton( - 'RATE', - action: actions.Action.rateReceived, + break; + case actions.Action.payInvoice: + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'PAY INVOICE', + action: actions.Action.payInvoice, backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), - ) - ]; - } else { - return []; - } - - case Status.active: - // According to Mostro FSM: active state - final widgets = []; - - // Role-specific actions according to FSM - if (userRole == Role.buyer) { - // FSM: Buyer can fiat-sent - if (tradeState.lastAction != actions.Action.fiatSentOk && - tradeState.lastAction != actions.Action.fiatSent) { + onPressed: () => context.push('/pay_invoice/$orderId'), + )); + } + break; + case actions.Action.addInvoice: + if (userRole == Role.buyer) { + widgets.add(_buildNostrButton( + 'ADD INVOICE', + action: actions.Action.addInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/add_invoice/$orderId'), + )); + } + break; + case actions.Action.fiatSent: + case actions.Action.fiatSentOk: + if (userRole == Role.buyer && + tradeState.action != actions.Action.fiatSentOk && + tradeState.action != actions.Action.fiatSent) { widgets.add(_buildNostrButton( 'FIAT SENT', action: actions.Action.fiatSentOk, @@ -307,20 +264,14 @@ class TradeDetailScreen extends ConsumerWidget { .sendFiatSent(), )); } - - // FSM: Buyer can cancel - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - - // FSM: Buyer can dispute - if (tradeState.lastAction != actions.Action.disputeInitiatedByYou && - tradeState.lastAction != actions.Action.disputeInitiatedByPeer && - tradeState.lastAction != actions.Action.dispute) { + break; + case actions.Action.disputeInitiatedByYou: + case actions.Action.disputeInitiatedByPeer: + case actions.Action.dispute: + // Only allow dispute if not already disputed + if (tradeState.action != actions.Action.disputeInitiatedByYou && + tradeState.action != actions.Action.disputeInitiatedByPeer && + tradeState.action != actions.Action.dispute) { widgets.add(_buildNostrButton( 'DISPUTE', action: actions.Action.disputeInitiatedByYou, @@ -330,101 +281,51 @@ class TradeDetailScreen extends ConsumerWidget { .disputeOrder(), )); } - } else if (userRole == Role.seller) { - // FSM: Seller can cancel - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - - // FSM: Seller can dispute - if (tradeState.lastAction != actions.Action.disputeInitiatedByYou && - tradeState.lastAction != actions.Action.disputeInitiatedByPeer && - tradeState.lastAction != actions.Action.dispute) { + break; + case actions.Action.release: + case actions.Action.released: + if (userRole == Role.seller) { widgets.add(_buildNostrButton( - 'DISPUTE', - action: actions.Action.disputeInitiatedByYou, - backgroundColor: AppTheme.red1, + 'RELEASE', + action: actions.Action.release, + backgroundColor: AppTheme.mostroGreen, onPressed: () => ref .read(orderNotifierProvider(orderId).notifier) - .disputeOrder(), + .releaseOrder(), )); } - } - - // Rate button if applicable (common for both roles) - if (tradeState.lastAction == actions.Action.rate) { - widgets.add(_buildNostrButton( - 'RATE', - action: actions.Action.rateReceived, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), - )); - } - - widgets.add( - _buildContactButton(context), - ); - - return widgets; - - case Status.fiatSent: - // According to Mostro FSM: fiat-sent state - final widgets = []; - - if (userRole == Role.seller) { - // FSM: Seller can release - widgets.add(_buildNostrButton( - 'RELEASE SATS', - action: actions.Action.released, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .releaseOrder(), - )); - - // FSM: Seller can cancel + break; + case actions.Action.takeSell: + if (userRole == Role.buyer) { + widgets.add(_buildNostrButton( + 'TAKE SELL', + action: actions.Action.takeSell, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/take_sell/$orderId'), + )); + } + break; + case actions.Action.takeBuy: + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'TAKE BUY', + action: actions.Action.takeBuy, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/take_buy/$orderId'), + )); + } + break; + case actions.Action.cooperativeCancelInitiatedByYou: + case actions.Action.cooperativeCancelInitiatedByPeer: widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, + 'COOPERATIVE CANCEL', + action: actions.Action.cooperativeCancelInitiatedByYou, backgroundColor: AppTheme.red1, onPressed: () => ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); - - // FSM: Seller can dispute - widgets.add(_buildNostrButton( - 'DISPUTE', - action: actions.Action.disputeInitiatedByYou, - backgroundColor: AppTheme.red1, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .disputeOrder(), - )); - } else if (userRole == Role.buyer) { - // FSM: Buyer can only dispute in fiat-sent state - widgets.add(_buildNostrButton( - 'DISPUTE', - action: actions.Action.disputeInitiatedByYou, - backgroundColor: AppTheme.red1, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .disputeOrder(), - )); - } - - return widgets; - - case Status.cooperativelyCanceled: - // According to Mostro FSM: cooperatively-canceled state - final widgets = []; - - // Add confirm cancel if cooperative cancel was initiated by peer - if (tradeState.lastAction == - actions.Action.cooperativeCancelInitiatedByPeer) { + break; + case actions.Action.cooperativeCancelAccepted: widgets.add(_buildNostrButton( 'CONFIRM CANCEL', action: actions.Action.cooperativeCancelAccepted, @@ -432,63 +333,68 @@ class TradeDetailScreen extends ConsumerWidget { onPressed: () => ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); - } - - return widgets; - - case Status.success: - // According to Mostro FSM: success state - // Both buyer and seller can only rate - final widgets = []; - - // FSM: Both roles can rate counterparty if not already rated - if (tradeState.lastAction != actions.Action.rateReceived) { + break; + case actions.Action.purchaseCompleted: widgets.add(_buildNostrButton( - 'RATE', - action: actions.Action.rateReceived, + 'COMPLETE PURCHASE', + action: actions.Action.purchaseCompleted, backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .releaseOrder(), // This usually triggers completion )); - } - - return widgets; - - case Status.inProgress: - // According to Mostro FSM: in-progress is a transitional state - // This is not explicitly in the FSM but we follow cancel rules as active state - final widgets = []; - - // Both roles can cancel during in-progress state, similar to active - widgets.add(_buildNostrButton( - 'CANCEL', - action: actions.Action.canceled, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - - if (userRole == Role.seller && - tradeState.lastAction == actions.Action.payInvoice) { + break; + case actions.Action.rate: + case actions.Action.rateUser: + case actions.Action.rateReceived: widgets.add(_buildNostrButton( - 'PAY INVOICE', - action: actions.Action.payInvoice, + 'RATE', + action: actions.Action.rateReceived, backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/pay_invoice/$orderId'), + onPressed: () => context.push('/rate_user/$orderId'), )); - } - - return widgets; + break; + case actions.Action.holdInvoicePaymentAccepted: + case actions.Action.holdInvoicePaymentSettled: + case actions.Action.holdInvoicePaymentCanceled: + // These are system actions, not user actions, so no button needed + break; + case actions.Action.buyerInvoiceAccepted: + case actions.Action.buyerTookOrder: + case actions.Action.waitingSellerToPay: + case actions.Action.waitingBuyerInvoice: + case actions.Action.adminCancel: + case actions.Action.adminCanceled: + case actions.Action.adminSettle: + case actions.Action.adminSettled: + case actions.Action.adminAddSolver: + case actions.Action.adminTakeDispute: + case actions.Action.adminTookDispute: + case actions.Action.paymentFailed: + case actions.Action.invoiceUpdated: + case actions.Action.sendDm: + case actions.Action.tradePubkey: + case actions.Action.cantDo: + // Not user-facing or not relevant as a button + break; + default: + // Optionally handle unknown or unimplemented actions + break; + } + } - // Terminal states according to Mostro FSM - case Status.expired: - case Status.dispute: - case Status.completedByAdmin: - case Status.canceledByAdmin: - case Status.settledByAdmin: - case Status.canceled: - // No actions allowed in these terminal states - return []; + // Special case for RATE button after settlement + if (tradeState.status == Status.settledHoldInvoice && + tradeState.action == actions.Action.rate) { + widgets.add(_buildNostrButton( + 'RATE', + action: actions.Action.rateReceived, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + )); } + + return widgets; } /// Helper method to build a NostrResponsiveButton with common properties diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 533a7c9e..8c42c93e 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -22,7 +22,7 @@ class MostroMessageDetail extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tradeState = ref.watch(tradeStateProvider(orderId)); - if (tradeState.lastAction == null || tradeState.orderPayload == null) { + if (tradeState.action == null || tradeState.order == null) { return const CustomCard( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), @@ -52,7 +52,7 @@ class MostroMessageDetail extends ConsumerWidget { style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 16), - Text('${tradeState.status} - ${tradeState.lastAction}'), + Text('${tradeState.status} - ${tradeState.action}'), ], ), ), @@ -66,8 +66,8 @@ class MostroMessageDetail extends ConsumerWidget { WidgetRef ref, TradeState tradeState, ) { - final action = tradeState.lastAction; - final orderPayload = tradeState.orderPayload; + final action = tradeState.action; + final orderPayload = tradeState.order; switch (action) { case actions.Action.newOrder: final expHrs = @@ -188,12 +188,13 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.cantDo: return _getCantDoMessage(context, ref, tradeState); default: - return 'No message found for action ${tradeState.lastAction}'; + return 'No message found for action ${tradeState.action}'; } } - String _getCantDoMessage(BuildContext context, WidgetRef ref, TradeState tradeState) { - final orderPayload = tradeState.orderPayload; + String _getCantDoMessage( + BuildContext context, WidgetRef ref, TradeState tradeState) { + final orderPayload = tradeState.order; final status = tradeState.status; final cantDoReason = ref .read(cantDoNotifierProvider(orderPayload?.id ?? '')) @@ -202,7 +203,9 @@ class MostroMessageDetail extends ConsumerWidget { case CantDoReason.invalidSignature: return S.of(context)!.invalidSignature; case CantDoReason.notAllowedByStatus: - return S.of(context)!.notAllowedByStatus(orderPayload?.id ?? '', status); + return S + .of(context)! + .notAllowedByStatus(orderPayload?.id ?? '', status); case CantDoReason.outOfRangeFiatAmount: return S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); case CantDoReason.outOfRangeSatsAmount: @@ -220,7 +223,7 @@ class MostroMessageDetail extends ConsumerWidget { case CantDoReason.pendingOrderExists: return S.of(context)!.pendingOrderExists; default: - return '${status.toString()} - ${tradeState.lastAction}'; + return '${status.toString()} - ${tradeState.action}'; } } } From be19c5d5d1de00d64ee9a59e88182bcf54750007 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 14 May 2025 14:46:31 -0700 Subject: [PATCH 03/26] refactor: consolidate order notifiers into single OrderNotifier and migrate to new SessionNotifier --- android/app/src/main/AndroidManifest.xml | 5 +- lib/data/enums.dart | 5 + lib/data/models.dart | 8 +- .../chat/notifiers/chat_room_notifier.dart | 2 +- .../chat/notifiers/chat_rooms_notifier.dart | 2 +- .../chat/providers/chat_room_providers.dart | 2 +- .../chat/screens/chat_room_screen.dart | 2 +- .../chat/screens/chat_rooms_list.dart | 2 +- .../home/providers/home_order_providers.dart | 2 +- .../key_manager/key_management_screen.dart | 2 +- .../notfiers/abstract_mostro_notifier.dart | 229 +++++++++--------- .../order/notfiers/add_order_notifier.dart | 45 ++-- .../order/notfiers/cant_do_notifier.dart | 24 -- .../order/notfiers/dispute_notifier.dart | 18 -- .../order/notfiers/order_notifier.dart | 44 ++-- .../notfiers/payment_request_notifier.dart | 20 -- .../providers/order_notifier_provider.dart | 37 --- .../screens/pay_lightning_invoice_screen.dart | 2 +- lib/features/order/widgets/mostro_button.dart | 71 ------ .../trades/providers/trades_provider.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 2 +- .../widgets/mostro_message_detail_widget.dart | 89 +++---- .../trades/widgets/trades_list_item.dart | 2 +- lib/services/mostro_service.dart | 43 +--- lib/shared/providers/app_init_provider.dart | 31 +-- .../providers/mostro_service_provider.dart | 2 +- ...er.dart => session_notifier_provider.dart} | 0 lib/shared/providers/session_providers.dart | 33 --- 28 files changed, 226 insertions(+), 500 deletions(-) create mode 100644 lib/data/enums.dart delete mode 100644 lib/features/order/notfiers/cant_do_notifier.dart delete mode 100644 lib/features/order/notfiers/dispute_notifier.dart delete mode 100644 lib/features/order/notfiers/payment_request_notifier.dart delete mode 100644 lib/features/order/widgets/mostro_button.dart rename lib/shared/providers/{session_manager_provider.dart => session_notifier_provider.dart} (100%) delete mode 100644 lib/shared/providers/session_providers.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b871ba6e..16601635 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -46,9 +46,8 @@ - - - + + diff --git a/lib/data/enums.dart b/lib/data/enums.dart new file mode 100644 index 00000000..9057d515 --- /dev/null +++ b/lib/data/enums.dart @@ -0,0 +1,5 @@ +export 'package:mostro_mobile/data/models/enums/action.dart'; +export 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; +export 'package:mostro_mobile/data/models/enums/order_type.dart'; +export 'package:mostro_mobile/data/models/enums/role.dart'; +export 'package:mostro_mobile/data/models/enums/status.dart'; \ No newline at end of file diff --git a/lib/data/models.dart b/lib/data/models.dart index 338f713d..84f69724 100644 --- a/lib/data/models.dart +++ b/lib/data/models.dart @@ -6,11 +6,7 @@ export 'package:mostro_mobile/data/models/nostr_event.dart'; export 'package:mostro_mobile/data/models/order.dart'; export 'package:mostro_mobile/data/models/payload.dart'; export 'package:mostro_mobile/data/models/payment_request.dart'; +export 'package:mostro_mobile/data/models/peer.dart'; export 'package:mostro_mobile/data/models/rating_user.dart'; export 'package:mostro_mobile/data/models/rating.dart'; -export 'package:mostro_mobile/data/models/session.dart'; - -export 'package:mostro_mobile/data/models/enums/order_type.dart'; -export 'package:mostro_mobile/data/models/enums/role.dart'; -export 'package:mostro_mobile/data/models/enums/action.dart'; -export 'package:mostro_mobile/data/models/enums/status.dart'; \ No newline at end of file +export 'package:mostro_mobile/data/models/session.dart'; \ No newline at end of file diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index 2e2305fb..04fb666b 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -8,7 +8,7 @@ import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/services/lifecycle_manager.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_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class ChatRoomNotifier extends StateNotifier { /// Reload the chat room by re-subscribing to events. diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart index 26de1aff..2d77ced3 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -3,7 +3,7 @@ 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_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class ChatRoomsNotifier extends StateNotifier> { final SessionNotifier sessionNotifier; diff --git a/lib/features/chat/providers/chat_room_providers.dart b/lib/features/chat/providers/chat_room_providers.dart index 0d8cb728..05929ecf 100644 --- a/lib/features/chat/providers/chat_room_providers.dart +++ b/lib/features/chat/providers/chat_room_providers.dart @@ -2,7 +2,7 @@ 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_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final chatRoomsNotifierProvider = StateNotifierProvider>( diff --git a/lib/features/chat/screens/chat_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index 2f96decb..adda9b2c 100644 --- a/lib/features/chat/screens/chat_room_screen.dart +++ b/lib/features/chat/screens/chat_room_screen.dart @@ -10,7 +10,7 @@ import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/clickable_text_widget.dart'; class ChatRoomScreen extends ConsumerStatefulWidget { diff --git a/lib/features/chat/screens/chat_rooms_list.dart b/lib/features/chat/screens/chat_rooms_list.dart index 8a869605..8bc3c7bd 100644 --- a/lib/features/chat/screens/chat_rooms_list.dart +++ b/lib/features/chat/screens/chat_rooms_list.dart @@ -7,7 +7,7 @@ import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_drawer.dart'; diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index d9aa1c30..300a7f05 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -4,7 +4,7 @@ import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final homeOrderTypeProvider = StateProvider((ref) => OrderType.sell); diff --git a/lib/features/key_manager/key_management_screen.dart b/lib/features/key_manager/key_management_screen.dart index de9c3c8d..075a9a3a 100644 --- a/lib/features/key_manager/key_management_screen.dart +++ b/lib/features/key_manager/key_management_screen.dart @@ -5,7 +5,7 @@ import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/widgets/privacy_switch_widget.dart'; diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index b765c49f..a13d6ecc 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -1,33 +1,28 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/config.dart'; -import 'package:mostro_mobile/data/models/cant_do.dart'; -import 'package:mostro_mobile/data/models/dispute.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/models/peer.dart'; -import 'package:mostro_mobile/core/mostro_fsm.dart'; +import 'package:mostro_mobile/data/enums.dart'; +import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class AbstractMostroNotifier extends StateNotifier { final String orderId; final Ref ref; - - ProviderSubscription>? subscription; final logger = Logger(); - // Keep local FSM state in sync with every incoming MostroMessage. Status _currentStatus = Status.pending; - Status get currentStatus => _currentStatus; + late Session session; + Status get status => _currentStatus; + Action get action => state.action; + + ProviderSubscription>? subscription; AbstractMostroNotifier( this.orderId, @@ -36,20 +31,19 @@ class AbstractMostroNotifier extends StateNotifier { Future sync() async { final storage = ref.read(mostroStorageProvider); - final latestMessage = await storage.getMessageById(orderId); + final latestMessage = + await storage.getLatestMessageOfTypeById(orderId); if (latestMessage != null) { state = latestMessage; - // Bootstrap FSM status from the order payload if present. final orderPayload = latestMessage.getPayload(); _currentStatus = orderPayload?.status ?? _currentStatus; } } void subscribe() { - // Use the mostroMessageStream provider that directly watches Sembast storage changes subscription = ref.listen( mostroMessageStreamProvider(orderId), - (_, AsyncValue next) { + (_, next) { next.when( data: (MostroMessage? msg) { if (msg != null) { @@ -68,63 +62,120 @@ class AbstractMostroNotifier extends StateNotifier { } void handleEvent(MostroMessage event) { - // Update FSM first so UI can react to new `Status` if needed. - _currentStatus = MostroFSM.nextStatus(_currentStatus, event.action); - - // Persist the message as the latest state for the order. - state = event; - - handleOrderUpdate(); - } - - void handleCantDo(CantDo? cantDo) { - final notifProvider = ref.read(notificationProvider.notifier); - notifProvider.showInformation(Action.cantDo, values: { - 'action': cantDo?.cantDoReason.toString(), - }); - } - - void handleOrderUpdate() { final navProvider = ref.read(navigationProvider.notifier); final notifProvider = ref.read(notificationProvider.notifier); final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; - switch (state.action) { - case Action.addInvoice: - navProvider.go('/add_invoice/$orderId'); - break; - case Action.cantDo: - final cantDo = state.getPayload(); - notifProvider.showInformation(state.action, - values: {'action': cantDo?.cantDoReason.toString()}); - break; + switch (event.action) { case Action.newOrder: - navProvider.go('/order_confirmed/${state.id!}'); + state = event; + navProvider.go('/order_confirmed/${event.id!}'); break; case Action.payInvoice: - navProvider.go('/pay_invoice/${state.id!}'); + state = event; + navProvider.go('/pay_invoice/${event.id!}'); + break; + case Action.fiatSentOk: + state = event; + final peer = event.getPayload(); + notifProvider.showInformation(event.action, values: { + 'buyer_npub': peer?.publicKey ?? '{buyer_npub}', + }); + break; + case Action.released: + notifProvider.showInformation(event.action, values: { + 'seller_npub': '', + }); + break; + case Action.canceled: + ref + .read(mostroStorageProvider) + .deleteAllMessagesByOrderId(session.orderId!); + ref + .read(sessionNotifierProvider.notifier) + .deleteSession(session.orderId!); + navProvider.go('/'); + notifProvider.showInformation(event.action, values: {'id': orderId}); + dispose(); + break; + case Action.cooperativeCancelInitiatedByYou: + notifProvider.showInformation(event.action, values: { + 'id': event.id, + }); + break; + case Action.cooperativeCancelInitiatedByPeer: + notifProvider.showInformation(event.action, values: { + 'id': event.id!, + }); + break; + case Action.disputeInitiatedByYou: + final dispute = event.getPayload()!; + notifProvider.showInformation(event.action, values: { + 'id': event.id!, + 'user_token': dispute.disputeId, + }); + break; + case Action.disputeInitiatedByPeer: + final dispute = event.getPayload()!; + notifProvider.showInformation(event.action, values: { + 'id': event.id!, + 'user_token': dispute.disputeId, + }); + break; + case Action.cooperativeCancelAccepted: + notifProvider.showInformation(event.action, values: { + 'id': event.id!, + }); + break; + case Action.holdInvoicePaymentAccepted: + final order = event.getPayload(); + notifProvider.showInformation(event.action, values: { + 'seller_npub': order?.sellerTradePubkey ?? 'Unknown', + 'id': order?.id, + '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 = Peer(publicKey: order!.sellerTradePubkey!); + sessionProvider.updateSession( + orderId, + (s) => s.peer = peer, + ); + final chat = ref.read(chatRoomsProvider(orderId).notifier); + chat.subscribe(); + break; + case Action.holdInvoicePaymentSettled: + notifProvider.showInformation(event.action, values: { + 'buyer_npub': 'buyerTradePubkey', + }); break; case Action.waitingSellerToPay: navProvider.go('/'); - notifProvider.showInformation(state.action, values: { - 'id': state.id, + notifProvider.showInformation(event.action, values: { + 'id': event.id, 'expiration_seconds': mostroInstance?.expirationSeconds ?? Config.expirationSeconds, }); break; case Action.waitingBuyerInvoice: - notifProvider.showInformation(state.action, values: { + notifProvider.showInformation(event.action, values: { 'expiration_seconds': mostroInstance?.expirationSeconds ?? Config.expirationSeconds, }); break; + case Action.addInvoice: + navProvider.go('/add_invoice/$orderId'); + break; case Action.buyerTookOrder: - final order = state.getPayload(); + final order = event.getPayload(); if (order == null) { logger.e('Buyer took order, but order is null'); break; } - notifProvider.showInformation(state.action, values: { + notifProvider.showInformation(event.action, values: { 'buyer_npub': order.buyerTradePubkey ?? 'Unknown', 'fiat_code': order.fiatCode, 'fiat_amount': order.fiatAmount, @@ -143,84 +194,26 @@ class AbstractMostroNotifier extends StateNotifier { final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); break; - case Action.canceled: - navProvider.go('/'); - notifProvider.showInformation(state.action, values: {'id': orderId}); - break; - case Action.holdInvoicePaymentAccepted: - final order = state.getPayload(); - notifProvider.showInformation(state.action, values: { - 'seller_npub': order?.sellerTradePubkey ?? 'Unknown', - 'id': order?.id, - '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 = Peer(publicKey: order!.sellerTradePubkey!); - sessionProvider.updateSession( - orderId, - (s) => s.peer = peer, + case Action.cantDo: + final cantDo = event.getPayload(); + ref.read(notificationProvider.notifier).showInformation( + event.action, + values: { + 'action': cantDo?.cantDoReason.toString(), + }, ); - final chat = ref.read(chatRoomsProvider(orderId).notifier); - chat.subscribe(); - break; - case Action.fiatSentOk: - final peer = state.getPayload(); - notifProvider.showInformation(state.action, values: { - 'buyer_npub': peer?.publicKey ?? '{buyer_npub}', - }); - break; - case Action.holdInvoicePaymentSettled: - notifProvider.showInformation(state.action, values: { - 'buyer_npub': 'buyerTradePubkey', - }); - break; - case Action.rate: - case Action.rateReceived: - case Action.cooperativeCancelInitiatedByYou: - notifProvider.showInformation(state.action, values: { - 'id': state.id, - }); break; case Action.adminSettled: - notifProvider.showInformation(state.action, values: {}); + notifProvider.showInformation(event.action, values: {}); break; case Action.paymentFailed: - notifProvider.showInformation(state.action, values: { + notifProvider.showInformation(event.action, values: { 'payment_attempts': -1, 'payment_retries_interval': -1000 }); break; - case Action.released: - notifProvider.showInformation(state.action, values: { - 'seller_npub': '', - }); - case Action.disputeInitiatedByPeer: - final dispute = state.getPayload()!; - notifProvider.showInformation(state.action, values: { - 'id': state.id!, - 'user_token': dispute.disputeId, - }); - break; - case Action.disputeInitiatedByYou: - final dispute = state.getPayload()!; - notifProvider.showInformation(state.action, values: { - 'id': state.id!, - 'user_token': dispute.disputeId, - }); - case Action.cooperativeCancelAccepted: - notifProvider.showInformation(state.action, values: { - 'id': state.id!, - }); - case Action.cooperativeCancelInitiatedByPeer: - notifProvider.showInformation(state.action, values: { - 'id': state.id!, - }); default: - notifProvider.showInformation(state.action, values: {}); + notifProvider.showInformation(event.action, values: {}); break; } } diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 7a1d235a..2b0e4d66 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -1,14 +1,14 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/cant_do.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/enums.dart'; +import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class AddOrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; @@ -16,16 +16,14 @@ class AddOrderNotifier extends AbstractMostroNotifier { AddOrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); - // Generate a unique requestId from the orderId but with better uniqueness // Take a portion of the UUID and combine with current timestamp to ensure uniqueness final uuid = orderId.replaceAll('-', ''); final timestamp = DateTime.now().microsecondsSinceEpoch; - // Use only the first 8 chars of UUID combined with current timestamp for uniqueness // This avoids potential collisions from truncation while keeping values in int range - requestId = (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & 0x7FFFFFFF; - + requestId = + (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & 0x7FFFFFFF; subscribe(); } @@ -38,9 +36,10 @@ class AddOrderNotifier extends AbstractMostroNotifier { data: (msg) { if (msg != null) { if (msg.payload is Order) { - state = msg; if (msg.action == Action.newOrder) { - confirmOrder(msg); + _confirmOrder(msg); + } else { + logger.i('AddOrderNotifier: received ${msg.action}'); } } else if (msg.payload is CantDo) { _handleCantDo(msg); @@ -55,9 +54,8 @@ class AddOrderNotifier extends AbstractMostroNotifier { } void _handleCantDo(MostroMessage message) { - final notifProvider = ref.read(notificationProvider.notifier); final cantDo = message.getPayload(); - notifProvider.showInformation( + ref.read(notificationProvider.notifier).showInformation( message.action, values: { 'action': cantDo?.cantDoReason.toString(), @@ -65,22 +63,29 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); } - Future confirmOrder(MostroMessage confirmedOrder) async { - final orderNotifier = ref.watch( - orderNotifierProvider(confirmedOrder.id!).notifier, - ); - handleOrderUpdate(); - orderNotifier.subscribe(); + Future _confirmOrder(MostroMessage message) async { + state = message; + session.orderId = message.id; + ref.read(sessionNotifierProvider.notifier).saveSession(session); + ref.read(orderNotifierProvider(message.id!).notifier).subscribe(); + ref.read(navigationProvider.notifier).go( + '/order_confirmed/${message.id!}', + ); dispose(); } Future submitOrder(Order order) async { - final message = MostroMessage( + state = MostroMessage( action: Action.newOrder, id: null, requestId: requestId, payload: order, ); - await mostroService.submitOrder(message); + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); + session = await sessionNotifier.newSession( + role: order.kind == OrderType.buy ? Role.buyer : Role.seller, + ); + mostroService.subscribe(session); + await mostroService.submitOrder(state); } } diff --git a/lib/features/order/notfiers/cant_do_notifier.dart b/lib/features/order/notfiers/cant_do_notifier.dart deleted file mode 100644 index 79d3b376..00000000 --- a/lib/features/order/notfiers/cant_do_notifier.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:mostro_mobile/data/models/cant_do.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; -import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; - -class CantDoNotifier extends AbstractMostroNotifier { - CantDoNotifier(super.orderId, super.ref) { - sync(); - subscribe(); - } - - @override - void handleEvent(MostroMessage event) { - if (event.payload is! CantDo) return; - - final cantDo = event.getPayload(); - - final notifProvider = ref.read(notificationProvider.notifier); - notifProvider.showInformation(Action.cantDo, values: { - 'action': cantDo?.cantDoReason.toString() ?? '', - }); - } -} diff --git a/lib/features/order/notfiers/dispute_notifier.dart b/lib/features/order/notfiers/dispute_notifier.dart deleted file mode 100644 index 59b3442b..00000000 --- a/lib/features/order/notfiers/dispute_notifier.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:mostro_mobile/data/models/dispute.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; - -class DisputeNotifier extends AbstractMostroNotifier { - DisputeNotifier(super.orderId, super.ref) { - sync(); - subscribe(); - } - - @override - void handleEvent(MostroMessage event) { - if (event.payload is Dispute) { - state = event; - handleOrderUpdate(); - } - } -} diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 9138d3e1..cb6cb23e 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -1,40 +1,31 @@ import 'dart:async'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; -import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; + late Order _order; + Order get order => _order; + OrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); sync(); subscribe(); } - - @override - void handleEvent(MostroMessage event) { - // Forward all messages so UI reacts to CantDo, Peer, PaymentRequest, etc. - state = event; - handleOrderUpdate(); - } - - Future submitOrder(Order order) async { - final message = MostroMessage( - action: Action.newOrder, - id: null, - payload: order, - ); - await mostroService.submitOrder(message); - } - Future takeSellOrder( String orderId, int? amount, String? lnAddress) async { + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); + session = await sessionNotifier.newSession( + orderId: orderId, + role: order.kind == OrderType.buy ? Role.buyer : Role.seller, + ); + mostroService.subscribe(session); await mostroService.takeSellOrder( orderId, amount, @@ -43,6 +34,12 @@ class OrderNotifier extends AbstractMostroNotifier { } Future takeBuyOrder(String orderId, int? amount) async { + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); + session = await sessionNotifier.newSession( + orderId: orderId, + role: order.kind == OrderType.buy ? Role.buyer : Role.seller, + ); + mostroService.subscribe(session); await mostroService.takeBuyOrder( orderId, amount, @@ -80,11 +77,4 @@ class OrderNotifier extends AbstractMostroNotifier { ); } - @override - void dispose() { - ref.invalidate(cantDoNotifierProvider(orderId)); - ref.invalidate(paymentNotifierProvider(orderId)); - ref.invalidate(disputeNotifierProvider(orderId)); - super.dispose(); - } } diff --git a/lib/features/order/notfiers/payment_request_notifier.dart b/lib/features/order/notfiers/payment_request_notifier.dart deleted file mode 100644 index 31c67675..00000000 --- a/lib/features/order/notfiers/payment_request_notifier.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/payment_request.dart'; -import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; - -class PaymentRequestNotifier extends AbstractMostroNotifier { - PaymentRequestNotifier(super.orderId, super.ref) { - sync(); - subscribe(); - } - - @override - void handleEvent(MostroMessage event) { - // Only react to PaymentRequest payloads; delegate full handling to the - // base notifier so that the Finite-State Machine and generic side-effects - // (navigation, notifications, etc.) remain consistent. - if (event.payload is PaymentRequest) { - super.handleEvent(event); - } - } -} diff --git a/lib/features/order/providers/order_notifier_provider.dart b/lib/features/order/providers/order_notifier_provider.dart index 69fa4848..8ef57ff8 100644 --- a/lib/features/order/providers/order_notifier_provider.dart +++ b/lib/features/order/providers/order_notifier_provider.dart @@ -2,22 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/order/notfiers/add_order_notifier.dart'; -import 'package:mostro_mobile/features/order/notfiers/dispute_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'; -import 'package:mostro_mobile/features/order/notfiers/payment_request_notifier.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; -import 'package:mostro_mobile/features/order/notfiers/cant_do_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'order_notifier_provider.g.dart'; final orderNotifierProvider = StateNotifierProvider.family( (ref, orderId) { - // Initialize all related notifiers - ref.read(cantDoNotifierProvider(orderId)); - ref.read(paymentNotifierProvider(orderId)); - ref.read(disputeNotifierProvider(orderId)); - return OrderNotifier( orderId, ref, @@ -35,35 +27,6 @@ final addOrderNotifierProvider = }, ); -final cantDoNotifierProvider = - StateNotifierProvider.family( - (ref, orderId) { - return CantDoNotifier( - orderId, - ref, - ); - }, -); - -final paymentNotifierProvider = - StateNotifierProvider.family( - (ref, orderId) { - return PaymentRequestNotifier( - orderId, - ref, - ); - }, -); - -final disputeNotifierProvider = - StateNotifierProvider.family( - (ref, orderId) { - return DisputeNotifier( - orderId, - ref, - ); - }, -); // This provider tracks the currently selected OrderType tab @riverpod diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index f7dc7645..734b9bbd 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -22,7 +22,7 @@ class _PayLightningInvoiceScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final order = ref.read(paymentNotifierProvider(widget.orderId)); + final order = ref.read(orderNotifierProvider(widget.orderId)); final lnInvoice = order.getPayload()?.lnInvoice ?? ''; final orderNotifier = ref.read(orderNotifierProvider(widget.orderId).notifier); diff --git a/lib/features/order/widgets/mostro_button.dart b/lib/features/order/widgets/mostro_button.dart deleted file mode 100644 index b64ea9ba..00000000 --- a/lib/features/order/widgets/mostro_button.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/features/order/widgets/action_button_config.dart'; -import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; - -// Use ButtonStyleType from mostro_reactive_button.dart - -/// A unified button component for Mostro actions -/// -/// This widget combines configuration from MostroButtonConfig with -/// the reactive behavior of MostroReactiveButton, creating a -/// consistent button UI across the app. -class MostroButton extends ConsumerWidget { - final MostroButtonConfig config; - - const MostroButton({ - super.key, - required this.config, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // If we have an orderId and action, we can create a reactive button - if (config.orderId != null) { - return MostroReactiveButton( - label: config.label, - buttonStyle: config.buttonStyle, - orderId: config.orderId!, - action: config.action, - backgroundColor: config.color, - onPressed: config.onPressed, - showSuccessIndicator: config.showSuccessIndicator, - timeout: config.timeout, - ); - } - - // Otherwise create a regular button with the right style - return _buildButton(context); - } - - Widget _buildButton(BuildContext context) { - switch (config.buttonStyle) { - case ButtonStyleType.raised: - return ElevatedButton( - onPressed: config.onPressed, - style: ElevatedButton.styleFrom( - backgroundColor: config.color, - ), - child: Text(config.label), - ); - - case ButtonStyleType.outlined: - return OutlinedButton( - onPressed: config.onPressed, - style: OutlinedButton.styleFrom( - foregroundColor: config.color, - ), - child: Text(config.label), - ); - - case ButtonStyleType.text: - return TextButton( - onPressed: config.onPressed, - style: TextButton.styleFrom( - foregroundColor: config.color, - ), - child: Text(config.label), - ); - } - } -} diff --git a/lib/features/trades/providers/trades_provider.dart b/lib/features/trades/providers/trades_provider.dart index 0cb02587..b209bf07 100644 --- a/lib/features/trades/providers/trades_provider.dart +++ b/lib/features/trades/providers/trades_provider.dart @@ -5,7 +5,7 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final _logger = Logger(); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 16067c68..f353af15 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -14,7 +14,7 @@ import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; import 'package:mostro_mobile/features/trades/models/trade_state.dart'; import 'package:mostro_mobile/features/trades/providers/trade_state_provider.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 8c42c93e..5163f4f4 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -3,15 +3,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/dispute.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/features/trades/models/trade_state.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; -import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; -import 'package:mostro_mobile/features/trades/providers/trade_state_provider.dart'; import 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; class MostroMessageDetail extends ConsumerWidget { @@ -20,19 +18,11 @@ class MostroMessageDetail extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tradeState = ref.watch(tradeStateProvider(orderId)); - - if (tradeState.action == null || tradeState.order == null) { - return const CustomCard( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ); - } + final orderState = ref.watch(orderNotifierProvider(orderId).notifier); final actionText = _getActionText( context, ref, - tradeState, ); return CustomCard( padding: const EdgeInsets.all(16), @@ -52,7 +42,7 @@ class MostroMessageDetail extends ConsumerWidget { style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 16), - Text('${tradeState.status} - ${tradeState.action}'), + Text('${orderState.status} - ${orderState.action}'), ], ), ), @@ -64,8 +54,8 @@ class MostroMessageDetail extends ConsumerWidget { String _getActionText( BuildContext context, WidgetRef ref, - TradeState tradeState, ) { + final tradeState = ref.watch(orderNotifierProvider(orderId).notifier); final action = tradeState.action; final orderPayload = tradeState.order; switch (action) { @@ -75,7 +65,7 @@ class MostroMessageDetail extends ConsumerWidget { '24'; return S.of(context)!.newOrder(int.tryParse(expHrs) ?? 24); case actions.Action.canceled: - return S.of(context)!.canceled(orderPayload?.id ?? ''); + return S.of(context)!.canceled(orderPayload.id ?? ''); case actions.Action.payInvoice: final expSecs = ref .read(orderRepositoryProvider) @@ -83,10 +73,10 @@ class MostroMessageDetail extends ConsumerWidget { ?.expirationSeconds ?? 900; return S.of(context)!.payInvoice( - orderPayload?.amount.toString() ?? '', + orderPayload.amount.toString(), '${expSecs ~/ 60} minutes', - orderPayload?.fiatAmount.toString() ?? '', - orderPayload?.fiatCode ?? '', + orderPayload.fiatAmount.toString(), + orderPayload.fiatCode, ); case actions.Action.addInvoice: final expSecs = ref @@ -95,9 +85,9 @@ class MostroMessageDetail extends ConsumerWidget { ?.expirationSeconds ?? 900; return S.of(context)!.addInvoice( - orderPayload?.amount.toString() ?? '', - orderPayload?.fiatCode ?? '', - orderPayload?.fiatAmount.toString() ?? '', + orderPayload.amount.toString(), + orderPayload.fiatCode, + orderPayload.fiatAmount.toString(), expSecs, ); case actions.Action.waitingSellerToPay: @@ -108,7 +98,7 @@ class MostroMessageDetail extends ConsumerWidget { 900; return S .of(context)! - .waitingSellerToPay(orderPayload?.id ?? '', expSecs); + .waitingSellerToPay(orderPayload.id ?? '', expSecs); case actions.Action.waitingBuyerInvoice: final expSecs = ref .read(orderRepositoryProvider) @@ -119,23 +109,23 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.buyerInvoiceAccepted: return S.of(context)!.buyerInvoiceAccepted; case actions.Action.holdInvoicePaymentAccepted: - final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload.id ?? '')); return S.of(context)!.holdInvoicePaymentAccepted( - orderPayload?.fiatAmount.toString() ?? '', - orderPayload?.fiatCode ?? '', - orderPayload?.paymentMethod ?? '', + orderPayload.fiatAmount.toString(), + orderPayload.fiatCode, + orderPayload.paymentMethod, session?.peer?.publicKey ?? '', ); case actions.Action.buyerTookOrder: - final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload.id ?? '')); return S.of(context)!.buyerTookOrder( session?.peer?.publicKey ?? '', - orderPayload?.fiatCode ?? '', - orderPayload?.fiatAmount.toString() ?? '', - orderPayload?.paymentMethod ?? '', + orderPayload.fiatCode, + orderPayload.fiatAmount.toString(), + orderPayload.paymentMethod, ); case actions.Action.fiatSentOk: - final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload.id ?? '')); return session!.role == Role.buyer ? S.of(context)!.fiatSentOkBuyer(session.peer!.publicKey) : S.of(context)!.fiatSentOkSeller(session.peer!.publicKey); @@ -150,35 +140,27 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.rateReceived: return S.of(context)!.rateReceived; case actions.Action.cooperativeCancelInitiatedByYou: - return S - .of(context)! - .cooperativeCancelInitiatedByYou(orderPayload?.id ?? ''); + return S.of(context)!.cooperativeCancelInitiatedByYou(orderPayload.id ?? ''); case actions.Action.cooperativeCancelInitiatedByPeer: - return S - .of(context)! - .cooperativeCancelInitiatedByPeer(orderPayload?.id ?? ''); + return S.of(context)!.cooperativeCancelInitiatedByPeer(orderPayload.id ?? ''); case actions.Action.cooperativeCancelAccepted: - return S.of(context)!.cooperativeCancelAccepted(orderPayload?.id ?? ''); + return S.of(context)!.cooperativeCancelAccepted(orderPayload.id ?? ''); case actions.Action.disputeInitiatedByYou: final payload = ref - .read(disputeNotifierProvider(orderPayload?.id ?? '')) + .read(orderNotifierProvider(orderId)) .getPayload(); - return S - .of(context)! - .disputeInitiatedByYou(orderPayload?.id ?? '', payload!.disputeId); + return S.of(context)!.disputeInitiatedByYou(orderPayload.id!, payload!.disputeId); case actions.Action.disputeInitiatedByPeer: final payload = ref - .read(disputeNotifierProvider(orderPayload?.id ?? '')) + .read(orderNotifierProvider(orderId)) .getPayload(); - return S - .of(context)! - .disputeInitiatedByPeer(orderPayload?.id ?? '', payload!.disputeId); + return S.of(context)!.disputeInitiatedByPeer(orderPayload.id!, payload!.disputeId); case actions.Action.adminTookDispute: return S.of(context)!.adminTookDisputeUsers('{admin token}'); case actions.Action.adminCanceled: - return S.of(context)!.adminCanceledUsers(orderPayload?.id ?? ''); + return S.of(context)!.adminCanceledUsers(orderPayload.id ?? ''); case actions.Action.adminSettled: - return S.of(context)!.adminSettledUsers(orderPayload?.id ?? ''); + return S.of(context)!.adminSettledUsers(orderPayload.id ?? ''); case actions.Action.paymentFailed: return S.of(context)!.paymentFailed('{attempts}', '{retries}'); case actions.Action.invoiceUpdated: @@ -186,18 +168,19 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.holdInvoicePaymentCanceled: return S.of(context)!.holdInvoicePaymentCanceled; case actions.Action.cantDo: - return _getCantDoMessage(context, ref, tradeState); + return _getCantDoMessage(context, ref); default: return 'No message found for action ${tradeState.action}'; } } String _getCantDoMessage( - BuildContext context, WidgetRef ref, TradeState tradeState) { + BuildContext context, WidgetRef ref) { +final tradeState = ref.watch(orderNotifierProvider(orderId).notifier); final orderPayload = tradeState.order; final status = tradeState.status; final cantDoReason = ref - .read(cantDoNotifierProvider(orderPayload?.id ?? '')) + .read(orderNotifierProvider(orderId)) .getPayload(); switch (cantDoReason) { case CantDoReason.invalidSignature: @@ -205,7 +188,7 @@ class MostroMessageDetail extends ConsumerWidget { case CantDoReason.notAllowedByStatus: return S .of(context)! - .notAllowedByStatus(orderPayload?.id ?? '', status); + .notAllowedByStatus(orderPayload.id ?? '', status); case CantDoReason.outOfRangeFiatAmount: return S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); case CantDoReason.outOfRangeSatsAmount: diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 1a56b486..28c92ae1 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -7,7 +7,7 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/time_provider.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 98a55cf0..74a88771 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,12 +1,11 @@ import 'dart:convert'; import 'package:dart_nostr/nostr/model/export.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; -import 'package:mostro_mobile/features/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/services/lifecycle_manager.dart'; -import 'package:mostro_mobile/shared/notifiers/order_action_notifier.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'; @@ -89,22 +88,7 @@ class MostroService { result[0]['timestamp'] = decryptedEvent.createdAt?.millisecondsSinceEpoch; final msg = MostroMessage.fromJson(result[0]); final messageStorage = ref.read(mostroStorageProvider); - - if (msg.id != null) { - if (await messageStorage.hasMessageByKey(decryptedEvent.id!)) return; - ref.read(orderActionNotifierProvider(msg.id!).notifier).set(msg.action); - } - if (msg.action == Action.canceled) { - ref.read(orderNotifierProvider(session.orderId!).notifier).dispose(); - await messageStorage.deleteAllMessagesByOrderId(session.orderId!); - await _sessionNotifier.deleteSession(session.orderId!); - return; - } await messageStorage.addMessage(decryptedEvent.id!, msg); - if (session.orderId == null && msg.id != null) { - session.orderId = msg.id; - await _sessionNotifier.saveSession(session); - } }); } @@ -113,20 +97,18 @@ class MostroService { } Future submitOrder(MostroMessage order) async { - final session = await publishOrder(order); - subscribe(session); + await publishOrder(order); } Future takeBuyOrder(String orderId, int? amount) async { final amt = amount != null ? Amount(amount: amount) : null; - final session = await publishOrder( + await publishOrder( MostroMessage( action: Action.takeBuy, id: orderId, payload: amt, ), ); - subscribe(session); } Future takeSellOrder( @@ -141,15 +123,13 @@ class MostroService { ? Amount(amount: amount) : null; - final session = await publishOrder( + await publishOrder( MostroMessage( action: Action.takeSell, id: orderId, payload: payload, ), ); - - subscribe(session); } Future sendInvoice(String orderId, String invoice, int? amount) async { @@ -204,14 +184,16 @@ class MostroService { } Future submitRating(String orderId, int rating) async { - await publishOrder(MostroMessage( - action: Action.rateUser, - id: orderId, - payload: RatingUser(userRating: rating), - )); + await publishOrder( + MostroMessage( + action: Action.rateUser, + id: orderId, + payload: RatingUser(userRating: rating), + ), + ); } - Future publishOrder(MostroMessage order) async { + Future publishOrder(MostroMessage order) async { final session = await _getSession(order); final event = await order.wrap( tradeKey: session.tradeKey, @@ -221,7 +203,6 @@ class MostroService { ); await ref.read(nostrServiceProvider).publishEvent(event); - return session; } Role? _getRole(MostroMessage order) { diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index bcf9404a..ae2ba6a0 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -1,7 +1,4 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:logger/logger.dart'; -import 'package:mostro_mobile/data/repositories/mostro_storage.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'; @@ -14,8 +11,7 @@ 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_manager_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; final appInitializerProvider = FutureProvider((ref) async { final nostrService = ref.read(nostrServiceProvider); @@ -83,11 +79,9 @@ final appInitializerProvider = FutureProvider((ref) async { order.action, ); - // Explicitly initialize EACH notifier in the family - // to ensure they're all properly set up for this orderId - ref.read(paymentNotifierProvider(session.orderId!).notifier).sync(); - ref.read(cantDoNotifierProvider(session.orderId!).notifier).sync(); - ref.read(disputeNotifierProvider(session.orderId!).notifier).sync(); + // Explicitly initialize order notifier + // to ensure it's all properly set up for this orderId + ref.read(orderNotifierProvider(session.orderId!).notifier).sync(); } // Read the order notifier provider last, which will watch all the above @@ -102,20 +96,3 @@ final appInitializerProvider = FutureProvider((ref) async { } } }); - -Future clearAppData(MostroStorage mostroStorage) async { - final logger = Logger(); - // 1) SharedPreferences - final prefs = await SharedPreferences.getInstance(); - await prefs.clear(); - logger.i("Shared Preferences Cleared"); - - // 2) Flutter Secure Storage - const secureStorage = FlutterSecureStorage(); - await secureStorage.deleteAll(); - logger.i("Shared Storage Cleared"); - - // 3) MostroStorage - mostroStorage.deleteAllMessages(); - logger.i("Mostro Message Storage cleared"); -} diff --git a/lib/shared/providers/mostro_service_provider.dart b/lib/shared/providers/mostro_service_provider.dart index 1129784a..d89bab29 100644 --- a/lib/shared/providers/mostro_service_provider.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -2,7 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/repositories/event_storage.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_manager_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'; diff --git a/lib/shared/providers/session_manager_provider.dart b/lib/shared/providers/session_notifier_provider.dart similarity index 100% rename from lib/shared/providers/session_manager_provider.dart rename to lib/shared/providers/session_notifier_provider.dart diff --git a/lib/shared/providers/session_providers.dart b/lib/shared/providers/session_providers.dart deleted file mode 100644 index ea4bba57..00000000 --- a/lib/shared/providers/session_providers.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'; -import 'package:mostro_mobile/features/order/notfiers/cant_do_notifier.dart'; -import 'package:mostro_mobile/features/order/notfiers/payment_request_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -class SessionProviders { - final String orderId; - final OrderNotifier orderNotifier; - final PaymentRequestNotifier paymentRequestNotifier; - final CantDoNotifier cantDoNotifier; - - SessionProviders({ - required this.orderId, - required Ref ref, - }) : orderNotifier = OrderNotifier(orderId, ref), - paymentRequestNotifier = PaymentRequestNotifier(orderId, ref), - cantDoNotifier = CantDoNotifier(orderId, ref); - - void dispose() { - orderNotifier.dispose(); - paymentRequestNotifier.dispose(); - cantDoNotifier.dispose(); - } -} - -@riverpod -SessionProviders sessionProviders(Ref ref, String orderId) { - final providers = SessionProviders(orderId: orderId, ref: ref); - //ref.onDispose(providers.dispose); - return providers; -} - From 51c056b8afa93d91b0f36cc2d22ab92ebfa3e155 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 15 May 2025 12:09:06 -0700 Subject: [PATCH 04/26] refactor: consolidate provider imports and improve session management with request IDs --- .../notfiers/abstract_mostro_notifier.dart | 29 +++++++------------ .../order/notfiers/add_order_notifier.dart | 10 ++----- .../order/notfiers/order_notifier.dart | 20 ++++++++++--- lib/services/mostro_service.dart | 25 ++++------------ lib/shared/notifiers/session_notifier.dart | 14 +++++++-- lib/shared/providers.dart | 7 +++++ 6 files changed, 53 insertions(+), 52 deletions(-) create mode 100644 lib/shared/providers.dart diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index a13d6ecc..d94f69e5 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -3,24 +3,19 @@ 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'; +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:mostro_mobile/shared/providers/mostro_storage_provider.dart'; -import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; -import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; -import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class AbstractMostroNotifier extends StateNotifier { final String orderId; final Ref ref; final logger = Logger(); - Status _currentStatus = Status.pending; + Status status = Status.pending; + Action action = Action.newOrder; late Session session; - Status get status => _currentStatus; - Action get action => state.action; ProviderSubscription>? subscription; @@ -29,17 +24,6 @@ class AbstractMostroNotifier extends StateNotifier { this.ref, ) : super(MostroMessage(action: Action.newOrder, id: orderId)); - Future sync() async { - final storage = ref.read(mostroStorageProvider); - final latestMessage = - await storage.getLatestMessageOfTypeById(orderId); - if (latestMessage != null) { - state = latestMessage; - final orderPayload = latestMessage.getPayload(); - _currentStatus = orderPayload?.status ?? _currentStatus; - } - } - void subscribe() { subscription = ref.listen( mostroMessageStreamProvider(orderId), @@ -68,14 +52,17 @@ class AbstractMostroNotifier extends StateNotifier { switch (event.action) { case Action.newOrder: + action = event.action; state = event; navProvider.go('/order_confirmed/${event.id!}'); break; case Action.payInvoice: + action = event.action; state = event; navProvider.go('/pay_invoice/${event.id!}'); break; case Action.fiatSentOk: + action = event.action; state = event; final peer = event.getPayload(); notifProvider.showInformation(event.action, values: { @@ -83,11 +70,15 @@ class AbstractMostroNotifier extends StateNotifier { }); break; case Action.released: + action = event.action; + state = event; notifProvider.showInformation(event.action, values: { 'seller_npub': '', }); break; case Action.canceled: + action = event.action; + state = event; ref .read(mostroStorageProvider) .deleteAllMessagesByOrderId(session.orderId!); diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 2b0e4d66..89a61a77 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -2,13 +2,10 @@ 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/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'; import 'package:mostro_mobile/services/mostro_service.dart'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; -import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class AddOrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; @@ -16,12 +13,8 @@ class AddOrderNotifier extends AbstractMostroNotifier { AddOrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); - // Generate a unique requestId from the orderId but with better uniqueness - // Take a portion of the UUID and combine with current timestamp to ensure uniqueness final uuid = orderId.replaceAll('-', ''); final timestamp = DateTime.now().microsecondsSinceEpoch; - // Use only the first 8 chars of UUID combined with current timestamp for uniqueness - // This avoids potential collisions from truncation while keeping values in int range requestId = (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & 0x7FFFFFFF; subscribe(); @@ -83,6 +76,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); final sessionNotifier = ref.read(sessionNotifierProvider.notifier); session = await sessionNotifier.newSession( + requestId: requestId, role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); mostroService.subscribe(session); diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index cb6cb23e..2880adaf 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -1,16 +1,14 @@ import 'dart:async'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models/order.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'; -import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; - late Order _order; - Order get order => _order; + late Order order; OrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); @@ -18,6 +16,20 @@ class OrderNotifier extends AbstractMostroNotifier { subscribe(); } + Future sync() async { + final storage = ref.read(mostroStorageProvider); + final latestOrder = + await storage.getLatestMessageOfTypeById(orderId); + if (latestOrder != null) { + order = latestOrder.getPayload()!; + status = order.status; + } + final newState = await storage.getLatestMessageById(orderId); + if (newState != null) { + state = newState; + } + } + Future takeSellOrder( String orderId, int? amount, String? lnAddress) async { final sessionNotifier = ref.read(sessionNotifierProvider.notifier); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 74a88771..8577ba7d 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -205,26 +205,13 @@ class MostroService { await ref.read(nostrServiceProvider).publishEvent(event); } - Role? _getRole(MostroMessage order) { - final payload = order.getPayload(); - - return order.action == Action.newOrder - ? payload?.kind == OrderType.buy - ? Role.buyer - : Role.seller - : order.action == Action.takeBuy - ? Role.seller - : order.action == Action.takeSell - ? Role.buyer - : null; - } - Future _getSession(MostroMessage order) async { - final role = _getRole(order); - return (order.id != null) - ? _sessionNotifier.getSessionByOrderId(order.id!) ?? - await _sessionNotifier.newSession(orderId: order.id, role: role) - : await _sessionNotifier.newSession(role: role); + if (order.requestId != null) { + return _sessionNotifier.getSessionByRequestId(order.requestId!)!; + } else if (order.id != null) { + return _sessionNotifier.getSessionByOrderId(order.id!)!; + } + throw Exception('No session found for order'); } void updateSettings(Settings settings) { diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index 6b6a2e85..2f227e99 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -13,6 +13,7 @@ class SessionNotifier extends StateNotifier> { Settings _settings; final Map _sessions = {}; +final Map _requestIdToSession = {}; Timer? _cleanupTimer; static const int sessionExpirationHours = 36; @@ -43,7 +44,7 @@ class SessionNotifier extends StateNotifier> { _settings = settings.copyWith(); } - Future newSession({String? orderId, Role? role}) async { + Future newSession({String? orderId, int? requestId, Role? role}) async { final masterKey = _keyManager.masterKeyPair!; final keyIndex = await _keyManager.getCurrentKeyIndex(); final tradeKey = await _keyManager.deriveTradeKey(); @@ -62,7 +63,8 @@ class SessionNotifier extends StateNotifier> { _sessions[orderId] = session; await _storage.putSession(session); state = sessions; - } else { + } else if (requestId != null) { + _requestIdToSession[requestId] = session; state = [...sessions, session]; } return session; @@ -85,6 +87,14 @@ class SessionNotifier extends StateNotifier> { } } + Session? getSessionByRequestId(int requestId) { + try { + return _requestIdToSession[requestId]; + } on StateError { + return null; + } + } + Session? getSessionByOrderId(String orderId) { try { return _sessions[orderId]; diff --git a/lib/shared/providers.dart b/lib/shared/providers.dart new file mode 100644 index 00000000..bff66b97 --- /dev/null +++ b/lib/shared/providers.dart @@ -0,0 +1,7 @@ +export 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +export 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +export 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +export 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +export 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +export 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; +export 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; \ No newline at end of file From 66bf71c4091c433a632ccf44cb8e07029f44e74b Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 19 May 2025 10:50:08 -0700 Subject: [PATCH 05/26] refactor: migrate order state management from MostroMessage to OrderState model --- lib/core/mostro_fsm.dart | 166 ++++++++++-------- .../key_manager/key_manager_provider.dart | 18 -- lib/features/key_manager/key_notifier.dart | 12 -- lib/features/order/models/order_state.dart | 85 +++++++++ .../notfiers/abstract_mostro_notifier.dart | 45 +++-- .../order/notfiers/add_order_notifier.dart | 36 ++-- .../order/notfiers/order_notifier.dart | 34 ++-- .../providers/order_notifier_provider.dart | 5 +- .../providers/order_status_provider.dart | 25 --- .../screens/pay_lightning_invoice_screen.dart | 3 +- .../screens/payment_confirmation_screen.dart | 9 +- lib/features/trades/models/trade_state.dart | 6 +- .../trades/screens/trade_detail_screen.dart | 4 +- .../widgets/mostro_message_detail_widget.dart | 89 +++++----- .../widgets/mostro_reactive_button.dart | 42 +++-- 15 files changed, 326 insertions(+), 253 deletions(-) delete mode 100644 lib/features/key_manager/key_notifier.dart create mode 100644 lib/features/order/models/order_state.dart delete mode 100644 lib/features/order/providers/order_status_provider.dart diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart index dace852f..5fd60a61 100644 --- a/lib/core/mostro_fsm.dart +++ b/lib/core/mostro_fsm.dart @@ -1,104 +1,124 @@ import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; /// Finite-State-Machine helper for Mostro order lifecycles. -/// -/// This table was generated directly from the authoritative specification. /// Only *state–transition → next-state* information is encoded here. /// All auxiliary / neutral notifications intentionally map to /// the **same** state so that `nextStatus` always returns a non-null value. class MostroFSM { - MostroFSM._(); - - /// Nested map: *currentStatus → { action → nextStatus }*. - static final Map> _transitions = { + /// Nested map: *currentStatus → { role → { action → nextStatus } }*. + static final Map>> _transitions = { // ───────────────────────── MATCHING / TAKING ──────────────────────── Status.pending: { - Action.takeSell: Status.waitingBuyerInvoice, // can go to waitingPayment - Action.takeBuy: Status.waitingPayment, - Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.buyer: { + Action.takeSell: Status.waitingBuyerInvoice, + Action.cancel: Status.canceled, + }, + Role.seller: { + Action.takeBuy: Status.waitingPayment, + Action.cancel: Status.canceled, + }, + Role.admin: {}, }, - // ───────────────────────── INVOICING ──────────────────────────────── Status.waitingBuyerInvoice: { - Action.waitingBuyerInvoice: Status.active, - Action.addInvoice: Status.waitingPayment, // can go to active - Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.buyer: { + Action.addInvoice: Status.waitingPayment, + Action.cancel: Status.canceled, + }, + Role.seller: {}, + Role.admin: {}, }, - // ───────────────────────── HOLD INVOICE PAYMENT ──────────────────── Status.waitingPayment: { - Action.payInvoice: Status.waitingBuyerInvoice, // can go to active - Action.waitingSellerToPay: Status.waitingBuyerInvoice, - Action.holdInvoicePaymentAccepted: Status.active, - Action.holdInvoicePaymentCanceled: Status.canceled, - Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.seller: { + Action.payInvoice: Status.active, + Action.cancel: Status.canceled, + }, + Role.buyer: {}, + Role.admin: {}, }, - - // ───────────────────────── ACTIVE TRADE ──────────────────────────── + // ───────────────────────── ACTIVE ──────────────────────────── Status.active: { - Action.holdInvoicePaymentAccepted: Status.fiatSent, - Action.buyerTookOrder: Status.fiatSent, - - Action.fiatSent: Status.fiatSent, - Action.cooperativeCancelInitiatedByYou: Status.cooperativelyCanceled, - Action.cooperativeCancelInitiatedByPeer: Status.cooperativelyCanceled, - Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.buyer: { + Action.fiatSent: Status.fiatSent, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + }, + Role.seller: { + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + }, + Role.admin: {}, }, - - // ───────────────────────── AFTER FIAT SENT ───────────────────────── + // ───────────────────────── FIAT SENT ───────────────────────── Status.fiatSent: { - Action.release: Status.settledHoldInvoice, - Action.holdInvoicePaymentSettled: Status.settledHoldInvoice, - Action.cooperativeCancelInitiatedByYou: Status.cooperativelyCanceled, - Action.cooperativeCancelInitiatedByPeer: Status.cooperativelyCanceled, - Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.buyer: { + Action.holdInvoicePaymentSettled: Status.settledHoldInvoice, + Action.disputeInitiatedByYou: Status.dispute, + }, + Role.seller: { + Action.release: Status.settledHoldInvoice, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + }, + Role.admin: {}, }, - - // ───────────────────────── AFTER HOLD INVOICE SETTLED ────────────── + // ───────────────────────── SETTLED HOLD INVOICE ──────────────────── Status.settledHoldInvoice: { - Action.purchaseCompleted: Status.success, - Action.disputeInitiatedByYou: Status.dispute, - Action.disputeInitiatedByPeer: Status.dispute, + Role.buyer: { + // Both parties wait for completion or admin intervention + }, + Role.seller: { + // Both parties wait for completion or admin intervention + }, + Role.admin: { + // Admin can intervene if needed + }, }, - - // ───────────────────────── DISPUTE BRANCH ────────────────────────── + // ───────────────────────── SUCCESS ──────────────────────────────── + Status.success: { + Role.buyer: { + Action.rate: Status.success, + }, + Role.seller: { + Action.rate: Status.success, + }, + Role.admin: { + // Admin can rate or intervene if protocol allows + }, + }, + // ───────────────────────── DISPUTE ──────────────────────────────── Status.dispute: { - Action.adminSettle: Status.settledByAdmin, - Action.adminSettled: Status.settledByAdmin, - Action.adminCancel: Status.canceledByAdmin, - Action.adminCanceled: Status.canceledByAdmin, + Role.buyer: { + // Wait for admin to resolve + }, + Role.seller: { + // Wait for admin to resolve + }, + Role.admin: { + Action.adminSettle: Status.settledByAdmin, + Action.adminSettled: Status.settledByAdmin, + Action.adminCancel: Status.canceledByAdmin, + Action.adminCanceled: Status.canceledByAdmin, + }, + }, + // ───────────────────────── CANCELED ──────────────────────────────── + Status.canceled: { + Role.buyer: {}, + Role.seller: {}, + Role.admin: {}, }, }; - /// Returns the next `Status` after applying [action] to [current]. - /// If the action does **not** cause a state change, the same status - /// is returned. This makes it easier to call without additional - /// null-checking. - static Status nextStatus(Status? current, Action action) { - // Note: Initial state handled externally — we start from `pending` when the - // very first `newOrder` message arrives, so there is no `Status.start` - // entry here. - // If current is null (unknown), treat as pending so that first transition - // works for historical messages. - final safeCurrent = current ?? Status.pending; - return _transitions[safeCurrent]?[action] ?? safeCurrent; + /// Returns the next status for a given current status, role, and action. + static Status? nextStatus(Status current, Role role, Action action) { + return _transitions[current]?[role]?[action]; } - /// Returns a list of all actions that can be applied to [status]. - /// This is used to determine which buttons to show in the UI and - /// to validate user input. - static List actionsForStatus(Status status) { - return _transitions[status]!.keys.toList(); + /// Get all possible actions for a status and role + static List possibleActions(Status current, Role role) { + return _transitions[current]?[role]?.keys.toList() ?? []; } } diff --git a/lib/features/key_manager/key_manager_provider.dart b/lib/features/key_manager/key_manager_provider.dart index e3e665f5..cce501be 100644 --- a/lib/features/key_manager/key_manager_provider.dart +++ b/lib/features/key_manager/key_manager_provider.dart @@ -1,10 +1,7 @@ -import 'package:dart_nostr/nostr/core/key_pairs.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; import 'package:mostro_mobile/features/key_manager/key_manager.dart'; -import 'package:mostro_mobile/features/key_manager/key_notifier.dart'; import 'package:mostro_mobile/features/key_manager/key_storage.dart'; -import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/shared/providers/storage_providers.dart'; final keyManagerProvider = Provider((ref) { @@ -17,18 +14,3 @@ final keyManagerProvider = Provider((ref) { return KeyManager(keyStorage, keyDerivator); }); -// Provide the KeyNotifier + current master key -final masterKeyNotifierProvider = - StateNotifierProvider((ref) { - final manager = ref.watch(keyManagerProvider); - final settings = ref.watch(settingsProvider); - return KeyNotifier(manager, settings.copyWith()); -}); - -final tradeKeyNotifierProvider = - StateNotifierProvider.family((ref, userSessionId) { - final manager = ref.watch(keyManagerProvider); - - final settings = ref.watch(settingsProvider); - return KeyNotifier(manager, settings.copyWith()); -}); diff --git a/lib/features/key_manager/key_notifier.dart b/lib/features/key_manager/key_notifier.dart deleted file mode 100644 index ef4294ed..00000000 --- a/lib/features/key_manager/key_notifier.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:dart_nostr/nostr/core/key_pairs.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/features/key_manager/key_manager.dart'; -import 'package:mostro_mobile/features/settings/settings.dart'; - -class KeyNotifier extends StateNotifier { - final KeyManager _keyManager; - final Settings _settings; - - KeyNotifier(this._keyManager, this._settings) - : super(_keyManager.masterKeyPair); -} diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart new file mode 100644 index 00000000..ffe7e0d9 --- /dev/null +++ b/lib/features/order/models/order_state.dart @@ -0,0 +1,85 @@ +import 'package:mostro_mobile/data/models.dart'; +import 'package:mostro_mobile/data/enums.dart'; + +class OrderState { + final Status status; + final Action action; + final Order? order; + final PaymentRequest? paymentRequest; + final CantDo? cantDo; + final Dispute? dispute; + + OrderState({ + required this.status, + required this.action, + required this.order, + this.paymentRequest, + this.cantDo, + this.dispute, + }); + +factory OrderState.fromMostroMessage(MostroMessage message) { + return OrderState( + status: message.getPayload()?.status ?? Status.pending, + action: message.action, + order: message.getPayload(), + paymentRequest: message.getPayload(), + cantDo: message.getPayload(), + dispute: message.getPayload(), + ); +} + + @override + String toString() => + 'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OrderState && + other.status == status && + other.action == action && + other.order == order && + other.paymentRequest == paymentRequest && + other.cantDo == cantDo && + other.dispute == dispute; + + @override + int get hashCode => Object.hash( + status, + action, + order, + paymentRequest, + cantDo, + dispute, + ); + + OrderState copyWith({ + Status? status, + Action? action, + Order? order, + PaymentRequest? paymentRequest, + CantDo? cantDo, + Dispute? dispute, + }) { + return OrderState( + status: status ?? this.status, + action: action ?? this.action, + order: order ?? this.order, + paymentRequest: paymentRequest ?? this.paymentRequest, + cantDo: cantDo ?? this.cantDo, + dispute: dispute ?? this.dispute, + ); + } + + OrderState updateFromMostroMessage(MostroMessage message) { + return copyWith( + status: message.getPayload()?.status ?? status, + action: message.action, + order: message.getPayload() ?? order, + paymentRequest: message.getPayload() ?? paymentRequest, + cantDo: message.getPayload() ?? cantDo, + dispute: message.getPayload() ?? dispute, + ); + } +} diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index d94f69e5..e600c163 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -3,18 +3,16 @@ 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'; +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'; -class AbstractMostroNotifier extends StateNotifier { +class AbstractMostroNotifier extends StateNotifier { final String orderId; final Ref ref; final logger = Logger(); - Status status = Status.pending; - Action action = Action.newOrder; - late Session session; ProviderSubscription>? subscription; @@ -22,7 +20,8 @@ class AbstractMostroNotifier extends StateNotifier { AbstractMostroNotifier( this.orderId, this.ref, - ) : super(MostroMessage(action: Action.newOrder, id: orderId)); + ) : super(OrderState( + action: Action.newOrder, status: Status.pending, order: null)); void subscribe() { subscription = ref.listen( @@ -52,33 +51,47 @@ class AbstractMostroNotifier extends StateNotifier { switch (event.action) { case Action.newOrder: - action = event.action; - state = event; - navProvider.go('/order_confirmed/${event.id!}'); + state = OrderState( + action: event.action, + status: event.getPayload()!.status, + order: event.getPayload()!, + ); break; case Action.payInvoice: - action = event.action; - state = event; + state = OrderState( + action: event.action, + status: event.getPayload()!.status, + order: event.getPayload()!, + ); navProvider.go('/pay_invoice/${event.id!}'); break; case Action.fiatSentOk: - action = event.action; - state = event; + state = OrderState( + action: event.action, + status: event.getPayload()!.status, + order: event.getPayload()!, + ); final peer = event.getPayload(); notifProvider.showInformation(event.action, values: { 'buyer_npub': peer?.publicKey ?? '{buyer_npub}', }); break; case Action.released: - action = event.action; - state = event; + state = OrderState( + action: event.action, + status: event.getPayload()!.status, + order: event.getPayload()!, + ); notifProvider.showInformation(event.action, values: { 'seller_npub': '', }); break; case Action.canceled: - action = event.action; - state = event; + state = OrderState( + action: event.action, + status: event.getPayload()!.status, + order: event.getPayload()!, + ); ref .read(mostroStorageProvider) .deleteAllMessagesByOrderId(session.orderId!); diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 89a61a77..aaa978b8 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -2,6 +2,7 @@ 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'; @@ -13,11 +14,15 @@ class AddOrderNotifier extends AbstractMostroNotifier { AddOrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); + requestId = _requestIdFromOrderId(orderId); + subscribe(); + } + + int _requestIdFromOrderId(String orderId) { final uuid = orderId.replaceAll('-', ''); final timestamp = DateTime.now().microsecondsSinceEpoch; - requestId = - (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & 0x7FFFFFFF; - subscribe(); + return (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & + 0x7FFFFFFF; } @override @@ -35,7 +40,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { logger.i('AddOrderNotifier: received ${msg.action}'); } } else if (msg.payload is CantDo) { - _handleCantDo(msg); + handleEvent(msg); } } }, @@ -46,18 +51,10 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); } - void _handleCantDo(MostroMessage message) { - final cantDo = message.getPayload(); - ref.read(notificationProvider.notifier).showInformation( - message.action, - values: { - 'action': cantDo?.cantDoReason.toString(), - }, - ); - } - Future _confirmOrder(MostroMessage message) async { - state = message; + final order = message.getPayload(); + + state = OrderState(status: order!.status, action: message.action, order: order); session.orderId = message.id; ref.read(sessionNotifierProvider.notifier).saveSession(session); ref.read(orderNotifierProvider(message.id!).notifier).subscribe(); @@ -68,7 +65,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { } Future submitOrder(Order order) async { - state = MostroMessage( + final message = MostroMessage( action: Action.newOrder, id: null, requestId: requestId, @@ -80,6 +77,11 @@ class AddOrderNotifier extends AbstractMostroNotifier { role: order.kind == OrderType.buy ? Role.buyer : Role.seller, ); mostroService.subscribe(session); - await mostroService.submitOrder(state); + await mostroService.submitOrder(message); + state = OrderState( + action: message.action, + status: Status.pending, + order: order, + ); } } diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 2880adaf..56a18298 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; @@ -8,8 +10,6 @@ import 'package:mostro_mobile/services/mostro_service.dart'; class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; - late Order order; - OrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); sync(); @@ -18,15 +18,18 @@ class OrderNotifier extends AbstractMostroNotifier { Future sync() async { final storage = ref.read(mostroStorageProvider); - final latestOrder = - await storage.getLatestMessageOfTypeById(orderId); - if (latestOrder != null) { - order = latestOrder.getPayload()!; - status = order.status; + final messages = await storage.getAllMessagesForOrderId(orderId); + if (messages.isEmpty) { + return; } - final newState = await storage.getLatestMessageById(orderId); - if (newState != null) { - state = newState; + final msg = messages.firstWhereOrNull((m) => m.action != Action.cantDo); + if (msg?.payload is Order) { + state = OrderState(status: msg!.getPayload()!.status, action: msg.action, order: msg.getPayload()!); + } else { + final orderMsg = await storage.getLatestMessageOfTypeById(orderId); + if (orderMsg != null) { + state = OrderState(status: orderMsg.getPayload()!.status, action: orderMsg.action, order: orderMsg.getPayload()!); + } } } @@ -35,7 +38,7 @@ class OrderNotifier extends AbstractMostroNotifier { final sessionNotifier = ref.read(sessionNotifierProvider.notifier); session = await sessionNotifier.newSession( orderId: orderId, - role: order.kind == OrderType.buy ? Role.buyer : Role.seller, + role: Role.buyer, ); mostroService.subscribe(session); await mostroService.takeSellOrder( @@ -49,7 +52,7 @@ class OrderNotifier extends AbstractMostroNotifier { final sessionNotifier = ref.read(sessionNotifierProvider.notifier); session = await sessionNotifier.newSession( orderId: orderId, - role: order.kind == OrderType.buy ? Role.buyer : Role.seller, + role: Role.seller, ); mostroService.subscribe(session); await mostroService.takeBuyOrder( @@ -58,7 +61,11 @@ class OrderNotifier extends AbstractMostroNotifier { ); } - Future sendInvoice(String orderId, String invoice, int? amount) async { + Future sendInvoice( + String orderId, + String invoice, + int? amount, + ) async { await mostroService.sendInvoice( orderId, invoice, @@ -88,5 +95,4 @@ class OrderNotifier extends AbstractMostroNotifier { rating, ); } - } diff --git a/lib/features/order/providers/order_notifier_provider.dart b/lib/features/order/providers/order_notifier_provider.dart index 8ef57ff8..f12cbd35 100644 --- a/lib/features/order/providers/order_notifier_provider.dart +++ b/lib/features/order/providers/order_notifier_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/notfiers/add_order_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; @@ -8,7 +9,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'order_notifier_provider.g.dart'; final orderNotifierProvider = - StateNotifierProvider.family( + StateNotifierProvider.family( (ref, orderId) { return OrderNotifier( orderId, @@ -18,7 +19,7 @@ final orderNotifierProvider = ); final addOrderNotifierProvider = - StateNotifierProvider.family( + StateNotifierProvider.family( (ref, orderId) { return AddOrderNotifier( orderId, diff --git a/lib/features/order/providers/order_status_provider.dart b/lib/features/order/providers/order_status_provider.dart deleted file mode 100644 index 33cab9aa..00000000 --- a/lib/features/order/providers/order_status_provider.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/core/mostro_fsm.dart'; -import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; - -/// Exposes a live [Status] stream for a given order based on the full history -/// of stored `MostroMessage`s. Any new message automatically recomputes the -/// status using the canonical [MostroFSM]. -final orderStatusProvider = StreamProvider.family((ref, orderId) { - final storage = ref.watch(mostroStorageProvider); - - Status computeStatus(Iterable messages) { - var status = Status.pending; // default starting point - for (final m in messages) { - status = MostroFSM.nextStatus(status, m.action); - } - return status; - } - - return storage - .watchAllMessages(orderId) // emits list whenever new message saved - .map((messages) => computeStatus(messages)) - .distinct(); -}); diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index 734b9bbd..1746123d 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/payment_request.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; @@ -23,7 +22,7 @@ class _PayLightningInvoiceScreenState @override Widget build(BuildContext context) { final order = ref.read(orderNotifierProvider(widget.orderId)); - final lnInvoice = order.getPayload()?.lnInvoice ?? ''; + final lnInvoice = order.paymentRequest?.lnInvoice ?? ''; final orderNotifier = ref.read(orderNotifierProvider(widget.orderId).notifier); diff --git a/lib/features/order/screens/payment_confirmation_screen.dart b/lib/features/order/screens/payment_confirmation_screen.dart index 1ae9caa3..266efe32 100644 --- a/lib/features/order/screens/payment_confirmation_screen.dart +++ b/lib/features/order/screens/payment_confirmation_screen.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/cant_do.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as action; @@ -37,7 +36,7 @@ class PaymentConfirmationScreen extends ConsumerWidget { ); } - Widget _buildBody(BuildContext context, WidgetRef ref, MostroMessage state) { + Widget _buildBody(BuildContext context, WidgetRef ref, OrderState state) { switch (state.action) { case action.Action.purchaseCompleted: final satoshis = 0; @@ -105,10 +104,10 @@ class PaymentConfirmationScreen extends ConsumerWidget { ); case action.Action.cantDo: - final error = state.getPayload()?.cantDoReason; + final error = state.cantDo; return Center( child: Text( - 'Error: $error', + 'Error: ${error?.cantDoReason}', style: const TextStyle(color: AppTheme.cream1), ), ); diff --git a/lib/features/trades/models/trade_state.dart b/lib/features/trades/models/trade_state.dart index 67e28a7e..5d7aa5b6 100644 --- a/lib/features/trades/models/trade_state.dart +++ b/lib/features/trades/models/trade_state.dart @@ -1,10 +1,10 @@ import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart' as actions; +import 'package:mostro_mobile/data/models/enums/action.dart'; class TradeState { final Status status; - final actions.Action? action; + final Action? action; final Order? order; TradeState({ @@ -30,7 +30,7 @@ class TradeState { TradeState copyWith({ Status? status, - actions.Action? action, + Action? action, Order? order, }) { return TradeState( diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index f353af15..22354dc3 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -211,8 +211,8 @@ class TradeDetailScreen extends ConsumerWidget { BuildContext context, WidgetRef ref, TradeState tradeState) { final session = ref.watch(sessionProvider(orderId)); final userRole = session?.role; - - final userActions = MostroFSM.actionsForStatus(tradeState.status); + + final userActions = MostroFSM.possibleActions(tradeState.status, userRole!); if (userActions.isEmpty) return []; final widgets = []; diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 5163f4f4..57d096d0 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -1,16 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/dispute.dart'; -import 'package:mostro_mobile/data/models/enums/role.dart'; +import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/generated/l10n.dart'; -import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; -import 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; class MostroMessageDetail extends ConsumerWidget { final String orderId; @@ -18,7 +15,7 @@ class MostroMessageDetail extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final orderState = ref.watch(orderNotifierProvider(orderId).notifier); + final orderState = ref.watch(orderNotifierProvider(orderId)); final actionText = _getActionText( context, @@ -55,7 +52,7 @@ class MostroMessageDetail extends ConsumerWidget { BuildContext context, WidgetRef ref, ) { - final tradeState = ref.watch(orderNotifierProvider(orderId).notifier); + final tradeState = ref.watch(orderNotifierProvider(orderId)); final action = tradeState.action; final orderPayload = tradeState.order; switch (action) { @@ -65,7 +62,7 @@ class MostroMessageDetail extends ConsumerWidget { '24'; return S.of(context)!.newOrder(int.tryParse(expHrs) ?? 24); case actions.Action.canceled: - return S.of(context)!.canceled(orderPayload.id ?? ''); + return S.of(context)!.canceled(orderPayload?.id ?? ''); case actions.Action.payInvoice: final expSecs = ref .read(orderRepositoryProvider) @@ -73,10 +70,10 @@ class MostroMessageDetail extends ConsumerWidget { ?.expirationSeconds ?? 900; return S.of(context)!.payInvoice( - orderPayload.amount.toString(), + orderPayload?.amount.toString() ?? '', '${expSecs ~/ 60} minutes', - orderPayload.fiatAmount.toString(), - orderPayload.fiatCode, + orderPayload?.fiatAmount.toString() ?? '', + orderPayload?.fiatCode ?? '', ); case actions.Action.addInvoice: final expSecs = ref @@ -85,9 +82,9 @@ class MostroMessageDetail extends ConsumerWidget { ?.expirationSeconds ?? 900; return S.of(context)!.addInvoice( - orderPayload.amount.toString(), - orderPayload.fiatCode, - orderPayload.fiatAmount.toString(), + orderPayload?.amount.toString() ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.fiatAmount.toString() ?? '', expSecs, ); case actions.Action.waitingSellerToPay: @@ -98,7 +95,7 @@ class MostroMessageDetail extends ConsumerWidget { 900; return S .of(context)! - .waitingSellerToPay(orderPayload.id ?? '', expSecs); + .waitingSellerToPay(orderPayload?.id ?? '', expSecs); case actions.Action.waitingBuyerInvoice: final expSecs = ref .read(orderRepositoryProvider) @@ -109,23 +106,23 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.buyerInvoiceAccepted: return S.of(context)!.buyerInvoiceAccepted; case actions.Action.holdInvoicePaymentAccepted: - final session = ref.watch(sessionProvider(orderPayload.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); return S.of(context)!.holdInvoicePaymentAccepted( - orderPayload.fiatAmount.toString(), - orderPayload.fiatCode, - orderPayload.paymentMethod, + orderPayload?.fiatAmount.toString() ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.paymentMethod ?? '', session?.peer?.publicKey ?? '', ); case actions.Action.buyerTookOrder: - final session = ref.watch(sessionProvider(orderPayload.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); return S.of(context)!.buyerTookOrder( session?.peer?.publicKey ?? '', - orderPayload.fiatCode, + orderPayload!.fiatCode, orderPayload.fiatAmount.toString(), orderPayload.paymentMethod, ); case actions.Action.fiatSentOk: - final session = ref.watch(sessionProvider(orderPayload.id ?? '')); + final session = ref.watch(sessionProvider(orderPayload!.id ?? '')); return session!.role == Role.buyer ? S.of(context)!.fiatSentOkBuyer(session.peer!.publicKey) : S.of(context)!.fiatSentOkSeller(session.peer!.publicKey); @@ -140,27 +137,27 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.rateReceived: return S.of(context)!.rateReceived; case actions.Action.cooperativeCancelInitiatedByYou: - return S.of(context)!.cooperativeCancelInitiatedByYou(orderPayload.id ?? ''); + return S + .of(context)! + .cooperativeCancelInitiatedByYou(orderPayload!.id ?? ''); case actions.Action.cooperativeCancelInitiatedByPeer: - return S.of(context)!.cooperativeCancelInitiatedByPeer(orderPayload.id ?? ''); + return S + .of(context)! + .cooperativeCancelInitiatedByPeer(orderPayload!.id ?? ''); case actions.Action.cooperativeCancelAccepted: - return S.of(context)!.cooperativeCancelAccepted(orderPayload.id ?? ''); + return S.of(context)!.cooperativeCancelAccepted(orderPayload!.id ?? ''); case actions.Action.disputeInitiatedByYou: - final payload = ref - .read(orderNotifierProvider(orderId)) - .getPayload(); - return S.of(context)!.disputeInitiatedByYou(orderPayload.id!, payload!.disputeId); + final payload = ref.read(orderNotifierProvider(orderId)).dispute; + return S.of(context)!.disputeInitiatedByYou(orderPayload!.id!, payload!.disputeId); case actions.Action.disputeInitiatedByPeer: - final payload = ref - .read(orderNotifierProvider(orderId)) - .getPayload(); - return S.of(context)!.disputeInitiatedByPeer(orderPayload.id!, payload!.disputeId); + final payload = ref.read(orderNotifierProvider(orderId)).dispute; + return S.of(context)!.disputeInitiatedByPeer(orderPayload!.id!, payload!.disputeId); case actions.Action.adminTookDispute: return S.of(context)!.adminTookDisputeUsers('{admin token}'); case actions.Action.adminCanceled: - return S.of(context)!.adminCanceledUsers(orderPayload.id ?? ''); + return S.of(context)!.adminCanceledUsers(orderPayload!.id ?? ''); case actions.Action.adminSettled: - return S.of(context)!.adminSettledUsers(orderPayload.id ?? ''); + return S.of(context)!.adminSettledUsers(orderPayload!.id ?? ''); case actions.Action.paymentFailed: return S.of(context)!.paymentFailed('{attempts}', '{retries}'); case actions.Action.invoiceUpdated: @@ -174,21 +171,17 @@ class MostroMessageDetail extends ConsumerWidget { } } - String _getCantDoMessage( - BuildContext context, WidgetRef ref) { -final tradeState = ref.watch(orderNotifierProvider(orderId).notifier); - final orderPayload = tradeState.order; - final status = tradeState.status; - final cantDoReason = ref - .read(orderNotifierProvider(orderId)) - .getPayload(); - switch (cantDoReason) { + String _getCantDoMessage(BuildContext context, WidgetRef ref) { + final tradeState = ref.watch(orderNotifierProvider(orderId)); + final cantDo = tradeState.cantDo; + if (cantDo == null) { + return ''; + } + switch (cantDo.cantDoReason) { case CantDoReason.invalidSignature: return S.of(context)!.invalidSignature; case CantDoReason.notAllowedByStatus: - return S - .of(context)! - .notAllowedByStatus(orderPayload.id ?? '', status); + return S.of(context)!.notAllowedByStatus(tradeState.order!.id!, tradeState.status); case CantDoReason.outOfRangeFiatAmount: return S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); case CantDoReason.outOfRangeSatsAmount: @@ -206,7 +199,7 @@ final tradeState = ref.watch(orderNotifierProvider(orderId).notifier); case CantDoReason.pendingOrderExists: return S.of(context)!.pendingOrderExists; default: - return '${status.toString()} - ${tradeState.action}'; + return '${tradeState.status.toString()} - ${tradeState.action}'; } } } diff --git a/lib/shared/widgets/mostro_reactive_button.dart b/lib/shared/widgets/mostro_reactive_button.dart index 5dd7a76d..00cdb309 100644 --- a/lib/shared/widgets/mostro_reactive_button.dart +++ b/lib/shared/widgets/mostro_reactive_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; 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'; @@ -71,22 +72,31 @@ class _MostroReactiveButtonState extends ConsumerState { @override Widget build(BuildContext context) { - ref.listen>(mostroMessageStreamProvider(widget.orderId), - (prev, next) { - next.whenData((msg) { - if (msg == null || msg.action == _lastSeenAction) return; - _lastSeenAction = msg.action; - if (!_loading) return; - if (msg.action == actions.Action.cantDo || - msg.action == widget.action) { - setState(() { - _loading = false; - _showSuccess = - widget.showSuccessIndicator && msg.action == widget.action; - }); - } - }); - }); + final orderState = ref.watch(orderNotifierProvider(widget.orderId)); + if (orderState.action != widget.action) { + return const SizedBox.shrink(); + } + + ref.listen( + mostroMessageStreamProvider(widget.orderId), + (_, next) { + next.whenData( + (msg) { + if (msg == null || msg.action == _lastSeenAction) return; + _lastSeenAction = msg.action; + if (!_loading) return; + if (msg.action == actions.Action.cantDo || + msg.action == widget.action) { + setState(() { + _loading = false; + _showSuccess = + widget.showSuccessIndicator && msg.action == widget.action; + }); + } + }, + ); + }, + ); Widget childWidget; if (_loading) { From 664be5232a6469c965f31ac88a266f8021ac300f Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 27 May 2025 17:53:40 -0700 Subject: [PATCH 06/26] refactor: update background service notification handling and improve order state management --- lib/background/background.dart | 4 ---- lib/features/order/notfiers/abstract_mostro_notifier.dart | 6 ++++-- lib/features/trades/screens/trade_detail_screen.dart | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/background/background.dart b/lib/background/background.dart index cd86c25f..5960d524 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -16,10 +16,6 @@ Future serviceMain(ServiceInstance service) async { // If on Android, set up a permanent notification so the OS won't kill it. if (service is AndroidServiceInstance) { service.setAsForegroundService(); - service.setForegroundNotificationInfo( - title: "Mostro P2P", - content: "Connected to Mostro service", - ); } final Map> activeSubscriptions = {}; diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index e600c163..94cf5513 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -58,10 +58,12 @@ class AbstractMostroNotifier extends StateNotifier { ); break; case Action.payInvoice: + final pr = event.getPayload(); + final order = pr?.order; state = OrderState( action: event.action, - status: event.getPayload()!.status, - order: event.getPayload()!, + status: order?.status ?? state.status, + order: order, ); navProvider.go('/pay_invoice/${event.id!}'); break; diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index b6d737af..d161f859 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -132,7 +132,7 @@ class TradeDetailScreen extends ConsumerWidget { ), const SizedBox(height: 16), Text( - 'Payment methods: $methodText', + 'Payment methods: $method', style: textTheme.bodyLarge, ), ], From 46dc3051946a5efab013ca6454f1e1479d4e3de6 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 28 May 2025 12:23:45 -0700 Subject: [PATCH 07/26] refactor: streamline OrderState updates and improve session handling in notifiers --- lib/features/order/models/order_state.dart | 30 +++++++++-------- .../notfiers/abstract_mostro_notifier.dart | 32 +++---------------- .../order/notfiers/order_notifier.dart | 12 +++++-- lib/shared/notifiers/session_notifier.dart | 1 - 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index ffe7e0d9..58724a38 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -18,16 +18,16 @@ class OrderState { this.dispute, }); -factory OrderState.fromMostroMessage(MostroMessage message) { - return OrderState( - status: message.getPayload()?.status ?? Status.pending, - action: message.action, - order: message.getPayload(), - paymentRequest: message.getPayload(), - cantDo: message.getPayload(), - dispute: message.getPayload(), - ); -} + factory OrderState.fromMostroMessage(MostroMessage message) { + return OrderState( + status: message.getPayload()?.status ?? Status.pending, + action: message.action, + order: message.getPayload(), + paymentRequest: message.getPayload(), + cantDo: message.getPayload(), + dispute: message.getPayload(), + ); + } @override String toString() => @@ -72,11 +72,15 @@ factory OrderState.fromMostroMessage(MostroMessage message) { ); } - OrderState updateFromMostroMessage(MostroMessage message) { + OrderState updateWith(MostroMessage message) { return copyWith( status: message.getPayload()?.status ?? status, - action: message.action, - order: message.getPayload() ?? order, + action: message.action != Action.cantDo ? message.action : action, + order: message.payload is Order + ? message.getPayload() + : message.payload is PaymentRequest + ? message.getPayload()!.order + : order, paymentRequest: message.getPayload() ?? paymentRequest, cantDo: message.getPayload() ?? cantDo, dispute: message.getPayload() ?? dispute, diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 94cf5513..4a6b185d 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -49,51 +49,26 @@ class AbstractMostroNotifier extends StateNotifier { final notifProvider = ref.read(notificationProvider.notifier); final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; + state = state.updateWith(event); + switch (event.action) { case Action.newOrder: - state = OrderState( - action: event.action, - status: event.getPayload()!.status, - order: event.getPayload()!, - ); break; case Action.payInvoice: - final pr = event.getPayload(); - final order = pr?.order; - state = OrderState( - action: event.action, - status: order?.status ?? state.status, - order: order, - ); navProvider.go('/pay_invoice/${event.id!}'); break; case Action.fiatSentOk: - state = OrderState( - action: event.action, - status: event.getPayload()!.status, - order: event.getPayload()!, - ); final peer = event.getPayload(); notifProvider.showInformation(event.action, values: { 'buyer_npub': peer?.publicKey ?? '{buyer_npub}', }); break; case Action.released: - state = OrderState( - action: event.action, - status: event.getPayload()!.status, - order: event.getPayload()!, - ); notifProvider.showInformation(event.action, values: { 'seller_npub': '', }); break; case Action.canceled: - state = OrderState( - action: event.action, - status: event.getPayload()!.status, - order: event.getPayload()!, - ); ref .read(mostroStorageProvider) .deleteAllMessagesByOrderId(session.orderId!); @@ -173,6 +148,9 @@ class AbstractMostroNotifier extends StateNotifier { }); break; case Action.addInvoice: + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); + sessionNotifier.saveSession(session); + navProvider.go('/add_invoice/$orderId'); break; case Action.buyerTookOrder: diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 56a18298..b6f8806d 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -24,11 +24,19 @@ class OrderNotifier extends AbstractMostroNotifier { } final msg = messages.firstWhereOrNull((m) => m.action != Action.cantDo); if (msg?.payload is Order) { - state = OrderState(status: msg!.getPayload()!.status, action: msg.action, order: msg.getPayload()!); + state = OrderState( + status: msg!.getPayload()!.status, + action: msg.action, + order: msg.getPayload()!, + ); } else { final orderMsg = await storage.getLatestMessageOfTypeById(orderId); if (orderMsg != null) { - state = OrderState(status: orderMsg.getPayload()!.status, action: orderMsg.action, order: orderMsg.getPayload()!); + state = OrderState( + status: orderMsg.getPayload()!.status, + action: orderMsg.action, + order: orderMsg.getPayload()!, + ); } } } diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index 2f227e99..cef40247 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -61,7 +61,6 @@ final Map _requestIdToSession = {}; if (orderId != null) { _sessions[orderId] = session; - await _storage.putSession(session); state = sessions; } else if (requestId != null) { _requestIdToSession[requestId] = session; From fa214e38dbda1aca1f354d9d3f960573516b8635 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 28 May 2025 15:25:58 -0700 Subject: [PATCH 08/26] refactor: adjust TradeDetailScreen to use OrderState --- lib/features/trades/screens/trade_detail_screen.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index d161f859..20f939b4 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -9,6 +9,7 @@ import 'package:mostro_mobile/core/mostro_fsm.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; import 'package:mostro_mobile/features/trades/models/trade_state.dart'; @@ -27,7 +28,7 @@ class TradeDetailScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tradeState = ref.watch(tradeStateProvider(orderId)); + final tradeState = ref.watch(orderNotifierProvider(orderId)); // If message is null or doesn't have an Order payload, show loading final orderPayload = tradeState.order; if (orderPayload == null) { @@ -79,7 +80,7 @@ class TradeDetailScreen extends ConsumerWidget { } /// Builds a card showing the user is "selling/buying X sats for Y fiat" etc. - Widget _buildSellerAmount(WidgetRef ref, TradeState tradeState) { + Widget _buildSellerAmount(WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); final selling = session!.role == Role.seller ? 'selling' : 'buying'; @@ -208,7 +209,7 @@ class TradeDetailScreen extends ConsumerWidget { /// Additional checks use `message.action` to refine which button to show. /// Following the Mostro protocol state machine for order flow. List _buildActionButtons( - BuildContext context, WidgetRef ref, TradeState tradeState) { + BuildContext context, WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); final userRole = session?.role; From 23b90aacf3f8f8d06c1833d190a097b8990d247f Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 29 May 2025 21:14:33 -0700 Subject: [PATCH 09/26] fix: update trade detail actions and navigation flow for better UX --- lib/core/mostro_fsm.dart | 71 +++++++++++++++++-- .../order/screens/take_order_screen.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 14 ++-- .../widgets/mostro_reactive_button.dart | 6 +- 4 files changed, 77 insertions(+), 16 deletions(-) diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart index 5fd60a61..2e9d2633 100644 --- a/lib/core/mostro_fsm.dart +++ b/lib/core/mostro_fsm.dart @@ -7,6 +7,61 @@ import 'package:mostro_mobile/data/models/enums/status.dart'; /// All auxiliary / neutral notifications intentionally map to /// the **same** state so that `nextStatus` always returns a non-null value. class MostroFSM { + /// Private constructor to prevent instantiation + MostroFSM._(); + + static final buyer = { + Status.pending: { + Action.takeSell: Status.waitingBuyerInvoice, + // A seller has taken the order + Action.waitingSellerToPay: Status.waitingPayment, + Action.cancel: Status.canceled, + }, + Status.waitingBuyerInvoice: { + Action.addInvoice: Status.waitingPayment, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + Status.waitingPayment: { + Action.waitingSellerToPay: Status.waitingPayment, + Action.holdInvoicePaymentAccepted: Status.active, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + Status.active: { + Action.holdInvoicePaymentAccepted: Status.active, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + Status.fiatSent: {} + }; + + static final seller = { + Status.pending: { + Action.takeBuy: Status.waitingPayment, + Action.cancel: Status.canceled, + }, + Status.waitingPayment: { + Action.payInvoice: Status.waitingBuyerInvoice, + Action.cancel: Status.canceled, + }, + Status.waitingBuyerInvoice: { + Action.waitingBuyerInvoice: Status.waitingPayment, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + Status.active: { + Action.buyerTookOrder: Status.active, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + Status.fiatSent: { + Action.release: Status.settledHoldInvoice, + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + }, + }; + /// Nested map: *currentStatus → { role → { action → nextStatus } }*. static final Map>> _transitions = { // ───────────────────────── MATCHING / TAKING ──────────────────────── @@ -14,10 +69,12 @@ class MostroFSM { Role.buyer: { Action.takeSell: Status.waitingBuyerInvoice, Action.cancel: Status.canceled, + Action.dispute: Status.dispute, }, Role.seller: { Action.takeBuy: Status.waitingPayment, Action.cancel: Status.canceled, + Action.dispute: Status.dispute, }, Role.admin: {}, }, @@ -27,7 +84,9 @@ class MostroFSM { Action.addInvoice: Status.waitingPayment, Action.cancel: Status.canceled, }, - Role.seller: {}, + Role.seller: { + Action.cancel: Status.canceled, + }, Role.admin: {}, }, // ───────────────────────── HOLD INVOICE PAYMENT ──────────────────── @@ -36,19 +95,23 @@ class MostroFSM { Action.payInvoice: Status.active, Action.cancel: Status.canceled, }, - Role.buyer: {}, + Role.buyer: { + Action.cancel: Status.canceled, + }, Role.admin: {}, }, // ───────────────────────── ACTIVE ──────────────────────────── Status.active: { Role.buyer: { + Action.holdInvoicePaymentAccepted: Status.active, Action.fiatSent: Status.fiatSent, Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, + Action.dispute: Status.dispute, }, Role.seller: { Action.cancel: Status.canceled, - Action.disputeInitiatedByYou: Status.dispute, + Action.dispute: Status.dispute, + Action.buyerTookOrder: Status.active, }, Role.admin: {}, }, diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index b167035a..8f4dfd0d 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -160,7 +160,7 @@ class TakeOrderScreen extends ConsumerWidget { children: [ OutlinedButton( onPressed: () { - context.go('/'); + context.pop(); }, style: AppTheme.theme.outlinedButtonTheme.style, child: const Text('CLOSE'), diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 20f939b4..5ae39d6c 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -12,8 +12,6 @@ import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; -import 'package:mostro_mobile/features/trades/models/trade_state.dart'; -import 'package:mostro_mobile/features/trades/providers/trade_state_provider.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; @@ -222,7 +220,6 @@ class TradeDetailScreen extends ConsumerWidget { // FSM-driven action mapping: ensure all actions are handled switch (action) { case actions.Action.cancel: - case actions.Action.canceled: widgets.add(_buildNostrButton( 'CANCEL', action: action, @@ -252,10 +249,7 @@ class TradeDetailScreen extends ConsumerWidget { } break; case actions.Action.fiatSent: - case actions.Action.fiatSentOk: - if (userRole == Role.buyer && - tradeState.action != actions.Action.fiatSentOk && - tradeState.action != actions.Action.fiatSent) { + if (userRole == Role.buyer) { widgets.add(_buildNostrButton( 'FIAT SENT', action: actions.Action.fiatSentOk, @@ -345,6 +339,9 @@ class TradeDetailScreen extends ConsumerWidget { .releaseOrder(), // This usually triggers completion )); break; + case actions.Action.buyerTookOrder: + widgets.add(_buildContactButton(context)); + break; case actions.Action.rate: case actions.Action.rateUser: case actions.Action.rateReceived: @@ -356,12 +353,13 @@ class TradeDetailScreen extends ConsumerWidget { )); break; case actions.Action.holdInvoicePaymentAccepted: + widgets.add(_buildContactButton(context)); + break; case actions.Action.holdInvoicePaymentSettled: case actions.Action.holdInvoicePaymentCanceled: // These are system actions, not user actions, so no button needed break; case actions.Action.buyerInvoiceAccepted: - case actions.Action.buyerTookOrder: case actions.Action.waitingSellerToPay: case actions.Action.waitingBuyerInvoice: case actions.Action.adminCancel: diff --git a/lib/shared/widgets/mostro_reactive_button.dart b/lib/shared/widgets/mostro_reactive_button.dart index 00cdb309..51e59798 100644 --- a/lib/shared/widgets/mostro_reactive_button.dart +++ b/lib/shared/widgets/mostro_reactive_button.dart @@ -73,9 +73,9 @@ class _MostroReactiveButtonState extends ConsumerState { @override Widget build(BuildContext context) { final orderState = ref.watch(orderNotifierProvider(widget.orderId)); - if (orderState.action != widget.action) { - return const SizedBox.shrink(); - } + //if (orderState.action != widget.action) { + //return const SizedBox.shrink(); + //} ref.listen( mostroMessageStreamProvider(widget.orderId), From 0595ae28f7e3ced1b46bb731313b11f5bc6ad845 Mon Sep 17 00:00:00 2001 From: Biz Date: Thu, 29 May 2025 22:57:31 -0700 Subject: [PATCH 10/26] feat: add order state actions map and release button for sellers --- lib/core/mostro_fsm.dart | 10 +++ lib/features/order/models/order_state.dart | 80 ++++++++++++++++++- .../trades/screens/trade_detail_screen.dart | 11 +++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart index 2e9d2633..e86d5d92 100644 --- a/lib/core/mostro_fsm.dart +++ b/lib/core/mostro_fsm.dart @@ -10,6 +10,14 @@ class MostroFSM { /// Private constructor to prevent instantiation MostroFSM._(); + static final admin = { + (Status.active, Action.fiatSentOk): { + Action.cancel: Status.canceled, + Action.dispute: Status.dispute, + Action.release: Status.settledHoldInvoice, + } + }; + static final buyer = { Status.pending: { Action.takeSell: Status.waitingBuyerInvoice, @@ -112,6 +120,8 @@ class MostroFSM { Action.cancel: Status.canceled, Action.dispute: Status.dispute, Action.buyerTookOrder: Status.active, + Action.release: Status.fiatSent, + Action.rate: Status.success, }, Role.admin: {}, }, diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 58724a38..75638a06 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -77,13 +77,85 @@ class OrderState { status: message.getPayload()?.status ?? status, action: message.action != Action.cantDo ? message.action : action, order: message.payload is Order - ? message.getPayload() - : message.payload is PaymentRequest - ? message.getPayload()!.order - : order, + ? message.getPayload() + : message.payload is PaymentRequest + ? message.getPayload()!.order + : order, paymentRequest: message.getPayload() ?? paymentRequest, cantDo: message.getPayload() ?? cantDo, dispute: message.getPayload() ?? dispute, ); } + + final actions = { + Role.seller: { + Status.pending: { + Action.takeBuy: [ + Action.takeBuy, + Action.cancel, + ], + Action.waitingBuyerInvoice: [ + Action.cancel, + ], + Action.payInvoice: [ + Action.payInvoice, + Action.cancel, + ], + }, + Status.active: { + Action.buyerTookOrder: [ + Action.buyerTookOrder, + Action.cancel, + Action.dispute, + ], + Action.fiatSentOk: [ + Action.cancel, + Action.dispute, + Action.release, + ], + Action.rate: [ + Action.rate, + ], + Action.purchaseCompleted: [] + }, + Status.waitingPayment: { + Action.payInvoice: [ + Action.payInvoice, + Action.cancel, + ], + }, + }, + Role.buyer: { + Status.pending: { + Action.takeSell: [ + Action.takeSell, + Action.cancel, + ], + }, + Status.waitingBuyerInvoice: { + Action.addInvoice: [ + Action.addInvoice, + Action.cancel, + ], + Action.waitingSellerToPay: [ + Action.cancel, + ], + }, + Status.active: { + Action.holdInvoicePaymentAccepted: [ + Action.fiatSent, + Action.cancel, + Action.dispute, + ], + Action.fiatSentOk: [ + Action.cancel, + Action.dispute, + ], + Action.rate: [ + Action.rate, + ], + Action.purchaseCompleted: [], + }, + }, + }; } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 5ae39d6c..00a1f6d4 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -278,6 +278,17 @@ class TradeDetailScreen extends ConsumerWidget { } break; case actions.Action.release: + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'RELEASE', + action: actions.Action.release, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .releaseOrder(), + )); + } + break; case actions.Action.released: if (userRole == Role.seller) { widgets.add(_buildNostrButton( From f6c693cc0adf0fe97644eb1874773b721880ce86 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 11:29:00 -0700 Subject: [PATCH 11/26] refactor: update order state management and session handling in FSM --- lib/core/mostro_fsm.dart | 1 + lib/features/order/models/order_state.dart | 3 ++- .../order/notfiers/abstract_mostro_notifier.dart | 13 ++++++++++++- lib/features/order/notfiers/add_order_notifier.dart | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart index e86d5d92..5aa4ae90 100644 --- a/lib/core/mostro_fsm.dart +++ b/lib/core/mostro_fsm.dart @@ -40,6 +40,7 @@ class MostroFSM { Action.holdInvoicePaymentAccepted: Status.active, Action.cancel: Status.canceled, Action.dispute: Status.dispute, + Action.rate: Status.success, }, Status.fiatSent: {} }; diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 75638a06..79a697c2 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -87,7 +87,7 @@ class OrderState { ); } - final actions = { + static final actions = { Role.seller: { Status.pending: { Action.takeBuy: [ @@ -154,6 +154,7 @@ class OrderState { Action.rate: [ Action.rate, ], + Action.rateReceived: [], Action.purchaseCompleted: [], }, }, diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 4a6b185d..7f8b723e 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -4,6 +4,7 @@ import 'package:mostro_mobile/core/config.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/features/order/providers/order_notifier_provider.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'; @@ -21,7 +22,16 @@ class AbstractMostroNotifier extends StateNotifier { this.orderId, this.ref, ) : super(OrderState( - action: Action.newOrder, status: Status.pending, order: null)); + action: Action.newOrder, status: Status.pending, order: null)) { + + final oldSession = ref + .read(sessionNotifierProvider.notifier) + .getSessionByOrderId(orderId); + if (oldSession != null) { + session = oldSession; + } + + } void subscribe() { subscription = ref.listen( @@ -78,6 +88,7 @@ class AbstractMostroNotifier extends StateNotifier { navProvider.go('/'); notifProvider.showInformation(event.action, values: {'id': orderId}); dispose(); + ref.invalidate(orderNotifierProvider(orderId)); break; case Action.cooperativeCancelInitiatedByYou: notifProvider.showInformation(event.action, values: { diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index aaa978b8..83a94582 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -62,6 +62,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { '/order_confirmed/${message.id!}', ); dispose(); + ref.invalidate(addOrderNotifierProvider(orderId)); } Future submitOrder(Order order) async { From 0e3ffe168fa638a15b27c118f42baef0ba0458cb Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 14:05:53 -0700 Subject: [PATCH 12/26] refactor: migrate order notifiers to use new state management pattern --- lib/data/repositories/mostro_storage.dart | 14 - lib/features/home/screens/home_screen.dart | 84 +-- .../home/widgets/order_list_item.dart | 20 +- .../order/screens/take_order_screen.dart | 1 + test/mocks.mocks.dart | 44 +- test/notifiers/add_order_notifier_test.dart | 34 +- test/notifiers/take_order_notifier_test.dart | 22 +- test/services/mostro_service_test.dart | 16 +- test/services/mostro_service_test.mocks.dart | 652 +++++++----------- 9 files changed, 312 insertions(+), 575 deletions(-) diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart index 6de134c7..92510942 100644 --- a/lib/data/repositories/mostro_storage.dart +++ b/lib/data/repositories/mostro_storage.dart @@ -35,20 +35,6 @@ class MostroStorage extends BaseStorage { } } - /// Retrieve a MostroMessage by ID - Future getMessageById( - String orderId, - ) async { - final t = T; - final id = '$t:$orderId'; - try { - return await getItem(id); - } catch (e, stack) { - _logger.e('Error deserializing message $id', error: e, stackTrace: stack); - return null; - } - } - /// Get all messages Future> getAllMessages() async { try { diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart index 44016426..06d2ad53 100644 --- a/lib/features/home/screens/home_screen.dart +++ b/lib/features/home/screens/home_screen.dart @@ -88,82 +88,6 @@ class HomeScreen extends ConsumerWidget { ); } - PreferredSizeWidget _buildAppBar() { - return AppBar( - backgroundColor: AppTheme.backgroundDark, - elevation: 0, - leadingWidth: 60, - toolbarHeight: 56, - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container( - height: 1, - color: Colors.white.withOpacity(0.1), - ), - ), - leading: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Builder( - builder: (context) => IconButton( - icon: const HeroIcon( - HeroIcons.bars3, - style: HeroIconStyle.outline, - color: Colors.white, - size: 24, - ), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ), - ), - ), - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: Stack( - alignment: Alignment.center, - children: [ - IconButton( - icon: const HeroIcon( - HeroIcons.bell, - style: HeroIconStyle.outline, - color: Colors.white, - size: 24, - ), - onPressed: () { - // Action for notifications - }, - ), - // Indicator for the number of notifications - Positioned( - top: 12, - right: 8, - child: Container( - width: 18, - height: 18, - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - child: const Center( - child: Text( - '6', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ], - ), - ), - ], - ); - } - Widget _buildTabs(WidgetRef ref) { final orderType = ref.watch(homeOrderTypeProvider); @@ -172,7 +96,7 @@ class HomeScreen extends ConsumerWidget { color: AppTheme.backgroundDark, border: Border( bottom: BorderSide( - color: Colors.white.withOpacity(0.1), + color: Colors.white.withValues(alpha: 0.1), width: 1.0, ), ), @@ -260,10 +184,10 @@ class HomeScreen extends ConsumerWidget { decoration: BoxDecoration( color: AppTheme.backgroundInput, borderRadius: BorderRadius.circular(30), - border: Border.all(color: Colors.white.withOpacity(0.05)), + border: Border.all(color: Colors.white.withValues(alpha: 0.05)), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 4, offset: const Offset(0, 2), ), @@ -292,7 +216,7 @@ class HomeScreen extends ConsumerWidget { margin: const EdgeInsets.symmetric(horizontal: 8), height: 16, width: 1, - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), ), Text( "${filteredOrders.length} offers", diff --git a/lib/features/home/widgets/order_list_item.dart b/lib/features/home/widgets/order_list_item.dart index 054d30a2..dae879f8 100644 --- a/lib/features/home/widgets/order_list_item.dart +++ b/lib/features/home/widgets/order_list_item.dart @@ -36,7 +36,7 @@ class OrderListItem extends ConsumerWidget { borderRadius: BorderRadius.circular(20), boxShadow: AppTheme.cardShadow, border: Border.all( - color: Colors.white.withOpacity(0.05), + color: Colors.white.withValues(alpha: 0.05), width: 1, ), ), @@ -50,8 +50,8 @@ class OrderListItem extends ConsumerWidget { ? context.push('/take_buy/${order.orderId}') : context.push('/take_sell/${order.orderId}'); }, - highlightColor: Colors.white.withOpacity(0.05), - splashColor: Colors.white.withOpacity(0.03), + highlightColor: Colors.white.withValues(alpha: 0.05), + splashColor: Colors.white.withValues(alpha: 0.03), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -70,13 +70,13 @@ class OrderListItem extends ConsumerWidget { borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.6), + color: Colors.black.withValues(alpha: 0.6), blurRadius: 4, offset: const Offset(0, 2), spreadRadius: -1, ), BoxShadow( - color: Colors.white.withOpacity(0.08), + color: Colors.white.withValues(alpha: 0.08), blurRadius: 1, offset: const Offset(0, -1), spreadRadius: 0, @@ -168,13 +168,13 @@ class OrderListItem extends ConsumerWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), blurRadius: 6, offset: const Offset(0, 3), spreadRadius: -2, ), BoxShadow( - color: Colors.white.withOpacity(0.08), + color: Colors.white.withValues(alpha: 0.08), blurRadius: 1, offset: const Offset(0, -1), spreadRadius: 0, @@ -212,13 +212,13 @@ class OrderListItem extends ConsumerWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), blurRadius: 6, offset: const Offset(0, 3), spreadRadius: -2, ), BoxShadow( - color: Colors.white.withOpacity(0.08), + color: Colors.white.withValues(alpha: 0.08), blurRadius: 1, offset: const Offset(0, -1), spreadRadius: 0, @@ -272,7 +272,7 @@ class OrderListItem extends ConsumerWidget { color: starColor, size: 14); } else { return Icon(Icons.star_border, - color: starColor.withOpacity(0.3), size: 14); + color: starColor.withValues(alpha: 0.3), size: 14); } }), ), diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 8f4dfd0d..0bceb132 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -20,6 +20,7 @@ class TakeOrderScreen extends ConsumerWidget { final TextEditingController _fiatAmountController = TextEditingController(); final TextEditingController _lndAddressController = TextEditingController(); final TextTheme textTheme = AppTheme.theme.textTheme; + TakeOrderScreen({super.key, required this.orderId, required this.orderType}); @override diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index e67cf929..45acbb9c 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -8,11 +8,11 @@ import 'dart:async' as _i5; import 'package:dart_nostr/nostr/model/export.dart' as _i8; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mostro_mobile/data/models.dart' as _i3; +import 'package:mostro_mobile/data/models.dart' as _i4; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' as _i7; import 'package:mostro_mobile/features/settings/settings.dart' as _i6; -import 'package:mostro_mobile/services/mostro_service.dart' as _i4; +import 'package:mostro_mobile/services/mostro_service.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -39,20 +39,10 @@ class _FakeRef_0 extends _i1.SmartFake ); } -class _FakeSession_1 extends _i1.SmartFake implements _i3.Session { - _FakeSession_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [MostroService]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroService extends _i1.Mock implements _i4.MostroService { +class MockMostroService extends _i1.Mock implements _i3.MostroService { MockMostroService() { _i1.throwOnMissingStub(this); } @@ -76,7 +66,7 @@ class MockMostroService extends _i1.Mock implements _i4.MostroService { ); @override - void subscribe(_i3.Session? session) => super.noSuchMethod( + void subscribe(_i4.Session? session) => super.noSuchMethod( Invocation.method( #subscribe, [session], @@ -85,14 +75,14 @@ class MockMostroService extends _i1.Mock implements _i4.MostroService { ); @override - _i3.Session? getSessionByOrderId(String? orderId) => + _i4.Session? getSessionByOrderId(String? orderId) => (super.noSuchMethod(Invocation.method( #getSessionByOrderId, [orderId], - )) as _i3.Session?); + )) as _i4.Session?); @override - _i5.Future submitOrder(_i3.MostroMessage<_i3.Payload>? order) => + _i5.Future submitOrder(_i4.MostroMessage<_i4.Payload>? order) => (super.noSuchMethod( Invocation.method( #submitOrder, @@ -215,20 +205,15 @@ class MockMostroService extends _i1.Mock implements _i4.MostroService { ) as _i5.Future); @override - _i5.Future<_i3.Session> publishOrder(_i3.MostroMessage<_i3.Payload>? order) => + _i5.Future publishOrder(_i4.MostroMessage<_i4.Payload>? order) => (super.noSuchMethod( Invocation.method( #publishOrder, [order], ), - returnValue: _i5.Future<_i3.Session>.value(_FakeSession_1( - this, - Invocation.method( - #publishOrder, - [order], - ), - )), - ) as _i5.Future<_i3.Session>); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override void updateSettings(_i6.Settings? settings) => super.noSuchMethod( @@ -323,12 +308,11 @@ class MockOpenOrdersRepository extends _i1.Mock ); @override - _i5.Future reloadData() => (super.noSuchMethod( + void reloadData() => super.noSuchMethod( Invocation.method( #reloadData, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValueForMissingStub: null, + ); } diff --git a/test/notifiers/add_order_notifier_test.dart b/test/notifiers/add_order_notifier_test.dart index c7855eb3..54beb90e 100644 --- a/test/notifiers/add_order_notifier_test.dart +++ b/test/notifiers/add_order_notifier_test.dart @@ -1,10 +1,8 @@ -import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; @@ -35,10 +33,10 @@ void main() { /// Helper that sets up the mock repository so that when `publishOrder` is /// called, it returns a Stream based on `confirmationJson`. void configureMockPublishOrder(Map confirmationJson) { - final confirmationMessage = MostroMessage.fromJson(confirmationJson); - when(mockMostroService.publishOrder(any)).thenAnswer((invocation) async { + //final confirmationMessage = MostroMessage.fromJson(confirmationJson); + when(mockMostroService.submitOrder(any)).thenAnswer((invocation) async { // Return a stream that emits the confirmation message once. - return Stream.value(confirmationMessage); + //return Stream.value(confirmationMessage); }); } @@ -86,16 +84,16 @@ void main() { ); final notifier = - container.read(orderNotifierProvider(testUuid).notifier); + container.read(addOrderNotifierProvider(testUuid).notifier); // Submit the order await notifier.submitOrder(newSellOrder); // Retrieve the final state - final state = container.read(orderNotifierProvider(testUuid)); + final state = container.read(addOrderNotifierProvider(testUuid)); expect(state, isNotNull); - final confirmedOrder = state.getPayload(); + final confirmedOrder = state.order; expect(confirmedOrder, isNotNull); expect(confirmedOrder!.kind, equals(OrderType.sell)); expect(confirmedOrder.status.value, equals('pending')); @@ -136,7 +134,7 @@ void main() { configureMockPublishOrder(confirmationJsonSellRange); container = ProviderContainer(overrides: [ - mostroRepositoryProvider.overrideWithValue(mockMostroService), + mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), ]); @@ -153,13 +151,13 @@ void main() { ); final notifier = - container.read(orderNotifierProvider(testUuid).notifier); + container.read(addOrderNotifierProvider(testUuid).notifier); await notifier.submitOrder(newSellRangeOrder); final state = container.read(orderNotifierProvider(testUuid)); expect(state, isNotNull); - final confirmedOrder = state.getPayload(); + final confirmedOrder = state.order; expect(confirmedOrder, isNotNull); expect(confirmedOrder!.kind, equals(OrderType.sell)); expect(confirmedOrder.status.value, equals('pending')); @@ -200,7 +198,7 @@ void main() { configureMockPublishOrder(confirmationJsonBuy); container = ProviderContainer(overrides: [ - mostroRepositoryProvider.overrideWithValue(mockMostroService), + mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), ]); @@ -215,13 +213,13 @@ void main() { ); final notifier = - container.read(orderNotifierProvider(testUuid).notifier); + container.read(addOrderNotifierProvider(testUuid).notifier); await notifier.submitOrder(newBuyOrder); - final state = container.read(orderNotifierProvider(testUuid)); + final state = container.read(addOrderNotifierProvider(testUuid)); expect(state, isNotNull); - final confirmedOrder = state.getPayload(); + final confirmedOrder = state.order; expect(confirmedOrder, isNotNull); expect(confirmedOrder!.kind, equals(OrderType.buy)); expect(confirmedOrder.status.value, equals('pending')); @@ -261,7 +259,7 @@ void main() { configureMockPublishOrder(confirmationJsonBuyInvoice); container = ProviderContainer(overrides: [ - mostroRepositoryProvider.overrideWithValue(mockMostroService), + mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), ]); @@ -277,13 +275,13 @@ void main() { ); final notifier = - container.read(orderNotifierProvider(testUuid).notifier); + container.read(addOrderNotifierProvider(testUuid).notifier); await notifier.submitOrder(newBuyOrderWithInvoice); final state = container.read(orderNotifierProvider(testUuid)); expect(state, isNotNull); - final confirmedOrder = state.getPayload(); + final confirmedOrder = state.order; expect(confirmedOrder, isNotNull); expect(confirmedOrder!.kind, equals(OrderType.buy)); expect(confirmedOrder.status.value, equals('pending')); diff --git a/test/notifiers/take_order_notifier_test.dart b/test/notifiers/take_order_notifier_test.dart index 71a1d541..e192cb6d 100644 --- a/test/notifiers/take_order_notifier_test.dart +++ b/test/notifiers/take_order_notifier_test.dart @@ -1,10 +1,8 @@ -import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; @@ -71,8 +69,8 @@ void main() { // Stub the repository’s takeBuyOrder method. when(mockMostroService.takeBuyOrder(any, any)).thenAnswer((_) async { - final msg = MostroMessage.fromJson(confirmationJsonTakeBuy); - return Stream.value(msg); + //final msg = MostroMessage.fromJson(confirmationJsonTakeBuy); + //return Stream.value(msg); }); // Override the repository provider with our mock. @@ -119,8 +117,8 @@ void main() { }; when(mockMostroService.takeSellOrder(any, any, any)).thenAnswer((_) async { - final msg = MostroMessage.fromJson(confirmationJsonTakeSell); - return Stream.value(msg); + //final msg = MostroMessage.fromJson(confirmationJsonTakeSell); + //return Stream.value(msg); }); // Override the repository provider with our mock. @@ -137,7 +135,7 @@ void main() { final state = container.read(orderNotifierProvider(testOrderId)); expect(state, isNotNull); expect(state.action, equals(Action.addInvoice)); - final orderPayload = state.getPayload(); + final orderPayload = state.order; expect(orderPayload, isNotNull); expect(orderPayload!.amount, equals(0)); expect(orderPayload.fiatCode, equals('VES')); @@ -173,8 +171,8 @@ void main() { }; when(mockMostroService.takeSellOrder(any, any, any)).thenAnswer((_) async { - final msg = MostroMessage.fromJson(confirmationJsonSellRange); - return Stream.value(msg); + //final msg = MostroMessage.fromJson(confirmationJsonSellRange); + //return Stream.value(msg); }); // Override the repository provider with our mock. @@ -191,7 +189,7 @@ void main() { final state = container.read(orderNotifierProvider(testOrderId)); expect(state, isNotNull); expect(state.action, equals(Action.addInvoice)); - final orderPayload = state.getPayload(); + final orderPayload = state.order; expect(orderPayload, isNotNull); expect(orderPayload!.minAmount, equals(10)); expect(orderPayload.maxAmount, equals(20)); @@ -211,8 +209,8 @@ void main() { }; when(mockMostroService.takeSellOrder(any, any, any)).thenAnswer((_) async { - final msg = MostroMessage.fromJson(confirmationJsonSellLN); - return Stream.value(msg); + //final msg = MostroMessage.fromJson(confirmationJsonSellLN); + //return Stream.value(msg); }); // Override the repository provider with our mock. diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 1950e43a..8cc8033d 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -1,14 +1,13 @@ 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/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/features/key_manager/key_derivator.dart'; -import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; @@ -17,26 +16,21 @@ import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'mostro_service_test.mocks.dart'; import 'mostro_service_helper_functions.dart'; -@GenerateMocks([NostrService, SessionNotifier, MostroStorage]) +@GenerateMocks([NostrService, SessionNotifier, Ref]) void main() { late MostroService mostroService; late KeyDerivator keyDerivator; late MockNostrService mockNostrService; late MockSessionNotifier mockSessionNotifier; - late MockMostroStorage mockSessionStorage; + late MockRef mockRef; final mockServerTradeIndex = MockServerTradeIndex(); setUp(() { mockNostrService = MockNostrService(); mockSessionNotifier = MockSessionNotifier(); - mockSessionStorage = MockMostroStorage(); - mostroService = MostroService( - mockNostrService, - mockSessionNotifier, - Settings(relays: [], fullPrivacyMode: true, mostroPublicKey: 'xxx'), - mockSessionStorage, - ); + mockRef = MockRef(); + mostroService = MostroService(mockSessionNotifier, mockRef); keyDerivator = KeyDerivator("m/44'/1237'/38383'/0"); }); diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart index dc80e4cf..a43c6ff1 100644 --- a/test/services/mostro_service_test.mocks.dart +++ b/test/services/mostro_service_test.mocks.dart @@ -3,23 +3,19 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; +import 'dart:async' as _i7; import 'package:dart_nostr/dart_nostr.dart' as _i3; -import 'package:dart_nostr/nostr/model/relay_informations.dart' as _i10; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i13; +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 _i11; -import 'package:mostro_mobile/data/models/enums/role.dart' as _i14; -import 'package:mostro_mobile/data/models/mostro_message.dart' as _i7; -import 'package:mostro_mobile/data/models/payload.dart' as _i6; +import 'package:mockito/src/dummies.dart' as _i9; +import 'package:mostro_mobile/data/models/enums/role.dart' as _i11; import 'package:mostro_mobile/data/models/session.dart' as _i4; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart' as _i16; import 'package:mostro_mobile/features/settings/settings.dart' as _i2; -import 'package:mostro_mobile/services/nostr_service.dart' as _i8; -import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i12; -import 'package:sembast/sembast.dart' as _i5; -import 'package:state_notifier/state_notifier.dart' as _i15; +import 'package:mostro_mobile/services/nostr_service.dart' as _i6; +import 'package:mostro_mobile/shared/notifiers/session_notifier.dart' as _i10; +import 'package:state_notifier/state_notifier.dart' as _i12; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -75,8 +71,9 @@ class _FakeSession_3 extends _i1.SmartFake implements _i4.Session { ); } -class _FakeDatabase_4 extends _i1.SmartFake implements _i5.Database { - _FakeDatabase_4( +class _FakeProviderContainer_4 extends _i1.SmartFake + implements _i5.ProviderContainer { + _FakeProviderContainer_4( Object parent, Invocation parentInvocation, ) : super( @@ -85,9 +82,8 @@ class _FakeDatabase_4 extends _i1.SmartFake implements _i5.Database { ); } -class _FakeStoreRef_5 - extends _i1.SmartFake implements _i5.StoreRef { - _FakeStoreRef_5( +class _FakeKeepAliveLink_5 extends _i1.SmartFake implements _i5.KeepAliveLink { + _FakeKeepAliveLink_5( Object parent, Invocation parentInvocation, ) : super( @@ -96,19 +92,9 @@ class _FakeStoreRef_5 ); } -class _FakeMostroMessage_6 extends _i1.SmartFake - implements _i7.MostroMessage { - _FakeMostroMessage_6( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeFilter_7 extends _i1.SmartFake implements _i5.Filter { - _FakeFilter_7( +class _FakeProviderSubscription_6 extends _i1.SmartFake + implements _i5.ProviderSubscription { + _FakeProviderSubscription_6( Object parent, Invocation parentInvocation, ) : super( @@ -120,7 +106,7 @@ class _FakeFilter_7 extends _i1.SmartFake implements _i5.Filter { /// A class which mocks [NostrService]. /// /// See the documentation for Mockito's code generation for more information. -class MockNostrService extends _i1.Mock implements _i8.NostrService { +class MockNostrService extends _i1.Mock implements _i6.NostrService { MockNostrService() { _i1.throwOnMissingStub(this); } @@ -150,90 +136,90 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ) as bool); @override - _i9.Future init(_i2.Settings? settings) => (super.noSuchMethod( + _i7.Future init(_i2.Settings? settings) => (super.noSuchMethod( Invocation.method( #init, [settings], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i9.Future updateSettings(_i2.Settings? newSettings) => + _i7.Future updateSettings(_i2.Settings? newSettings) => (super.noSuchMethod( Invocation.method( #updateSettings, [newSettings], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i9.Future<_i10.RelayInformations?> getRelayInfo(String? relayUrl) => + _i7.Future<_i8.RelayInformations?> getRelayInfo(String? relayUrl) => (super.noSuchMethod( Invocation.method( #getRelayInfo, [relayUrl], ), - returnValue: _i9.Future<_i10.RelayInformations?>.value(), - ) as _i9.Future<_i10.RelayInformations?>); + returnValue: _i7.Future<_i8.RelayInformations?>.value(), + ) as _i7.Future<_i8.RelayInformations?>); @override - _i9.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod( + _i7.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod( Invocation.method( #publishEvent, [event], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i9.Future> fecthEvents(_i3.NostrFilter? filter) => + _i7.Future> fecthEvents(_i3.NostrFilter? filter) => (super.noSuchMethod( Invocation.method( #fecthEvents, [filter], ), - returnValue: _i9.Future>.value(<_i3.NostrEvent>[]), - ) as _i9.Future>); + returnValue: _i7.Future>.value(<_i3.NostrEvent>[]), + ) as _i7.Future>); @override - _i9.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrRequest? request) => + _i7.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrRequest? request) => (super.noSuchMethod( Invocation.method( #subscribeToEvents, [request], ), - returnValue: _i9.Stream<_i3.NostrEvent>.empty(), - ) as _i9.Stream<_i3.NostrEvent>); + returnValue: _i7.Stream<_i3.NostrEvent>.empty(), + ) as _i7.Stream<_i3.NostrEvent>); @override - _i9.Future disconnectFromRelays() => (super.noSuchMethod( + _i7.Future disconnectFromRelays() => (super.noSuchMethod( Invocation.method( #disconnectFromRelays, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i9.Future<_i3.NostrKeyPairs> generateKeyPair() => (super.noSuchMethod( + _i7.Future<_i3.NostrKeyPairs> generateKeyPair() => (super.noSuchMethod( Invocation.method( #generateKeyPair, [], ), - returnValue: _i9.Future<_i3.NostrKeyPairs>.value(_FakeNostrKeyPairs_1( + returnValue: _i7.Future<_i3.NostrKeyPairs>.value(_FakeNostrKeyPairs_1( this, Invocation.method( #generateKeyPair, [], ), )), - ) as _i9.Future<_i3.NostrKeyPairs>); + ) as _i7.Future<_i3.NostrKeyPairs>); @override _i3.NostrKeyPairs generateKeyPairFromPrivateKey(String? privateKey) => @@ -257,7 +243,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { #getMostroPubKey, [], ), - returnValue: _i11.dummyValue( + returnValue: _i9.dummyValue( this, Invocation.method( #getMostroPubKey, @@ -267,7 +253,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ) as String); @override - _i9.Future<_i3.NostrEvent> createNIP59Event( + _i7.Future<_i3.NostrEvent> createNIP59Event( String? content, String? recipientPubKey, String? senderPrivateKey, @@ -281,7 +267,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { senderPrivateKey, ], ), - returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( this, Invocation.method( #createNIP59Event, @@ -292,10 +278,10 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ], ), )), - ) as _i9.Future<_i3.NostrEvent>); + ) as _i7.Future<_i3.NostrEvent>); @override - _i9.Future<_i3.NostrEvent> decryptNIP59Event( + _i7.Future<_i3.NostrEvent> decryptNIP59Event( _i3.NostrEvent? event, String? privateKey, ) => @@ -307,7 +293,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { privateKey, ], ), - returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( this, Invocation.method( #decryptNIP59Event, @@ -317,10 +303,10 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ], ), )), - ) as _i9.Future<_i3.NostrEvent>); + ) as _i7.Future<_i3.NostrEvent>); @override - _i9.Future createRumor( + _i7.Future createRumor( _i3.NostrKeyPairs? senderKeyPair, String? wrapperKey, String? recipientPubKey, @@ -336,7 +322,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { content, ], ), - returnValue: _i9.Future.value(_i11.dummyValue( + returnValue: _i7.Future.value(_i9.dummyValue( this, Invocation.method( #createRumor, @@ -348,10 +334,10 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ], ), )), - ) as _i9.Future); + ) as _i7.Future); @override - _i9.Future createSeal( + _i7.Future createSeal( _i3.NostrKeyPairs? senderKeyPair, String? wrapperKey, String? recipientPubKey, @@ -367,7 +353,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { encryptedContent, ], ), - returnValue: _i9.Future.value(_i11.dummyValue( + returnValue: _i7.Future.value(_i9.dummyValue( this, Invocation.method( #createSeal, @@ -379,10 +365,10 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ], ), )), - ) as _i9.Future); + ) as _i7.Future); @override - _i9.Future<_i3.NostrEvent> createWrap( + _i7.Future<_i3.NostrEvent> createWrap( _i3.NostrKeyPairs? wrapperKeyPair, String? sealedContent, String? recipientPubKey, @@ -396,7 +382,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { recipientPubKey, ], ), - returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + returnValue: _i7.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( this, Invocation.method( #createWrap, @@ -407,7 +393,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { ], ), )), - ) as _i9.Future<_i3.NostrEvent>); + ) as _i7.Future<_i3.NostrEvent>); @override void unsubscribe(String? id) => super.noSuchMethod( @@ -422,7 +408,7 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { /// A class which mocks [SessionNotifier]. /// /// See the documentation for Mockito's code generation for more information. -class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { +class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { MockSessionNotifier() { _i1.throwOnMissingStub(this); } @@ -434,7 +420,7 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { ) as List<_i4.Session>); @override - set onError(_i13.ErrorListener? _onError) => super.noSuchMethod( + set onError(_i5.ErrorListener? _onError) => super.noSuchMethod( Invocation.setter( #onError, _onError, @@ -449,10 +435,10 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { ) as bool); @override - _i9.Stream> get stream => (super.noSuchMethod( + _i7.Stream> get stream => (super.noSuchMethod( Invocation.getter(#stream), - returnValue: _i9.Stream>.empty(), - ) as _i9.Stream>); + returnValue: _i7.Stream>.empty(), + ) as _i7.Stream>); @override List<_i4.Session> get state => (super.noSuchMethod( @@ -482,14 +468,14 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { ) as bool); @override - _i9.Future init() => (super.noSuchMethod( + _i7.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void updateSettings(_i2.Settings? settings) => super.noSuchMethod( @@ -501,9 +487,10 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { ); @override - _i9.Future<_i4.Session> newSession({ + _i7.Future<_i4.Session> newSession({ String? orderId, - _i14.Role? role, + int? requestId, + _i11.Role? role, }) => (super.noSuchMethod( Invocation.method( @@ -511,31 +498,57 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { [], { #orderId: orderId, + #requestId: requestId, #role: role, }, ), - returnValue: _i9.Future<_i4.Session>.value(_FakeSession_3( + returnValue: _i7.Future<_i4.Session>.value(_FakeSession_3( this, Invocation.method( #newSession, [], { #orderId: orderId, + #requestId: requestId, #role: role, }, ), )), - ) as _i9.Future<_i4.Session>); + ) as _i7.Future<_i4.Session>); @override - _i9.Future saveSession(_i4.Session? session) => (super.noSuchMethod( + _i7.Future saveSession(_i4.Session? session) => (super.noSuchMethod( Invocation.method( #saveSession, [session], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + 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) => @@ -552,33 +565,33 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { )) as _i4.Session?); @override - _i9.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( + _i7.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( Invocation.method( #loadSession, [keyIndex], ), - returnValue: _i9.Future<_i4.Session?>.value(), - ) as _i9.Future<_i4.Session?>); + returnValue: _i7.Future<_i4.Session?>.value(), + ) as _i7.Future<_i4.Session?>); @override - _i9.Future reset() => (super.noSuchMethod( + _i7.Future reset() => (super.noSuchMethod( Invocation.method( #reset, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i9.Future deleteSession(String? sessionId) => (super.noSuchMethod( + _i7.Future deleteSession(String? sessionId) => (super.noSuchMethod( Invocation.method( #deleteSession, [sessionId], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void dispose() => super.noSuchMethod( @@ -606,8 +619,8 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { ) as bool); @override - _i13.RemoveListener addListener( - _i15.Listener>? listener, { + _i5.RemoveListener addListener( + _i12.Listener>? listener, { bool? fireImmediately = true, }) => (super.noSuchMethod( @@ -617,387 +630,226 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { {#fireImmediately: fireImmediately}, ), returnValue: () {}, - ) as _i13.RemoveListener); + ) as _i5.RemoveListener); } -/// A class which mocks [MostroStorage]. +/// A class which mocks [Ref]. /// /// See the documentation for Mockito's code generation for more information. -class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { - MockMostroStorage() { +class MockRef extends _i1.Mock + implements _i5.Ref { + MockRef() { _i1.throwOnMissingStub(this); } @override - _i5.Database get db => (super.noSuchMethod( - Invocation.getter(#db), - returnValue: _FakeDatabase_4( - this, - Invocation.getter(#db), - ), - ) as _i5.Database); - - @override - _i5.StoreRef> get store => (super.noSuchMethod( - Invocation.getter(#store), - returnValue: _FakeStoreRef_5>( + _i5.ProviderContainer get container => (super.noSuchMethod( + Invocation.getter(#container), + returnValue: _FakeProviderContainer_4( this, - Invocation.getter(#store), + Invocation.getter(#container), ), - ) as _i5.StoreRef>); + ) as _i5.ProviderContainer); @override - _i9.Future addMessage( - String? key, - _i7.MostroMessage<_i6.Payload>? message, - ) => - (super.noSuchMethod( + T refresh(_i5.Refreshable? provider) => (super.noSuchMethod( Invocation.method( - #addMessage, - [ - key, - message, - ], + #refresh, + [provider], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future<_i7.MostroMessage<_i6.Payload>?> - getMessageById(String? orderId) => - (super.noSuchMethod( - Invocation.method( - #getMessageById, - [orderId], - ), - returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); - - @override - _i9.Future>> getAllMessages() => - (super.noSuchMethod( - Invocation.method( - #getAllMessages, - [], - ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); - - @override - _i9.Future deleteAllMessages() => (super.noSuchMethod( - Invocation.method( - #deleteAllMessages, - [], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future deleteAllMessagesByOrderId(String? orderId) => - (super.noSuchMethod( - Invocation.method( - #deleteAllMessagesByOrderId, - [orderId], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future>> - getMessagesOfType() => (super.noSuchMethod( - Invocation.method( - #getMessagesOfType, - [], - ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); - - @override - _i9.Future<_i7.MostroMessage<_i6.Payload>?> - getLatestMessageOfTypeById(String? orderId) => - (super.noSuchMethod( - Invocation.method( - #getLatestMessageOfTypeById, - [orderId], - ), - returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); - - @override - _i9.Future>> getMessagesForId( - String? orderId) => - (super.noSuchMethod( - Invocation.method( - #getMessagesForId, - [orderId], - ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); - - @override - _i7.MostroMessage<_i6.Payload> fromDbMap( - String? key, - Map? jsonMap, - ) => - (super.noSuchMethod( - Invocation.method( - #fromDbMap, - [ - key, - jsonMap, - ], - ), - returnValue: _FakeMostroMessage_6<_i6.Payload>( + returnValue: _i9.dummyValue( this, Invocation.method( - #fromDbMap, - [ - key, - jsonMap, - ], + #refresh, + [provider], ), ), - ) as _i7.MostroMessage<_i6.Payload>); + ) as T); @override - Map toDbMap(_i7.MostroMessage<_i6.Payload>? item) => - (super.noSuchMethod( + void invalidate(_i5.ProviderOrFamily? provider) => super.noSuchMethod( Invocation.method( - #toDbMap, - [item], + #invalidate, + [provider], ), - returnValue: {}, - ) as Map); + returnValueForMissingStub: null, + ); @override - _i9.Future hasMessageByKey(String? key) => (super.noSuchMethod( + void notifyListeners() => super.noSuchMethod( Invocation.method( - #hasMessageByKey, - [key], + #notifyListeners, + [], ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); + returnValueForMissingStub: null, + ); @override - _i9.Future<_i7.MostroMessage<_i6.Payload>?> getLatestMessageById( - String? orderId) => - (super.noSuchMethod( + void listenSelf( + void Function( + State?, + State, + )? listener, { + void Function( + Object, + StackTrace, + )? onError, + }) => + super.noSuchMethod( Invocation.method( - #getLatestMessageById, - [orderId], + #listenSelf, + [listener], + {#onError: onError}, ), - returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); + returnValueForMissingStub: null, + ); @override - _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchLatestMessage( - String? orderId) => - (super.noSuchMethod( + void invalidateSelf() => super.noSuchMethod( Invocation.method( - #watchLatestMessage, - [orderId], + #invalidateSelf, + [], ), - returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), - ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); + returnValueForMissingStub: null, + ); @override - _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchLatestMessageOfType( - String? orderId) => - (super.noSuchMethod( + void onAddListener(void Function()? cb) => super.noSuchMethod( Invocation.method( - #watchLatestMessageOfType, - [orderId], + #onAddListener, + [cb], ), - returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), - ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); + returnValueForMissingStub: null, + ); @override - _i9.Stream>> watchAllMessages( - String? orderId) => - (super.noSuchMethod( + void onRemoveListener(void Function()? cb) => super.noSuchMethod( Invocation.method( - #watchAllMessages, - [orderId], + #onRemoveListener, + [cb], ), - returnValue: _i9.Stream>>.empty(), - ) as _i9.Stream>>); + returnValueForMissingStub: null, + ); @override - _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchByRequestId( - int? requestId) => - (super.noSuchMethod( + void onResume(void Function()? cb) => super.noSuchMethod( Invocation.method( - #watchByRequestId, - [requestId], + #onResume, + [cb], ), - returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), - ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); + returnValueForMissingStub: null, + ); @override - _i9.Future>> getAllMessagesForOrderId( - String? orderId) => - (super.noSuchMethod( + void onCancel(void Function()? cb) => super.noSuchMethod( Invocation.method( - #getAllMessagesForOrderId, - [orderId], + #onCancel, + [cb], ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); + returnValueForMissingStub: null, + ); @override - _i9.Future putItem( - String? id, - _i7.MostroMessage<_i6.Payload>? item, - ) => - (super.noSuchMethod( + void onDispose(void Function()? cb) => super.noSuchMethod( Invocation.method( - #putItem, - [ - id, - item, - ], + #onDispose, + [cb], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValueForMissingStub: null, + ); @override - _i9.Future<_i7.MostroMessage<_i6.Payload>?> getItem(String? id) => - (super.noSuchMethod( + T read(_i5.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( - #getItem, - [id], + #read, + [provider], ), - returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); - - @override - _i9.Future hasItem(String? id) => (super.noSuchMethod( - Invocation.method( - #hasItem, - [id], + returnValue: _i9.dummyValue( + this, + Invocation.method( + #read, + [provider], + ), ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); + ) as T); @override - _i9.Future deleteItem(String? id) => (super.noSuchMethod( + bool exists(_i5.ProviderBase? provider) => (super.noSuchMethod( Invocation.method( - #deleteItem, - [id], + #exists, + [provider], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: false, + ) as bool); @override - _i9.Future deleteAll() => (super.noSuchMethod( + T watch(_i5.ProviderListenable? provider) => (super.noSuchMethod( Invocation.method( - #deleteAll, - [], + #watch, + [provider], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - - @override - _i9.Future deleteWhere(_i5.Filter? filter) => (super.noSuchMethod( - Invocation.method( - #deleteWhere, - [filter], + returnValue: _i9.dummyValue( + this, + Invocation.method( + #watch, + [provider], + ), ), - returnValue: _i9.Future.value(0), - ) as _i9.Future); + ) as T); @override - _i9.Future>> find({ - _i5.Filter? filter, - List<_i5.SortOrder>? sort, - int? limit, - int? offset, - }) => - (super.noSuchMethod( + _i5.KeepAliveLink keepAlive() => (super.noSuchMethod( Invocation.method( - #find, + #keepAlive, [], - { - #filter: filter, - #sort: sort, - #limit: limit, - #offset: offset, - }, ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); - - @override - _i9.Future>> getAll() => - (super.noSuchMethod( - Invocation.method( - #getAll, - [], + returnValue: _FakeKeepAliveLink_5( + this, + Invocation.method( + #keepAlive, + [], + ), ), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[]), - ) as _i9.Future>>); + ) as _i5.KeepAliveLink); @override - _i9.Stream>> watch({ - _i5.Filter? filter, - List<_i5.SortOrder>? sort, + _i5.ProviderSubscription listen( + _i5.ProviderListenable? provider, + void Function( + T?, + T, + )? listener, { + void Function( + Object, + StackTrace, + )? onError, + bool? fireImmediately, }) => (super.noSuchMethod( Invocation.method( - #watch, - [], - { - #filter: filter, - #sort: sort, - }, - ), - returnValue: _i9.Stream>>.empty(), - ) as _i9.Stream>>); - - @override - _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchById(String? id) => - (super.noSuchMethod( - Invocation.method( - #watchById, - [id], - ), - returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), - ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); - - @override - _i5.Filter eq( - String? field, - Object? value, - ) => - (super.noSuchMethod( - Invocation.method( - #eq, + #listen, [ - field, - value, + provider, + listener, ], + { + #onError: onError, + #fireImmediately: fireImmediately, + }, ), - returnValue: _FakeFilter_7( + returnValue: _FakeProviderSubscription_6( this, Invocation.method( - #eq, + #listen, [ - field, - value, + provider, + listener, ], + { + #onError: onError, + #fireImmediately: fireImmediately, + }, ), ), - ) as _i5.Filter); + ) as _i5.ProviderSubscription); } From 963ddd11cb432786a704cead6dbaeb54f4ca6b6b Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 15:34:01 -0700 Subject: [PATCH 13/26] refactor: optimize order deletion flow and add soft delete filter to base storage --- lib/data/repositories/base_storage.dart | 4 +++- lib/features/order/notfiers/abstract_mostro_notifier.dart | 5 ++--- lib/features/order/notfiers/add_order_notifier.dart | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/data/repositories/base_storage.dart b/lib/data/repositories/base_storage.dart index e5490494..de125a5f 100644 --- a/lib/data/repositories/base_storage.dart +++ b/lib/data/repositories/base_storage.dart @@ -76,7 +76,9 @@ abstract class BaseStorage { .toList(growable: false); } - Future> getAll() => find(); + Future> getAll() => find( + filter: Filter.notEquals('deleted', true), + ); Stream> watch({ Filter? filter, diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 7f8b723e..7cbada66 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -81,14 +81,13 @@ class AbstractMostroNotifier extends StateNotifier { case Action.canceled: ref .read(mostroStorageProvider) - .deleteAllMessagesByOrderId(session.orderId!); + .deleteAllMessagesByOrderId(orderId); ref .read(sessionNotifierProvider.notifier) - .deleteSession(session.orderId!); + .deleteSession(orderId); navProvider.go('/'); notifProvider.showInformation(event.action, values: {'id': orderId}); dispose(); - ref.invalidate(orderNotifierProvider(orderId)); break; case Action.cooperativeCancelInitiatedByYou: notifProvider.showInformation(event.action, values: { diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index 83a94582..aaa978b8 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -62,7 +62,6 @@ class AddOrderNotifier extends AbstractMostroNotifier { '/order_confirmed/${message.id!}', ); dispose(); - ref.invalidate(addOrderNotifierProvider(orderId)); } Future submitOrder(Order order) async { From fc01b10bd5e226d69a29a64ffb7fd33400f1cd96 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 16:29:36 -0700 Subject: [PATCH 14/26] refactor: replace dispose() calls with ref.invalidateSelf() for proper state cleanup --- lib/features/order/notfiers/abstract_mostro_notifier.dart | 3 +-- lib/features/order/notfiers/add_order_notifier.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 7cbada66..5bd38232 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -4,7 +4,6 @@ import 'package:mostro_mobile/core/config.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/features/order/providers/order_notifier_provider.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'; @@ -87,7 +86,7 @@ class AbstractMostroNotifier extends StateNotifier { .deleteSession(orderId); navProvider.go('/'); notifProvider.showInformation(event.action, values: {'id': orderId}); - dispose(); + ref.invalidateSelf(); break; case Action.cooperativeCancelInitiatedByYou: notifProvider.showInformation(event.action, values: { diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index aaa978b8..efe0970b 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -61,7 +61,7 @@ class AddOrderNotifier extends AbstractMostroNotifier { ref.read(navigationProvider.notifier).go( '/order_confirmed/${message.id!}', ); - dispose(); + ref.invalidateSelf(); } Future submitOrder(Order order) async { From 28cc13313201ccb83ba668ed13ab717fc2f72854 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 18:24:27 -0700 Subject: [PATCH 15/26] fix: update ref.read to ref.watch for reactive order state updates --- lib/features/order/screens/pay_lightning_invoice_screen.dart | 4 ++-- lib/features/order/screens/take_order_screen.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index 1746123d..8e39f923 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -21,10 +21,10 @@ class _PayLightningInvoiceScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final order = ref.read(orderNotifierProvider(widget.orderId)); + final order = ref.watch(orderNotifierProvider(widget.orderId)); final lnInvoice = order.paymentRequest?.lnInvoice ?? ''; final orderNotifier = - ref.read(orderNotifierProvider(widget.orderId).notifier); + ref.watch(orderNotifierProvider(widget.orderId).notifier); return Scaffold( backgroundColor: AppTheme.dark1, diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 0bceb132..f4b39938 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -152,7 +152,7 @@ class TakeOrderScreen extends ConsumerWidget { Widget _buildActionButtons( BuildContext context, WidgetRef ref, NostrEvent order) { - final orderDetailsNotifier = ref.read( + final orderDetailsNotifier = ref.watch( orderNotifierProvider(order.orderId!).notifier, ); From fa8828b4ad16ee400609191ce1dc9f6187550dea Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 20:36:32 -0700 Subject: [PATCH 16/26] refactor: consolidate trade state logic into order state model --- lib/features/order/models/order_state.dart | 6 ++- lib/features/trades/models/trade_state.dart | 42 ------------------- .../providers/trade_state_provider.dart | 26 ------------ .../trades/screens/trade_detail_screen.dart | 5 +-- 4 files changed, 7 insertions(+), 72 deletions(-) delete mode 100644 lib/features/trades/models/trade_state.dart delete mode 100644 lib/features/trades/providers/trade_state_provider.dart diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 79a697c2..55bac938 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -87,7 +87,11 @@ class OrderState { ); } - static final actions = { + List getActions(Role role) { + return actions[role]![status]![action] ?? []; + } + + static final Map>>> actions = { Role.seller: { Status.pending: { Action.takeBuy: [ diff --git a/lib/features/trades/models/trade_state.dart b/lib/features/trades/models/trade_state.dart deleted file mode 100644 index 5d7aa5b6..00000000 --- a/lib/features/trades/models/trade_state.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; - -class TradeState { - final Status status; - final Action? action; - final Order? order; - - TradeState({ - required this.status, - required this.action, - required this.order, - }); - - @override - String toString() => - 'TradeState(status: $status, action: $action, order: $order)'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TradeState && - other.status == status && - other.action == action && - other.order == order; - - @override - int get hashCode => Object.hash(status, action, order); - - TradeState copyWith({ - Status? status, - Action? action, - Order? order, - }) { - return TradeState( - status: status ?? this.status, - action: action ?? this.action, - order: order ?? this.order, - ); - } -} diff --git a/lib/features/trades/providers/trade_state_provider.dart b/lib/features/trades/providers/trade_state_provider.dart deleted file mode 100644 index 1f65f29c..00000000 --- a/lib/features/trades/providers/trade_state_provider.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart' as actions; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/features/trades/models/trade_state.dart'; -import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; -import 'package:collection/collection.dart'; - -/// Provides a reactive TradeState for a given orderId. - -final tradeStateProvider = - Provider.family.autoDispose((ref, orderId) { - final messagesAsync = ref.watch(mostroMessageHistoryProvider(orderId)); - final lastOrderMessageAsync = ref.watch(mostroOrderStreamProvider(orderId)); - - final messages = messagesAsync.value ?? []; - final lastActionMessage = - messages.firstWhereOrNull((m) => m.action != actions.Action.cantDo); - final orderPayload = lastOrderMessageAsync.value?.getPayload(); - - return TradeState( - status: orderPayload?.status ?? Status.pending, - action: lastActionMessage?.action, - order: orderPayload, - ); -}); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 00a1f6d4..5c6e6fa0 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/core/mostro_fsm.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; @@ -211,7 +210,7 @@ class TradeDetailScreen extends ConsumerWidget { final session = ref.watch(sessionProvider(orderId)); final userRole = session?.role; - final userActions = MostroFSM.possibleActions(tradeState.status, userRole!); + final userActions = tradeState.getActions(userRole!); if (userActions.isEmpty) return []; final widgets = []; @@ -347,7 +346,7 @@ class TradeDetailScreen extends ConsumerWidget { backgroundColor: AppTheme.mostroGreen, onPressed: () => ref .read(orderNotifierProvider(orderId).notifier) - .releaseOrder(), // This usually triggers completion + .releaseOrder(), )); break; case actions.Action.buyerTookOrder: From 5fdfe7bae2cf0ca0e711448523dcb7dd8a7de3e3 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 31 May 2025 22:57:03 -0700 Subject: [PATCH 17/26] feat: add peer handling to OrderState and improve action flow management --- lib/features/order/models/order_state.dart | 24 ++++++++++++++++--- .../notfiers/abstract_mostro_notifier.dart | 23 ++++++++---------- .../trades/screens/trade_detail_screen.dart | 1 + 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 55bac938..db351b11 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -8,6 +8,7 @@ class OrderState { final PaymentRequest? paymentRequest; final CantDo? cantDo; final Dispute? dispute; +final Peer? peer; OrderState({ required this.status, @@ -16,6 +17,7 @@ class OrderState { this.paymentRequest, this.cantDo, this.dispute, + this.peer, }); factory OrderState.fromMostroMessage(MostroMessage message) { @@ -26,12 +28,13 @@ class OrderState { paymentRequest: message.getPayload(), cantDo: message.getPayload(), dispute: message.getPayload(), + peer: message.getPayload(), ); } @override String toString() => - 'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute)'; + 'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute, peer: $peer)'; @override bool operator ==(Object other) => @@ -42,7 +45,8 @@ class OrderState { other.order == order && other.paymentRequest == paymentRequest && other.cantDo == cantDo && - other.dispute == dispute; + other.dispute == dispute && + other.peer == peer; @override int get hashCode => Object.hash( @@ -52,6 +56,7 @@ class OrderState { paymentRequest, cantDo, dispute, + peer, ); OrderState copyWith({ @@ -61,6 +66,7 @@ class OrderState { PaymentRequest? paymentRequest, CantDo? cantDo, Dispute? dispute, + Peer? peer, }) { return OrderState( status: status ?? this.status, @@ -69,6 +75,7 @@ class OrderState { paymentRequest: paymentRequest ?? this.paymentRequest, cantDo: cantDo ?? this.cantDo, dispute: dispute ?? this.dispute, + peer: peer ?? this.peer, ); } @@ -84,6 +91,7 @@ class OrderState { paymentRequest: message.getPayload() ?? paymentRequest, cantDo: message.getPayload() ?? cantDo, dispute: message.getPayload() ?? dispute, + peer: message.getPayload() ?? peer, ); } @@ -120,7 +128,12 @@ class OrderState { Action.rate: [ Action.rate, ], - Action.purchaseCompleted: [] + Action.purchaseCompleted: [], + Action.holdInvoicePaymentSettled: [], + Action.cooperativeCancelInitiatedByPeer: [ + Action.cancel, + ], + }, Status.waitingPayment: { Action.payInvoice: [ @@ -135,6 +148,9 @@ class OrderState { Action.takeSell, Action.cancel, ], + Action.newOrder: [ + Action.cancel, + ], }, Status.waitingBuyerInvoice: { Action.addInvoice: [ @@ -147,6 +163,7 @@ class OrderState { }, Status.active: { Action.holdInvoicePaymentAccepted: [ + Action.holdInvoicePaymentAccepted, Action.fiatSent, Action.cancel, Action.dispute, @@ -160,6 +177,7 @@ class OrderState { ], Action.rateReceived: [], Action.purchaseCompleted: [], + Action.paymentFailed: [], }, }, }; diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 5bd38232..89bbc312 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -22,14 +22,11 @@ class AbstractMostroNotifier extends StateNotifier { this.ref, ) : super(OrderState( action: Action.newOrder, status: Status.pending, order: null)) { - - final oldSession = ref - .read(sessionNotifierProvider.notifier) - .getSessionByOrderId(orderId); + final oldSession = + ref.read(sessionNotifierProvider.notifier).getSessionByOrderId(orderId); if (oldSession != null) { session = oldSession; } - } void subscribe() { @@ -58,13 +55,17 @@ class AbstractMostroNotifier extends StateNotifier { final notifProvider = ref.read(notificationProvider.notifier); final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; - state = state.updateWith(event); + if (mounted) { + state = state.updateWith(event); + } switch (event.action) { case Action.newOrder: break; case Action.payInvoice: - navProvider.go('/pay_invoice/${event.id!}'); + if (event.payload is PaymentRequest) { + navProvider.go('/pay_invoice/${event.id!}'); + } break; case Action.fiatSentOk: final peer = event.getPayload(); @@ -78,12 +79,8 @@ class AbstractMostroNotifier extends StateNotifier { }); break; case Action.canceled: - ref - .read(mostroStorageProvider) - .deleteAllMessagesByOrderId(orderId); - ref - .read(sessionNotifierProvider.notifier) - .deleteSession(orderId); + ref.read(mostroStorageProvider).deleteAllMessagesByOrderId(orderId); + ref.read(sessionNotifierProvider.notifier).deleteSession(orderId); navProvider.go('/'); notifProvider.showInformation(event.action, values: {'id': orderId}); ref.invalidateSelf(); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 5c6e6fa0..057ce170 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -364,6 +364,7 @@ class TradeDetailScreen extends ConsumerWidget { break; case actions.Action.holdInvoicePaymentAccepted: widgets.add(_buildContactButton(context)); + break; case actions.Action.holdInvoicePaymentSettled: case actions.Action.holdInvoicePaymentCanceled: From e0bab49a8944339f9a3f02e6a726cfc28f7afe38 Mon Sep 17 00:00:00 2001 From: Biz Date: Sun, 1 Jun 2025 12:52:59 -0700 Subject: [PATCH 18/26] feat: add cancel action to newOrder state in order workflow --- lib/features/order/models/order_state.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index db351b11..7beae2c4 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -113,6 +113,9 @@ final Peer? peer; Action.payInvoice, Action.cancel, ], + Action.newOrder: [ + Action.cancel, + ], }, Status.active: { Action.buyerTookOrder: [ From bea2f0071ca23755fb85dfe5d97b6c5a79b971b5 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 4 Jun 2025 12:35:01 -0700 Subject: [PATCH 19/26] feat: enhance order state management and error handling in MostroService --- lib/features/order/models/order_state.dart | 3 ++ .../notfiers/abstract_mostro_notifier.dart | 4 +- .../order/notfiers/add_order_notifier.dart | 8 ++-- .../order/notfiers/order_notifier.dart | 45 +++++++++++-------- .../screens/payment_confirmation_screen.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 23 ++++------ lib/services/mostro_service.dart | 14 ++++-- 7 files changed, 57 insertions(+), 42 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 7beae2c4..cbb935ce 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -178,6 +178,9 @@ final Peer? peer; Action.rate: [ Action.rate, ], + Action.cooperativeCancelInitiatedByPeer: [ + Action.cancel, + ], Action.rateReceived: [], Action.purchaseCompleted: [], Action.paymentFailed: [], diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 89bbc312..0194b9f9 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -70,7 +70,7 @@ class AbstractMostroNotifier extends StateNotifier { case Action.fiatSentOk: final peer = event.getPayload(); notifProvider.showInformation(event.action, values: { - 'buyer_npub': peer?.publicKey ?? '{buyer_npub}', + 'buyer_npub': peer?.publicKey ?? 'Unknown', }); break; case Action.released: @@ -136,7 +136,7 @@ class AbstractMostroNotifier extends StateNotifier { break; case Action.holdInvoicePaymentSettled: notifProvider.showInformation(event.action, values: { - 'buyer_npub': 'buyerTradePubkey', + 'buyer_npub': state.order?.buyerTradePubkey ?? 'Unknown', }); break; case Action.waitingSellerToPay: diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index efe0970b..781d1de2 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -20,9 +20,11 @@ class AddOrderNotifier extends AbstractMostroNotifier { int _requestIdFromOrderId(String orderId) { final uuid = orderId.replaceAll('-', ''); - final timestamp = DateTime.now().microsecondsSinceEpoch; - return (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & - 0x7FFFFFFF; + // 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); } @override diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index b6f8806d..2fc5bb7b 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -17,27 +17,36 @@ class OrderNotifier extends AbstractMostroNotifier { } Future sync() async { - final storage = ref.read(mostroStorageProvider); - final messages = await storage.getAllMessagesForOrderId(orderId); - if (messages.isEmpty) { - return; - } - final msg = messages.firstWhereOrNull((m) => m.action != Action.cantDo); - if (msg?.payload is Order) { - state = OrderState( - status: msg!.getPayload()!.status, - action: msg.action, - order: msg.getPayload()!, - ); - } else { - final orderMsg = await storage.getLatestMessageOfTypeById(orderId); - if (orderMsg != null) { + try { + final storage = ref.read(mostroStorageProvider); + final messages = await storage.getAllMessagesForOrderId(orderId); + if (messages.isEmpty) { + return; + } + final msg = messages.firstWhereOrNull((m) => m.action != Action.cantDo); + if (msg?.payload is Order) { state = OrderState( - status: orderMsg.getPayload()!.status, - action: orderMsg.action, - order: orderMsg.getPayload()!, + status: msg!.getPayload()!.status, + action: msg.action, + order: msg.getPayload()!, ); + } else { + final orderMsg = + await storage.getLatestMessageOfTypeById(orderId); + if (orderMsg != null) { + state = OrderState( + status: orderMsg.getPayload()!.status, + action: orderMsg.action, + order: orderMsg.getPayload()!, + ); + } } + } catch (e, stack) { + logger.e( + 'Error syncing order state', + error: e, + stackTrace: stack, + ); } } diff --git a/lib/features/order/screens/payment_confirmation_screen.dart b/lib/features/order/screens/payment_confirmation_screen.dart index 266efe32..8f925d69 100644 --- a/lib/features/order/screens/payment_confirmation_screen.dart +++ b/lib/features/order/screens/payment_confirmation_screen.dart @@ -39,7 +39,7 @@ class PaymentConfirmationScreen extends ConsumerWidget { Widget _buildBody(BuildContext context, WidgetRef ref, OrderState state) { switch (state.action) { case action.Action.purchaseCompleted: - final satoshis = 0; + final satoshis = state.order?.amount ?? 0; return Center( child: Container( diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 057ce170..1344428a 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -209,8 +209,12 @@ class TradeDetailScreen extends ConsumerWidget { BuildContext context, WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); final userRole = session?.role; - - final userActions = tradeState.getActions(userRole!); + + if (userRole == null) { + return []; + } + + final userActions = tradeState.getActions(userRole); if (userActions.isEmpty) return []; final widgets = []; @@ -288,18 +292,6 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; - case actions.Action.released: - if (userRole == Role.seller) { - widgets.add(_buildNostrButton( - 'RELEASE', - action: actions.Action.release, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .releaseOrder(), - )); - } - break; case actions.Action.takeSell: if (userRole == Role.buyer) { widgets.add(_buildNostrButton( @@ -364,7 +356,7 @@ class TradeDetailScreen extends ConsumerWidget { break; case actions.Action.holdInvoicePaymentAccepted: widgets.add(_buildContactButton(context)); - + break; case actions.Action.holdInvoicePaymentSettled: case actions.Action.holdInvoicePaymentCanceled: @@ -385,6 +377,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.sendDm: case actions.Action.tradePubkey: case actions.Action.cantDo: + case actions.Action.released: // Not user-facing or not relevant as a button break; default: diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 8577ba7d..6d55a0a0 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -207,11 +207,19 @@ class MostroService { Future _getSession(MostroMessage order) async { if (order.requestId != null) { - return _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) { - return _sessionNotifier.getSessionByOrderId(order.id!)!; + final session = _sessionNotifier.getSessionByOrderId(order.id!); + if (session == null) { + throw Exception('No session found for orderId: ${order.id}'); + } + return session; } - throw Exception('No session found for order'); + throw Exception('Order has neither requestId nor orderId'); } void updateSettings(Settings settings) { From ced72931101ebd91fec6260ab3020a66729a8fc0 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 7 Jun 2025 21:34:52 -0700 Subject: [PATCH 20/26] Add integration tests for order creation and update test helpers - Created `new_order_with_mocks_test.dart` to test the creation of BUY and SELL orders with mocked providers. - Added `test_helpers.dart` to provide fake implementations for shared preferences and secure storage. - Updated `mostro_storage.dart` to log messages with order IDs instead of payload types. - Enhanced `order_state.dart` to log updates with action types. - Modified `abstract_mostro_notifier.dart` to log received messages. - Fixed potential null issues in `add_lightning_invoice_screen.dart` by providing default values. - Updated UI components in various screens to include keys for widget testing. - Removed the default widget test file as it was not relevant. - Updated mocks to include `SharedPreferencesAsync` for better testing coverage. --- .../new_order_with_mocks_test.dart | 87 +++++ integration_test/test_helpers.dart | 322 ++++++++++++++++++ lib/data/repositories/mostro_storage.dart | 9 +- lib/features/order/models/order_state.dart | 6 +- .../notfiers/abstract_mostro_notifier.dart | 1 + .../screens/add_lightning_invoice_screen.dart | 2 +- .../order/screens/add_order_screen.dart | 3 +- .../order/screens/take_order_screen.dart | 1 + .../order/widgets/action_buttons.dart | 1 + .../order/widgets/amount_section.dart | 1 + .../order/widgets/currency_section.dart | 5 +- .../widgets/payment_methods_section.dart | 1 + .../order/widgets/premium_section.dart | 1 + .../order/widgets/price_type_section.dart | 27 +- lib/main.dart | 2 +- lib/services/mostro_service.dart | 6 +- lib/services/nostr_service.dart | 1 - lib/shared/widgets/add_order_button.dart | 3 + test/mocks.dart | 3 +- test/mocks.mocks.dart | 195 +++++++++++ test/notifiers/add_order_notifier_test.dart | 7 + test/notifiers/take_order_notifier_test.dart | 7 +- test/widget_test.dart | 29 -- 23 files changed, 663 insertions(+), 57 deletions(-) create mode 100644 integration_test/new_order_with_mocks_test.dart create mode 100644 integration_test/test_helpers.dart delete mode 100644 test/widget_test.dart diff --git a/integration_test/new_order_with_mocks_test.dart b/integration_test/new_order_with_mocks_test.dart new file mode 100644 index 00000000..f6b9ceb1 --- /dev/null +++ b/integration_test/new_order_with_mocks_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter/material.dart'; +import 'test_helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Create Orders with mocked providers', () { + testWidgets('User creates BUY order with VES=100 at premium 1', (tester) async { + await pumpTestApp(tester); + + final createOrderButton = find.byKey(const Key('addOrderButton')); + expect(createOrderButton, findsOneWidget); + await tester.tap(createOrderButton); + await tester.pumpAndSettle(); + + final buyButton = find.byKey(const Key('buyButton')); + expect(buyButton, findsOneWidget); + await tester.tap(buyButton); + await tester.pumpAndSettle(); + + final fiatCodeDropdown = find.byKey(const Key('fiatCodeDropdown')); + await tester.tap(fiatCodeDropdown); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('currency_VES'))); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key('fiatAmountField')), '100'); + await tester.pumpAndSettle(); + + // Skipping tap on 'fixedSwitch' because default is already market and premium slider is visible + final premiumSlider = find.byKey(const Key('premiumSlider')); + await tester.drag(premiumSlider, const Offset(50, 0)); + await tester.pumpAndSettle(); + + //final paymentMethodField = find.byKey(const Key('paymentMethodField')); + //await tester.enterText(paymentMethodField, 'face to face'); + //await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('submitOrderButton'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('homeButton')), findsOneWidget); + }); + + testWidgets('User creates SELL order with VES=100 at premium 1', (tester) async { + await pumpTestApp(tester); + + final createOrderButton = find.byKey(const Key('addOrderButton')); + expect(createOrderButton, findsOneWidget); + await tester.tap(createOrderButton); + await tester.pumpAndSettle(); + + final sellButton = find.byKey(const Key('sellButton')); + expect(sellButton, findsOneWidget); + await tester.tap(sellButton); + await tester.pumpAndSettle(); + + final fiatCodeDropdown = find.byKey(const Key('fiatCodeDropdown')); + await tester.tap(fiatCodeDropdown); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('currency_VES'))); + await tester.pumpAndSettle(); + + await tester.enterText(find.byKey(const Key('fiatAmountField')), '100'); + await tester.pumpAndSettle(); + + final fixedSwitch = find.byKey(const Key('fixedSwitch')); + await tester.tap(fixedSwitch); + await tester.pumpAndSettle(); + + final premiumSlider = find.byKey(const Key('premiumSlider')); + await tester.drag(premiumSlider, const Offset(50, 0)); + await tester.pumpAndSettle(); + + final paymentMethodField = find.byKey(const Key('paymentMethodField')); + await tester.enterText(paymentMethodField, 'face to face'); + await tester.pumpAndSettle(); + + await tester.tap(find.text('SUBMIT')); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('homeButton')), findsOneWidget); + }); + }); +} diff --git a/integration_test/test_helpers.dart b/integration_test/test_helpers.dart new file mode 100644 index 00000000..e5567776 --- /dev/null +++ b/integration_test/test_helpers.dart @@ -0,0 +1,322 @@ +import 'dart:async'; +import 'package:dart_nostr/nostr/model/request/filter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/data/models/currency.dart'; +import 'package:mostro_mobile/background/abstract_background_service.dart'; +import 'package:mostro_mobile/core/app.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as mostro_action; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/models/session.dart'; +import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/features/settings/settings_notifier.dart'; +import 'package:mostro_mobile/features/settings/settings_provider.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/shared/notifiers/navigation_notifier.dart'; +import 'package:mostro_mobile/shared/providers/app_init_provider.dart'; +import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/mostro_database_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/navigation_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/storage_providers.dart'; +import 'package:sembast/sembast_memory.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class FakeSharedPreferencesAsync implements SharedPreferencesAsync { + final Map _store = {}; + @override + Future getString(String key) async => _store[key] as String?; + @override + Future setString(String key, String value) async { + _store[key] = value; + return true; + } + @override + Future getInt(String key) async => _store[key] as int?; + @override + Future setInt(String key, int value) async { + _store[key] = value; + return true; + } + + @override + Future clear({Set? allowList}) async { + _store.clear(); + } + + @override + Future containsKey(String key) async { + return _store.containsKey(key); + } + + @override + Future getBool(String key) async { + return _store[key] as bool?; + } + + @override + Future getDouble(String key) async { + return _store[key] as double?; + } + + @override + Future> getKeys({Set? allowList}) async { + return _store.keys.toSet(); + } + + @override + Future?> getStringList(String key) async { + return _store[key] as List?; + } + + @override + Future remove(String key) async { + _store.remove(key); + return true; + } + + @override + Future setBool(String key, bool value) async { + _store[key] = value; + return true; + } + + @override + Future setDouble(String key, double value) async { + _store[key] = value; + return true; + } + + @override + Future setStringList(String key, List value) async { + _store[key] = value; + return true; + } + + @override + Future> getAll({Set? allowList}) async { + return Map.fromEntries(_store.entries.where((e) => e.value != null).map((e) => MapEntry(e.key, e.value!))); + } +} + +class FakeSecureStorage implements FlutterSecureStorage { + final Map _store = {}; + @override + Future deleteAll({AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + _store.clear(); + } + + @override + Future read({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + return _store[key]; + } + + @override + Future write({required String key, required String? value, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + _store[key] = value; + } + + // Unused methods + @override + Future delete({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + _store.remove(key); + } + + @override + Future> readAll({AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, AppleOptions? mOptions, WindowsOptions? wOptions, WebOptions? webOptions}) async { + return Map.fromEntries(_store.entries.where((e) => e.value != null).map((e) => MapEntry(e.key, e.value!))); + } + + @override + // TODO: implement aOptions + AndroidOptions get aOptions => throw UnimplementedError(); + + @override + Future containsKey({required String key, AppleOptions? iOptions, AndroidOptions? aOptions, LinuxOptions? lOptions, WebOptions? webOptions, AppleOptions? mOptions, WindowsOptions? wOptions}) { + // TODO: implement containsKey + throw UnimplementedError(); + } + + @override + // TODO: implement iOptions + IOSOptions get iOptions => throw UnimplementedError(); + + @override + Future isCupertinoProtectedDataAvailable() { + // TODO: implement isCupertinoProtectedDataAvailable + throw UnimplementedError(); + } + + @override + // TODO: implement lOptions + LinuxOptions get lOptions => throw UnimplementedError(); + + @override + // TODO: implement mOptions + AppleOptions get mOptions => throw UnimplementedError(); + + @override + // TODO: implement onCupertinoProtectedDataAvailabilityChanged + Stream? get onCupertinoProtectedDataAvailabilityChanged => throw UnimplementedError(); + + @override + void registerListener({required String key, required ValueChanged listener}) { + // TODO: implement registerListener + } + + @override + void unregisterAllListeners() { + // TODO: implement unregisterAllListeners + } + + @override + void unregisterAllListenersForKey({required String key}) { + // TODO: implement unregisterAllListenersForKey + } + + @override + void unregisterListener({required String key, required ValueChanged listener}) { + // TODO: implement unregisterListener + } + + @override + // TODO: implement wOptions + WindowsOptions get wOptions => throw UnimplementedError(); + + @override + // TODO: implement webOptions + WebOptions get webOptions => throw UnimplementedError(); +} + +class FakeBackgroundService implements BackgroundService { + @override + Future init() async {} + @override + void subscribe(List filters) {} + @override + void updateSettings(settings) {} + @override + Future setForegroundStatus(bool isForeground) async {} + @override + Future unsubscribe(String subscriptionId) async => true; + @override + Future unsubscribeAll() async {} + @override + Future getActiveSubscriptionCount() async => 0; + @override + bool get isRunning => false; +} + +class FakeMostroService implements MostroService { + FakeMostroService(this.ref); + @override + final Ref ref; + + @override + void init() {} + + @override + void subscribe(Session session) {} + + @override + Session? getSessionByOrderId(String orderId) => null; + + @override + Future submitOrder(MostroMessage order) async { + final storage = ref.read(mostroStorageProvider); + final orderMsg = MostroMessage( + action: mostro_action.Action.newOrder, + id: 'order_${order.requestId}', + requestId: order.requestId, + payload: order.payload as Order, + ); + await storage.addMessage('msg_${order.requestId}', orderMsg); + } + + @override + Future takeBuyOrder(String orderId, int? amount) async {} + + @override + Future takeSellOrder(String orderId, int? amount, String? lnAddress) async {} + + @override + Future sendInvoice(String orderId, String invoice, int? amount) async {} + + @override + Future cancelOrder(String orderId) async {} + + @override + Future sendFiatSent(String orderId) async {} + + @override + Future releaseOrder(String orderId) async {} + + @override + Future disputeOrder(String orderId) async {} + + @override + Future submitRating(String orderId, int rating) async {} + + @override + Future publishOrder(MostroMessage order) => throw UnimplementedError(); + + @override + void updateSettings(Settings settings) {} +} + +Future pumpTestApp(WidgetTester tester) async { + final prefs = FakeSharedPreferencesAsync(); + final secure = FakeSecureStorage(); + final db = await databaseFactoryMemory.openDatabase('mostro.db'); + final eventsDb = await databaseFactoryMemory.openDatabase('events.db'); + final storage = MostroStorage(db: db); + final settingsNotifier = SettingsNotifier(prefs); + await settingsNotifier.init(); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + settingsProvider.overrideWith((ref) => settingsNotifier), + currencyCodesProvider.overrideWith((ref) async => { + 'VES': Currency( + symbol: 'Bs', + name: 'Venezuelan Bolívar', + symbolNative: 'Bs', + code: 'VES', + emoji: '🇻🇪', + decimalDigits: 2, + namePlural: 'Venezuelan bolívars', + price: false, + ), + 'USD': Currency( + symbol: '\$', + name: 'US Dollar', + symbolNative: '\$', + code: 'USD', + emoji: '🇺🇸', + decimalDigits: 2, + namePlural: 'US dollars', + price: false, + ), + }), + sharedPreferencesProvider.overrideWithValue(prefs), + secureStorageProvider.overrideWithValue(secure), + mostroDatabaseProvider.overrideWithValue(db), + eventDatabaseProvider.overrideWithValue(eventsDb), + mostroStorageProvider.overrideWithValue(storage), + backgroundServiceProvider.overrideWithValue(FakeBackgroundService()), + mostroServiceProvider.overrideWith((ref) => FakeMostroService(ref)), + navigationProvider.overrideWith((ref) => NavigationNotifier()), + appInitializerProvider.overrideWith((ref) => Future.value()), + ], + child: const MostroApp(), + ), + ); + await tester.pumpAndSettle(); +} diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart index 92510942..9975fa36 100644 --- a/lib/data/repositories/mostro_storage.dart +++ b/lib/data/repositories/mostro_storage.dart @@ -23,7 +23,7 @@ class MostroStorage extends BaseStorage { await store.record(id).put(db, dbMap); _logger.i( - 'Saved message of type ${message.payload?.runtimeType} with id $id', + 'Saved message of type ${message.action} with order id ${message.id}', ); } catch (e, stack) { _logger.e( @@ -130,10 +130,9 @@ class MostroStorage extends BaseStorage { limit: 1, ), ); - - return query - .onSnapshots(db) - .map((snaps) => snaps.isEmpty ? null : MostroMessage.fromJson(snaps.first.value)); + + return query.onSnapshots(db).map((snaps) => + snaps.isEmpty ? null : MostroMessage.fromJson(snaps.first.value)); } /// Stream of the latest message for an order whose payload is of type T diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index cbb935ce..5b59bc89 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -1,3 +1,4 @@ +import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/data/enums.dart'; @@ -8,7 +9,8 @@ class OrderState { final PaymentRequest? paymentRequest; final CantDo? cantDo; final Dispute? dispute; -final Peer? peer; + final Peer? peer; + final _logger = Logger(); OrderState({ required this.status, @@ -80,6 +82,7 @@ final Peer? peer; } OrderState updateWith(MostroMessage message) { + _logger.i('Updating OrderState Action: ${message.action}'); return copyWith( status: message.getPayload()?.status ?? status, action: message.action != Action.cantDo ? message.action : action, @@ -136,7 +139,6 @@ final Peer? peer; Action.cooperativeCancelInitiatedByPeer: [ Action.cancel, ], - }, Status.waitingPayment: { Action.payInvoice: [ diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index 0194b9f9..cb32348b 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -35,6 +35,7 @@ class AbstractMostroNotifier extends StateNotifier { (_, next) { next.when( data: (MostroMessage? msg) { + logger.i('Received message: ${msg?.toJson()}'); if (msg != null) { handleEvent(msg); } diff --git a/lib/features/order/screens/add_lightning_invoice_screen.dart b/lib/features/order/screens/add_lightning_invoice_screen.dart index 634a68f1..3d1495c7 100644 --- a/lib/features/order/screens/add_lightning_invoice_screen.dart +++ b/lib/features/order/screens/add_lightning_invoice_screen.dart @@ -81,7 +81,7 @@ class _AddLightningInvoiceScreenState ); } }, - amount: amount!, + amount: amount ?? 0, ), ), ), diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index 39c62f82..68334a13 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -204,13 +204,14 @@ class _AddOrderScreenState extends ConsumerState { decoration: BoxDecoration( border: Border( top: BorderSide( - color: Colors.white.withOpacity(0.1), + color: Colors.white.withValues(alpha: 0.1), width: 1, ), ), ), padding: const EdgeInsets.only(top: 16), child: ActionButtons( + key: const Key('addOrderButtons'), onCancel: () => context.pop(), onSubmit: _submitOrder, currentRequestId: _currentRequestId, diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index f4b39938..c9061346 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -195,6 +195,7 @@ class TakeOrderScreen extends ConsumerWidget { child: const Text('Cancel'), ), ElevatedButton( + key: const Key('submitAmountButton'), onPressed: () { final inputAmount = int.tryParse( _fiatAmountController.text.trim()); diff --git a/lib/features/order/widgets/action_buttons.dart b/lib/features/order/widgets/action_buttons.dart index 8b632300..6501c405 100644 --- a/lib/features/order/widgets/action_buttons.dart +++ b/lib/features/order/widgets/action_buttons.dart @@ -42,6 +42,7 @@ class ActionButtons extends StatelessWidget { child: SizedBox( height: 48, child: MostroReactiveButton( + key: const Key('submitOrderButton'), label: 'Submit', buttonStyle: ButtonStyleType.raised, orderId: currentRequestId?.toString() ?? '', diff --git a/lib/features/order/widgets/amount_section.dart b/lib/features/order/widgets/amount_section.dart index 7b8e6768..cc5b8cef 100644 --- a/lib/features/order/widgets/amount_section.dart +++ b/lib/features/order/widgets/amount_section.dart @@ -23,6 +23,7 @@ class AmountSection extends StatelessWidget { icon: const Icon(Icons.credit_card, color: Color(0xFF8CC63F), size: 18), iconBackgroundColor: const Color(0xFF8CC63F).withOpacity(0.3), child: TextFormField( + key: const Key('fiatAmountField'), controller: controller, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( diff --git a/lib/features/order/widgets/currency_section.dart b/lib/features/order/widgets/currency_section.dart index ca23c661..063fb925 100644 --- a/lib/features/order/widgets/currency_section.dart +++ b/lib/features/order/widgets/currency_section.dart @@ -22,7 +22,7 @@ class CurrencySection extends ConsumerWidget { : 'Select the Fiat Currency you want to receive', icon: const Text('\$', style: TextStyle(color: Color(0xFF8CC63F), fontSize: 18)), - iconBackgroundColor: const Color(0xFF764BA2).withOpacity(0.3), + iconBackgroundColor: const Color(0xFF764BA2).withValues(alpha: 0.3), child: currenciesAsync.when( loading: () => const Text('Loading currencies...', style: TextStyle(color: Colors.white)), @@ -39,6 +39,7 @@ class CurrencySection extends ConsumerWidget { } return InkWell( + key: const Key('fiatCodeDropdown'), onTap: () { _showCurrencySelectionDialog(context, ref, onCurrencySelected); }, @@ -46,6 +47,7 @@ class CurrencySection extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( + key: Key('currency_$selectedFiatCode'), children: [ Text(flag, style: const TextStyle(fontSize: 18)), const SizedBox(width: 8), @@ -162,6 +164,7 @@ class CurrencySection extends ConsumerWidget { final isSelected = code == selectedCode; return ListTile( + key: Key('currency_$code'), leading: Text( currency.emoji.isNotEmpty ? currency.emoji diff --git a/lib/features/order/widgets/payment_methods_section.dart b/lib/features/order/widgets/payment_methods_section.dart index 5330f3e0..e2c1806d 100644 --- a/lib/features/order/widgets/payment_methods_section.dart +++ b/lib/features/order/widgets/payment_methods_section.dart @@ -32,6 +32,7 @@ class PaymentMethodsSection extends ConsumerWidget { ? Padding( padding: const EdgeInsets.fromLTRB(0, 8, 0, 0), child: TextField( + key: const Key('paymentMethodField'), controller: customController, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( diff --git a/lib/features/order/widgets/premium_section.dart b/lib/features/order/widgets/premium_section.dart index 6ddbf759..eb87ea56 100644 --- a/lib/features/order/widgets/premium_section.dart +++ b/lib/features/order/widgets/premium_section.dart @@ -38,6 +38,7 @@ class PremiumSection extends StatelessWidget { trackHeight: 4, ), child: Slider( + key: const Key('premiumSlider'), value: value, min: -10, max: 10, diff --git a/lib/features/order/widgets/price_type_section.dart b/lib/features/order/widgets/price_type_section.dart index 321ecde6..8f3d39b0 100644 --- a/lib/features/order/widgets/price_type_section.dart +++ b/lib/features/order/widgets/price_type_section.dart @@ -24,7 +24,19 @@ class PriceTypeSection extends StatelessWidget { return FormSection( title: 'Price type', icon: priceTypeIcon, - iconBackgroundColor: AppTheme.purpleAccent.withOpacity(0.3), // Purple color consistent with other sections + iconBackgroundColor: AppTheme.purpleAccent.withValues(alpha: 0.3), + // Add info icon as extra content + extraContent: Padding( + padding: const EdgeInsets.only(right: 16, bottom: 8), + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.info_outline, + size: 14, + color: AppTheme.textSubtle, + ), + ), + ), // Purple color consistent with other sections child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -45,6 +57,7 @@ class PriceTypeSection extends StatelessWidget { ), ), Switch( + key: const Key('fixedSwitch'), value: isMarketRate, activeColor: AppTheme.purpleAccent, // Keep the purple accent color onChanged: onToggle, @@ -53,18 +66,6 @@ class PriceTypeSection extends StatelessWidget { ), ], ), - // Add info icon as extra content - extraContent: Padding( - padding: const EdgeInsets.only(right: 16, bottom: 8), - child: Align( - alignment: Alignment.centerRight, - child: Icon( - Icons.info_outline, - size: 14, - color: AppTheme.textSubtle, - ), - ), - ), ); } } diff --git a/lib/main.dart b/lib/main.dart index 65330b9e..83882abd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - if (Platform.isAndroid) { + if (Platform.isAndroid && !Platform.environment.containsKey('FLUTTER_TEST')) { await requestNotificationPermissionIfNeeded(); } diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 6d55a0a0..f132df8c 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -10,10 +10,12 @@ 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(); Settings _settings; @@ -85,10 +87,12 @@ class MostroService { final result = jsonDecode(decryptedEvent.content!); if (result is! List) return; - result[0]['timestamp'] = decryptedEvent.createdAt?.millisecondsSinceEpoch; final msg = MostroMessage.fromJson(result[0]); final messageStorage = ref.read(mostroStorageProvider); await messageStorage.addMessage(decryptedEvent.id!, msg); + _logger.i( + 'Received message of type ${msg.action} with order id ${msg.id}', + ); }); } diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index 5dd88796..fc0fd826 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -80,7 +80,6 @@ class NostrService { event, timeout: Config.nostrConnectionTimeout, ); - _logger.i('Event published successfully'); } catch (e) { _logger.w('Failed to publish event: $e'); rethrow; diff --git a/lib/shared/widgets/add_order_button.dart b/lib/shared/widgets/add_order_button.dart index daacacfe..dd34f5e0 100644 --- a/lib/shared/widgets/add_order_button.dart +++ b/lib/shared/widgets/add_order_button.dart @@ -76,6 +76,7 @@ class _AddOrderButtonState extends State mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( + key: const Key('buyButton'), onPressed: _isMenuOpen ? () => _navigateToCreateOrder(context, 'buy') : null, @@ -94,6 +95,7 @@ class _AddOrderButtonState extends State ), const SizedBox(width: 8), ElevatedButton.icon( + key: const Key('sellButton'), onPressed: _isMenuOpen ? () => _navigateToCreateOrder(context, 'sell') : null, @@ -117,6 +119,7 @@ class _AddOrderButtonState extends State // Botón principal siempre visible FloatingActionButton( + key: const Key('addOrderButton'), onPressed: _toggleMenu, backgroundColor: _isMenuOpen ? Colors.grey.shade700 : AppTheme.activeColor, diff --git a/test/mocks.dart b/test/mocks.dart index b64e487c..c594c110 100644 --- a/test/mocks.dart +++ b/test/mocks.dart @@ -1,6 +1,7 @@ import 'package:mockito/annotations.dart'; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -@GenerateMocks([MostroService, OpenOrdersRepository]) +@GenerateMocks([MostroService, OpenOrdersRepository, SharedPreferencesAsync]) void main() {} diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 45acbb9c..08834299 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -13,6 +13,7 @@ import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' as _i7; import 'package:mostro_mobile/features/settings/settings.dart' as _i6; import 'package:mostro_mobile/services/mostro_service.dart' as _i3; +import 'package:shared_preferences/src/shared_preferences_async.dart' as _i9; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -316,3 +317,197 @@ class MockOpenOrdersRepository extends _i1.Mock returnValueForMissingStub: null, ); } + +/// 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 _i9.SharedPreferencesAsync { + MockSharedPreferencesAsync() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future> getKeys({Set? allowList}) => + (super.noSuchMethod( + Invocation.method( + #getKeys, + [], + {#allowList: allowList}, + ), + returnValue: _i5.Future>.value({}), + ) as _i5.Future>); + + @override + _i5.Future> getAll({Set? allowList}) => + (super.noSuchMethod( + Invocation.method( + #getAll, + [], + {#allowList: allowList}, + ), + returnValue: + _i5.Future>.value({}), + ) as _i5.Future>); + + @override + _i5.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getDouble(String? key) => (super.noSuchMethod( + Invocation.method( + #getDouble, + [key], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future?> getStringList(String? key) => (super.noSuchMethod( + Invocation.method( + #getStringList, + [key], + ), + returnValue: _i5.Future?>.value(), + ) as _i5.Future?>); + + @override + _i5.Future containsKey(String? key) => (super.noSuchMethod( + Invocation.method( + #containsKey, + [key], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + + @override + _i5.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setDouble( + String? key, + double? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setDouble, + [ + key, + value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setStringList( + String? key, + List? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setStringList, + [ + key, + value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future remove(String? key) => (super.noSuchMethod( + Invocation.method( + #remove, + [key], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future clear({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #clear, + [], + {#allowList: allowList}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} diff --git a/test/notifiers/add_order_notifier_test.dart b/test/notifiers/add_order_notifier_test.dart index 54beb90e..1c57edc2 100644 --- a/test/notifiers/add_order_notifier_test.dart +++ b/test/notifiers/add_order_notifier_test.dart @@ -7,6 +7,7 @@ import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'package:mostro_mobile/shared/providers/storage_providers.dart'; import '../mocks.mocks.dart'; @@ -17,6 +18,7 @@ void main() { late ProviderContainer container; late MockMostroService mockMostroService; late MockOpenOrdersRepository mockOrdersRepository; + late MockSharedPreferencesAsync mockSharedPreferencesAsync; const testUuid = "test_uuid"; @@ -24,6 +26,7 @@ void main() { container = ProviderContainer(); mockMostroService = MockMostroService(); mockOrdersRepository = MockOpenOrdersRepository(); + mockSharedPreferencesAsync = MockSharedPreferencesAsync(); }); tearDown(() { @@ -70,6 +73,7 @@ void main() { container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), + sharedPreferencesProvider.overrideWithValue(mockSharedPreferencesAsync), ]); // Create a new sell (fixed) order. @@ -136,6 +140,7 @@ void main() { container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), + sharedPreferencesProvider.overrideWithValue(mockSharedPreferencesAsync), ]); final newSellRangeOrder = Order( @@ -200,6 +205,7 @@ void main() { container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), + sharedPreferencesProvider.overrideWithValue(mockSharedPreferencesAsync), ]); final newBuyOrder = Order( @@ -261,6 +267,7 @@ void main() { container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), orderRepositoryProvider.overrideWithValue(mockOrdersRepository), + sharedPreferencesProvider.overrideWithValue(mockSharedPreferencesAsync), ]); final newBuyOrderWithInvoice = Order( diff --git a/test/notifiers/take_order_notifier_test.dart b/test/notifiers/take_order_notifier_test.dart index e192cb6d..7d92d16d 100644 --- a/test/notifiers/take_order_notifier_test.dart +++ b/test/notifiers/take_order_notifier_test.dart @@ -5,6 +5,7 @@ import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/storage_providers.dart'; import '../mocks.mocks.dart'; @@ -14,12 +15,13 @@ void main() { group('Take Order Notifiers - Mockito tests', () { late ProviderContainer container; late MockMostroService mockMostroService; + late MockSharedPreferencesAsync mockPreferences; const testOrderId = "test_order_id"; setUp(() { // Create a new instance of the mock repository. mockMostroService = MockMostroService(); - + mockPreferences = MockSharedPreferencesAsync(); }); tearDown(() { @@ -124,6 +126,7 @@ void main() { // Override the repository provider with our mock. container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), + sharedPreferencesProvider.overrideWithValue(mockPreferences), ]); final takeSellNotifier = @@ -178,6 +181,7 @@ void main() { // Override the repository provider with our mock. container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), + sharedPreferencesProvider.overrideWithValue(mockPreferences), ]); final takeSellNotifier = @@ -216,6 +220,7 @@ void main() { // Override the repository provider with our mock. container = ProviderContainer(overrides: [ mostroServiceProvider.overrideWithValue(mockMostroService), + sharedPreferencesProvider.overrideWithValue(mockPreferences), ]); final takeSellNotifier = diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 170015b6..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mostro_mobile/core/app.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MostroApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From aadce3b48a34fb0d12fb9fc103eafedcd146b069 Mon Sep 17 00:00:00 2001 From: Biz Date: Sat, 7 Jun 2025 21:56:27 -0700 Subject: [PATCH 21/26] fix: downgrade intl package version to resolve compatibility issues --- pubspec.lock | 24 ++++++++++++------------ pubspec.yaml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 945f74f9..180f5b1e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,10 +50,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" base58check: dependency: transitive description: @@ -354,10 +354,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -749,10 +749,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.19.0" introduction_screen: dependency: "direct main" description: @@ -789,10 +789,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -1507,10 +1507,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -1539,10 +1539,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 52fd59ce..c1da6886 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: bitcoin_icons: ^0.0.4 collection: ^1.18.0 elliptic: ^0.3.11 - intl: ^0.20.2 + intl: ^0.19.0 uuid: ^3.0.7 flutter_secure_storage: ^10.0.0-beta.4 go_router: ^15.0.0 From e0c99065cf304ac92fe147881e06a09800f1f844 Mon Sep 17 00:00:00 2001 From: Biz Date: Sun, 8 Jun 2025 09:29:16 -0700 Subject: [PATCH 22/26] refactor: remove system tray dependency and update packages to latest versions --- .github/workflows/main.yml | 5 +- README.md | 9 + lib/generated/intl/messages_all.dart | 63 ------- lib/generated/intl/messages_en.dart | 162 ----------------- lib/shared/utils/tray_manager.dart | 70 ------- linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 171 +++++++++--------- pubspec.yaml | 11 +- test/mocks.mocks.dart | 2 +- test/services/mostro_service_test.mocks.dart | 50 ++--- .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 14 files changed, 127 insertions(+), 427 deletions(-) delete mode 100644 lib/generated/intl/messages_all.dart delete mode 100644 lib/generated/intl/messages_en.dart delete mode 100644 lib/shared/utils/tray_manager.dart diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1a8c253..1b0384e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,12 +25,15 @@ jobs: - name: Set Up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.0' + flutter-version: '3.32.2' channel: 'stable' - name: Install Dependencies run: flutter pub get + - name: Generate localization and other required files + run: dart run build_runner build -d + - name: Extract version from pubspec.yaml id: extract_version run: | diff --git a/README.md b/README.md index cd79f2bb..51abbe84 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,15 @@ This project is a mobile interface that facilitates peer-to-peer bitcoin trading flutter pub get ``` +3. Generate localization and other required files: + + ```bash + dart run build_runner build -d + ``` + +> **Note:** +> These commands generate files needed by `flutter_intl` and any other code generators. You must run them after installing dependencies and whenever you update localization files or code generation sources. If you skip this step, you may encounter missing file errors when running the app. + ## Running the App ### On Emulator/Simulator diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart deleted file mode 100644 index 203415cc..00000000 --- a/lib/generated/intl/messages_all.dart +++ /dev/null @@ -1,63 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that looks up messages for specific locales by -// delegating to the appropriate library. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:implementation_imports, file_names, unnecessary_new -// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering -// ignore_for_file:argument_type_not_assignable, invalid_assignment -// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases -// ignore_for_file:comment_references - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; -import 'package:intl/src/intl_helpers.dart'; - -import 'messages_en.dart' as messages_en; - -typedef Future LibraryLoader(); -Map _deferredLibraries = { - 'en': () => new SynchronousFuture(null), -}; - -MessageLookupByLibrary? _findExact(String localeName) { - switch (localeName) { - case 'en': - return messages_en.messages; - default: - return null; - } -} - -/// User programs should call this before using [localeName] for messages. -Future initializeMessages(String localeName) { - var availableLocale = Intl.verifiedLocale( - localeName, (locale) => _deferredLibraries[locale] != null, - onFailure: (_) => null); - if (availableLocale == null) { - return new SynchronousFuture(false); - } - var lib = _deferredLibraries[availableLocale]; - lib == null ? new SynchronousFuture(false) : lib(); - initializeInternalMessageLookup(() => new CompositeMessageLookup()); - messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); - return new SynchronousFuture(true); -} - -bool _messagesExistFor(String locale) { - try { - return _findExact(locale) != null; - } catch (e) { - return false; - } -} - -MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { - var actualLocale = - Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); - if (actualLocale == null) return null; - return _findExact(actualLocale); -} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart deleted file mode 100644 index 7f19523f..00000000 --- a/lib/generated/intl/messages_en.dart +++ /dev/null @@ -1,162 +0,0 @@ -// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart -// This is a library that provides messages for a en locale. All the -// messages from the main program should be duplicated here with the same -// function name. - -// Ignore issues from commonly used lints in this file. -// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new -// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering -// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases -// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes -// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes - -import 'package:intl/intl.dart'; -import 'package:intl/message_lookup_by_library.dart'; - -final messages = new MessageLookup(); - -typedef String MessageIfAbsent(String messageStr, List args); - -class MessageLookup extends MessageLookupByLibrary { - String get localeName => 'en'; - - static String m0(amount, fiat_code, fiat_amount, expiration_seconds) => - "Please send me an invoice for ${amount} satoshis equivalent to ${fiat_code} ${fiat_amount}. This is where I\'ll send the funds upon completion of the trade. If you don\'t provide the invoice within ${expiration_seconds} this trade will be cancelled."; - - static String m1(npub) => "You have successfully added the solver ${npub}."; - - static String m2(id) => "You have cancelled the order ID: ${id}!"; - - static String m3(id) => "Admin has cancelled the order ID: ${id}!"; - - static String m4(id) => "You have completed the order ID: ${id}!"; - - static String m5(id) => "Admin has completed the order ID: ${id}!"; - - static String m6(details) => - "Here are the details of the dispute order you have taken: ${details}. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed."; - - static String m7(admin_npub) => - "The solver ${admin_npub} will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token."; - - static String m8(buyer_npub, fiat_code, fiat_amount, payment_method) => - "Get in touch with the buyer, this is their npub ${buyer_npub} to inform them how to send you ${fiat_code} ${fiat_amount} through ${payment_method}. I will notify you once the buyer indicates the fiat money has been sent. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first."; - - static String m9(id) => "You have cancelled the order ID: ${id}!"; - - static String m10(action) => - "You are not allowed to ${action} for this order!"; - - static String m11(id) => "Order ${id} has been successfully cancelled!"; - - static String m12(id) => - "Your counterparty wants to cancel order ID: ${id}. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message."; - - static String m13(id) => - "You have initiated the cancellation of the order ID: ${id}. Your counterparty must agree to the cancellation too. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first."; - - static String m14(id, user_token) => - "Your counterparty has initiated a dispute for order Id: ${id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: ${user_token}."; - - static String m15(id, user_token) => - "You have initiated a dispute for order Id: ${id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: ${user_token}."; - - static String m16(seller_npub) => - "I have informed ${seller_npub} that you have sent the fiat money. When the seller confirms they have received your fiat money, they should release the funds. If they refuse, you can open a dispute."; - - static String m17(buyer_npub) => - "${buyer_npub} has informed that they have sent you the fiat money. Once you confirm receipt, please release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, tap the release button."; - - static String m18(seller_npub, id, fiat_code, fiat_amount, payment_method) => - "Get in touch with the seller, this is their npub ${seller_npub} to get the details on how to send the fiat money for the order ${id}, you must send ${fiat_code} ${fiat_amount} using ${payment_method}. Once you send the fiat money, please let me know with fiat-sent."; - - static String m19(buyer_npub) => - "Your Sats sale has been completed after confirming the payment from ${buyer_npub}."; - - static String m20(amount) => - "The amount stated in the invoice is incorrect. Please send an invoice with an amount of ${amount} satoshis, an invoice without an amount, or a lightning address."; - - static String m21(action) => - "You did not create this order and are not authorized to ${action} it."; - - static String m22(expiration_hours) => - "Your offer has been published! Please wait until another user picks your order. It will be available for ${expiration_hours} hours. You can cancel this order before another user picks it up by executing: cancel."; - - static String m23(action, id, order_status) => - "You are not allowed to ${action} because order Id ${id} status is ${order_status}."; - - static String m24(min_amount, max_amount) => - "The requested amount is incorrect and may be outside the acceptable range. The minimum is ${min_amount} and the maximum is ${max_amount}."; - - static String m25(min_order_amount, max_order_amount) => - "The allowed Sats amount for this Mostro is between min ${min_order_amount} and max ${max_order_amount}. Please enter an amount within this range."; - - static String m26(amount, fiat_code, fiat_amount, expiration_seconds) => - "Please pay this hold invoice of ${amount} Sats for ${fiat_code} ${fiat_amount} to start the operation. If you do not pay it within ${expiration_seconds} the trade will be cancelled."; - - static String m27(payment_attempts, payment_retries_interval) => - "I tried to send you the Sats but the payment of your invoice failed. I will try ${payment_attempts} more times in ${payment_retries_interval} minutes window. Please ensure your node/wallet is online."; - - static String m28(seller_npub) => - "${seller_npub} has already released the Sats! Expect your invoice to be paid any time. Remember your wallet needs to be online to receive through the Lightning Network."; - - static String m29(expiration_seconds) => - "Payment received! Your Sats are now \'held\' in your own wallet. Please wait a bit. I\'ve requested the buyer to provide an invoice. Once they do, I\'ll connect you both. If they do not do so within ${expiration_seconds} your Sats will be available in your wallet again and the trade will be cancelled."; - - static String m30(id, expiration_seconds) => - "Please wait a bit. I\'ve sent a payment request to the seller to send the Sats for the order ID ${id}. Once the payment is made, I\'ll connect you both. If the seller doesn\'t complete the payment within ${expiration_seconds} minutes the trade will be cancelled."; - - final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "addInvoice": m0, - "adminAddSolver": m1, - "adminCanceledAdmin": m2, - "adminCanceledUsers": m3, - "adminSettledAdmin": m4, - "adminSettledUsers": m5, - "adminTookDisputeAdmin": m6, - "adminTookDisputeUsers": m7, - "buyerInvoiceAccepted": MessageLookupByLibrary.simpleMessage( - "Invoice has been successfully saved!"), - "buyerTookOrder": m8, - "canceled": m9, - "cantDo": m10, - "cooperativeCancelAccepted": m11, - "cooperativeCancelInitiatedByPeer": m12, - "cooperativeCancelInitiatedByYou": m13, - "disputeInitiatedByPeer": m14, - "disputeInitiatedByYou": m15, - "fiatSentOkBuyer": m16, - "fiatSentOkSeller": m17, - "holdInvoicePaymentAccepted": m18, - "holdInvoicePaymentCanceled": MessageLookupByLibrary.simpleMessage( - "The invoice was cancelled; your Sats will be available in your wallet again."), - "holdInvoicePaymentSettled": m19, - "incorrectInvoiceAmountBuyerAddInvoice": m20, - "incorrectInvoiceAmountBuyerNewOrder": MessageLookupByLibrary.simpleMessage( - "An invoice with non-zero amount was received for the new order. Please send an invoice with a zero amount or no invoice at all."), - "invalidSatsAmount": MessageLookupByLibrary.simpleMessage( - "The specified Sats amount is invalid."), - "invoiceUpdated": MessageLookupByLibrary.simpleMessage( - "Invoice has been successfully updated!"), - "isNotYourDispute": MessageLookupByLibrary.simpleMessage( - "This dispute was not assigned to you!"), - "isNotYourOrder": m21, - "newOrder": m22, - "notAllowedByStatus": m23, - "notFound": MessageLookupByLibrary.simpleMessage("Dispute not found."), - "outOfRangeFiatAmount": m24, - "outOfRangeSatsAmount": m25, - "payInvoice": m26, - "paymentFailed": m27, - "purchaseCompleted": MessageLookupByLibrary.simpleMessage( - "Your satoshis purchase has been completed successfully. I have paid your invoice, enjoy sound money!"), - "rate": MessageLookupByLibrary.simpleMessage( - "Please rate your counterparty"), - "rateReceived": - MessageLookupByLibrary.simpleMessage("Rating successfully saved!"), - "released": m28, - "waitingBuyerInvoice": m29, - "waitingSellerToPay": m30 - }; -} diff --git a/lib/shared/utils/tray_manager.dart b/lib/shared/utils/tray_manager.dart deleted file mode 100644 index b7151d8b..00000000 --- a/lib/shared/utils/tray_manager.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:system_tray/system_tray.dart'; - -class TrayManager { - static final TrayManager _instance = TrayManager._internal(); - factory TrayManager() => _instance; - - final SystemTray _tray = SystemTray(); - - TrayManager._internal(); - - Future init( - GlobalKey navigatorKey, { - String iconPath = 'assets/images/launcher-icon.png', - }) async { - try { - await _tray.initSystemTray( - iconPath: iconPath, - toolTip: "Mostro is running", - title: '', - ); - - final menu = Menu(); - - menu.buildFrom([ - MenuItemLabel( - label: 'Open Mostro', - onClicked: (menuItem) { - navigatorKey.currentState?.pushNamed('/'); - }, - ), - MenuItemLabel( - label: 'Quit', - onClicked: (menuItem) async { - await dispose(); - Future.delayed(const Duration(milliseconds: 300), () { - if (Platform.isAndroid || Platform.isIOS) { - SystemNavigator.pop(); - } else { - exit(0); // Only as a last resort on desktop - } - }); - }, - ), - ]); - - await _tray.setContextMenu(menu); - - // Handle tray icon click (e.g., double click = open) - _tray.registerSystemTrayEventHandler((eventName) { - if (eventName == kSystemTrayEventClick) { - navigatorKey.currentState?.pushNamed('/'); - } - }); - } catch (e) { - debugPrint('Failed to initialize system tray: $e'); - } - } - - Future dispose() async { - try { - await _tray.destroy(); - } catch (e) { - debugPrint('Failed to destroy system tray: $e'); - } - } -} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 54a1276e..d0e7f797 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,13 +7,9 @@ #include "generated_plugin_registrant.h" #include -#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); - g_autoptr(FlPluginRegistrar) system_tray_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin"); - system_tray_plugin_register_with_registrar(system_tray_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 72b5a632..b29e9ba0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux - system_tray ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 30dc8f04..717dfd4b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,7 +10,6 @@ import flutter_secure_storage_darwin import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import system_tray func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) @@ -18,5 +17,4 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 180f5b1e..265e5a8e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "82.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.4.5" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.1" archive: dependency: transitive description: @@ -50,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" base58check: dependency: transitive description: @@ -162,10 +157,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.15" build_runner_core: dependency: transitive description: @@ -186,10 +181,10 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.10.1" characters: dependency: transitive description: @@ -202,10 +197,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" circular_countdown: dependency: "direct main" description: @@ -266,10 +261,10 @@ packages: dependency: transitive description: name: coverage - sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" + sha256: aa07dbe5f2294c827b7edb9a87bba44a9c15a3cc81bc8da2ca19b37322d30080 url: "https://pub.dev" source: hosted - version: "1.13.1" + version: "1.14.1" crypto: dependency: "direct main" description: @@ -298,10 +293,18 @@ packages: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" + url: "https://pub.dev" + source: hosted + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "1.0.0+7.4.5" dart_nostr: dependency: "direct main" description: @@ -314,10 +317,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" dbus: dependency: transitive description: @@ -354,10 +357,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -500,18 +503,18 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: "33b3e0269ae9d51669957a923f2376bee96299b09915d856395af8c4238aebfa" + sha256: b94a50aabbe56ef254f95f3be75640f99120429f0a153b2dc30143cffc9bfdf3 url: "https://pub.dev" source: hosted - version: "19.1.0" + version: "19.2.1" flutter_local_notifications_linux: dependency: transitive description: @@ -627,10 +630,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -656,10 +659,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2b9ba6d4c247457c35a6622f1dee6aab6694a4e15237ff7c32320345044112b6" + sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175 url: "https://pub.dev" source: hosted - version: "15.1.1" + version: "15.1.3" google_fonts: dependency: "direct main" description: @@ -704,10 +707,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -728,10 +731,10 @@ packages: dependency: transitive description: name: idb_shim - sha256: d3dae2085f2dcc9d05b851331fddb66d57d3447ff800de9676b396795436e135 + sha256: "109ce57e7ae8a758e806c24669bf7809f0e4c0bc390159589819061ec2ca75c0" url: "https://pub.dev" source: hosted - version: "2.6.5+1" + version: "2.6.6+2" image: dependency: transitive description: @@ -749,10 +752,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" introduction_screen: dependency: "direct main" description: @@ -789,10 +792,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -821,10 +824,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" local_auth: dependency: "direct main" description: @@ -889,14 +892,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.257.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -933,10 +928,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a" url: "https://pub.dev" source: hosted - version: "5.4.5" + version: "5.4.6" nip44: dependency: "direct main" description: @@ -1174,10 +1169,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: @@ -1190,18 +1185,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" sembast: dependency: "direct main" description: name: sembast - sha256: d3f0d0ba501a5f1fd7d6c8532ee01385977c8a069c334635dae390d059ae3d6d + sha256: "7119cf6f79bd1d48c8ec7943f4facd96c15ab26823021ed0792a487c7cd34441" url: "https://pub.dev" source: hosted - version: "3.8.5" + version: "3.8.5+1" sembast_web: dependency: "direct main" description: @@ -1294,10 +1289,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 @@ -1307,10 +1302,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_map_stack_trace: dependency: transitive description: @@ -1335,6 +1330,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -1391,14 +1394,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - system_tray: - dependency: "direct main" - description: - name: system_tray - sha256: "40444e5de8ed907822a98694fd031b8accc3cb3c0baa547634ce76189cf3d9cf" - url: "https://pub.dev" - source: hosted - version: "2.0.3" term_glyph: dependency: transitive description: @@ -1467,10 +1462,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -1491,10 +1486,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -1507,10 +1502,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: @@ -1523,26 +1518,26 @@ packages: 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: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" webkit_inspection_protocol: dependency: transitive description: @@ -1555,10 +1550,10 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.0" xdg_directories: dependency: transitive description: @@ -1584,5 +1579,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <=3.9.9" + dart: ">=3.8.0 <=3.9.9" flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index c1da6886..8a5a7863 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,8 +48,8 @@ dependencies: bitcoin_icons: ^0.0.4 collection: ^1.18.0 elliptic: ^0.3.11 - intl: ^0.19.0 - uuid: ^3.0.7 + intl: ^0.20.2 + uuid: ^4.5.1 flutter_secure_storage: ^10.0.0-beta.4 go_router: ^15.0.0 bip39: ^1.0.6 @@ -76,7 +76,6 @@ dependencies: flutter_local_notifications: ^19.0.0 flutter_background_service: ^5.1.0 - system_tray: ^2.0.3 path_provider: ^2.1.5 permission_handler: ^12.0.0+1 @@ -89,14 +88,14 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 test: ^1.25.7 integration_test: sdk: flutter flutter_intl: ^0.0.1 - mockito: ^5.4.4 + mockito: ^5.4.5 build_runner: ^2.4.0 - riverpod_generator: + riverpod_generator: ^2.6.5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 08834299..fa54b693 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in mostro_mobile/test/mocks.dart. // Do not manually edit this file. diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart index a43c6ff1..c97bf088 100644 --- a/test/services/mostro_service_test.mocks.dart +++ b/test/services/mostro_service_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in mostro_mobile/test/services/mostro_service_test.dart. // Do not manually edit this file. @@ -120,6 +120,12 @@ class MockNostrService extends _i1.Mock implements _i6.NostrService { ), ) 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( @@ -129,12 +135,6 @@ class MockNostrService extends _i1.Mock implements _i6.NostrService { returnValueForMissingStub: null, ); - @override - bool get isInitialized => (super.noSuchMethod( - Invocation.getter(#isInitialized), - returnValue: false, - ) as bool); - @override _i7.Future init(_i2.Settings? settings) => (super.noSuchMethod( Invocation.method( @@ -419,15 +419,6 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { returnValue: <_i4.Session>[], ) as List<_i4.Session>); - @override - set onError(_i5.ErrorListener? _onError) => super.noSuchMethod( - Invocation.setter( - #onError, - _onError, - ), - returnValueForMissingStub: null, - ); - @override bool get mounted => (super.noSuchMethod( Invocation.getter(#mounted), @@ -446,15 +437,6 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { returnValue: <_i4.Session>[], ) as List<_i4.Session>); - @override - set state(List<_i4.Session>? value) => super.noSuchMethod( - Invocation.setter( - #state, - value, - ), - returnValueForMissingStub: null, - ); - @override List<_i4.Session> get debugState => (super.noSuchMethod( Invocation.getter(#debugState), @@ -467,6 +449,24 @@ class MockSessionNotifier extends _i1.Mock implements _i10.SessionNotifier { 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( diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 94cd0408..16383da0 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterSecureStorageWindowsPluginRegisterWithRegistrar( @@ -18,6 +17,4 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("LocalAuthPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); - SystemTrayPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SystemTrayPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 52df19df..372c2ba7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows local_auth_windows permission_handler_windows - system_tray ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 894b5b0b24d5b7584be8de56d44f96dde036d874 Mon Sep 17 00:00:00 2001 From: Biz Date: Mon, 9 Jun 2025 22:47:38 -0700 Subject: [PATCH 23/26] feat: enhance key management and session handling with new error handling and storage methods --- .github/workflows/main.yml | 8 ++++++++ .../key_manager/key_management_screen.dart | 10 +++++++++- lib/features/key_manager/key_manager.dart | 17 ++++++++++------ .../key_manager/key_manager_errors.dart | 8 ++++++++ lib/features/key_manager/key_storage.dart | 5 +++++ .../notfiers/abstract_mostro_notifier.dart | 6 ++++++ .../widgets/mostro_message_detail_widget.dart | 20 ++++++++++++------- lib/shared/notifiers/session_notifier.dart | 5 +++-- 8 files changed, 63 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b0384e7..3a451239 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,14 @@ jobs: flutter-version: '3.32.2' channel: 'stable' + - name: Cache Dart & Pub artifacts + uses: actions/cache@v3 + with: + path: | + ~/.pub-cache + .dart_tool + key: ${{ runner.os }}-dart-${{ hashFiles('**/pubspec.yaml') }} + - name: Install Dependencies run: flutter pub get diff --git a/lib/features/key_manager/key_management_screen.dart b/lib/features/key_manager/key_management_screen.dart index 075a9a3a..cad1b980 100644 --- a/lib/features/key_manager/key_management_screen.dart +++ b/lib/features/key_manager/key_management_screen.dart @@ -5,7 +5,7 @@ import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/widgets/privacy_switch_widget.dart'; @@ -55,8 +55,16 @@ class _KeyManagementScreenState extends ConsumerState { Future _generateNewMasterKey() async { final sessionNotifer = ref.read(sessionNotifierProvider.notifier); await sessionNotifer.reset(); + + final mostroStorage = ref.read(mostroStorageProvider); + await mostroStorage.deleteAll(); + + final eventStorage = ref.read(eventStorageProvider); + await eventStorage.deleteAll(); + final keyManager = ref.read(keyManagerProvider); await keyManager.generateAndStoreMasterKey(); + await _loadKeys(); } diff --git a/lib/features/key_manager/key_manager.dart b/lib/features/key_manager/key_manager.dart index f11075f4..db436b61 100644 --- a/lib/features/key_manager/key_manager.dart +++ b/lib/features/key_manager/key_manager.dart @@ -18,16 +18,15 @@ class KeyManager { await generateAndStoreMasterKey(); } masterKeyPair = await _getMasterKey(); - _masterKeyHex = await _storage.readMasterKey(); - tradeKeyIndex = await _storage.readTradeKeyIndex(); + tradeKeyIndex = await getCurrentKeyIndex(); } Future hasMasterKey() async { if (masterKeyPair != null) { return true; } - final masterKeyHex = await _storage.readMasterKey(); - return masterKeyHex != null; + _masterKeyHex = await _storage.readMasterKey(); + return _masterKeyHex != null; } /// Generate a new mnemonic, derive the master key, and store both @@ -42,7 +41,7 @@ class KeyManager { await _storage.storeMnemonic(mnemonic); await _storage.storeMasterKey(masterKeyHex); - await _storage.storeTradeKeyIndex(1); + await setCurrentKeyIndex(1); } Future importMnemonic(String mnemonic) async { @@ -76,7 +75,7 @@ class KeyManager { _derivator.derivePrivateKey(masterKeyHex, currentIndex); // increment index - await _storage.storeTradeKeyIndex(currentIndex + 1); + await setCurrentKeyIndex(currentIndex + 1); return NostrKeyPairs(private: tradePrivateHex); } @@ -109,6 +108,12 @@ class KeyManager { } Future setCurrentKeyIndex(int index) async { + if (index < 1) { + throw InvalidTradeKeyIndexException( + 'Trade key index must be greater than 0', + ); + } + tradeKeyIndex = index; await _storage.storeTradeKeyIndex(index); } } diff --git a/lib/features/key_manager/key_manager_errors.dart b/lib/features/key_manager/key_manager_errors.dart index e0433f8c..1bd0b2f1 100644 --- a/lib/features/key_manager/key_manager_errors.dart +++ b/lib/features/key_manager/key_manager_errors.dart @@ -12,4 +12,12 @@ class TradeKeyDerivationException implements Exception { @override String toString() => 'TradeKeyDerivationException: $message'; +} + +class InvalidTradeKeyIndexException implements Exception { + final String message; + InvalidTradeKeyIndexException(this.message); + + @override + String toString() => 'InvalidTradeKeyIndexException: $message'; } \ No newline at end of file diff --git a/lib/features/key_manager/key_storage.dart b/lib/features/key_manager/key_storage.dart index 2df86888..e149d25e 100644 --- a/lib/features/key_manager/key_storage.dart +++ b/lib/features/key_manager/key_storage.dart @@ -50,4 +50,9 @@ class KeyStorage { ) ?? 1; } + + Future clear() async { + await secureStorage.deleteAll(); + await sharedPrefs.clear(); + } } diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index cb32348b..babe929f 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -132,6 +132,9 @@ class AbstractMostroNotifier extends StateNotifier { orderId, (s) => s.peer = peer, ); + state = state.copyWith( + peer: peer, + ); final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); break; @@ -182,6 +185,9 @@ class AbstractMostroNotifier extends StateNotifier { orderId, (s) => s.peer = peer, ); + state = state.copyWith( + peer: peer, + ); final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); break; diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index 2af0389c..17e81bca 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -131,9 +131,8 @@ class MostroMessageDetail extends ConsumerWidget { session?.peer?.publicKey ?? '', ); case actions.Action.buyerTookOrder: - final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); return S.of(context)!.buyerTookOrder( - session?.peer?.publicKey ?? '', + tradeState.peer?.publicKey ?? '', orderPayload!.fiatCode, orderPayload.fiatAmount.toString(), orderPayload.paymentMethod, @@ -141,8 +140,8 @@ class MostroMessageDetail extends ConsumerWidget { case actions.Action.fiatSentOk: final session = ref.watch(sessionProvider(orderPayload!.id ?? '')); return session!.role == Role.buyer - ? S.of(context)!.fiatSentOkBuyer(session.peer!.publicKey) - : S.of(context)!.fiatSentOkSeller(session.peer!.publicKey); + ? S.of(context)!.fiatSentOkBuyer(tradeState.peer!.publicKey) + : S.of(context)!.fiatSentOkSeller(tradeState.peer!.publicKey); case actions.Action.released: return S.of(context)!.released('{seller_npub}'); case actions.Action.purchaseCompleted: @@ -165,10 +164,14 @@ class MostroMessageDetail extends ConsumerWidget { return S.of(context)!.cooperativeCancelAccepted(orderPayload!.id ?? ''); case actions.Action.disputeInitiatedByYou: final payload = ref.read(orderNotifierProvider(orderId)).dispute; - return S.of(context)!.disputeInitiatedByYou(orderPayload!.id!, payload!.disputeId); + return S + .of(context)! + .disputeInitiatedByYou(orderPayload!.id!, payload!.disputeId); case actions.Action.disputeInitiatedByPeer: final payload = ref.read(orderNotifierProvider(orderId)).dispute; - return S.of(context)!.disputeInitiatedByPeer(orderPayload!.id!, payload!.disputeId); + return S + .of(context)! + .disputeInitiatedByPeer(orderPayload!.id!, payload!.disputeId); case actions.Action.adminTookDispute: return S.of(context)!.adminTookDisputeUsers('{admin token}'); case actions.Action.adminCanceled: @@ -198,7 +201,10 @@ class MostroMessageDetail extends ConsumerWidget { case CantDoReason.invalidSignature: return S.of(context)!.invalidSignature; case CantDoReason.notAllowedByStatus: - return S.of(context)!.notAllowedByStatus(tradeState.order!.id!, tradeState.status); + return S.of(context)!.notAllowedByStatus( + orderId, + tradeState.status, + ); case CantDoReason.outOfRangeFiatAmount: return S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); case CantDoReason.outOfRangeSatsAmount: diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index cef40247..faf74a81 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -13,7 +13,7 @@ class SessionNotifier extends StateNotifier> { Settings _settings; final Map _sessions = {}; -final Map _requestIdToSession = {}; + final Map _requestIdToSession = {}; Timer? _cleanupTimer; static const int sessionExpirationHours = 36; @@ -44,7 +44,8 @@ final Map _requestIdToSession = {}; _settings = settings.copyWith(); } - Future newSession({String? orderId, int? requestId, Role? role}) async { + Future newSession( + {String? orderId, int? requestId, Role? role}) async { final masterKey = _keyManager.masterKeyPair!; final keyIndex = await _keyManager.getCurrentKeyIndex(); final tradeKey = await _keyManager.deriveTradeKey(); From 39cc5e3693631f87fc5ea6494b3778e5702e9b04 Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 10 Jun 2025 07:35:03 -0700 Subject: [PATCH 24/26] fix: ensure storage is cleared before storing new master key from mnemonic --- lib/features/key_manager/key_manager.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/features/key_manager/key_manager.dart b/lib/features/key_manager/key_manager.dart index db436b61..6e487cf8 100644 --- a/lib/features/key_manager/key_manager.dart +++ b/lib/features/key_manager/key_manager.dart @@ -39,6 +39,7 @@ class KeyManager { Future generateAndStoreMasterKeyFromMnemonic(String mnemonic) async { final masterKeyHex = _derivator.extendedKeyFromMnemonic(mnemonic); + await _storage.clear(); await _storage.storeMnemonic(mnemonic); await _storage.storeMasterKey(masterKeyHex); await setCurrentKeyIndex(1); From 30274646f3d6642ada904937a5c9eca904cc1a0e Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 10 Jun 2025 09:58:36 -0700 Subject: [PATCH 25/26] fix: ensure requestId and tradeIndex are properly converted to integers in fromJson method --- lib/data/models/mostro_message.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index ae600346..e4273fcb 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -41,12 +41,12 @@ class MostroMessage { factory MostroMessage.fromJson(Map json) { final timestamp = json['timestamp']; - json = json['order'] ?? json['cant-do'] ?? json; + final num requestId = json['request_id'] ?? 0; return MostroMessage( action: Action.fromString(json['action']), - requestId: json['request_id'], + requestId: requestId.toInt(), tradeIndex: json['trade_index'], id: json['id'], payload: json['payload'] != null @@ -71,14 +71,14 @@ class MostroMessage { ? Payload.fromJson(order['payload']) as T : null; - final tradeIndex = order['trade_index']; + final num tradeIndex = order['trade_index']; return MostroMessage( action: action, requestId: order['request_id'], id: order['id'], payload: payload, - tradeIndex: tradeIndex, + tradeIndex: tradeIndex.toInt(), ); } catch (e) { throw FormatException('Failed to deserialize MostroMessage: $e'); From e1d7fd179d062f605bb5df4dbe804eefa3cafe8a Mon Sep 17 00:00:00 2001 From: Biz Date: Tue, 10 Jun 2025 10:05:53 -0700 Subject: [PATCH 26/26] fix: ensure tradeIndex and requestId are properly handled in fromJson method --- lib/data/models/mostro_message.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index e4273fcb..f908bd44 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -71,11 +71,12 @@ class MostroMessage { ? Payload.fromJson(order['payload']) as T : null; - final num tradeIndex = order['trade_index']; + final num tradeIndex = order['trade_index'] ?? 0; + final num requestId = order['request_id'] ?? 0; return MostroMessage( action: action, - requestId: order['request_id'], + requestId: requestId.toInt(), id: order['id'], payload: payload, tradeIndex: tradeIndex.toInt(),