diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 5e6b5427..6994392a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Jun 14 02:07:21 ART 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 5b59bc89..0a6df8cc 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -82,78 +82,194 @@ class OrderState { } 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, + _logger.i('🔄 Updating OrderState with Action: ${message.action}'); + + // Preserve the current state entirely for cantDo messages - they are informational only + if (message.action == Action.cantDo) { + return this; + } + + // Determine the new status based on the action received + Status newStatus = _getStatusFromAction( + message.action, message.getPayload()?.status); + + // 🔍 DEBUG: Log status mapping + _logger.i('📊 Status mapping: ${message.action} → $newStatus'); + + // Preserve PaymentRequest correctly + PaymentRequest? newPaymentRequest; + if (message.payload is PaymentRequest) { + newPaymentRequest = message.getPayload(); + _logger.i('💳 New PaymentRequest found in message'); + } else { + newPaymentRequest = paymentRequest; // Preserve existing + } + + final newState = copyWith( + status: newStatus, + action: message.action, order: message.payload is Order ? message.getPayload() : message.payload is PaymentRequest ? message.getPayload()!.order : order, - paymentRequest: message.getPayload() ?? paymentRequest, + paymentRequest: newPaymentRequest, cantDo: message.getPayload() ?? cantDo, dispute: message.getPayload() ?? dispute, peer: message.getPayload() ?? peer, ); + + _logger.i('✅ New state: ${newState.status} - ${newState.action}'); + _logger + .i('💳 PaymentRequest preserved: ${newState.paymentRequest != null}'); + + return newState; + } + + /// Maps actions to their corresponding statuses based on mostrod DM messages + Status _getStatusFromAction(Action action, Status? payloadStatus) { + switch (action) { + // Actions that should set status to waiting-payment + case Action.waitingSellerToPay: + case Action.payInvoice: + return Status.waitingPayment; + + // Actions that should set status to waiting-buyer-invoice + case Action.waitingBuyerInvoice: + case Action.addInvoice: + return Status.waitingBuyerInvoice; + + // ✅ FIX: Cuando alguien toma una orden, debe cambiar el status inmediatamente + case Action.takeBuy: + // Cuando buyer toma sell order, seller debe esperar buyer invoice + return Status.waitingBuyerInvoice; + + case Action.takeSell: + // Cuando seller toma buy order, seller debe pagar invoice + return Status.waitingPayment; + + // Actions that should set status to active + case Action.buyerTookOrder: + case Action.holdInvoicePaymentAccepted: + case Action.holdInvoicePaymentSettled: + return Status.active; + + // Actions that should set status to fiat-sent + case Action.fiatSent: + case Action.fiatSentOk: + return Status.fiatSent; + + // Actions that should set status to success (completed) + case Action.purchaseCompleted: + case Action.released: + case Action.rate: + case Action.rateReceived: + return Status.success; + + // Actions that should set status to canceled + case Action.canceled: + case Action.adminCanceled: + case Action.cooperativeCancelAccepted: + return Status.canceled; + + // For actions that include Order payload, use the payload status + case Action.newOrder: + return payloadStatus ?? status; + + // For other actions, keep the current status unless payload has a different one + default: + return payloadStatus ?? status; + } } List getActions(Role role) { - return actions[role]![status]![action] ?? []; + return actions[role]?[status]?[action] ?? []; } static final Map>>> actions = { Role.seller: { Status.pending: { - Action.takeBuy: [ - Action.takeBuy, + Action.newOrder: [ Action.cancel, ], - Action.waitingBuyerInvoice: [ + Action.takeBuy: [ Action.cancel, ], + }, + Status.waitingPayment: { Action.payInvoice: [ Action.payInvoice, Action.cancel, ], - Action.newOrder: [ + Action.waitingSellerToPay: [ + Action.payInvoice, + Action.cancel, + ], + }, + Status.waitingBuyerInvoice: { + Action.waitingBuyerInvoice: [ + Action.cancel, + ], + Action.addInvoice: [ + Action.cancel, + ], + Action.takeBuy: [ Action.cancel, ], }, Status.active: { Action.buyerTookOrder: [ - Action.buyerTookOrder, Action.cancel, Action.dispute, ], - Action.fiatSentOk: [ + Action.holdInvoicePaymentAccepted: [ + Action.cancel, + Action.dispute, + ], + Action.holdInvoicePaymentSettled: [ Action.cancel, Action.dispute, + ], + }, + Status.fiatSent: { + Action.fiatSentOk: [ Action.release, + Action.cancel, + Action.dispute, ], + }, + Status.success: { Action.rate: [ Action.rate, ], - Action.purchaseCompleted: [], - Action.holdInvoicePaymentSettled: [], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cancel, + Action.purchaseCompleted: [ + Action.rate, ], - }, - Status.waitingPayment: { - Action.payInvoice: [ - Action.payInvoice, - Action.cancel, + Action.released: [ + Action.rate, ], + Action.rateReceived: [], + }, + Status.canceled: { + Action.canceled: [], + Action.adminCanceled: [], + Action.cooperativeCancelAccepted: [], }, }, Role.buyer: { Status.pending: { + Action.newOrder: [ + Action.cancel, + ], Action.takeSell: [ - Action.takeSell, Action.cancel, ], - Action.newOrder: [ + }, + Status.waitingPayment: { + Action.waitingSellerToPay: [ + Action.cancel, + ], + Action.takeSell: [ Action.cancel, ], }, @@ -162,30 +278,48 @@ class OrderState { Action.addInvoice, Action.cancel, ], - Action.waitingSellerToPay: [ + Action.waitingBuyerInvoice: [ Action.cancel, ], }, Status.active: { Action.holdInvoicePaymentAccepted: [ - Action.holdInvoicePaymentAccepted, Action.fiatSent, Action.cancel, Action.dispute, ], + Action.holdInvoicePaymentSettled: [ + Action.fiatSent, + Action.cancel, + Action.dispute, + ], + Action.buyerTookOrder: [ + Action.cancel, + Action.dispute, + ], + }, + Status.fiatSent: { Action.fiatSentOk: [ Action.cancel, Action.dispute, ], + }, + Status.success: { Action.rate: [ Action.rate, ], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cancel, + Action.purchaseCompleted: [ + Action.rate, + ], + Action.released: [ + Action.rate, ], Action.rateReceived: [], - Action.purchaseCompleted: [], - Action.paymentFailed: [], + }, + Status.canceled: { + Action.canceled: [], + Action.adminCanceled: [], + Action.cooperativeCancelAccepted: [], }, }, }; diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index babe929f..ec9d1abe 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -19,9 +19,14 @@ class AbstractMostroNotifier extends StateNotifier { AbstractMostroNotifier( this.orderId, - this.ref, - ) : super(OrderState( - action: Action.newOrder, status: Status.pending, order: null)) { + this.ref, { + OrderState? initialState, + }) : super(initialState ?? + OrderState( + action: Action.newOrder, + status: Status.pending, + order: null, + )) { final oldSession = ref.read(sessionNotifierProvider.notifier).getSessionByOrderId(orderId); if (oldSession != null) { diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 2fc5bb7b..e60c0869 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -9,41 +9,36 @@ import 'package:mostro_mobile/services/mostro_service.dart'; class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; - OrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); sync(); subscribe(); } - Future sync() async { try { final storage = ref.read(mostroStorageProvider); final messages = await storage.getAllMessagesForOrderId(orderId); if (messages.isEmpty) { + logger.w('No messages found for order $orderId'); return; } - 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()!, - ); + messages.sort((a, b) { + final timestampA = a.timestamp ?? 0; + final timestampB = b.timestamp ?? 0; + return timestampA.compareTo(timestampB); + }); + OrderState currentState = state; + for (final message in messages) { + if (message.action != Action.cantDo) { + currentState = currentState.updateWith(message); } } + state = currentState; + logger.i( + 'Synced order $orderId to state: ${state.status} - ${state.action}'); } catch (e, stack) { logger.e( - 'Error syncing order state', + 'Error syncing order state for $orderId', error: e, stackTrace: stack, ); diff --git a/lib/features/trades/providers/trades_provider.dart b/lib/features/trades/providers/trades_provider.dart index b209bf07..68e1eea7 100644 --- a/lib/features/trades/providers/trades_provider.dart +++ b/lib/features/trades/providers/trades_provider.dart @@ -12,24 +12,27 @@ final _logger = Logger(); final filteredTradesProvider = Provider>>((ref) { final allOrdersAsync = ref.watch(orderEventsProvider); final sessions = ref.watch(sessionNotifierProvider); - - _logger.d('Filtering trades: Orders state=${allOrdersAsync.toString().substring(0, math.min(100, allOrdersAsync.toString().length))}, Sessions count=${sessions.length}'); - + + _logger.d( + 'Filtering trades: Orders state=${allOrdersAsync.toString().substring(0, math.min(100, allOrdersAsync.toString().length))}, Sessions count=${sessions.length}'); + return allOrdersAsync.when( data: (allOrders) { final orderIds = sessions.map((s) => s.orderId).toSet(); - _logger.d('Got ${allOrders.length} orders and ${orderIds.length} sessions'); - + _logger + .d('Got ${allOrders.length} orders and ${orderIds.length} sessions'); + // Make a copy to avoid modifying the original list final sortedOrders = List.from(allOrders); - sortedOrders.sort((o1, o2) => o1.expirationDate.compareTo(o2.expirationDate)); - + sortedOrders + .sort((o1, o2) => o1.expirationDate.compareTo(o2.expirationDate)); + final filtered = sortedOrders.reversed .where((o) => orderIds.contains(o.orderId)) .where((o) => o.status != Status.canceled) .where((o) => o.status != Status.canceledByAdmin) .toList(); - + _logger.d('Filtered to ${filtered.length} trades'); return AsyncValue.data(filtered); }, diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 1344428a..709afda5 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -223,24 +223,42 @@ class TradeDetailScreen extends ConsumerWidget { // FSM-driven action mapping: ensure all actions are handled switch (action) { case actions.Action.cancel: + String buttonText; + Color buttonColor; + + if (tradeState.status == Status.active || + tradeState.status == Status.fiatSent) { + buttonText = 'COOPERATIVE CANCEL'; + buttonColor = AppTheme.red1; + } else { + buttonText = 'CANCEL'; + buttonColor = AppTheme.red1; + } + widgets.add(_buildNostrButton( - 'CANCEL', + buttonText, action: action, - backgroundColor: AppTheme.red1, + backgroundColor: buttonColor, onPressed: () => ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); break; + case actions.Action.payInvoice: if (userRole == Role.seller) { - widgets.add(_buildNostrButton( - 'PAY INVOICE', - action: actions.Action.payInvoice, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/pay_invoice/$orderId'), - )); + final hasPaymentRequest = tradeState.paymentRequest != null; + + if (hasPaymentRequest) { + widgets.add(_buildNostrButton( + 'PAY INVOICE', + action: actions.Action.payInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/pay_invoice/$orderId'), + )); + } } break; + case actions.Action.addInvoice: if (userRole == Role.buyer) { widgets.add(_buildNostrButton( @@ -251,11 +269,12 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + case actions.Action.fiatSent: if (userRole == Role.buyer) { widgets.add(_buildNostrButton( 'FIAT SENT', - action: actions.Action.fiatSentOk, + action: actions.Action.fiatSent, backgroundColor: AppTheme.mostroGreen, onPressed: () => ref .read(orderNotifierProvider(orderId).notifier) @@ -263,6 +282,7 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + case actions.Action.disputeInitiatedByYou: case actions.Action.disputeInitiatedByPeer: case actions.Action.dispute: @@ -280,6 +300,7 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + case actions.Action.release: if (userRole == Role.seller) { widgets.add(_buildNostrButton( @@ -292,6 +313,7 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + case actions.Action.takeSell: if (userRole == Role.buyer) { widgets.add(_buildNostrButton( @@ -302,6 +324,7 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + case actions.Action.takeBuy: if (userRole == Role.seller) { widgets.add(_buildNostrButton( @@ -312,25 +335,31 @@ class TradeDetailScreen extends ConsumerWidget { )); } break; + + // ✅ CASOS DE COOPERATIVE CANCEL: Ahora estos se manejan cuando el usuario ya inició/recibió cooperative cancel case actions.Action.cooperativeCancelInitiatedByYou: - case actions.Action.cooperativeCancelInitiatedByPeer: + // El usuario ya inició cooperative cancel, ahora debe esperar respuesta widgets.add(_buildNostrButton( - 'COOPERATIVE CANCEL', + 'CANCEL PENDING', action: actions.Action.cooperativeCancelInitiatedByYou, - backgroundColor: AppTheme.red1, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + backgroundColor: Colors.grey, + onPressed: null, )); break; - case actions.Action.cooperativeCancelAccepted: + + case actions.Action.cooperativeCancelInitiatedByPeer: widgets.add(_buildNostrButton( - 'CONFIRM CANCEL', + 'ACCEPT CANCEL', action: actions.Action.cooperativeCancelAccepted, backgroundColor: AppTheme.red1, onPressed: () => ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), )); break; + + case actions.Action.cooperativeCancelAccepted: + break; + case actions.Action.purchaseCompleted: widgets.add(_buildNostrButton( 'COMPLETE PURCHASE', @@ -341,27 +370,31 @@ class TradeDetailScreen extends ConsumerWidget { .releaseOrder(), )); break; + case actions.Action.buyerTookOrder: widgets.add(_buildContactButton(context)); break; + case actions.Action.rate: case actions.Action.rateUser: case actions.Action.rateReceived: widgets.add(_buildNostrButton( 'RATE', - action: actions.Action.rateReceived, + action: actions.Action.rate, backgroundColor: AppTheme.mostroGreen, onPressed: () => context.push('/rate_user/$orderId'), )); break; + + case actions.Action.holdInvoicePaymentSettled: 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.waitingSellerToPay: case actions.Action.waitingBuyerInvoice: @@ -380,23 +413,13 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.released: // Not user-facing or not relevant as a button break; + default: // Optionally handle unknown or unimplemented actions break; } } - // 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; } @@ -404,7 +427,7 @@ class TradeDetailScreen extends ConsumerWidget { Widget _buildNostrButton( String label, { required actions.Action action, - required VoidCallback onPressed, + required VoidCallback? onPressed, Color? backgroundColor, }) { return MostroReactiveButton( @@ -413,7 +436,7 @@ class TradeDetailScreen extends ConsumerWidget { orderId: orderId, action: action, backgroundColor: backgroundColor, - onPressed: onPressed, + onPressed: onPressed ?? () {}, // Provide empty function when null showSuccessIndicator: true, timeout: const Duration(seconds: 30), ); diff --git a/lib/features/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index afc56b2f..b3fb50a1 100644 --- a/lib/features/trades/screens/trades_screen.dart +++ b/lib/features/trades/screens/trades_screen.dart @@ -1,10 +1,8 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/shared/widgets/order_filter.dart'; import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; import 'package:mostro_mobile/features/trades/widgets/trades_list.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; @@ -18,9 +16,9 @@ class TradesScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Watch the async trades data final tradesAsync = ref.watch(filteredTradesProvider); - + return Scaffold( - backgroundColor: AppTheme.dark1, + backgroundColor: AppTheme.backgroundDark, appBar: const MostroAppBar(), drawer: const MostroAppDrawer(), body: RefreshIndicator( @@ -30,122 +28,94 @@ class TradesScreen extends ConsumerWidget { // Then refresh the filtered trades provider ref.invalidate(filteredTradesProvider); }, - child: Container( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 16), - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'MY TRADES', - style: TextStyle(color: AppTheme.mostroGreen), - ), - ), - // Use the async value pattern to handle different states - tradesAsync.when( - data: (trades) => _buildFilterButton(context, ref, trades), - loading: () => _buildFilterButton(context, ref, []), - error: (error, _) => _buildFilterButton(context, ref, []), - ), - const SizedBox(height: 6.0), - Expanded( - child: tradesAsync.when( - data: (trades) => _buildOrderList(trades), - loading: () => const Center( - child: CircularProgressIndicator(), + child: Column( + children: [ + Expanded( + child: Column( + children: [ + // Header with dark background + Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: AppTheme.backgroundDark, + border: Border( + bottom: BorderSide(color: Colors.white24, width: 0.5), + ), + ), + child: const Text( + 'My Active Trades', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), ), - error: (error, _) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 60, - ), - const SizedBox(height: 16), - Text( - 'Error loading trades', - style: TextStyle(color: AppTheme.cream1), - ), - Text( - error.toString(), - style: TextStyle(color: AppTheme.cream1, fontSize: 12), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - ref.invalidate(orderEventsProvider); - ref.invalidate(filteredTradesProvider); - }, - child: const Text('Retry'), - ), - ], + // Content area with dark background + Expanded( + child: Container( + decoration: const BoxDecoration( + color: AppTheme.backgroundDark, + ), + child: Column( + children: [ + // Espacio superior + const SizedBox(height: 16.0), + Expanded( + child: tradesAsync.when( + data: (trades) => _buildOrderList(trades), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 60, + ), + const SizedBox(height: 16), + Text( + 'Error loading trades', + style: TextStyle(color: AppTheme.cream1), + ), + Text( + error.toString(), + style: TextStyle( + color: AppTheme.cream1, fontSize: 12), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.invalidate(orderEventsProvider); + ref.invalidate(filteredTradesProvider); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ], + ), ), ), - ), + ], ), - const BottomNavBar(), - ], - ), + ), + const BottomNavBar(), + ], ), ), ); } - Widget _buildFilterButton(BuildContext context, WidgetRef ref, List trades) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - OutlinedButton.icon( - onPressed: () { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: OrderFilter(), - ); - }, - ); - }, - icon: const HeroIcon(HeroIcons.funnel, - style: HeroIconStyle.outline, color: AppTheme.cream1), - label: - const Text("FILTER", style: TextStyle(color: AppTheme.cream1)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: AppTheme.cream1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - ), - const SizedBox(width: 8), - Text( - "${trades.length} trades", - style: const TextStyle(color: AppTheme.cream1), - ), - // Add a manual refresh button - IconButton( - icon: const Icon(Icons.refresh, color: AppTheme.cream1), - onPressed: () { - // Use the ref from the build method - ref.invalidate(orderEventsProvider); - ref.invalidate(filteredTradesProvider); - }, - ), - ], - ), - ); - } + // Función eliminada: _buildFilterButton Widget _buildOrderList(List trades) { if (trades.isEmpty) { diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 28c92ae1..55f95368 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -2,14 +2,14 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; +// package:mostro_mobile/core/app_theme.dart is not used import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.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/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'; class TradesListItem extends ConsumerWidget { @@ -20,277 +20,238 @@ class TradesListItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(timeProvider); + final session = ref.watch(sessionProvider(trade.orderId!)); + final role = session?.role; + final isBuying = role == Role.buyer; + final orderState = ref.watch(orderNotifierProvider(trade.orderId!)); + + // Determine if the user is the creator of the order based on role and order type + final isCreator = isBuying + ? trade.orderType == OrderType.buy + : trade.orderType == OrderType.sell; return GestureDetector( onTap: () { context.push('/trade_detail/${trade.orderId}'); }, - child: CustomCard( - color: AppTheme.dark1, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 16), - _buildSessionDetails(context, ref), - const SizedBox(height: 8), - ], - ), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildStatusChip(trade.status), - Text( - '${trade.expiration}', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - ), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: const Color(0xFF1D212C), // Mismo color que el fondo de órdenes en home + borderRadius: BorderRadius.circular(12.0), ), - ], - ); - } - - Widget _buildSessionDetails(BuildContext context, WidgetRef ref) { - final session = ref.watch(sessionProvider(trade.orderId!)); - return Row( - children: [ - _getOrderOffering(context, trade, session!.role), - const SizedBox(width: 16), - Expanded( - flex: 3, - child: _buildPaymentMethod(context), - ), - ], - ); - } - - Widget _getOrderOffering( - BuildContext context, - NostrEvent trade, - Role? role, - ) { - String offering = role == Role.buyer ? 'Buying' : 'Selling'; - String amountText = (trade.amount != null && trade.amount != '0') - ? ' ${trade.amount!}' - : ''; - - return Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - children: [ - _buildStyledTextSpan( - context, - offering, - amountText, - isValue: true, - isBold: true, - ), - TextSpan( - text: 'sats', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - const SizedBox(height: 8.0), - RichText( - text: TextSpan( - children: [ - _buildStyledTextSpan( - context, - 'for ', - '${trade.fiatAmount}', - isValue: true, - isBold: true, - ), - TextSpan( - text: - '${trade.currency} ${CurrencyUtils.getFlagFromCurrency(trade.currency!)} ', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontSize: 16.0, - ), - ), - TextSpan( - text: '(${trade.premium}%)', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontSize: 16.0, - ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left side - Trade info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // First row: Buy/Sell Bitcoin text + status and role chips + Row( + children: [ + Text( + isBuying ? 'Buying Bitcoin' : 'Selling Bitcoin', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + _buildStatusChip(orderState.status), + const SizedBox(width: 8), + _buildRoleChip(isCreator), + ], + ), + const SizedBox(height: 8), + // Second row: Flag + Amount and currency + Premium/Discount + Row( + children: [ + Text( + CurrencyUtils.getFlagFromCurrency( + trade.currency ?? '') ?? + '', + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(width: 4), + Text( + '${trade.fiatAmount.minimum} ${trade.currency ?? ''}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + // Show premium/discount if different from zero + if (trade.premium != null && trade.premium != '0') + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: + double.tryParse(trade.premium!) != null && + double.parse(trade.premium!) > 0 + ? Colors.green.shade700 + : Colors.red.shade700, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${double.tryParse(trade.premium!) != null && double.parse(trade.premium!) > 0 ? '+' : ''}${trade.premium}%', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + // Third row: Payment methods (muestra todos los métodos de pago separados por comas) + trade.paymentMethods.isNotEmpty + ? Text( + trade.paymentMethods.join(', '), + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 14, + ), + ) + : Text( + 'Bank Transfer', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 14, + ), + ), + ], ), - ], - ), + ), + // Right side - Arrow icon + const Icon( + Icons.chevron_right, + color: Colors.white, + size: 24, + ), + ], ), - ], + ), ), ); } - Widget _buildPaymentMethod(BuildContext context) { - String method = trade.paymentMethods.isNotEmpty - ? trade.paymentMethods[0] - : 'No payment method'; - - return Row( - children: [ - HeroIcon( - _getPaymentMethodIcon(method), - style: HeroIconStyle.outline, - color: AppTheme.cream1, - size: 16, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - method, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: AppTheme.grey2, - ), - overflow: TextOverflow.ellipsis, - softWrap: true, - ), + Widget _buildRoleChip(bool isCreator) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: isCreator ? Colors.blue.shade700 : Colors.teal.shade700, // Cambiado de verde a teal para "Taken by you" + borderRadius: BorderRadius.circular(12), // Más redondeado + ), + child: Text( + isCreator ? 'Created by you' : 'Taken by you', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, ), - ], - ); - } - - HeroIcons _getPaymentMethodIcon(String method) { - switch (method.toLowerCase()) { - case 'wire transfer': - case 'transferencia bancaria': - return HeroIcons.buildingLibrary; - case 'revolut': - return HeroIcons.creditCard; - default: - return HeroIcons.banknotes; - } - } - - TextSpan _buildStyledTextSpan( - BuildContext context, - String label, - String value, { - bool isValue = false, - bool isBold = false, - }) { - return TextSpan( - text: label, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.cream1, - fontWeight: FontWeight.normal, - fontSize: isValue ? 16.0 : 24.0, - ), - children: isValue - ? [ - TextSpan( - text: '$value ', - style: Theme.of(context).textTheme.displayLarge?.copyWith( - fontWeight: isBold ? FontWeight.bold : FontWeight.normal, - fontSize: 24.0, - color: AppTheme.cream1, - ), - ), - ] - : [], + ), ); } Widget _buildStatusChip(Status status) { - Color backgroundColor; - Color textColor = AppTheme.cream1; - String label; + Color backgroundColor; + Color textColor; + String label; - switch (status) { - case Status.active: - backgroundColor = AppTheme.red1; - label = 'Active'; - break; - case Status.canceled: - backgroundColor = AppTheme.grey; - label = 'Canceled'; - break; - case Status.canceledByAdmin: - backgroundColor = AppTheme.red2; - label = 'Canceled by Admin'; - break; - case Status.settledByAdmin: - backgroundColor = AppTheme.yellow; - label = 'Settled by Admin'; - break; - case Status.completedByAdmin: - backgroundColor = AppTheme.grey2; - label = 'Completed by Admin'; - break; - case Status.dispute: - backgroundColor = AppTheme.red1; - label = 'Dispute'; - break; - case Status.expired: - backgroundColor = AppTheme.grey; - label = 'Expired'; - break; - case Status.fiatSent: - backgroundColor = Colors.indigo; - label = 'Fiat Sent'; - break; - case Status.settledHoldInvoice: - backgroundColor = Colors.teal; - label = 'Settled Hold Invoice'; - break; - case Status.pending: - backgroundColor = AppTheme.mostroGreen; - textColor = Colors.black; - label = 'Pending'; - break; - case Status.success: - backgroundColor = Colors.green; - label = 'Success'; - break; - case Status.waitingBuyerInvoice: - backgroundColor = Colors.lightBlue; - label = 'Waiting Buyer Invoice'; - break; - case Status.waitingPayment: - backgroundColor = Colors.lightBlueAccent; - label = 'Waiting Payment'; - break; - case Status.cooperativelyCanceled: - backgroundColor = Colors.deepOrange; - label = 'Cooperatively Canceled'; - break; - case Status.inProgress: - backgroundColor = Colors.blueGrey; - label = 'In Progress'; - break; - } + switch (status) { + case Status.active: + backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia + textColor = const Color(0xFF93C5FD); // Azul claro + label = 'Active'; + break; + case Status.pending: + backgroundColor = const Color(0xFF854D0E).withOpacity(0.3); // Ámbar oscuro con transparencia + textColor = const Color(0xFFFCD34D); // Ámbar claro + label = 'Pending'; + break; + // ✅ SOLUCION PROBLEMA 1: Agregar casos específicos para waitingPayment y waitingBuyerInvoice + case Status.waitingPayment: + backgroundColor = const Color(0xFF7C2D12).withOpacity(0.3); // Naranja oscuro con transparencia + textColor = const Color(0xFFFED7AA); // Naranja claro + label = 'Waiting payment'; // En lugar de "Pending" + break; + case Status.waitingBuyerInvoice: + backgroundColor = const Color(0xFF7C2D12).withOpacity(0.3); // Naranja oscuro con transparencia + textColor = const Color(0xFFFED7AA); // Naranja claro + label = 'Waiting invoice'; // En lugar de "Pending" + break; + case Status.fiatSent: + backgroundColor = const Color(0xFF065F46).withOpacity(0.3); // Verde oscuro con transparencia + textColor = const Color(0xFF6EE7B7); // Verde claro + label = 'Fiat-sent'; + break; + case Status.canceled: + case Status.canceledByAdmin: + case Status.cooperativelyCanceled: + backgroundColor = Colors.grey.shade800.withOpacity(0.3); + textColor = Colors.grey.shade300; + label = 'Canceled'; + break; + case Status.settledByAdmin: + case Status.settledHoldInvoice: + backgroundColor = const Color(0xFF581C87).withOpacity(0.3); // Morado oscuro con transparencia + textColor = const Color(0xFFC084FC); // Morado claro + label = 'Settled'; + break; + case Status.completedByAdmin: + backgroundColor = const Color(0xFF065F46).withOpacity(0.3); // Verde oscuro con transparencia + textColor = const Color(0xFF6EE7B7); // Verde claro + label = 'Completed'; + break; + case Status.dispute: + backgroundColor = const Color(0xFF7F1D1D).withOpacity(0.3); // Rojo oscuro con transparencia + textColor = const Color(0xFFFCA5A5); // Rojo claro + label = 'Dispute'; + break; + case Status.expired: + backgroundColor = Colors.grey.shade800.withOpacity(0.3); + textColor = Colors.grey.shade300; + label = 'Expired'; + break; + case Status.success: + backgroundColor = const Color(0xFF065F46).withOpacity(0.3); // Verde oscuro con transparencia + textColor = const Color(0xFF6EE7B7); // Verde claro + label = 'Success'; + break; + default: + backgroundColor = Colors.grey.shade800.withOpacity(0.3); + textColor = Colors.grey.shade300; + label = status.toString(); // Fallback para mostrar el status real + break; + } - return Chip( - backgroundColor: backgroundColor, - visualDensity: VisualDensity.compact, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4.0), - side: BorderSide.none, - ), - label: Text( - label, - style: TextStyle(color: textColor, fontSize: 12.0), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w500, ), - ); - } + ), + ); +} }