From 0da4171ce5c53c271e3be38949cccc804f0b6e87 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 4 Jun 2025 14:40:00 -0300 Subject: [PATCH 01/11] initial my trades change and fix bottom nav bar --- .../trades/screens/trades_screen.dart | 123 ++++--- .../trades/widgets/trades_list_item.dart | 326 ++++++------------ 2 files changed, 171 insertions(+), 278 deletions(-) diff --git a/lib/features/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index afc56b2f..44bbc610 100644 --- a/lib/features/trades/screens/trades_screen.dart +++ b/lib/features/trades/screens/trades_screen.dart @@ -30,69 +30,80 @@ 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), + child: Column( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + color: AppTheme.dark2, ), - ), - // 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(), - ), - 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), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + 'My Active Trades', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - Text( - error.toString(), - style: TextStyle(color: AppTheme.cream1, fontSize: 12), - textAlign: TextAlign.center, + ), + ), + // 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(), ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - ref.invalidate(orderEventsProvider); - ref.invalidate(filteredTradesProvider); - }, - child: const Text('Retry'), + 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(), + ], ), ), ); diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 1a56b486..2da3d4c5 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -2,14 +2,12 @@ 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'; 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/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,276 +18,160 @@ 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; 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, - ), - ), - ], - ); - } - - 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: Container( + margin: const EdgeInsets.only(bottom: 1), + decoration: BoxDecoration( + color: AppTheme.dark1, + border: Border( + bottom: BorderSide(color: Colors.grey.shade900, width: 1), ), - ], - ), - ); - } - - 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, + 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 + Status + Row( + children: [ + Text( + isBuying ? 'Buying Bitcoin' : 'Selling Bitcoin', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + _buildStatusChip(trade.status), + ], + ), + const SizedBox(height: 8), + // Second row: Amount and currency + Row( + children: [ + Text( + '${isBuying ? trade.currency : trade.amount} ${CurrencyUtils.getFlagFromCurrency(trade.currency!)}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 4), + // Third row: Payment method + Text( + trade.paymentMethods.isNotEmpty + ? trade.paymentMethods.first + : 'Bank Transfer', + style: TextStyle( + color: Colors.grey.shade400, + fontSize: 14, + ), + ), + ], ), - overflow: TextOverflow.ellipsis, - softWrap: true, + ), + // Right side - Arrow icon + const Icon( + Icons.chevron_right, + color: Colors.white, + size: 24, + ), + ], ), ), - ], - ); - } - - 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; + Color textColor = Colors.white; String label; switch (status) { case Status.active: - backgroundColor = AppTheme.red1; + backgroundColor = Colors.blue; label = 'Active'; break; - case Status.canceled: - backgroundColor = AppTheme.grey; - label = 'Canceled'; + case Status.pending: + backgroundColor = Colors.amber.shade800; + label = 'Pending'; break; + case Status.fiatSent: + backgroundColor = Colors.green.shade700; + label = 'Fiat-sent'; + break; + case Status.canceled: case Status.canceledByAdmin: - backgroundColor = AppTheme.red2; - label = 'Canceled by Admin'; + case Status.cooperativelyCanceled: + backgroundColor = Colors.grey.shade700; + label = 'Canceled'; break; case Status.settledByAdmin: - backgroundColor = AppTheme.yellow; - label = 'Settled by Admin'; + case Status.settledHoldInvoice: + backgroundColor = Colors.purple; + label = 'Settled'; break; case Status.completedByAdmin: - backgroundColor = AppTheme.grey2; - label = 'Completed by Admin'; + backgroundColor = Colors.green; + label = 'Completed'; break; case Status.dispute: - backgroundColor = AppTheme.red1; + backgroundColor = Colors.red; label = 'Dispute'; break; case Status.expired: - backgroundColor = AppTheme.grey; + backgroundColor = Colors.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'; + backgroundColor = Colors.blue.shade300; + label = 'Waiting Invoice'; break; case Status.waitingPayment: - backgroundColor = Colors.lightBlueAccent; + backgroundColor = Colors.blue.shade400; label = 'Waiting Payment'; break; - case Status.cooperativelyCanceled: - backgroundColor = Colors.deepOrange; - label = 'Cooperatively Canceled'; - break; case Status.inProgress: - backgroundColor = Colors.blueGrey; + backgroundColor = Colors.blue.shade700; label = 'In Progress'; 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, + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(4), ), - label: Text( + child: Text( label, - style: TextStyle(color: textColor, fontSize: 12.0), + style: TextStyle( + color: textColor, + fontSize: 12, + fontWeight: FontWeight.w500, + ), ), ); } From 81cc9eeba72fa4c27224b8599d5808c0046a285e Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 4 Jun 2025 14:40:00 -0300 Subject: [PATCH 02/11] fix: better items design --- .../trades/screens/trades_screen.dart | 176 +++++++----------- .../trades/widgets/trades_list_item.dart | 89 ++++++--- 2 files changed, 131 insertions(+), 134 deletions(-) diff --git a/lib/features/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index 44bbc610..429bb553 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'; @@ -33,73 +31,80 @@ class TradesScreen extends ConsumerWidget { child: Column( children: [ Expanded( - child: Container( - decoration: const BoxDecoration( - color: AppTheme.dark2, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - 'My Active Trades', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), + child: Column( + children: [ + // Header with dark background + Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: AppTheme.dark1, + border: Border( + bottom: BorderSide(color: Colors.white24, width: 0.5), ), ), - // 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, []), + child: const Text( + 'My Active Trades', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - const SizedBox(height: 6.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, + ), + // Content area with dark background + Expanded( + child: Container( + decoration: const BoxDecoration( + color: AppTheme.dark1, + ), + child: Column( + children: [ + // Espacio superior + const SizedBox(height: 16.0), + Expanded( + child: tradesAsync.when( + data: (trades) => _buildOrderList(trades), + loading: () => const Center( + child: CircularProgressIndicator(), ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - ref.invalidate(orderEventsProvider); - ref.invalidate(filteredTradesProvider); - }, - child: const Text('Retry'), + 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(), @@ -109,54 +114,7 @@ class TradesScreen extends ConsumerWidget { ); } - 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 2da3d4c5..b131f5ba 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -21,18 +21,18 @@ class TradesListItem extends ConsumerWidget { final session = ref.watch(sessionProvider(trade.orderId!)); final role = session?.role; final isBuying = role == Role.buyer; + // Determine if the user is the creator of the order based on available information + final isCreator = session != null && role != null; return GestureDetector( onTap: () { context.push('/trade_detail/${trade.orderId}'); }, child: Container( - margin: const EdgeInsets.only(bottom: 1), + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), decoration: BoxDecoration( - color: AppTheme.dark1, - border: Border( - bottom: BorderSide(color: Colors.grey.shade900, width: 1), - ), + color: AppTheme.dark2, // Más oscuro para los items + borderRadius: BorderRadius.circular(12.0), ), child: Padding( padding: const EdgeInsets.all(16.0), @@ -44,7 +44,7 @@ class TradesListItem extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // First row: Buy/Sell Bitcoin + Status + // First row: Buy/Sell Bitcoin text + status and role chips Row( children: [ Text( @@ -55,20 +55,29 @@ class TradesListItem extends ConsumerWidget { fontWeight: FontWeight.w600, ), ), - const SizedBox(width: 8), + const Spacer(), _buildStatusChip(trade.status), + const SizedBox(width: 8), + _buildRoleChip(isCreator), ], ), const SizedBox(height: 8), - // Second row: Amount and currency + // Second row: Flag + Amount and currency Row( children: [ Text( - '${isBuying ? trade.currency : trade.amount} ${CurrencyUtils.getFlagFromCurrency(trade.currency!)}', + CurrencyUtils.getFlagFromCurrency(trade.currency ?? '') ?? '', + style: const TextStyle( + fontSize: 16, + ), + ), + const SizedBox(width: 4), + Text( + '${trade.amount ?? '0'} ${trade.currency ?? ''}', style: const TextStyle( color: Colors.white, fontSize: 16, - fontWeight: FontWeight.w500, + fontWeight: FontWeight.bold, ), ), ], @@ -100,61 +109,91 @@ class TradesListItem extends ConsumerWidget { ); } + Widget _buildRoleChip(bool isCreator) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: isCreator ? Colors.blue.shade700 : Colors.purple.shade700, + 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, + ), + ), + ); + } + Widget _buildStatusChip(Status status) { Color backgroundColor; - Color textColor = Colors.white; + Color textColor; // Ya no siempre blanca String label; switch (status) { case Status.active: - backgroundColor = Colors.blue; + backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + textColor = const Color(0xFF93C5FD); // Azul claro label = 'Active'; break; case Status.pending: - backgroundColor = Colors.amber.shade800; + backgroundColor = const Color(0xFF854D0E); // Ámbar oscuro + textColor = const Color(0xFFFCD34D); // Ámbar claro label = 'Pending'; break; case Status.fiatSent: - backgroundColor = Colors.green.shade700; + backgroundColor = const Color(0xFF065F46); // Verde oscuro + textColor = const Color(0xFF6EE7B7); // Verde claro label = 'Fiat-sent'; break; case Status.canceled: case Status.canceledByAdmin: case Status.cooperativelyCanceled: - backgroundColor = Colors.grey.shade700; + backgroundColor = Colors.grey.shade800; + textColor = Colors.grey.shade300; label = 'Canceled'; break; case Status.settledByAdmin: case Status.settledHoldInvoice: - backgroundColor = Colors.purple; + backgroundColor = const Color(0xFF581C87); // Morado oscuro + textColor = const Color(0xFFC084FC); // Morado claro label = 'Settled'; break; case Status.completedByAdmin: - backgroundColor = Colors.green; + backgroundColor = const Color(0xFF065F46); // Verde oscuro + textColor = const Color(0xFF6EE7B7); // Verde claro label = 'Completed'; break; case Status.dispute: - backgroundColor = Colors.red; + backgroundColor = const Color(0xFF7F1D1D); // Rojo oscuro + textColor = const Color(0xFFFCA5A5); // Rojo claro label = 'Dispute'; break; case Status.expired: - backgroundColor = Colors.grey; + backgroundColor = Colors.grey.shade800; + textColor = Colors.grey.shade300; label = 'Expired'; break; case Status.success: - backgroundColor = Colors.green; + backgroundColor = const Color(0xFF065F46); // Verde oscuro + textColor = const Color(0xFF6EE7B7); // Verde claro label = 'Success'; break; case Status.waitingBuyerInvoice: - backgroundColor = Colors.blue.shade300; + backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + textColor = const Color(0xFF93C5FD); // Azul claro label = 'Waiting Invoice'; break; case Status.waitingPayment: - backgroundColor = Colors.blue.shade400; + backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + textColor = const Color(0xFF93C5FD); // Azul claro label = 'Waiting Payment'; break; case Status.inProgress: - backgroundColor = Colors.blue.shade700; + backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + textColor = const Color(0xFF93C5FD); // Azul claro label = 'In Progress'; break; } @@ -163,12 +202,12 @@ class TradesListItem extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(12), // Más redondeado ), child: Text( label, style: TextStyle( - color: textColor, + color: textColor, // Color específico para cada estado fontSize: 12, fontWeight: FontWeight.w500, ), From a09b1b591ad5f15ad0503dde181098d945243d65 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 5 Jun 2025 18:30:00 -0300 Subject: [PATCH 03/11] fix: error showing the amount in active trades --- .../trades/widgets/trades_list_item.dart | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index b131f5ba..23b847d2 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -73,7 +73,7 @@ class TradesListItem extends ConsumerWidget { ), const SizedBox(width: 4), Text( - '${trade.amount ?? '0'} ${trade.currency ?? ''}', + '${trade.fiatAmount.minimum} ${trade.currency ?? ''}', style: const TextStyle( color: Colors.white, fontSize: 16, @@ -83,16 +83,22 @@ class TradesListItem extends ConsumerWidget { ], ), const SizedBox(height: 4), - // Third row: Payment method - Text( - trade.paymentMethods.isNotEmpty - ? trade.paymentMethods.first - : 'Bank Transfer', - style: TextStyle( - color: Colors.grey.shade400, - fontSize: 14, - ), - ), + // 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, + ), + ), ], ), ), From 345ce713c70acab9ad8bcea82fce81c14fe46802 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 14 Jun 2025 03:11:04 -0300 Subject: [PATCH 04/11] improve colors --- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../trades/screens/trades_screen.dart | 11 +-- .../trades/widgets/trades_list_item.dart | 71 ++++++++++++++----- 3 files changed, 60 insertions(+), 25 deletions(-) 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/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index 429bb553..b3fb50a1 100644 --- a/lib/features/trades/screens/trades_screen.dart +++ b/lib/features/trades/screens/trades_screen.dart @@ -16,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( @@ -38,7 +38,7 @@ class TradesScreen extends ConsumerWidget { width: double.infinity, padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: AppTheme.dark1, + color: AppTheme.backgroundDark, border: Border( bottom: BorderSide(color: Colors.white24, width: 0.5), ), @@ -56,7 +56,7 @@ class TradesScreen extends ConsumerWidget { Expanded( child: Container( decoration: const BoxDecoration( - color: AppTheme.dark1, + color: AppTheme.backgroundDark, ), child: Column( children: [ @@ -84,7 +84,8 @@ class TradesScreen extends ConsumerWidget { ), Text( error.toString(), - style: TextStyle(color: AppTheme.cream1, fontSize: 12), + style: TextStyle( + color: AppTheme.cream1, fontSize: 12), textAlign: TextAlign.center, ), const SizedBox(height: 16), diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index fa1dc4b4..c8fbcf6f 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -5,7 +5,9 @@ import 'package:go_router/go_router.dart'; 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/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/utils/currency_utils.dart'; @@ -21,8 +23,12 @@ class TradesListItem extends ConsumerWidget { final session = ref.watch(sessionProvider(trade.orderId!)); final role = session?.role; final isBuying = role == Role.buyer; - // Determine if the user is the creator of the order based on available information - final isCreator = session != null && role != null; + 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: () { @@ -31,7 +37,7 @@ class TradesListItem extends ConsumerWidget { child: Container( margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), decoration: BoxDecoration( - color: AppTheme.dark2, // Más oscuro para los items + color: const Color(0xFF1D212C), // Mismo color que el fondo de órdenes en home borderRadius: BorderRadius.circular(12.0), ), child: Padding( @@ -56,17 +62,19 @@ class TradesListItem extends ConsumerWidget { ), ), const Spacer(), - _buildStatusChip(trade.status), + _buildStatusChip(orderState.status), const SizedBox(width: 8), _buildRoleChip(isCreator), ], ), const SizedBox(height: 8), - // Second row: Flag + Amount and currency + // Second row: Flag + Amount and currency + Premium/Discount Row( children: [ Text( - CurrencyUtils.getFlagFromCurrency(trade.currency ?? '') ?? '', + CurrencyUtils.getFlagFromCurrency( + trade.currency ?? '') ?? + '', style: const TextStyle( fontSize: 16, ), @@ -80,6 +88,31 @@ class TradesListItem extends ConsumerWidget { 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), @@ -119,7 +152,7 @@ class TradesListItem extends ConsumerWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( - color: isCreator ? Colors.blue.shade700 : Colors.purple.shade700, + 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( @@ -140,65 +173,65 @@ class TradesListItem extends ConsumerWidget { switch (status) { case Status.active: - backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + 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); // Ámbar oscuro + backgroundColor = const Color(0xFF854D0E).withOpacity(0.3); // Ámbar oscuro con transparencia textColor = const Color(0xFFFCD34D); // Ámbar claro label = 'Pending'; break; case Status.fiatSent: - backgroundColor = const Color(0xFF065F46); // Verde oscuro + 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; + backgroundColor = Colors.grey.shade800.withOpacity(0.3); textColor = Colors.grey.shade300; label = 'Canceled'; break; case Status.settledByAdmin: case Status.settledHoldInvoice: - backgroundColor = const Color(0xFF581C87); // Morado oscuro + 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); // Verde oscuro + 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); // Rojo oscuro + 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; + backgroundColor = Colors.grey.shade800.withOpacity(0.3); textColor = Colors.grey.shade300; label = 'Expired'; break; case Status.success: - backgroundColor = const Color(0xFF065F46); // Verde oscuro + backgroundColor = const Color(0xFF065F46).withOpacity(0.3); // Verde oscuro con transparencia textColor = const Color(0xFF6EE7B7); // Verde claro label = 'Success'; break; case Status.waitingBuyerInvoice: - backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia textColor = const Color(0xFF93C5FD); // Azul claro label = 'Waiting Invoice'; break; case Status.waitingPayment: - backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia textColor = const Color(0xFF93C5FD); // Azul claro label = 'Waiting Payment'; break; case Status.inProgress: - backgroundColor = const Color(0xFF1E3A8A); // Azul oscuro + backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia textColor = const Color(0xFF93C5FD); // Azul claro label = 'In Progress'; break; From 6363aa91aef9aff0a39e5da9a346b75cd86c2042 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Mon, 16 Jun 2025 21:22:47 -0300 Subject: [PATCH 05/11] feat: implement action-to-status mapping and update Mostro's public key --- lib/features/order/models/order_state.dart | 55 +++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 5b59bc89..2dbcc7c0 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -83,8 +83,12 @@ class OrderState { OrderState updateWith(MostroMessage message) { _logger.i('Updating OrderState Action: ${message.action}'); + + // Determine the new status based on the action received + Status newStatus = _getStatusFromAction(message.action, message.getPayload()?.status); + return copyWith( - status: message.getPayload()?.status ?? status, + status: newStatus, action: message.action != Action.cantDo ? message.action : action, order: message.payload is Order ? message.getPayload() @@ -98,8 +102,55 @@ class OrderState { ); } + /// 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: + return Status.waitingPayment; + + // Actions that should set status to waiting-buyer-invoice + case Action.waitingBuyerInvoice: + case Action.addInvoice: + return Status.waitingBuyerInvoice; + + // 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.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: + case Action.takeSell: + case Action.takeBuy: + 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 = { From e4b2cd67d9971301ed07cd766387bd4a8a460db2 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Mon, 16 Jun 2025 21:42:09 -0300 Subject: [PATCH 06/11] fix: preserve order state for cantDo messages and update Mostro pubkey --- lib/features/order/models/order_state.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 2dbcc7c0..3a762548 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -84,12 +84,17 @@ class OrderState { OrderState updateWith(MostroMessage message) { _logger.i('Updating OrderState 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); return copyWith( status: newStatus, - action: message.action != Action.cantDo ? message.action : action, + action: message.action, order: message.payload is Order ? message.getPayload() : message.payload is PaymentRequest From d583e3f1c79348fa396ae15e49552a391ac58c81 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Tue, 17 Jun 2025 19:13:52 -0300 Subject: [PATCH 07/11] fix: update rate button logic and status transitions in trade detail screen and order state --- lib/features/order/models/order_state.dart | 91 ++++++++-- .../trades/screens/trade_detail_screen.dart | 23 ++- .../trades/widgets/trades_list_item.dart | 169 +++++++++--------- 3 files changed, 177 insertions(+), 106 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 3a762548..6074a6a7 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -83,15 +83,16 @@ class OrderState { OrderState updateWith(MostroMessage message) { _logger.i('Updating OrderState 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); - + Status newStatus = _getStatusFromAction( + message.action, message.getPayload()?.status); + return copyWith( status: newStatus, action: message.action, @@ -113,41 +114,41 @@ class OrderState { // Actions that should set status to waiting-payment case Action.waitingSellerToPay: return Status.waitingPayment; - + // Actions that should set status to waiting-buyer-invoice case Action.waitingBuyerInvoice: case Action.addInvoice: return Status.waitingBuyerInvoice; - + // 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.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: case Action.takeSell: case Action.takeBuy: return payloadStatus ?? status; - + // For other actions, keep the current status unless payload has a different one default: return payloadStatus ?? status; @@ -176,16 +177,32 @@ class OrderState { Action.cancel, ], }, + Status.waitingPayment: { + Action.waitingSellerToPay: [ + Action.cancel, + ], + Action.payInvoice: [ + Action.payInvoice, + Action.cancel, + ], + }, + Status.waitingBuyerInvoice: { + Action.waitingBuyerInvoice: [ + Action.cancel, + ], + Action.addInvoice: [ + Action.cancel, + ], + }, Status.active: { Action.buyerTookOrder: [ - Action.buyerTookOrder, Action.cancel, Action.dispute, ], Action.fiatSentOk: [ + Action.release, Action.cancel, Action.dispute, - Action.release, ], Action.rate: [ Action.rate, @@ -196,10 +213,23 @@ class OrderState { Action.cancel, ], }, - Status.waitingPayment: { - Action.payInvoice: [ - Action.payInvoice, + Status.fiatSent: { + Action.fiatSentOk: [ + Action.release, Action.cancel, + Action.dispute, + ], + }, + Status.success: { + Action.rate: [ + Action.rate, + ], + Action.rateReceived: [], + Action.purchaseCompleted: [ + Action.rate, + ], + Action.released: [ + Action.rate, ], }, }, @@ -213,18 +243,25 @@ class OrderState { Action.cancel, ], }, + Status.waitingPayment: { + Action.waitingSellerToPay: [ + Action.cancel, + ], + }, Status.waitingBuyerInvoice: { Action.addInvoice: [ Action.addInvoice, Action.cancel, ], + Action.waitingBuyerInvoice: [ + Action.cancel, + ], Action.waitingSellerToPay: [ Action.cancel, ], }, Status.active: { Action.holdInvoicePaymentAccepted: [ - Action.holdInvoicePaymentAccepted, Action.fiatSent, Action.cancel, Action.dispute, @@ -243,6 +280,24 @@ class OrderState { Action.purchaseCompleted: [], Action.paymentFailed: [], }, - }, + Status.fiatSent: { + Action.fiatSentOk: [ + Action.cancel, + Action.dispute, + ], + }, + Status.success: { + Action.rate: [ + Action.rate, + ], + Action.rateReceived: [], + Action.purchaseCompleted: [ + Action.rate, + ], + Action.released: [ + Action.rate, + ], + }, + } }; } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 1344428a..a3d97ca9 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -349,14 +349,13 @@ class TradeDetailScreen extends ConsumerWidget { 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.holdInvoicePaymentAccepted: widgets.add(_buildContactButton(context)); - break; case actions.Action.holdInvoicePaymentSettled: case actions.Action.holdInvoicePaymentCanceled: @@ -386,12 +385,28 @@ class TradeDetailScreen extends ConsumerWidget { } } - // Special case for RATE button after settlement + if (tradeState.status == Status.success) { + bool hasRateButton = widgets.any((widget) => + widget is MostroReactiveButton && widget.toString().contains('RATE')); + + if (!hasRateButton && + (tradeState.action == actions.Action.purchaseCompleted || + tradeState.action == actions.Action.released || + tradeState.action == actions.Action.rate)) { + widgets.add(_buildNostrButton( + 'RATE', + action: actions.Action.rate, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + )); + } + } + if (tradeState.status == Status.settledHoldInvoice && tradeState.action == actions.Action.rate) { widgets.add(_buildNostrButton( 'RATE', - action: actions.Action.rateReceived, + action: actions.Action.rate, backgroundColor: AppTheme.mostroGreen, onPressed: () => context.push('/rate_user/$orderId'), )); diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index c8fbcf6f..55f95368 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -2,7 +2,7 @@ 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: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'; @@ -167,90 +167,91 @@ class TradesListItem extends ConsumerWidget { } Widget _buildStatusChip(Status status) { - Color backgroundColor; - Color textColor; // Ya no siempre blanca - String label; + Color backgroundColor; + Color textColor; + String label; - 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; - 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; - case Status.waitingBuyerInvoice: - backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia - textColor = const Color(0xFF93C5FD); // Azul claro - label = 'Waiting Invoice'; - break; - case Status.waitingPayment: - backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia - textColor = const Color(0xFF93C5FD); // Azul claro - label = 'Waiting Payment'; - break; - case Status.inProgress: - backgroundColor = const Color(0xFF1E3A8A).withOpacity(0.3); // Azul oscuro con transparencia - textColor = const Color(0xFF93C5FD); // Azul claro - 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 Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(12), // Más redondeado + 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, ), - child: Text( - label, - style: TextStyle( - color: textColor, // Color específico para cada estado - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ); - } + ), + ); +} } From 435d17d405b5c07d0eaa3fb5e0de19b1e35f95c6 Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 18 Jun 2025 23:41:50 -0700 Subject: [PATCH 08/11] Update order_state.dart refactor: expand order state transitions to advance to the next status --- lib/features/order/models/order_state.dart | 260 +++++++++++++++++++-- 1 file changed, 236 insertions(+), 24 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 6074a6a7..26dd3e28 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -113,6 +113,7 @@ class OrderState { 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 @@ -131,6 +132,10 @@ class OrderState { case Action.fiatSentOk: return Status.fiatSent; + // Actions that should set status to settled-hold-invoice + case Action.release: + return Status.settledHoldInvoice; + // Actions that should set status to success (completed) case Action.purchaseCompleted: case Action.released: @@ -139,17 +144,57 @@ class OrderState { // Actions that should set status to canceled case Action.canceled: + return Status.canceled; + + // Actions that should set status to canceled-by-admin + case Action.adminCancel: case Action.adminCanceled: + return Status.canceledByAdmin; + + // Actions that should set status to settled-by-admin + case Action.adminSettle: + case Action.adminSettled: + return Status.settledByAdmin; + + // Actions that should set status to cooperatively-canceled case Action.cooperativeCancelAccepted: - return Status.canceled; + case Action.cooperativeCancelInitiatedByYou: + case Action.cooperativeCancelInitiatedByPeer: + return Status.cooperativelyCanceled; + + // Actions that should set status to dispute + case Action.dispute: + case Action.disputeInitiatedByYou: + case Action.disputeInitiatedByPeer: + return Status.dispute; - // For actions that include Order payload, use the payload status + // Actions that should set status to in-progress (dispute resolution) + case Action.adminAddSolver: + case Action.adminTakeDispute: + case Action.adminTookDispute: + return Status.inProgress; + + // For order creation and taking actions, use the payload status case Action.newOrder: case Action.takeSell: case Action.takeBuy: return payloadStatus ?? status; - // For other actions, keep the current status unless payload has a different one + // For informational actions that don't change state + case Action.cantDo: + case Action.rate: + case Action.rateUser: + case Action.sendDm: + case Action.tradePubkey: + case Action.invoiceUpdated: + return status; // Keep current status + + // For payment failure, keep current status but could trigger retry logic + case Action.paymentFailed: + case Action.holdInvoicePaymentCanceled: + return status; + + // For other actions, use payload status if available, otherwise keep current default: return payloadStatus ?? status; } @@ -165,53 +210,53 @@ class OrderState { Action.takeBuy: [ Action.takeBuy, Action.cancel, - ], - Action.waitingBuyerInvoice: [ - Action.cancel, - ], - Action.payInvoice: [ - Action.payInvoice, - Action.cancel, + Action.dispute, ], Action.newOrder: [ Action.cancel, + Action.dispute, ], }, Status.waitingPayment: { Action.waitingSellerToPay: [ + Action.payInvoice, Action.cancel, + Action.dispute, ], Action.payInvoice: [ Action.payInvoice, Action.cancel, + Action.dispute, ], }, Status.waitingBuyerInvoice: { Action.waitingBuyerInvoice: [ Action.cancel, + Action.dispute, ], Action.addInvoice: [ Action.cancel, + Action.dispute, ], }, Status.active: { Action.buyerTookOrder: [ Action.cancel, Action.dispute, + Action.holdInvoicePaymentSettled, ], Action.fiatSentOk: [ Action.release, Action.cancel, Action.dispute, ], - Action.rate: [ - Action.rate, - ], - Action.purchaseCompleted: [], - Action.holdInvoicePaymentSettled: [], Action.cooperativeCancelInitiatedByPeer: [ + Action.cooperativeCancelAccepted, Action.cancel, ], + Action.rate: [ + Action.rate, + ], }, Status.fiatSent: { Action.fiatSentOk: [ @@ -219,6 +264,18 @@ class OrderState { Action.cancel, Action.dispute, ], + Action.cooperativeCancelInitiatedByPeer: [ + Action.cooperativeCancelAccepted, + Action.cancel, + ], + }, + Status.settledHoldInvoice: { + Action.purchaseCompleted: [ + Action.rate, + ], + Action.released: [ + Action.rate, + ], }, Status.success: { Action.rate: [ @@ -232,12 +289,29 @@ class OrderState { Action.rate, ], }, + Status.dispute: {}, + Status.inProgress: {}, + Status.canceled: {}, + Status.canceledByAdmin: {}, + Status.settledByAdmin: { + Action.adminSettled: [ + Action.rate, + ], + }, + Status.completedByAdmin: { + Action.adminSettled: [ + Action.rate, + ], + }, + Status.cooperativelyCanceled: {}, + Status.expired: {}, }, Role.buyer: { Status.pending: { Action.takeSell: [ Action.takeSell, Action.cancel, + Action.dispute, ], Action.newOrder: [ Action.cancel, @@ -246,18 +320,19 @@ class OrderState { Status.waitingPayment: { Action.waitingSellerToPay: [ Action.cancel, + Action.dispute, + Action.cooperativeCancelInitiatedByYou, ], }, Status.waitingBuyerInvoice: { Action.addInvoice: [ Action.addInvoice, Action.cancel, + Action.dispute, ], Action.waitingBuyerInvoice: [ Action.cancel, - ], - Action.waitingSellerToPay: [ - Action.cancel, + Action.dispute, ], }, Status.active: { @@ -265,26 +340,47 @@ class OrderState { Action.fiatSent, Action.cancel, Action.dispute, + Action.holdInvoicePaymentSettled, ], - Action.fiatSentOk: [ + Action.buyerTookOrder: [ + Action.fiatSent, Action.cancel, Action.dispute, ], - Action.rate: [ - Action.rate, + Action.fiatSentOk: [ + Action.cancel, + Action.dispute, ], Action.cooperativeCancelInitiatedByPeer: [ + Action.cooperativeCancelAccepted, Action.cancel, ], - Action.rateReceived: [], - Action.purchaseCompleted: [], - Action.paymentFailed: [], + Action.paymentFailed: [ + Action.cancel, + Action.dispute, + ], }, Status.fiatSent: { + Action.fiatSent: [ + Action.cancel, + Action.dispute, + ], Action.fiatSentOk: [ Action.cancel, Action.dispute, ], + Action.cooperativeCancelInitiatedByPeer: [ + Action.cooperativeCancelAccepted, + Action.cancel, + ], + }, + Status.settledHoldInvoice: { + Action.purchaseCompleted: [ + Action.rate, + ], + Action.holdInvoicePaymentSettled: [ + Action.rate, + ], }, Status.success: { Action.rate: [ @@ -298,6 +394,122 @@ class OrderState { Action.rate, ], }, + Status.dispute: {}, + Status.inProgress: {}, + Status.canceled: {}, + Status.canceledByAdmin: {}, + Status.settledByAdmin: { + Action.adminSettled: [ + Action.rate, + ], + }, + Status.completedByAdmin: { + Action.adminSettled: [ + Action.rate, + ], + }, + Status.cooperativelyCanceled: {}, + Status.expired: {}, + }, + Role.admin: { + Status.pending: { + Action.newOrder: [ + Action.adminCancel, + ], + Action.takeBuy: [ + Action.adminCancel, + ], + Action.takeSell: [ + Action.adminCancel, + ], + }, + Status.waitingPayment: { + Action.waitingSellerToPay: [ + Action.adminCancel, + ], + Action.payInvoice: [ + Action.adminCancel, + ], + }, + Status.waitingBuyerInvoice: { + Action.waitingBuyerInvoice: [ + Action.adminCancel, + ], + Action.addInvoice: [ + Action.adminCancel, + ], + }, + Status.active: { + Action.buyerTookOrder: [ + Action.adminCancel, + Action.adminSettle, + ], + Action.holdInvoicePaymentAccepted: [ + Action.adminCancel, + Action.adminSettle, + ], + Action.fiatSentOk: [ + Action.adminCancel, + Action.adminSettle, + ], + }, + Status.fiatSent: { + Action.fiatSent: [ + Action.adminCancel, + Action.adminSettle, + ], + Action.fiatSentOk: [ + Action.adminCancel, + Action.adminSettle, + ], + }, + Status.settledHoldInvoice: { + Action.purchaseCompleted: [ + Action.adminSettle, + Action.adminCancel, + ], + Action.released: [ + Action.adminSettle, + Action.adminCancel, + ], + }, + Status.dispute: { + Action.dispute: [ + Action.adminSettle, + Action.adminCancel, + Action.adminAddSolver, + Action.adminTakeDispute, + ], + Action.disputeInitiatedByYou: [ + Action.adminSettle, + Action.adminCancel, + Action.adminAddSolver, + Action.adminTakeDispute, + ], + Action.disputeInitiatedByPeer: [ + Action.adminSettle, + Action.adminCancel, + Action.adminAddSolver, + Action.adminTakeDispute, + ], + }, + Status.inProgress: { + Action.adminTookDispute: [ + Action.adminSettle, + Action.adminCancel, + ], + Action.adminAddSolver: [ + Action.adminSettle, + Action.adminCancel, + ], + }, + Status.expired: {}, + Status.success: {}, + Status.canceled: {}, + Status.canceledByAdmin: {}, + Status.settledByAdmin: {}, + Status.completedByAdmin: {}, + Status.cooperativelyCanceled: {}, } }; } From f413c80aad431c0fe804fd12bfb73d1b71e519ec Mon Sep 17 00:00:00 2001 From: Biz Date: Wed, 18 Jun 2025 23:43:50 -0700 Subject: [PATCH 09/11] Update trade_detail_screen.dart Adjust visible buttons --- .../trades/screens/trade_detail_screen.dart | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index a3d97ca9..bab21ac2 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -354,10 +354,10 @@ class TradeDetailScreen extends ConsumerWidget { 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; @@ -385,33 +385,6 @@ class TradeDetailScreen extends ConsumerWidget { } } - if (tradeState.status == Status.success) { - bool hasRateButton = widgets.any((widget) => - widget is MostroReactiveButton && widget.toString().contains('RATE')); - - if (!hasRateButton && - (tradeState.action == actions.Action.purchaseCompleted || - tradeState.action == actions.Action.released || - tradeState.action == actions.Action.rate)) { - widgets.add(_buildNostrButton( - 'RATE', - action: actions.Action.rate, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), - )); - } - } - - if (tradeState.status == Status.settledHoldInvoice && - tradeState.action == actions.Action.rate) { - widgets.add(_buildNostrButton( - 'RATE', - action: actions.Action.rate, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), - )); - } - return widgets; } From 2979a070d6e8b993a828ebea1aac9b489a216d23 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 20 Jun 2025 15:30:03 -0300 Subject: [PATCH 10/11] fix: error with status success - rate --- lib/features/order/models/order_state.dart | 52 ++-------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 26dd3e28..0ededd10 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -113,7 +113,6 @@ class OrderState { 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 @@ -132,69 +131,26 @@ class OrderState { case Action.fiatSentOk: return Status.fiatSent; - // Actions that should set status to settled-hold-invoice - case Action.release: - return Status.settledHoldInvoice; - // 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: - return Status.canceled; - - // Actions that should set status to canceled-by-admin - case Action.adminCancel: case Action.adminCanceled: - return Status.canceledByAdmin; - - // Actions that should set status to settled-by-admin - case Action.adminSettle: - case Action.adminSettled: - return Status.settledByAdmin; - - // Actions that should set status to cooperatively-canceled case Action.cooperativeCancelAccepted: - case Action.cooperativeCancelInitiatedByYou: - case Action.cooperativeCancelInitiatedByPeer: - return Status.cooperativelyCanceled; - - // Actions that should set status to dispute - case Action.dispute: - case Action.disputeInitiatedByYou: - case Action.disputeInitiatedByPeer: - return Status.dispute; - - // Actions that should set status to in-progress (dispute resolution) - case Action.adminAddSolver: - case Action.adminTakeDispute: - case Action.adminTookDispute: - return Status.inProgress; + return Status.canceled; - // For order creation and taking actions, use the payload status + // For actions that include Order payload, use the payload status case Action.newOrder: case Action.takeSell: case Action.takeBuy: return payloadStatus ?? status; - // For informational actions that don't change state - case Action.cantDo: - case Action.rate: - case Action.rateUser: - case Action.sendDm: - case Action.tradePubkey: - case Action.invoiceUpdated: - return status; // Keep current status - - // For payment failure, keep current status but could trigger retry logic - case Action.paymentFailed: - case Action.holdInvoicePaymentCanceled: - return status; - - // For other actions, use payload status if available, otherwise keep current + // For other actions, keep the current status unless payload has a different one default: return payloadStatus ?? status; } From d68722a1a32aa53aef4731acb772f2cf59e10e2c Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Tue, 24 Jun 2025 01:30:18 -0300 Subject: [PATCH 11/11] feat: improve cooperative cancel flow and order state management --- lib/features/order/models/order_state.dart | 261 ++++-------------- .../notfiers/abstract_mostro_notifier.dart | 11 +- .../order/notfiers/order_notifier.dart | 33 +-- .../trades/providers/trades_provider.dart | 19 +- .../trades/screens/trade_detail_screen.dart | 71 +++-- 5 files changed, 144 insertions(+), 251 deletions(-) diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart index 0ededd10..0a6df8cc 100644 --- a/lib/features/order/models/order_state.dart +++ b/lib/features/order/models/order_state.dart @@ -82,7 +82,7 @@ class OrderState { } OrderState updateWith(MostroMessage message) { - _logger.i('Updating OrderState Action: ${message.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) { @@ -93,7 +93,19 @@ class OrderState { Status newStatus = _getStatusFromAction( message.action, message.getPayload()?.status); - return copyWith( + // 🔍 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 @@ -101,11 +113,17 @@ class OrderState { : 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 @@ -113,6 +131,7 @@ class OrderState { 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 @@ -120,6 +139,15 @@ class OrderState { 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: @@ -146,8 +174,6 @@ class OrderState { // For actions that include Order payload, use the payload status case Action.newOrder: - case Action.takeSell: - case Action.takeBuy: return payloadStatus ?? status; // For other actions, keep the current status unless payload has a different one @@ -163,55 +189,46 @@ class OrderState { static final Map>>> actions = { Role.seller: { Status.pending: { - Action.takeBuy: [ - Action.takeBuy, + Action.newOrder: [ Action.cancel, - Action.dispute, ], - Action.newOrder: [ + Action.takeBuy: [ Action.cancel, - Action.dispute, ], }, Status.waitingPayment: { - Action.waitingSellerToPay: [ + Action.payInvoice: [ Action.payInvoice, Action.cancel, - Action.dispute, ], - Action.payInvoice: [ + Action.waitingSellerToPay: [ Action.payInvoice, Action.cancel, - Action.dispute, ], }, Status.waitingBuyerInvoice: { Action.waitingBuyerInvoice: [ Action.cancel, - Action.dispute, ], Action.addInvoice: [ Action.cancel, - Action.dispute, + ], + Action.takeBuy: [ + Action.cancel, ], }, Status.active: { Action.buyerTookOrder: [ Action.cancel, Action.dispute, - Action.holdInvoicePaymentSettled, ], - Action.fiatSentOk: [ - Action.release, + Action.holdInvoicePaymentAccepted: [ Action.cancel, Action.dispute, ], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cooperativeCancelAccepted, + Action.holdInvoicePaymentSettled: [ Action.cancel, - ], - Action.rate: [ - Action.rate, + Action.dispute, ], }, Status.fiatSent: { @@ -220,75 +237,49 @@ class OrderState { Action.cancel, Action.dispute, ], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cooperativeCancelAccepted, - Action.cancel, - ], - }, - Status.settledHoldInvoice: { - Action.purchaseCompleted: [ - Action.rate, - ], - Action.released: [ - Action.rate, - ], }, Status.success: { Action.rate: [ Action.rate, ], - Action.rateReceived: [], Action.purchaseCompleted: [ Action.rate, ], Action.released: [ Action.rate, ], + Action.rateReceived: [], }, - Status.dispute: {}, - Status.inProgress: {}, - Status.canceled: {}, - Status.canceledByAdmin: {}, - Status.settledByAdmin: { - Action.adminSettled: [ - Action.rate, - ], + Status.canceled: { + Action.canceled: [], + Action.adminCanceled: [], + Action.cooperativeCancelAccepted: [], }, - Status.completedByAdmin: { - Action.adminSettled: [ - Action.rate, - ], - }, - Status.cooperativelyCanceled: {}, - Status.expired: {}, }, Role.buyer: { Status.pending: { - Action.takeSell: [ - Action.takeSell, + Action.newOrder: [ Action.cancel, - Action.dispute, ], - Action.newOrder: [ + Action.takeSell: [ Action.cancel, ], }, Status.waitingPayment: { Action.waitingSellerToPay: [ Action.cancel, - Action.dispute, - Action.cooperativeCancelInitiatedByYou, + ], + Action.takeSell: [ + Action.cancel, ], }, Status.waitingBuyerInvoice: { Action.addInvoice: [ Action.addInvoice, Action.cancel, - Action.dispute, ], Action.waitingBuyerInvoice: [ Action.cancel, - Action.dispute, ], }, Status.active: { @@ -296,176 +287,40 @@ class OrderState { Action.fiatSent, Action.cancel, Action.dispute, - Action.holdInvoicePaymentSettled, ], - Action.buyerTookOrder: [ + Action.holdInvoicePaymentSettled: [ Action.fiatSent, Action.cancel, Action.dispute, ], - Action.fiatSentOk: [ - Action.cancel, - Action.dispute, - ], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cooperativeCancelAccepted, - Action.cancel, - ], - Action.paymentFailed: [ + Action.buyerTookOrder: [ Action.cancel, Action.dispute, ], }, Status.fiatSent: { - Action.fiatSent: [ - Action.cancel, - Action.dispute, - ], Action.fiatSentOk: [ Action.cancel, Action.dispute, ], - Action.cooperativeCancelInitiatedByPeer: [ - Action.cooperativeCancelAccepted, - Action.cancel, - ], - }, - Status.settledHoldInvoice: { - Action.purchaseCompleted: [ - Action.rate, - ], - Action.holdInvoicePaymentSettled: [ - Action.rate, - ], }, Status.success: { Action.rate: [ Action.rate, ], - Action.rateReceived: [], Action.purchaseCompleted: [ Action.rate, ], Action.released: [ Action.rate, ], + Action.rateReceived: [], }, - Status.dispute: {}, - Status.inProgress: {}, - Status.canceled: {}, - Status.canceledByAdmin: {}, - Status.settledByAdmin: { - Action.adminSettled: [ - Action.rate, - ], - }, - Status.completedByAdmin: { - Action.adminSettled: [ - Action.rate, - ], + Status.canceled: { + Action.canceled: [], + Action.adminCanceled: [], + Action.cooperativeCancelAccepted: [], }, - Status.cooperativelyCanceled: {}, - Status.expired: {}, }, - Role.admin: { - Status.pending: { - Action.newOrder: [ - Action.adminCancel, - ], - Action.takeBuy: [ - Action.adminCancel, - ], - Action.takeSell: [ - Action.adminCancel, - ], - }, - Status.waitingPayment: { - Action.waitingSellerToPay: [ - Action.adminCancel, - ], - Action.payInvoice: [ - Action.adminCancel, - ], - }, - Status.waitingBuyerInvoice: { - Action.waitingBuyerInvoice: [ - Action.adminCancel, - ], - Action.addInvoice: [ - Action.adminCancel, - ], - }, - Status.active: { - Action.buyerTookOrder: [ - Action.adminCancel, - Action.adminSettle, - ], - Action.holdInvoicePaymentAccepted: [ - Action.adminCancel, - Action.adminSettle, - ], - Action.fiatSentOk: [ - Action.adminCancel, - Action.adminSettle, - ], - }, - Status.fiatSent: { - Action.fiatSent: [ - Action.adminCancel, - Action.adminSettle, - ], - Action.fiatSentOk: [ - Action.adminCancel, - Action.adminSettle, - ], - }, - Status.settledHoldInvoice: { - Action.purchaseCompleted: [ - Action.adminSettle, - Action.adminCancel, - ], - Action.released: [ - Action.adminSettle, - Action.adminCancel, - ], - }, - Status.dispute: { - Action.dispute: [ - Action.adminSettle, - Action.adminCancel, - Action.adminAddSolver, - Action.adminTakeDispute, - ], - Action.disputeInitiatedByYou: [ - Action.adminSettle, - Action.adminCancel, - Action.adminAddSolver, - Action.adminTakeDispute, - ], - Action.disputeInitiatedByPeer: [ - Action.adminSettle, - Action.adminCancel, - Action.adminAddSolver, - Action.adminTakeDispute, - ], - }, - Status.inProgress: { - Action.adminTookDispute: [ - Action.adminSettle, - Action.adminCancel, - ], - Action.adminAddSolver: [ - Action.adminSettle, - Action.adminCancel, - ], - }, - Status.expired: {}, - Status.success: {}, - Status.canceled: {}, - Status.canceledByAdmin: {}, - Status.settledByAdmin: {}, - Status.completedByAdmin: {}, - Status.cooperativelyCanceled: {}, - } }; } 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 bab21ac2..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,9 +370,11 @@ 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: @@ -354,13 +385,16 @@ class TradeDetailScreen extends ConsumerWidget { onPressed: () => context.push('/rate_user/$orderId'), )); break; + case actions.Action.holdInvoicePaymentSettled: case actions.Action.holdInvoicePaymentAccepted: widgets.add(_buildContactButton(context)); break; + case actions.Action.holdInvoicePaymentCanceled: // These are system actions, not user actions, so no button needed break; + case actions.Action.buyerInvoiceAccepted: case actions.Action.waitingSellerToPay: case actions.Action.waitingBuyerInvoice: @@ -379,6 +413,7 @@ 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; @@ -392,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( @@ -401,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), );