From 146c2c966ca6ca1fe7c485c91ff19464f4f302f4 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 3 Jul 2025 22:29:16 -0300 Subject: [PATCH 01/24] feat: redesign order detail screens with new UI components and layouts --- .../order/screens/take_order_screen.dart | 468 +++++++++--- .../trades/screens/trade_detail_screen.dart | 691 +++++++++++------- 2 files changed, 755 insertions(+), 404 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 459eefeb..79442d6a 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -29,7 +29,10 @@ class TakeOrderScreen extends ConsumerWidget { return Scaffold( backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: 'ORDER DETAILS'), + appBar: OrderAppBar( + title: orderType == OrderType.buy + ? 'BUY ORDER DETAILS' + : 'SELL ORDER DETAILS'), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( @@ -37,11 +40,16 @@ class TakeOrderScreen extends ConsumerWidget { const SizedBox(height: 16), _buildSellerAmount(ref, order!), const SizedBox(height: 16), + _buildPaymentMethod(order), + const SizedBox(height: 16), + _buildCreatedOn(order), + const SizedBox(height: 16), _buildOrderId(context), + const SizedBox(height: 16), + _buildCreatorReputation(order), const SizedBox(height: 24), _buildCountDownTime(order.expirationDate), const SizedBox(height: 36), - // Pass the full order to the action buttons widget. _buildActionButtons(context, ref, order), ], ), @@ -50,47 +58,45 @@ class TakeOrderScreen extends ConsumerWidget { } Widget _buildSellerAmount(WidgetRef ref, NostrEvent order) { - final selling = orderType == OrderType.sell ? 'selling' : 'buying'; - final amountString = - '${order.fiatAmount} ${order.currency} ${CurrencyUtils.getFlagFromCurrency(order.currency!)}'; - final satAmount = order.amount == '0' ? '' : ' ${order.amount}'; - final price = order.amount != '0' ? '' : 'at market price'; - final premium = int.parse(order.premium ?? '0'); - final premiumText = premium >= 0 - ? premium == 0 - ? '' - : 'with a +$premium% premium' - : 'with a -$premium% discount'; - final methods = order.paymentMethods.isNotEmpty - ? order.paymentMethods.join(', ') - : 'No payment method'; + final selling = orderType == OrderType.sell ? 'Selling' : 'Buying'; + final currencyFlag = CurrencyUtils.getFlagFromCurrency(order.currency!); + final amountString = '${order.fiatAmount} ${order.currency} $currencyFlag'; + final priceText = order.amount == '0' ? 'at market price' : ''; return CustomCard( padding: const EdgeInsets.all(16), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - spacing: 2, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Someone is $selling$satAmount sats for $amountString $price $premiumText', - style: AppTheme.theme.textTheme.bodyLarge, - softWrap: true, - ), - const SizedBox(height: 16), - Text( - 'Created on: ${formatDateTime(order.createdAt!)}', - style: textTheme.bodyLarge, + Text( + 'Someone is $selling Sats', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'for $amountString', + style: const TextStyle( + color: Colors.white70, + fontSize: 16, ), - const SizedBox(height: 16), + ), + if (priceText.isNotEmpty) ...[ + const SizedBox(width: 8), Text( - 'The payment methods are: $methods', - style: textTheme.bodyLarge, + priceText, + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), ), ], - ), + ], ), ], ), @@ -150,113 +156,333 @@ class TakeOrderScreen extends ConsumerWidget { ); } + Widget _buildPaymentMethod(NostrEvent order) { + final methods = order.paymentMethods.isNotEmpty + ? order.paymentMethods.join(', ') + : 'No payment method'; + + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon( + Icons.payment, + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Payment Method', + style: TextStyle( + color: Colors.white60, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + methods, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCreatedOn(NostrEvent order) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon( + Icons.schedule, + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Created On', + style: TextStyle( + color: Colors.white60, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + formatDateTime(order.createdAt!), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCreatorReputation(NostrEvent order) { + // For now, show placeholder data matching TradeDetailScreen + // In a real implementation, this would come from the order creator's data + const rating = 3.1; + const reviews = 15; + const days = 7; + + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Creator\'s Reputation', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Rating section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.star, + color: AppTheme.mostroGreen, + size: 16, + ), + const SizedBox(width: 4), + Text( + rating.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Rating', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Reviews section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat_bubble_outline, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + reviews.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Reviews', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Days section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today_outlined, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + days.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Days', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + Widget _buildActionButtons( BuildContext context, WidgetRef ref, NostrEvent order) { - final orderDetailsNotifier = ref.watch( - orderNotifierProvider(order.orderId!).notifier, - ); + final orderDetailsNotifier = + ref.read(orderNotifierProvider(orderId).notifier); + + final buttonText = + orderType == OrderType.buy ? 'SELL BITCOIN' : 'BUY BITCOIN'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - OutlinedButton( - onPressed: () { - context.pop(); - }, - style: AppTheme.theme.outlinedButtonTheme.style, - child: const Text('CLOSE'), + Expanded( + child: OutlinedButton( + onPressed: () => context.pop(), + style: AppTheme.theme.outlinedButtonTheme.style, + child: const Text('CLOSE'), + ), ), const SizedBox(width: 16), - ElevatedButton( - onPressed: () async { - // Check if the order is a range order. - if (order.fiatAmount.maximum != null) { - final enteredAmount = await showDialog( - context: context, - builder: (BuildContext context) { - String? errorText; - return StatefulBuilder( - builder: (BuildContext context, - void Function(void Function()) setState) { - return AlertDialog( - title: const Text('Enter Amount'), - content: TextField( - controller: _fiatAmountController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: - 'Enter an amount between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}', - errorText: errorText, + Expanded( + child: ElevatedButton( + onPressed: () async { + // Check if this is a range order + if (order.fiatAmount.minimum != order.fiatAmount.maximum) { + // Show dialog to get the amount + String? errorText; + final enteredAmount = await showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: const Text('Enter Amount'), + content: TextField( + controller: _fiatAmountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: + 'Enter an amount between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}', + errorText: errorText, + ), ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancel'), - ), - ElevatedButton( - key: const Key('submitAmountButton'), - onPressed: () { - final inputAmount = int.tryParse( - _fiatAmountController.text.trim()); - if (inputAmount == null) { - setState(() { - errorText = "Please enter a valid number."; - }); - } else if (inputAmount < - order.fiatAmount.minimum || - inputAmount > order.fiatAmount.maximum!) { - setState(() { - errorText = - "Amount must be between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}."; - }); - } else { - Navigator.of(context).pop(inputAmount); - } - }, - child: const Text('Submit'), - ), - ], - ); - }, - ); - }, - ); + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: const Text('Cancel'), + ), + ElevatedButton( + key: const Key('submitAmountButton'), + onPressed: () { + final inputAmount = int.tryParse( + _fiatAmountController.text.trim()); + if (inputAmount == null) { + setState(() { + errorText = "Please enter a valid number."; + }); + } else if (inputAmount < + order.fiatAmount.minimum || + inputAmount > order.fiatAmount.maximum!) { + setState(() { + errorText = + "Amount must be between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}."; + }); + } else { + Navigator.of(context).pop(inputAmount); + } + }, + child: const Text('Submit'), + ), + ], + ); + }, + ); + }, + ); - if (enteredAmount != null) { + if (enteredAmount != null) { + if (orderType == OrderType.buy) { + await orderDetailsNotifier.takeBuyOrder( + order.orderId!, enteredAmount); + } else { + final lndAddress = _lndAddressController.text.trim(); + await orderDetailsNotifier.takeSellOrder( + order.orderId!, + enteredAmount, + lndAddress.isEmpty ? null : lndAddress, + ); + } + } + } else { + // Not a range order – use the existing logic. + final fiatAmount = + int.tryParse(_fiatAmountController.text.trim()); if (orderType == OrderType.buy) { await orderDetailsNotifier.takeBuyOrder( - order.orderId!, enteredAmount); + order.orderId!, fiatAmount); } else { final lndAddress = _lndAddressController.text.trim(); await orderDetailsNotifier.takeSellOrder( order.orderId!, - enteredAmount, + fiatAmount, lndAddress.isEmpty ? null : lndAddress, ); } } - } else { - // Not a range order – use the existing logic. - final fiatAmount = - int.tryParse(_fiatAmountController.text.trim()); - if (orderType == OrderType.buy) { - await orderDetailsNotifier.takeBuyOrder( - order.orderId!, fiatAmount); - } else { - final lndAddress = _lndAddressController.text.trim(); - await orderDetailsNotifier.takeSellOrder( - order.orderId!, - fiatAmount, - lndAddress.isEmpty ? null : lndAddress, - ); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: Text(buttonText), ), - child: const Text('TAKE'), ), ], ); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 709afda5..4d1ae578 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -5,17 +5,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart' as actions; + import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; -import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; + import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; -import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; class TradeDetailScreen extends ConsumerWidget { final String orderId; @@ -40,36 +40,16 @@ class TradeDetailScreen extends ConsumerWidget { appBar: OrderAppBar(title: 'ORDER DETAILS'), body: Builder( builder: (context) { + // Check if this is a pending order (created by user but not taken yet) + final isPendingOrder = tradeState.status == Status.pending; + return SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const SizedBox(height: 16), - // Display basic info about the trade: - _buildSellerAmount(ref, tradeState), - const SizedBox(height: 16), - _buildOrderId(context), - const SizedBox(height: 16), - // Detailed info: includes the last Mostro message action text - MostroMessageDetail(orderId: orderId), - const SizedBox(height: 24), - _buildCountDownTime(orderPayload.expiresAt), - const SizedBox(height: 36), - Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: [ - _buildCloseButton(context), - ..._buildActionButtons( - context, - ref, - tradeState, - ), - ], - ), - ], - ), + child: isPendingOrder + ? _buildPendingOrderLayout( + ref, tradeState, context, orderPayload) + : _buildActiveOrderLayout( + ref, tradeState, context, orderPayload), ); }, ), @@ -203,275 +183,420 @@ class TradeDetailScreen extends ConsumerWidget { } /// Main action button area, switching on `orderPayload.status`. - /// Additional checks use `message.action` to refine which button to show. - /// Following the Mostro protocol state machine for order flow. - List _buildActionButtons( - BuildContext context, WidgetRef ref, OrderState tradeState) { - final session = ref.watch(sessionProvider(orderId)); - final userRole = session?.role; - - if (userRole == null) { - return []; - } - - final userActions = tradeState.getActions(userRole); - if (userActions.isEmpty) return []; - - final widgets = []; - - for (final action in userActions) { - // 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( - buttonText, - action: action, - backgroundColor: buttonColor, - onPressed: () => - ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), - )); - break; - - case actions.Action.payInvoice: - if (userRole == Role.seller) { - 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( - 'ADD INVOICE', - action: actions.Action.addInvoice, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/add_invoice/$orderId'), - )); - } - break; - - case actions.Action.fiatSent: - if (userRole == Role.buyer) { - widgets.add(_buildNostrButton( - 'FIAT SENT', - action: actions.Action.fiatSent, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .sendFiatSent(), - )); - } - break; - - case actions.Action.disputeInitiatedByYou: - case actions.Action.disputeInitiatedByPeer: - case actions.Action.dispute: - // Only allow dispute if not already disputed - if (tradeState.action != actions.Action.disputeInitiatedByYou && - tradeState.action != actions.Action.disputeInitiatedByPeer && - tradeState.action != actions.Action.dispute) { - widgets.add(_buildNostrButton( - 'DISPUTE', - action: actions.Action.disputeInitiatedByYou, - backgroundColor: AppTheme.red1, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .disputeOrder(), - )); - } - break; - - case actions.Action.release: - if (userRole == Role.seller) { - widgets.add(_buildNostrButton( - 'RELEASE', - action: actions.Action.release, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .releaseOrder(), - )); - } - break; - - case actions.Action.takeSell: - if (userRole == Role.buyer) { - widgets.add(_buildNostrButton( - 'TAKE SELL', - action: actions.Action.takeSell, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/take_sell/$orderId'), - )); - } - break; - - case actions.Action.takeBuy: - if (userRole == Role.seller) { - widgets.add(_buildNostrButton( - 'TAKE BUY', - action: actions.Action.takeBuy, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/take_buy/$orderId'), - )); - } - break; - // ✅ CASOS DE COOPERATIVE CANCEL: Ahora estos se manejan cuando el usuario ya inició/recibió cooperative cancel - case actions.Action.cooperativeCancelInitiatedByYou: - // El usuario ya inició cooperative cancel, ahora debe esperar respuesta - widgets.add(_buildNostrButton( - 'CANCEL PENDING', - action: actions.Action.cooperativeCancelInitiatedByYou, - backgroundColor: Colors.grey, - onPressed: null, - )); - break; - - case actions.Action.cooperativeCancelInitiatedByPeer: - widgets.add(_buildNostrButton( - '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', - action: actions.Action.purchaseCompleted, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => ref - .read(orderNotifierProvider(orderId).notifier) - .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.rate, - backgroundColor: AppTheme.mostroGreen, - onPressed: () => context.push('/rate_user/$orderId'), - )); - break; + /// Format the date time to a user-friendly string with UTC offset + String formatDateTime(DateTime dateTime) { + return DateFormat('MMM dd, yyyy HH:mm').format(dateTime); + } - case actions.Action.holdInvoicePaymentSettled: - case actions.Action.holdInvoicePaymentAccepted: - widgets.add(_buildContactButton(context)); - break; + /// Builds the layout for pending orders (simple layout like screenshot 2) + Widget _buildPendingOrderLayout(WidgetRef ref, OrderState tradeState, + BuildContext context, Order orderPayload) { + return Column( + children: [ + const SizedBox(height: 16), + // Information message + _buildPendingOrderInfo(), + const SizedBox(height: 16), + // Basic order info ("Someone is Selling Sats") + _buildSellerAmount(ref, tradeState), + const SizedBox(height: 16), + // Payment method card + _buildPaymentMethod(tradeState), + const SizedBox(height: 16), + // Created on card + _buildCreatedOn(tradeState), + const SizedBox(height: 16), + // Order ID + _buildOrderId(context), + const SizedBox(height: 16), + // Creator reputation + _buildCreatorReputation(tradeState), + const SizedBox(height: 16), + // Time remaining + _buildCountDownTime(orderPayload.expiresAt), + const SizedBox(height: 36), + // CLOSE and CANCEL buttons + _buildPendingOrderButtons(context, ref), + ], + ); + } - case actions.Action.holdInvoicePaymentCanceled: - // These are system actions, not user actions, so no button needed - break; + /// Builds the layout for active orders (clean layout like screenshot 2) + Widget _buildActiveOrderLayout(WidgetRef ref, OrderState tradeState, + BuildContext context, Order orderPayload) { + return Column( + children: [ + const SizedBox(height: 16), + // Display basic info about the trade: + _buildSellerAmount(ref, tradeState), + const SizedBox(height: 16), + // Payment method card + _buildPaymentMethod(tradeState), + const SizedBox(height: 16), + // Created on card + _buildCreatedOn(tradeState), + const SizedBox(height: 16), + // Order ID + _buildOrderId(context), + const SizedBox(height: 16), + // Creator reputation + _buildCreatorReputation(tradeState), + const SizedBox(height: 16), + // Time remaining + _buildCountDownTime(orderPayload.expiresAt), + const SizedBox(height: 36), + // Action buttons (CLOSE and main action) + _buildOrderActionButtons(context, ref, tradeState), + ], + ); + } - case actions.Action.buyerInvoiceAccepted: - case actions.Action.waitingSellerToPay: - case actions.Action.waitingBuyerInvoice: - case actions.Action.adminCancel: - case actions.Action.adminCanceled: - case actions.Action.adminSettle: - case actions.Action.adminSettled: - case actions.Action.adminAddSolver: - case actions.Action.adminTakeDispute: - case actions.Action.adminTookDispute: - case actions.Action.paymentFailed: - case actions.Action.invoiceUpdated: - case actions.Action.sendDm: - case actions.Action.tradePubkey: - case actions.Action.cantDo: - case actions.Action.released: - // Not user-facing or not relevant as a button - break; + /// Builds the information message for pending orders (only for creators) + Widget _buildPendingOrderInfo() { + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: AppTheme.mostroGreen, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Information', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + 'Your offer has been published and will be available for 24 hours. Other users can now take your order.', + style: TextStyle( + color: Colors.white70, + fontSize: 14, + height: 1.4, + ), + ), + ], + ), + ), + ); + } - default: - // Optionally handle unknown or unimplemented actions - break; - } - } + /// Builds payment method card + Widget _buildPaymentMethod(OrderState tradeState) { + final paymentMethod = tradeState.order?.paymentMethod ?? ''; + final paymentMethodsText = + paymentMethod.isNotEmpty ? paymentMethod : 'Not specified'; - return widgets; + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.payment, + color: AppTheme.mostroGreen, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Payment Method', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + paymentMethodsText, + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ); } - /// Helper method to build a NostrResponsiveButton with common properties - Widget _buildNostrButton( - String label, { - required actions.Action action, - required VoidCallback? onPressed, - Color? backgroundColor, - }) { - return MostroReactiveButton( - label: label, - buttonStyle: ButtonStyleType.raised, - orderId: orderId, - action: action, - backgroundColor: backgroundColor, - onPressed: onPressed ?? () {}, // Provide empty function when null - showSuccessIndicator: true, - timeout: const Duration(seconds: 30), + /// Builds created on date card + Widget _buildCreatedOn(OrderState tradeState) { + final createdAt = tradeState.order?.createdAt; + final createdText = createdAt != null + ? formatDateTime(DateTime.fromMillisecondsSinceEpoch(createdAt * 1000)) + : 'Unknown'; + + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Icon( + Icons.schedule, + color: AppTheme.mostroGreen, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Created On', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + createdText, + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), ); } - Widget _buildContactButton(BuildContext context) { - return ElevatedButton( - onPressed: () { - context.push('/chat_room/$orderId'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, + /// Builds creator reputation card with horizontal layout + Widget _buildCreatorReputation(OrderState tradeState) { + // For now, show placeholder data + // In a real implementation, this would come from the order creator's data + const rating = 3.1; + const reviews = 15; + const days = 7; + + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Creator\'s Reputation', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Rating section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.star, + color: AppTheme.mostroGreen, + size: 16, + ), + const SizedBox(width: 4), + Text( + rating.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Rating', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Reviews section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat_bubble_outline, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + reviews.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Reviews', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Days section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today_outlined, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + days.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Days', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ], + ), ), - child: const Text('CONTACT'), ); } - /// CLOSE - Widget _buildCloseButton(BuildContext context) { - return OutlinedButton( - onPressed: () => context.pop(), - style: AppTheme.theme.outlinedButtonTheme.style, - child: const Text('CLOSE'), + /// Builds buttons for pending orders (CLOSE and CANCEL) + Widget _buildPendingOrderButtons(BuildContext context, WidgetRef ref) { + return Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[800], + foregroundColor: Colors.white, + ), + child: const Text('CLOSE'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + // Handle cancel order action + final orderNotifier = + ref.read(orderNotifierProvider(orderId).notifier); + orderNotifier.cancelOrder(); + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[700], + foregroundColor: Colors.white, + ), + child: const Text('CANCEL'), + ), + ), + ], ); } - /// Format the date time to a user-friendly string with UTC offset - String formatDateTime(DateTime dt) { - final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); - final formattedDate = dateFormatter.format(dt); - final offset = dt.timeZoneOffset; - final sign = offset.isNegative ? '-' : '+'; - final hours = offset.inHours.abs().toString().padLeft(2, '0'); - final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); - final timeZoneName = dt.timeZoneName; - return '$formattedDate GMT $sign$hours$minutes ($timeZoneName)'; + /// Builds action buttons for orders from home (CLOSE and main action like SELL BITCOIN) + Widget _buildOrderActionButtons( + BuildContext context, WidgetRef ref, OrderState tradeState) { + return Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.grey), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text('CLOSE'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + // Navigate to take order screen or handle main action + context.push('/take-order/$orderId'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + tradeState.order?.kind.name == 'buy' + ? 'SELL BITCOIN' + : 'BUY BITCOIN', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ); } } From d0e4466336a4959f417ecd92ece2d2ea9bc410b8 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 3 Jul 2025 23:05:23 -0300 Subject: [PATCH 02/24] refactor: simplify trade detail screen with FSM-driven action buttons --- .../trades/screens/trade_detail_screen.dart | 714 ++++++++---------- 1 file changed, 304 insertions(+), 410 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 4d1ae578..916481ef 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -5,17 +5,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; - +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; - +import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; class TradeDetailScreen extends ConsumerWidget { final String orderId; @@ -40,16 +40,38 @@ class TradeDetailScreen extends ConsumerWidget { appBar: OrderAppBar(title: 'ORDER DETAILS'), body: Builder( builder: (context) { - // Check if this is a pending order (created by user but not taken yet) - final isPendingOrder = tradeState.status == Status.pending; - return SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: isPendingOrder - ? _buildPendingOrderLayout( - ref, tradeState, context, orderPayload) - : _buildActiveOrderLayout( - ref, tradeState, context, orderPayload), + child: Column( + children: [ + const SizedBox(height: 16), + // Display basic info about the trade: + _buildSellerAmount(ref, tradeState), + const SizedBox(height: 16), + _buildOrderId(context), + const SizedBox(height: 16), + // Detailed info: includes the last Mostro message action text + MostroMessageDetail(orderId: orderId), + const SizedBox(height: 24), + _buildCountDownTime(orderPayload.expiresAt != null + ? orderPayload.expiresAt! * 1000 + : null), + const SizedBox(height: 36), + Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ + _buildCloseButton(context), + ..._buildActionButtons( + context, + ref, + tradeState, + ), + ], + ), + ], + ), ); }, ), @@ -84,10 +106,9 @@ class TradeDetailScreen extends ConsumerWidget { final method = tradeState.order!.paymentMethod; final timestamp = formatDateTime( tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 - ? DateTime.fromMillisecondsSinceEpoch(tradeState.order!.createdAt!) - : DateTime.fromMillisecondsSinceEpoch( - tradeState.order!.createdAt ?? 0, - ), + ? DateTime.fromMillisecondsSinceEpoch( + tradeState.order!.createdAt! * 1000) + : session.startTime, ); return CustomCard( padding: const EdgeInsets.all(16), @@ -183,420 +204,293 @@ class TradeDetailScreen extends ConsumerWidget { } /// Main action button area, switching on `orderPayload.status`. + /// Additional checks use `message.action` to refine which button to show. + /// Following the Mostro protocol state machine for order flow. + List _buildActionButtons( + BuildContext context, WidgetRef ref, OrderState tradeState) { + final session = ref.watch(sessionProvider(orderId)); + final userRole = session?.role; - /// Format the date time to a user-friendly string with UTC offset - String formatDateTime(DateTime dateTime) { - return DateFormat('MMM dd, yyyy HH:mm').format(dateTime); - } + if (userRole == null) { + return []; + } - /// Builds the layout for pending orders (simple layout like screenshot 2) - Widget _buildPendingOrderLayout(WidgetRef ref, OrderState tradeState, - BuildContext context, Order orderPayload) { - return Column( - children: [ - const SizedBox(height: 16), - // Information message - _buildPendingOrderInfo(), - const SizedBox(height: 16), - // Basic order info ("Someone is Selling Sats") - _buildSellerAmount(ref, tradeState), - const SizedBox(height: 16), - // Payment method card - _buildPaymentMethod(tradeState), - const SizedBox(height: 16), - // Created on card - _buildCreatedOn(tradeState), - const SizedBox(height: 16), - // Order ID - _buildOrderId(context), - const SizedBox(height: 16), - // Creator reputation - _buildCreatorReputation(tradeState), - const SizedBox(height: 16), - // Time remaining - _buildCountDownTime(orderPayload.expiresAt), - const SizedBox(height: 36), - // CLOSE and CANCEL buttons - _buildPendingOrderButtons(context, ref), - ], - ); - } + final userActions = tradeState.getActions(userRole); + if (userActions.isEmpty) return []; - /// Builds the layout for active orders (clean layout like screenshot 2) - Widget _buildActiveOrderLayout(WidgetRef ref, OrderState tradeState, - BuildContext context, Order orderPayload) { - return Column( - children: [ - const SizedBox(height: 16), - // Display basic info about the trade: - _buildSellerAmount(ref, tradeState), - const SizedBox(height: 16), - // Payment method card - _buildPaymentMethod(tradeState), - const SizedBox(height: 16), - // Created on card - _buildCreatedOn(tradeState), - const SizedBox(height: 16), - // Order ID - _buildOrderId(context), - const SizedBox(height: 16), - // Creator reputation - _buildCreatorReputation(tradeState), - const SizedBox(height: 16), - // Time remaining - _buildCountDownTime(orderPayload.expiresAt), - const SizedBox(height: 36), - // Action buttons (CLOSE and main action) - _buildOrderActionButtons(context, ref, tradeState), - ], - ); - } + final widgets = []; - /// Builds the information message for pending orders (only for creators) - Widget _buildPendingOrderInfo() { - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: AppTheme.mostroGreen, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Information', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - 'Your offer has been published and will be available for 24 hours. Other users can now take your order.', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - height: 1.4, - ), - ), - ], - ), - ), - ); - } + for (final action in userActions) { + // FSM-driven action mapping: ensure all actions are handled + switch (action) { + case actions.Action.cancel: + String cancelMessage; - /// Builds payment method card - Widget _buildPaymentMethod(OrderState tradeState) { - final paymentMethod = tradeState.order?.paymentMethod ?? ''; - final paymentMethodsText = - paymentMethod.isNotEmpty ? paymentMethod : 'Not specified'; + if (tradeState.status == Status.active || + tradeState.status == Status.fiatSent) { + if (tradeState.action == + actions.Action.cooperativeCancelInitiatedByPeer) { + cancelMessage = + 'If you confirm, you will accept the cooperative cancellation initiated by your counterparty.'; + } else { + cancelMessage = + 'If you confirm, you will start a cooperative cancellation with your counterparty.'; + } + } else { + cancelMessage = 'Are you sure you want to cancel this trade?'; + } - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Icon( - Icons.payment, - color: AppTheme.mostroGreen, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Payment Method', - style: TextStyle( - color: Colors.white70, - fontSize: 12, + widgets.add(_buildNostrButton( + 'CANCEL', + action: action, + backgroundColor: AppTheme.red1, + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Cancel Trade'), + content: Text(cancelMessage), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: const Text('Cancel'), ), - ), - const SizedBox(height: 4), - Text( - paymentMethodsText, - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, + ElevatedButton( + onPressed: () { + context.pop(); + ref + .read(orderNotifierProvider(orderId).notifier) + .cancelOrder(); + }, + child: const Text('Confirm'), ), - ), - ], - ), - ), - ], - ), - ), - ); - } + ], + ), + ); + }, + )); + break; - /// Builds created on date card - Widget _buildCreatedOn(OrderState tradeState) { - final createdAt = tradeState.order?.createdAt; - final createdText = createdAt != null - ? formatDateTime(DateTime.fromMillisecondsSinceEpoch(createdAt * 1000)) - : 'Unknown'; + case actions.Action.payInvoice: + if (userRole == Role.seller) { + final hasPaymentRequest = tradeState.paymentRequest != null; - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Icon( - Icons.schedule, - color: AppTheme.mostroGreen, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Created On', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - Text( - createdText, - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), - ), - ); + 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( + 'ADD INVOICE', + action: actions.Action.addInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/add_invoice/$orderId'), + )); + } + break; + + case actions.Action.fiatSent: + if (userRole == Role.buyer) { + widgets.add(_buildNostrButton( + 'FIAT SENT', + action: actions.Action.fiatSent, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .sendFiatSent(), + )); + } + break; + + case actions.Action.disputeInitiatedByYou: + case actions.Action.disputeInitiatedByPeer: + case actions.Action.dispute: + // Only allow dispute if not already disputed + if (tradeState.action != actions.Action.disputeInitiatedByYou && + tradeState.action != actions.Action.disputeInitiatedByPeer && + tradeState.action != actions.Action.dispute) { + widgets.add(_buildNostrButton( + 'DISPUTE', + action: actions.Action.disputeInitiatedByYou, + backgroundColor: AppTheme.red1, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .disputeOrder(), + )); + } + break; + + case actions.Action.release: + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'RELEASE', + action: actions.Action.release, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .releaseOrder(), + )); + } + break; + + case actions.Action.takeSell: + if (userRole == Role.buyer) { + widgets.add(_buildNostrButton( + 'TAKE SELL', + action: actions.Action.takeSell, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/take_sell/$orderId'), + )); + } + break; + + case actions.Action.takeBuy: + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'TAKE BUY', + action: actions.Action.takeBuy, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/take_buy/$orderId'), + )); + } + break; + + // ✅ CASOS DE COOPERATIVE CANCEL: Ahora estos se manejan cuando el usuario ya inició/recibió cooperative cancel + case actions.Action.cooperativeCancelInitiatedByYou: + // El usuario ya inició cooperative cancel, ahora debe esperar respuesta + widgets.add(_buildNostrButton( + 'CANCEL PENDING', + action: actions.Action.cooperativeCancelInitiatedByYou, + backgroundColor: Colors.grey, + onPressed: null, + )); + break; + + case actions.Action.cooperativeCancelInitiatedByPeer: + widgets.add(_buildNostrButton( + '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', + action: actions.Action.purchaseCompleted, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .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.rate, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + )); + break; + + case actions.Action.sendDm: + widgets.add(_buildContactButton(context)); + break; + + case actions.Action.holdInvoicePaymentCanceled: + case actions.Action.buyerInvoiceAccepted: + case actions.Action.waitingSellerToPay: + case actions.Action.waitingBuyerInvoice: + case actions.Action.adminCancel: + case actions.Action.adminCanceled: + case actions.Action.adminSettle: + case actions.Action.adminSettled: + case actions.Action.adminAddSolver: + case actions.Action.adminTakeDispute: + case actions.Action.adminTookDispute: + case actions.Action.paymentFailed: + case actions.Action.invoiceUpdated: + case actions.Action.tradePubkey: + case actions.Action.cantDo: + case actions.Action.released: + break; + default: + break; + } + } + + return widgets; } - /// Builds creator reputation card with horizontal layout - Widget _buildCreatorReputation(OrderState tradeState) { - // For now, show placeholder data - // In a real implementation, this would come from the order creator's data - const rating = 3.1; - const reviews = 15; - const days = 7; + /// Helper method to build a NostrResponsiveButton with common properties + Widget _buildNostrButton( + String label, { + required actions.Action action, + required VoidCallback? onPressed, + Color? backgroundColor, + }) { + return MostroReactiveButton( + label: label, + buttonStyle: ButtonStyleType.raised, + orderId: orderId, + action: action, + backgroundColor: backgroundColor, + onPressed: onPressed ?? () {}, // Provide empty function when null + showSuccessIndicator: true, + timeout: const Duration(seconds: 30), + ); + } - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Creator\'s Reputation', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // Rating section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.star, - color: AppTheme.mostroGreen, - size: 16, - ), - const SizedBox(width: 4), - Text( - rating.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Rating', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Reviews section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat_bubble_outline, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - reviews.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Reviews', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Days section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.calendar_today_outlined, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - days.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Days', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ], - ), + Widget _buildContactButton(BuildContext context) { + return ElevatedButton( + onPressed: () { + context.push('/chat_room/$orderId'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, ), + child: const Text('CONTACT'), ); } - /// Builds buttons for pending orders (CLOSE and CANCEL) - Widget _buildPendingOrderButtons(BuildContext context, WidgetRef ref) { - return Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[800], - foregroundColor: Colors.white, - ), - child: const Text('CLOSE'), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () { - // Handle cancel order action - final orderNotifier = - ref.read(orderNotifierProvider(orderId).notifier); - orderNotifier.cancelOrder(); - Navigator.of(context).pop(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red[700], - foregroundColor: Colors.white, - ), - child: const Text('CANCEL'), - ), - ), - ], + /// CLOSE + Widget _buildCloseButton(BuildContext context) { + return OutlinedButton( + onPressed: () => context.go('/order_book'), + style: AppTheme.theme.outlinedButtonTheme.style, + child: const Text('CLOSE'), ); } - /// Builds action buttons for orders from home (CLOSE and main action like SELL BITCOIN) - Widget _buildOrderActionButtons( - BuildContext context, WidgetRef ref, OrderState tradeState) { - return Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: OutlinedButton.styleFrom( - foregroundColor: Colors.white, - side: const BorderSide(color: Colors.grey), - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text('CLOSE'), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () { - // Navigate to take order screen or handle main action - context.push('/take-order/$orderId'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: Text( - tradeState.order?.kind.name == 'buy' - ? 'SELL BITCOIN' - : 'BUY BITCOIN', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ], - ); + /// Format the date time to a user-friendly string with UTC offset + String formatDateTime(DateTime dt) { + final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); + final formattedDate = dateFormatter.format(dt); + final offset = dt.timeZoneOffset; + final sign = offset.isNegative ? '-' : '+'; + final hours = offset.inHours.abs().toString().padLeft(2, '0'); + final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); + final timeZoneName = dt.timeZoneName; + return '$formattedDate GMT $sign$hours$minutes ($timeZoneName)'; } } From bcb99ceb8e7bee326911b0a00873baf67d1d8a28 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 02:50:14 -0300 Subject: [PATCH 03/24] feat: add creator reputation card and update order details UI for pending trades --- .../trades/screens/trade_detail_screen.dart | 329 +++++++++++++++++- 1 file changed, 325 insertions(+), 4 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 0a2adac0..c353b860 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; @@ -35,6 +36,13 @@ class TradeDetailScreen extends ConsumerWidget { ); } + // Check if this is a pending order created by the user + final session = ref.watch(sessionProvider(orderId)); + final isPending = tradeState.status == Status.pending; + final isCreator = session!.role == Role.buyer + ? tradeState.order!.kind == OrderType.buy + : tradeState.order!.kind == OrderType.sell; + return Scaffold( backgroundColor: AppTheme.dark1, appBar: OrderAppBar(title: 'ORDER DETAILS'), @@ -50,8 +58,14 @@ class TradeDetailScreen extends ConsumerWidget { const SizedBox(height: 16), _buildOrderId(context), const SizedBox(height: 16), - // Detailed info: includes the last Mostro message action text - MostroMessageDetail(orderId: orderId), + // For pending orders created by the user, show creator's reputation + if (isPending && isCreator) ...[ + _buildCreatorReputation(), + const SizedBox(height: 16), + ] else ...[ // Use spread operator here too + // Detailed info: includes the last Mostro message action text + MostroMessageDetail(orderId: orderId), + ], const SizedBox(height: 24), _buildCountDownTime(orderPayload.expiresAt != null ? orderPayload.expiresAt! * 1000 @@ -81,8 +95,181 @@ class TradeDetailScreen extends ConsumerWidget { /// Builds a card showing the user is "selling/buying X sats for Y fiat" etc. Widget _buildSellerAmount(WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); + final isPending = tradeState.status == Status.pending; + + // Determine if the user is the creator of the order based on role and order type + final isCreator = session!.role == Role.buyer + ? tradeState.order!.kind == OrderType.buy + : tradeState.order!.kind == OrderType.sell; + + // For pending orders created by the user, show a notification message + if (isPending && isCreator) { + final selling = session.role == Role.seller ? 'Selling' : 'Buying'; + final currencyFlag = CurrencyUtils.getFlagFromCurrency( + tradeState.order!.fiatCode, + ); + + final amountString = + '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; + + // If `orderPayload.amount` is 0, the trade is "at market price" + final isZeroAmount = (tradeState.order!.amount == 0); + final priceText = isZeroAmount ? 'at market price' : ''; - final selling = session!.role == Role.seller ? 'selling' : 'buying'; + final paymentMethod = tradeState.order!.paymentMethod; + final createdOn = formatDateTime( + tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 + ? DateTime.fromMillisecondsSinceEpoch( + tradeState.order!.createdAt! * 1000) + : session.startTime, + ); + + return Column( + children: [ + CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.info_outline, + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the \'Cancel\' button.', + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 16), + CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Someone is $selling Sats', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'for $amountString', + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + if (priceText.isNotEmpty) ...[ + const SizedBox(width: 8), + Text( + priceText, + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), + ), + ], + ], + ), + ], + ), + ), + const SizedBox(height: 16), + CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.payment, + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Payment Method', + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + paymentMethod, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.calendar_today, + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Created On', + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + createdOn, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ); + } + + // For non-pending orders or orders not created by the user, use the original display + final selling = session.role == Role.seller ? 'selling' : 'buying'; final currencyFlag = CurrencyUtils.getFlagFromCurrency( tradeState.order!.fiatCode, ); @@ -116,7 +303,6 @@ class TradeDetailScreen extends ConsumerWidget { children: [ Expanded( child: Column( - // Using Column with spacing = 2 isn't standard; using SizedBoxes for spacing is fine. crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -481,6 +667,141 @@ class TradeDetailScreen extends ConsumerWidget { ); } + /// Build a card showing the creator's reputation with rating, reviews and days + Widget _buildCreatorReputation() { + // For now, show placeholder data matching the screenshot + // In a real implementation, this would come from the order creator's data + const rating = 3.1; + const reviews = 15; + const days = 7; + + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Creator\'s Reputation', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Rating section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.star, + color: AppTheme.mostroGreen, + size: 16, + ), + const SizedBox(width: 4), + Text( + rating.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Rating', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Reviews section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat_bubble_outline, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + reviews.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Reviews', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Days section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.calendar_today_outlined, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + days.toString(), + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + 'Days', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + /// Format the date time to a user-friendly string with UTC offset String formatDateTime(DateTime dt) { final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); From 91448c97118cd0ab4987bac65229e4b6bc8f5f95 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 03:13:42 -0300 Subject: [PATCH 04/24] refactor: extract reusable order card components into separate widgets --- .../order/screens/take_order_screen.dart | 252 +---------- .../trades/screens/trade_detail_screen.dart | 129 +----- lib/shared/widgets/order_cards.dart | 397 ++++++++++++++++++ 3 files changed, 421 insertions(+), 357 deletions(-) create mode 100644 lib/shared/widgets/order_cards.dart diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 79442d6a..de3722f9 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -3,13 +3,13 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/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/features/order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/shared/widgets/order_cards.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; @@ -104,36 +104,8 @@ class TakeOrderScreen extends ConsumerWidget { } Widget _buildOrderId(BuildContext context) { - return CustomCard( - padding: const EdgeInsets.all(2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SelectableText( - orderId, - style: TextStyle(color: AppTheme.mostroGreen), - ), - const SizedBox(width: 16), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: orderId)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Order ID copied to clipboard'), - duration: Duration(seconds: 2), - ), - ); - }, - icon: const Icon(Icons.copy), - style: IconButton.styleFrom( - foregroundColor: AppTheme.mostroGreen, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ) - ], - ), + return OrderIdCard( + orderId: orderId, ); } @@ -161,216 +133,30 @@ class TakeOrderScreen extends ConsumerWidget { ? order.paymentMethods.join(', ') : 'No payment method'; - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Icon( - Icons.payment, - color: Colors.white70, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Payment Method', - style: TextStyle( - color: Colors.white60, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - Text( - methods, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), + return PaymentMethodCard( + paymentMethod: methods, ); } Widget _buildCreatedOn(NostrEvent order) { - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Icon( - Icons.schedule, - color: Colors.white70, - size: 20, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Created On', - style: TextStyle( - color: Colors.white60, - fontSize: 12, - ), - ), - const SizedBox(height: 4), - Text( - formatDateTime(order.createdAt!), - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ], - ), + return CreatedDateCard( + createdDate: formatDateTime(order.createdAt!), ); } Widget _buildCreatorReputation(NostrEvent order) { - // For now, show placeholder data matching TradeDetailScreen - // In a real implementation, this would come from the order creator's data - const rating = 3.1; - const reviews = 15; - const days = 7; + // For now, show placeholder data matching TradeDetailScreen + // In a real implementation, this would come from the order creator's data + const rating = 3.1; + const reviews = 15; + const days = 7; - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Creator\'s Reputation', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // Rating section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.star, - color: AppTheme.mostroGreen, - size: 16, - ), - const SizedBox(width: 4), - Text( - rating.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Rating', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Reviews section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat_bubble_outline, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - reviews.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Reviews', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Days section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.calendar_today_outlined, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - days.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Days', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ); - } + return CreatorReputationCard( + rating: rating, + reviews: reviews, + days: days, + ); +} Widget _buildActionButtons( BuildContext context, WidgetRef ref, NostrEvent order) { @@ -385,7 +171,7 @@ class TakeOrderScreen extends ConsumerWidget { children: [ Expanded( child: OutlinedButton( - onPressed: () => context.pop(), + onPressed: () => Navigator.of(context).pop(), style: AppTheme.theme.outlinedButtonTheme.style, child: const Text('CLOSE'), ), diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index c353b860..39d20813 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -12,6 +12,7 @@ import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/shared/widgets/order_cards.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; @@ -675,130 +676,10 @@ class TradeDetailScreen extends ConsumerWidget { const reviews = 15; const days = 7; - return CustomCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Creator\'s Reputation', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - // Rating section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.star, - color: AppTheme.mostroGreen, - size: 16, - ), - const SizedBox(width: 4), - Text( - rating.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Rating', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Reviews section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat_bubble_outline, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - reviews.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Reviews', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - // Days section - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.calendar_today_outlined, - color: Colors.white70, - size: 16, - ), - const SizedBox(width: 4), - Text( - days.toString(), - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - 'Days', - style: TextStyle( - color: Colors.white70, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), + return CreatorReputationCard( + rating: rating, + reviews: reviews, + days: days, ); } diff --git a/lib/shared/widgets/order_cards.dart b/lib/shared/widgets/order_cards.dart new file mode 100644 index 00000000..f757acea --- /dev/null +++ b/lib/shared/widgets/order_cards.dart @@ -0,0 +1,397 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/shared/utils/currency_utils.dart'; + +/// Card that displays the order amount information (selling/buying sats for amount) +class OrderAmountCard extends StatelessWidget { + final String title; + final String amount; + final String currency; + final String? priceText; + final String? premiumText; + + const OrderAmountCard({ + Key? key, + required this.title, + required this.amount, + required this.currency, + this.priceText, + this.premiumText, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final currencyFlag = CurrencyUtils.getFlagFromCurrency(currency); + final amountString = '$amount $currency $currencyFlag'; + + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'for $amountString', + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + ), + ), + if (priceText != null && priceText!.isNotEmpty) ...[ + const SizedBox(width: 8), + Text( + priceText!, + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), + ), + ], + ], + ), + if (premiumText != null && premiumText!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + premiumText!, + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), + ), + ], + ], + ), + ); + } +} + +/// Card that displays the payment method +class PaymentMethodCard extends StatelessWidget { + final String paymentMethod; + + const PaymentMethodCard({Key? key, required this.paymentMethod}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon( + Icons.payment, + color: Colors.white70, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Payment Method', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + paymentMethod, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Card that displays the created date +class CreatedDateCard extends StatelessWidget { + final String createdDate; + + const CreatedDateCard({Key? key, required this.createdDate}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon( + Icons.calendar_today, + color: Colors.white70, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Created On', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + createdDate, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Card that displays the order ID with a copy button +class OrderIdCard extends StatelessWidget { + final String orderId; + + const OrderIdCard({Key? key, required this.orderId}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Text( + orderId, + style: const TextStyle( + color: AppTheme.mostroGreen, + fontSize: 14, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.copy, + color: Colors.white70, + size: 20, + ), + onPressed: () { + Clipboard.setData(ClipboardData(text: orderId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order ID copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + ], + ), + ); + } +} + +/// Card that displays the creator's reputation +class CreatorReputationCard extends StatelessWidget { + final double rating; + final int reviews; + final int days; + + const CreatorReputationCard({ + Key? key, + required this.rating, + required this.reviews, + required this.days, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Creator\'s Reputation', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Rating section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.star, + color: AppTheme.mostroGreen, + size: 16, + ), + const SizedBox(width: 4), + Text( + rating.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Rating', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Reviews section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.chat_bubble_outline, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + reviews.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Reviews', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Days section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.calendar_today_outlined, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + days.toString(), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + const Text( + 'Days', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +/// Card that displays a notification message with an icon +class NotificationMessageCard extends StatelessWidget { + final String message; + final IconData icon; + final Color iconColor; + + const NotificationMessageCard({ + Key? key, + required this.message, + this.icon = Icons.info_outline, + this.iconColor = Colors.white70, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + color: iconColor, + size: 24, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ), + ], + ), + ); + } +} From 35695bc28448a313a8aaa424bd9d00857080a2c3 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 03:28:55 -0300 Subject: [PATCH 05/24] refactor: update theme colors and order ID display across screens --- .../order/screens/take_order_screen.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 36 ++--------- lib/shared/widgets/custom_card.dart | 5 +- lib/shared/widgets/order_cards.dart | 59 +++++++++++-------- 4 files changed, 43 insertions(+), 59 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index de3722f9..35f8882c 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -28,7 +28,7 @@ class TakeOrderScreen extends ConsumerWidget { final order = ref.watch(eventProvider(orderId)); return Scaffold( - backgroundColor: AppTheme.dark1, + backgroundColor: AppTheme.backgroundDark, appBar: OrderAppBar( title: orderType == OrderType.buy ? 'BUY ORDER DETAILS' diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 39d20813..3fce6aea 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -32,7 +32,7 @@ class TradeDetailScreen extends ConsumerWidget { final orderPayload = tradeState.order; if (orderPayload == null) { return const Scaffold( - backgroundColor: AppTheme.dark1, + backgroundColor: AppTheme.backgroundDark, body: Center(child: CircularProgressIndicator()), ); } @@ -45,7 +45,7 @@ class TradeDetailScreen extends ConsumerWidget { : tradeState.order!.kind == OrderType.sell; return Scaffold( - backgroundColor: AppTheme.dark1, + backgroundColor: AppTheme.backgroundDark, appBar: OrderAppBar(title: 'ORDER DETAILS'), body: Builder( builder: (context) { @@ -331,36 +331,8 @@ class TradeDetailScreen extends ConsumerWidget { /// Show a card with the order ID that can be copied. Widget _buildOrderId(BuildContext context) { - return CustomCard( - padding: const EdgeInsets.all(2), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SelectableText( - orderId, - style: const TextStyle(color: AppTheme.mostroGreen), - ), - const SizedBox(width: 16), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: orderId)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Order ID copied to clipboard'), - duration: Duration(seconds: 2), - ), - ); - }, - icon: const Icon(Icons.copy), - style: IconButton.styleFrom( - foregroundColor: AppTheme.mostroGreen, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ) - ], - ), + return OrderIdCard( + orderId: orderId, ); } diff --git a/lib/shared/widgets/custom_card.dart b/lib/shared/widgets/custom_card.dart index d8288f98..e5a20c83 100644 --- a/lib/shared/widgets/custom_card.dart +++ b/lib/shared/widgets/custom_card.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; class CustomCard extends StatelessWidget { final Widget child; @@ -20,11 +19,11 @@ class CustomCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: color ?? AppTheme.dark2, + color: color ?? const Color(0xFF1D212C), margin: margin, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: borderSide ?? BorderSide(color: AppTheme.dark2), + side: borderSide ?? BorderSide(color: const Color(0xFF1D212C)), ), child: Padding( padding: padding, diff --git a/lib/shared/widgets/order_cards.dart b/lib/shared/widgets/order_cards.dart index f757acea..bf488334 100644 --- a/lib/shared/widgets/order_cards.dart +++ b/lib/shared/widgets/order_cards.dart @@ -179,32 +179,45 @@ class OrderIdCard extends StatelessWidget { Widget build(BuildContext context) { return CustomCard( padding: const EdgeInsets.all(16), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - orderId, - style: const TextStyle( - color: AppTheme.mostroGreen, - fontSize: 14, - ), - ), - ), - IconButton( - icon: const Icon( - Icons.copy, + const Text( + 'Order ID', + style: TextStyle( color: Colors.white70, - size: 20, + fontSize: 12, ), - onPressed: () { - Clipboard.setData(ClipboardData(text: orderId)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Order ID copied to clipboard'), - duration: Duration(seconds: 2), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + orderId, + style: const TextStyle( + color: AppTheme.mostroGreen, + fontSize: 14, + ), ), - ); - }, + ), + IconButton( + icon: const Icon( + Icons.copy, + color: Colors.white70, + size: 20, + ), + onPressed: () { + Clipboard.setData(ClipboardData(text: orderId)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order ID copied to clipboard'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + ], ), ], ), @@ -286,7 +299,7 @@ class CreatorReputationCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( - Icons.chat_bubble_outline, + Icons.person_outline, color: Colors.white70, size: 16, ), From 755e699c0ac3ed4872f681649d4da96caba40baf Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 03:36:05 -0300 Subject: [PATCH 06/24] refactor: simplify datetime formatting and update Mostro pubkey --- lib/features/order/screens/take_order_screen.dart | 12 ++++++++---- lib/features/trades/screens/trade_detail_screen.dart | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 35f8882c..c0c5471d 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -275,13 +275,17 @@ class TakeOrderScreen extends ConsumerWidget { } String formatDateTime(DateTime dt) { - final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); + // Formato más amigable: Día de semana, Día Mes Año a las HH:MM (Zona horaria) + final dateFormatter = DateFormat('EEE, MMM dd yyyy'); + final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + // Simplificar la zona horaria a solo GMT+/-XX final offset = dt.timeZoneOffset; final sign = offset.isNegative ? '-' : '+'; final hours = offset.inHours.abs().toString().padLeft(2, '0'); - final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); - final timeZoneName = dt.timeZoneName; - return '$formattedDate GMT $sign$hours$minutes ($timeZoneName)'; + + return '$formattedDate at $formattedTime (GMT$sign$hours)'; } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 3fce6aea..cbeb93ce 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -657,13 +657,17 @@ class TradeDetailScreen extends ConsumerWidget { /// Format the date time to a user-friendly string with UTC offset String formatDateTime(DateTime dt) { - final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); + // Formato más amigable: Día de semana, Día Mes Año a las HH:MM (Zona horaria) + final dateFormatter = DateFormat('EEE, MMM dd yyyy'); + final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + // Simplificar la zona horaria a solo GMT+/-XX final offset = dt.timeZoneOffset; final sign = offset.isNegative ? '-' : '+'; final hours = offset.inHours.abs().toString().padLeft(2, '0'); - final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); - final timeZoneName = dt.timeZoneName; - return '$formattedDate GMT $sign$hours$minutes ($timeZoneName)'; + + return '$formattedDate at $formattedTime (GMT$sign$hours)'; } } \ No newline at end of file From 4dca13f57625bee63cca4da344bf23ab1d2fee4d Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 03:53:21 -0300 Subject: [PATCH 07/24] feat: use real rating data from order events and update Mostro pubkey --- .../order/screens/take_order_screen.dart | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index c0c5471d..5be2d5fd 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -145,18 +145,20 @@ class TakeOrderScreen extends ConsumerWidget { } Widget _buildCreatorReputation(NostrEvent order) { - // For now, show placeholder data matching TradeDetailScreen - // In a real implementation, this would come from the order creator's data - const rating = 3.1; - const reviews = 15; - const days = 7; - - return CreatorReputationCard( - rating: rating, - reviews: reviews, - days: days, - ); -} + // Extraer la información de calificación directamente del evento + final ratingInfo = order.rating; + + // Usar la información real o valores predeterminados si no está disponible + final rating = ratingInfo?.totalRating ?? 0.0; + final reviews = ratingInfo?.totalReviews ?? 0; + final days = ratingInfo?.days ?? 0; + + return CreatorReputationCard( + rating: rating, + reviews: reviews, + days: days, + ); + } Widget _buildActionButtons( BuildContext context, WidgetRef ref, NostrEvent order) { From 9a96f9c7ef4025d38355df547af08aa7e2606a0a Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Fri, 4 Jul 2025 03:57:31 -0300 Subject: [PATCH 08/24] feat: add plugin registrants and update Mostro public key --- lib/features/trades/screens/trade_detail_screen.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index cbeb93ce..31dead78 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -61,7 +61,7 @@ class TradeDetailScreen extends ConsumerWidget { const SizedBox(height: 16), // For pending orders created by the user, show creator's reputation if (isPending && isCreator) ...[ - _buildCreatorReputation(), + _buildCreatorReputation(tradeState), const SizedBox(height: 16), ] else ...[ // Use spread operator here too // Detailed info: includes the last Mostro message action text @@ -641,9 +641,10 @@ class TradeDetailScreen extends ConsumerWidget { } /// Build a card showing the creator's reputation with rating, reviews and days - Widget _buildCreatorReputation() { - // For now, show placeholder data matching the screenshot - // In a real implementation, this would come from the order creator's data + Widget _buildCreatorReputation(OrderState tradeState) { + // En trade_detail_screen.dart, no tenemos acceso directo al rating como en NostrEvent + // Por ahora, usamos valores predeterminados + // TODO: Implementar la extracción de datos de calificación del creador const rating = 3.1; const reviews = 15; const days = 7; From 98a81be58571a9a39ad394713a279d68adc21d47 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 5 Jul 2025 08:48:30 -0300 Subject: [PATCH 09/24] refactor: update custom card colors to use AppTheme constants and add platform plugin registrants --- lib/shared/widgets/custom_card.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/shared/widgets/custom_card.dart b/lib/shared/widgets/custom_card.dart index e5a20c83..d1db9928 100644 --- a/lib/shared/widgets/custom_card.dart +++ b/lib/shared/widgets/custom_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; class CustomCard extends StatelessWidget { final Widget child; @@ -19,11 +20,11 @@ class CustomCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: color ?? const Color(0xFF1D212C), + color: color ?? AppTheme.dark1, margin: margin, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: borderSide ?? BorderSide(color: const Color(0xFF1D212C)), + side: borderSide ?? BorderSide(color: AppTheme.dark1), ), child: Padding( padding: padding, From e0d2e999c31e631c8e0c5a735a698bee26edb15e Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 5 Jul 2025 08:56:42 -0300 Subject: [PATCH 10/24] refactor: extract user creator check logic and handle null session cases --- .../trades/screens/trade_detail_screen.dart | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 31dead78..87d0893b 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -1,6 +1,5 @@ import 'package:circular_countdown/circular_countdown.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; @@ -18,6 +17,7 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; +import 'package:mostro_mobile/data/models/session.dart'; class TradeDetailScreen extends ConsumerWidget { final String orderId; @@ -40,9 +40,7 @@ class TradeDetailScreen extends ConsumerWidget { // Check if this is a pending order created by the user final session = ref.watch(sessionProvider(orderId)); final isPending = tradeState.status == Status.pending; - final isCreator = session!.role == Role.buyer - ? tradeState.order!.kind == OrderType.buy - : tradeState.order!.kind == OrderType.sell; + final isCreator = _isUserCreator(session, tradeState); return Scaffold( backgroundColor: AppTheme.backgroundDark, @@ -60,10 +58,11 @@ class TradeDetailScreen extends ConsumerWidget { _buildOrderId(context), const SizedBox(height: 16), // For pending orders created by the user, show creator's reputation - if (isPending && isCreator) ...[ + if (isPending && isCreator) ...[ _buildCreatorReputation(tradeState), const SizedBox(height: 16), - ] else ...[ // Use spread operator here too + ] else ...[ + // Use spread operator here too // Detailed info: includes the last Mostro message action text MostroMessageDetail(orderId: orderId), ], @@ -97,15 +96,13 @@ class TradeDetailScreen extends ConsumerWidget { Widget _buildSellerAmount(WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); final isPending = tradeState.status == Status.pending; - + // Determine if the user is the creator of the order based on role and order type - final isCreator = session!.role == Role.buyer - ? tradeState.order!.kind == OrderType.buy - : tradeState.order!.kind == OrderType.sell; + final isCreator = _isUserCreator(session, tradeState); // For pending orders created by the user, show a notification message if (isPending && isCreator) { - final selling = session.role == Role.seller ? 'Selling' : 'Buying'; + final selling = session?.role == Role.seller ? 'Selling' : 'Buying'; final currencyFlag = CurrencyUtils.getFlagFromCurrency( tradeState.order!.fiatCode, ); @@ -122,7 +119,7 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 ? DateTime.fromMillisecondsSinceEpoch( tradeState.order!.createdAt! * 1000) - : session.startTime, + : session?.startTime ?? DateTime.now(), ); return Column( @@ -178,7 +175,7 @@ class TradeDetailScreen extends ConsumerWidget { fontSize: 16, ), ), - if (priceText.isNotEmpty) ...[ + if (priceText.isNotEmpty) ...[ const SizedBox(width: 8), Text( priceText, @@ -268,9 +265,9 @@ class TradeDetailScreen extends ConsumerWidget { ], ); } - + // For non-pending orders or orders not created by the user, use the original display - final selling = session.role == Role.seller ? 'selling' : 'buying'; + final selling = session?.role == Role.seller ? 'selling' : 'buying'; final currencyFlag = CurrencyUtils.getFlagFromCurrency( tradeState.order!.fiatCode, ); @@ -296,7 +293,7 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 ? DateTime.fromMillisecondsSinceEpoch( tradeState.order!.createdAt! * 1000) - : session.startTime, + : session?.startTime ?? DateTime.now(), ); return CustomCard( padding: const EdgeInsets.all(16), @@ -387,7 +384,8 @@ class TradeDetailScreen extends ConsumerWidget { if (tradeState.status == Status.active || tradeState.status == Status.fiatSent) { - if (tradeState.action == actions.Action.cooperativeCancelInitiatedByPeer) { + if (tradeState.action == + actions.Action.cooperativeCancelInitiatedByPeer) { cancelMessage = 'If you confirm, you will accept the cooperative cancellation initiated by your counterparty.'; } else { @@ -521,9 +519,7 @@ 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: - // El usuario ya inició cooperative cancel, ahora debe esperar respuesta widgets.add(_buildNostrButton( 'CANCEL PENDING', action: actions.Action.cooperativeCancelInitiatedByYou, @@ -642,9 +638,9 @@ class TradeDetailScreen extends ConsumerWidget { /// Build a card showing the creator's reputation with rating, reviews and days Widget _buildCreatorReputation(OrderState tradeState) { - // En trade_detail_screen.dart, no tenemos acceso directo al rating como en NostrEvent - // Por ahora, usamos valores predeterminados - // TODO: Implementar la extracción de datos de calificación del creador + // In trade_detail_screen.dart, we don't have direct access to rating as in NostrEvent + // For now, we use default values + // TODO: Implement extraction of creator rating data const rating = 3.1; const reviews = 15; const days = 7; @@ -656,19 +652,28 @@ class TradeDetailScreen extends ConsumerWidget { ); } + /// Helper method to determine if the current user is the creator of the order + /// based on their role (buyer/seller) and the order type (buy/sell) + bool _isUserCreator(Session? session, OrderState tradeState) { + if (session == null || session.role == null || tradeState.order == null) { + return false; + } + return session.role == Role.buyer + ? tradeState.order!.kind == OrderType.buy + : tradeState.order!.kind == OrderType.sell; + } + /// Format the date time to a user-friendly string with UTC offset String formatDateTime(DateTime dt) { - // Formato más amigable: Día de semana, Día Mes Año a las HH:MM (Zona horaria) final dateFormatter = DateFormat('EEE, MMM dd yyyy'); final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - - // Simplificar la zona horaria a solo GMT+/-XX + final offset = dt.timeZoneOffset; final sign = offset.isNegative ? '-' : '+'; final hours = offset.inHours.abs().toString().padLeft(2, '0'); - + return '$formattedDate at $formattedTime (GMT$sign$hours)'; } -} \ No newline at end of file +} From ee60585eda7d917a9a75980732b02dbd7f753589 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 5 Jul 2025 09:13:00 -0300 Subject: [PATCH 11/24] refactor: extract trade detail cards into reusable components --- .../trades/screens/trade_detail_screen.dart | 207 +++--------------- 1 file changed, 33 insertions(+), 174 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 87d0893b..618bfcac 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -14,8 +14,6 @@ import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; import 'package:mostro_mobile/shared/widgets/order_cards.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -import 'package:mostro_mobile/shared/utils/currency_utils.dart'; -import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; import 'package:mostro_mobile/data/models/session.dart'; @@ -103,12 +101,7 @@ class TradeDetailScreen extends ConsumerWidget { // For pending orders created by the user, show a notification message if (isPending && isCreator) { final selling = session?.role == Role.seller ? 'Selling' : 'Buying'; - final currencyFlag = CurrencyUtils.getFlagFromCurrency( - tradeState.order!.fiatCode, - ); - - final amountString = - '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; + // Currency information is now handled by OrderAmountCard // If `orderPayload.amount` is 0, the trade is "at market price" final isZeroAmount = (tradeState.order!.amount == 0); @@ -124,143 +117,24 @@ class TradeDetailScreen extends ConsumerWidget { return Column( children: [ - CustomCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.info_outline, - color: Colors.white70, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the \'Cancel\' button.', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - ), - ], - ), - ], - ), + NotificationMessageCard( + message: 'You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the \'Cancel\' button.', + icon: Icons.info_outline, ), const SizedBox(height: 16), - CustomCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Someone is $selling Sats', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Text( - 'for $amountString', - style: const TextStyle( - color: Colors.white70, - fontSize: 16, - ), - ), - if (priceText.isNotEmpty) ...[ - const SizedBox(width: 8), - Text( - priceText, - style: const TextStyle( - color: Colors.white60, - fontSize: 14, - ), - ), - ], - ], - ), - ], - ), + OrderAmountCard( + title: 'Someone is $selling Sats', + amount: tradeState.order!.fiatAmount.toString(), + currency: tradeState.order!.fiatCode, + priceText: priceText, ), const SizedBox(height: 16), - CustomCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.payment, - color: Colors.white70, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Payment Method', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - paymentMethod, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), + PaymentMethodCard( + paymentMethod: paymentMethod, ), const SizedBox(height: 16), - CustomCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon( - Icons.calendar_today, - color: Colors.white70, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Created On', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - createdOn, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ], - ), + CreatedDateCard( + createdDate: createdOn, ), ], ); @@ -268,13 +142,7 @@ class TradeDetailScreen extends ConsumerWidget { // For non-pending orders or orders not created by the user, use the original display final selling = session?.role == Role.seller ? 'selling' : 'buying'; - final currencyFlag = CurrencyUtils.getFlagFromCurrency( - tradeState.order!.fiatCode, - ); - - final amountString = - '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; - + // If `orderPayload.amount` is 0, the trade is "at market price" final isZeroAmount = (tradeState.order!.amount == 0); final satText = isZeroAmount ? '' : ' ${tradeState.order!.amount}'; @@ -295,34 +163,25 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.order!.createdAt! * 1000) : session?.startTime ?? DateTime.now(), ); - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'You are $selling$satText sats for $amountString $priceText $premiumText', - style: AppTheme.theme.textTheme.bodyLarge, - softWrap: true, - ), - const SizedBox(height: 16), - Text( - 'Created on: $timestamp', - style: textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Text( - 'Payment methods: $method', - style: textTheme.bodyLarge, - ), - ], - ), - ), - ], - ), + + return Column( + children: [ + OrderAmountCard( + title: 'You are $selling$satText sats', + amount: tradeState.order!.fiatAmount.toString(), + currency: tradeState.order!.fiatCode, + priceText: priceText, + premiumText: premiumText, + ), + const SizedBox(height: 16), + PaymentMethodCard( + paymentMethod: method, + ), + const SizedBox(height: 16), + CreatedDateCard( + createdDate: timestamp, + ), + ], ); } From 5870f84f8007a7bf95a1abde57a0bc2df0d0b0ed Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Sat, 5 Jul 2025 09:16:35 -0300 Subject: [PATCH 12/24] feat: update order validation logic , add platform-specific plugin registrants --- .../order/screens/take_order_screen.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 5be2d5fd..16d39c55 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -145,14 +145,12 @@ class TakeOrderScreen extends ConsumerWidget { } Widget _buildCreatorReputation(NostrEvent order) { - // Extraer la información de calificación directamente del evento final ratingInfo = order.rating; - - // Usar la información real o valores predeterminados si no está disponible + final rating = ratingInfo?.totalRating ?? 0.0; final reviews = ratingInfo?.totalReviews ?? 0; final days = ratingInfo?.days ?? 0; - + return CreatorReputationCard( rating: rating, reviews: reviews, @@ -183,7 +181,8 @@ class TakeOrderScreen extends ConsumerWidget { child: ElevatedButton( onPressed: () async { // Check if this is a range order - if (order.fiatAmount.minimum != order.fiatAmount.maximum) { + if (order.fiatAmount.maximum != null && + order.fiatAmount.minimum != order.fiatAmount.maximum) { // Show dialog to get the amount String? errorText; final enteredAmount = await showDialog( @@ -218,7 +217,9 @@ class TakeOrderScreen extends ConsumerWidget { }); } else if (inputAmount < order.fiatAmount.minimum || - inputAmount > order.fiatAmount.maximum!) { + (order.fiatAmount.maximum != null && + inputAmount > + order.fiatAmount.maximum!)) { setState(() { errorText = "Amount must be between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}."; @@ -277,17 +278,15 @@ class TakeOrderScreen extends ConsumerWidget { } String formatDateTime(DateTime dt) { - // Formato más amigable: Día de semana, Día Mes Año a las HH:MM (Zona horaria) final dateFormatter = DateFormat('EEE, MMM dd yyyy'); final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - - // Simplificar la zona horaria a solo GMT+/-XX + final offset = dt.timeZoneOffset; final sign = offset.isNegative ? '-' : '+'; final hours = offset.inHours.abs().toString().padLeft(2, '0'); - + return '$formattedDate at $formattedTime (GMT$sign$hours)'; } } From 966765e10739f544f880a15f60fa129a74a823a7 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Mon, 7 Jul 2025 18:04:35 -0300 Subject: [PATCH 13/24] feat: add internationalization support for order screens and components WIP --- .../order/screens/take_order_screen.dart | 72 ++++++++----------- lib/features/order/widgets/order_app_bar.dart | 2 +- .../trades/screens/trade_detail_screen.dart | 66 +++++++---------- lib/l10n/intl_en.arb | 37 ++++++++++ lib/l10n/intl_es.arb | 39 +++++++++- lib/l10n/intl_it.arb | 39 +++++++++- lib/shared/widgets/order_cards.dart | 56 ++++++++------- 7 files changed, 201 insertions(+), 110 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 64473048..a64d7995 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -1,7 +1,7 @@ import 'package:circular_countdown/circular_countdown.dart'; import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:mostro_mobile/core/app_theme.dart'; @@ -29,21 +29,19 @@ class TakeOrderScreen extends ConsumerWidget { final order = ref.watch(eventProvider(orderId)); return Scaffold( - backgroundColor: AppTheme.backgroundDark, appBar: OrderAppBar( title: orderType == OrderType.buy - ? 'BUY ORDER DETAILS' - : 'SELL ORDER DETAILS'), - + ? S.of(context)!.buyOrderDetailsTitle + : S.of(context)!.sellOrderDetailsTitle), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( children: [ const SizedBox(height: 16), - _buildSellerAmount(ref, order!, context), + _buildSellerAmount(ref, order!), const SizedBox(height: 16), - _buildPaymentMethod(order), + _buildPaymentMethod(context, order), const SizedBox(height: 16), _buildCreatedOn(order), const SizedBox(height: 16), @@ -51,7 +49,7 @@ class TakeOrderScreen extends ConsumerWidget { const SizedBox(height: 16), _buildCreatorReputation(order), const SizedBox(height: 24), - _buildCountDownTime(order.expirationDate, context), + _buildCountDownTime(context, order.expirationDate), const SizedBox(height: 36), _buildActionButtons(context, ref, order), ], @@ -60,22 +58,21 @@ class TakeOrderScreen extends ConsumerWidget { ); } - Widget _buildSellerAmount(WidgetRef ref, NostrEvent order) { - final selling = orderType == OrderType.sell ? 'Selling' : 'Buying'; - final currencyFlag = CurrencyUtils.getFlagFromCurrency(order.currency!); - final amountString = '${order.fiatAmount} ${order.currency} $currencyFlag'; - final priceText = order.amount == '0' ? 'at market price' : ''; - - - return CustomCard( + return Builder( + builder: (context) { + final selling = orderType == OrderType.sell ? S.of(context)!.selling : S.of(context)!.buying; + final currencyFlag = CurrencyUtils.getFlagFromCurrency(order.currency!); + final amountString = '${order.fiatAmount} ${order.currency} $currencyFlag'; + final priceText = order.amount == '0' ? S.of(context)!.atMarketPrice : ''; + + return CustomCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Someone is $selling Sats', + selling == S.of(context)!.selling ? S.of(context)!.someoneIsSellingTitle : S.of(context)!.someoneIsBuyingTitle, style: const TextStyle( color: Colors.white, fontSize: 18, @@ -86,23 +83,20 @@ class TakeOrderScreen extends ConsumerWidget { Row( children: [ Text( - 'for $amountString', + S.of(context)!.forAmount(amountString), style: const TextStyle( color: Colors.white70, fontSize: 16, - ), ), if (priceText.isNotEmpty) ...[ const SizedBox(width: 8), Text( - priceText, style: const TextStyle( color: Colors.white60, fontSize: 14, ), - ), ], ], @@ -110,17 +104,17 @@ class TakeOrderScreen extends ConsumerWidget { ], ), ); + }, + ); } Widget _buildOrderId(BuildContext context) { - return OrderIdCard( orderId: orderId, - ); } - Widget _buildCountDownTime(DateTime expiration, BuildContext context) { + Widget _buildCountDownTime(BuildContext context, DateTime expiration) { Duration countdown = Duration(hours: 0); final now = DateTime.now(); if (expiration.isAfter(now)) { @@ -134,15 +128,15 @@ class TakeOrderScreen extends ConsumerWidget { countdownRemaining: countdown.inHours, ), const SizedBox(height: 16), - Text(S.of(context)!.timeLeft(countdown.toString().split('.')[0])), + Text(S.of(context)!.timeLeftLabel(countdown.toString().split('.')[0])), ], ); } - Widget _buildPaymentMethod(NostrEvent order) { + Widget _buildPaymentMethod(BuildContext context, NostrEvent order) { final methods = order.paymentMethods.isNotEmpty ? order.paymentMethods.join(', ') - : 'No payment method'; + : S.of(context)!.noPaymentMethod; return PaymentMethodCard( paymentMethod: methods, @@ -175,17 +169,16 @@ class TakeOrderScreen extends ConsumerWidget { ref.read(orderNotifierProvider(orderId).notifier); final buttonText = - orderType == OrderType.buy ? 'SELL BITCOIN' : 'BUY BITCOIN'; + orderType == OrderType.buy ? S.of(context)!.sellBitcoinButton : S.of(context)!.buyBitcoinButton; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop(), style: AppTheme.theme.outlinedButtonTheme.style, - child: const Text('CLOSE'), + child: Text(S.of(context)!.close), ), ), const SizedBox(width: 16), @@ -203,20 +196,19 @@ class TakeOrderScreen extends ConsumerWidget { return StatefulBuilder( builder: (context, setState) { return AlertDialog( - title: const Text('Enter Amount'), + title: Text(S.of(context)!.enterAmount), content: TextField( controller: _fiatAmountController, keyboardType: TextInputType.number, decoration: InputDecoration( - hintText: - 'Enter an amount between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}', + hintText: S.of(context)!.enterAmountBetween(order.fiatAmount.minimum.toString(), order.fiatAmount.maximum.toString()), errorText: errorText, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancel'), + child: Text(S.of(context)!.cancel), ), ElevatedButton( key: const Key('submitAmountButton'), @@ -225,7 +217,7 @@ class TakeOrderScreen extends ConsumerWidget { _fiatAmountController.text.trim()); if (inputAmount == null) { setState(() { - errorText = "Please enter a valid number."; + errorText = S.of(context)!.pleaseEnterValidNumber; }); } else if (inputAmount < order.fiatAmount.minimum || @@ -233,14 +225,13 @@ class TakeOrderScreen extends ConsumerWidget { inputAmount > order.fiatAmount.maximum!)) { setState(() { - errorText = - "Amount must be between ${order.fiatAmount.minimum} and ${order.fiatAmount.maximum}."; + errorText = S.of(context)!.amountMustBeBetween(order.fiatAmount.minimum.toString(), order.fiatAmount.maximum.toString()); }); } else { Navigator.of(context).pop(inputAmount); } }, - child: const Text('Submit'), + child: Text(S.of(context)!.submit), ), ], ); @@ -249,7 +240,6 @@ class TakeOrderScreen extends ConsumerWidget { }, ); - if (enteredAmount != null) { if (orderType == OrderType.buy) { await orderDetailsNotifier.takeBuyOrder( @@ -285,8 +275,6 @@ class TakeOrderScreen extends ConsumerWidget { ), child: Text(buttonText), ), - - ), ], ); diff --git a/lib/features/order/widgets/order_app_bar.dart b/lib/features/order/widgets/order_app_bar.dart index e8cd4f46..8f997d2d 100644 --- a/lib/features/order/widgets/order_app_bar.dart +++ b/lib/features/order/widgets/order_app_bar.dart @@ -19,7 +19,7 @@ class OrderAppBar extends StatelessWidget implements PreferredSizeWidget { HeroIcons.arrowLeft, color: AppTheme.cream1, ), - onPressed: () => context.go('/order_book'), + onPressed: () => context.go('/'), ), title: Text( title, diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index a10d1bfb..b6fe25a7 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -15,9 +15,8 @@ import 'package:mostro_mobile/shared/widgets/order_cards.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; - import 'package:mostro_mobile/data/models/session.dart'; - +import 'package:mostro_mobile/generated/l10n.dart'; class TradeDetailScreen extends ConsumerWidget { final String orderId; @@ -43,10 +42,8 @@ class TradeDetailScreen extends ConsumerWidget { final isCreator = _isUserCreator(session, tradeState); return Scaffold( - backgroundColor: AppTheme.backgroundDark, - appBar: OrderAppBar(title: 'ORDER DETAILS'), - + appBar: OrderAppBar(title: S.of(context)!.orderDetailsTitle), body: Builder( builder: (context) { return SingleChildScrollView( @@ -61,7 +58,7 @@ class TradeDetailScreen extends ConsumerWidget { const SizedBox(height: 16), // For pending orders created by the user, show creator's reputation if (isPending && isCreator) ...[ - _buildCreatorReputation(tradeState), + _buildCreatorReputation(context, tradeState), const SizedBox(height: 16), ] else ...[ // Use spread operator here too @@ -104,12 +101,12 @@ class TradeDetailScreen extends ConsumerWidget { // For pending orders created by the user, show a notification message if (isPending && isCreator) { - final selling = session?.role == Role.seller ? 'Selling' : 'Buying'; + final selling = session?.role == Role.seller ? S.of(context)!.selling : S.of(context)!.buying; // Currency information is now handled by OrderAmountCard // If `orderPayload.amount` is 0, the trade is "at market price" final isZeroAmount = (tradeState.order!.amount == 0); - final priceText = isZeroAmount ? 'at market price' : ''; + final priceText = isZeroAmount ? S.of(context)!.atMarketPrice : ''; final paymentMethod = tradeState.order!.paymentMethod; final createdOn = formatDateTime( @@ -119,16 +116,15 @@ class TradeDetailScreen extends ConsumerWidget { : session?.startTime ?? DateTime.now(), ); - return Column( children: [ NotificationMessageCard( - message: 'You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the \'Cancel\' button.', + message: S.of(context)!.youCreatedOfferMessage, icon: Icons.info_outline, ), const SizedBox(height: 16), OrderAmountCard( - title: 'Someone is $selling Sats', + title: selling == S.of(context)!.selling ? S.of(context)!.someoneIsSellingTitle : S.of(context)!.someoneIsBuyingTitle, amount: tradeState.order!.fiatAmount.toString(), currency: tradeState.order!.fiatCode, priceText: priceText, @@ -146,22 +142,19 @@ class TradeDetailScreen extends ConsumerWidget { } // For non-pending orders or orders not created by the user, use the original display - final selling = session?.role == Role.seller ? 'selling' : 'buying'; + final selling = session?.role == Role.seller ? S.of(context)!.selling : S.of(context)!.buying; - // If `orderPayload.amount` is 0, the trade is "at market price" final isZeroAmount = (tradeState.order!.amount == 0); final satText = isZeroAmount ? '' : ' ${tradeState.order!.amount}'; - final priceText = isZeroAmount ? ' ${S.of(context)!.atMarketPrice}' : ''; - + final priceText = isZeroAmount ? 'at market price' : ''; + final premium = tradeState.order!.premium; final premiumText = premium == 0 ? '' : (premium > 0) - ? S.of(context)!.withPremium(premium) - : S.of(context)!.withDiscount(premium); - - final isSellingRole = session!.role == Role.seller; + ? S.of(context)!.withPremiumPercent(premium.toString()) + : S.of(context)!.withDiscountPercent(premium.abs().toString()); // Payment method final method = tradeState.order!.paymentMethod; @@ -172,11 +165,10 @@ class TradeDetailScreen extends ConsumerWidget { : session?.startTime ?? DateTime.now(), ); - return Column( children: [ OrderAmountCard( - title: 'You are $selling$satText sats', + title: selling == S.of(context)!.selling ? S.of(context)!.youAreSellingTitle(satText) : S.of(context)!.youAreBuyingTitle(satText), amount: tradeState.order!.fiatAmount.toString(), currency: tradeState.order!.fiatCode, priceText: priceText, @@ -191,16 +183,13 @@ class TradeDetailScreen extends ConsumerWidget { createdDate: timestamp, ), ], - ); } /// Show a card with the order ID that can be copied. Widget _buildOrderId(BuildContext context) { - return OrderIdCard( orderId: orderId, - ); } @@ -225,7 +214,7 @@ class TradeDetailScreen extends ConsumerWidget { countdownRemaining: hoursLeft, ), const SizedBox(height: 16), - Text(S.of(context)!.timeLeft(difference.toString().split('.').first)), + Text(S.of(context)!.timeLeftLabel(difference.toString().split('.').first)), ], ); } @@ -255,28 +244,27 @@ class TradeDetailScreen extends ConsumerWidget { if (tradeState.status == Status.active || tradeState.status == Status.fiatSent) { - if (tradeState.action == actions.Action.cooperativeCancelInitiatedByPeer) { cancelMessage = 'If you confirm, you will accept the cooperative cancellation initiated by your counterparty.'; - } else { - cancelMessage = S.of(context)!.cooperativeCancelMessage; + cancelMessage = + 'If you confirm, you will start a cooperative cancellation with your counterparty.'; } } else { - cancelMessage = S.of(context)!.areYouSureCancel; + cancelMessage = 'Are you sure you want to cancel this trade?'; } widgets.add(_buildNostrButton( - S.of(context)!.cancel, + S.of(context)!.cancel.toUpperCase(), action: action, backgroundColor: AppTheme.red1, onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(S.of(context)!.cancelTrade), + title: Text(S.of(context)!.cancelTradeDialogTitle), content: Text(cancelMessage), actions: [ TextButton( @@ -328,7 +316,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.fiatSent: if (userRole == Role.buyer) { widgets.add(_buildNostrButton( - S.of(context)!.fiatSent, + S.of(context)!.fiatSentButton, action: actions.Action.fiatSent, backgroundColor: AppTheme.mostroGreen, onPressed: () => ref @@ -346,7 +334,7 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.action != actions.Action.disputeInitiatedByPeer && tradeState.action != actions.Action.dispute) { widgets.add(_buildNostrButton( - S.of(context)!.dispute, + S.of(context)!.disputeButton, action: actions.Action.disputeInitiatedByYou, backgroundColor: AppTheme.red1, onPressed: () => ref @@ -359,7 +347,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.release: if (userRole == Role.seller) { widgets.add(_buildNostrButton( - S.of(context)!.release, + S.of(context)!.release.toUpperCase(), action: actions.Action.release, backgroundColor: AppTheme.mostroGreen, onPressed: () => ref @@ -393,7 +381,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.cooperativeCancelInitiatedByYou: widgets.add(_buildNostrButton( - S.of(context)!.cancelPending, + S.of(context)!.cancelPendingButton, action: actions.Action.cooperativeCancelInitiatedByYou, backgroundColor: Colors.grey, onPressed: null, @@ -402,7 +390,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.cooperativeCancelInitiatedByPeer: widgets.add(_buildNostrButton( - S.of(context)!.acceptCancel, + S.of(context)!.acceptCancelButton, action: actions.Action.cooperativeCancelAccepted, backgroundColor: AppTheme.red1, onPressed: () => @@ -415,7 +403,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.purchaseCompleted: widgets.add(_buildNostrButton( - S.of(context)!.completePurchase, + S.of(context)!.completePurchaseButton, action: actions.Action.purchaseCompleted, backgroundColor: AppTheme.mostroGreen, onPressed: () => ref @@ -432,7 +420,7 @@ class TradeDetailScreen extends ConsumerWidget { case actions.Action.rateUser: case actions.Action.rateReceived: widgets.add(_buildNostrButton( - S.of(context)!.rate, + S.of(context)!.rate.toUpperCase(), action: actions.Action.rate, backgroundColor: AppTheme.mostroGreen, onPressed: () => context.push('/rate_user/$orderId'), @@ -509,7 +497,7 @@ class TradeDetailScreen extends ConsumerWidget { } /// Build a card showing the creator's reputation with rating, reviews and days - Widget _buildCreatorReputation(OrderState tradeState) { + Widget _buildCreatorReputation(BuildContext context, OrderState tradeState) { // In trade_detail_screen.dart, we don't have direct access to rating as in NostrEvent // For now, we use default values // TODO: Implement extraction of creator rating data diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 121a4019..fe00996b 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -123,6 +123,43 @@ "seamlessPeerToPeer": "Enjoy seamless peer-to-peer trades using our advanced protocols.", "skip": "Skip", "done": "Done", + + "@_comment_trade_detail_screen_new": "New Trade Detail Screen Strings", + "orderDetailsTitle": "ORDER DETAILS", + "buyOrderDetailsTitle": "BUY ORDER DETAILS", + "sellOrderDetailsTitle": "SELL ORDER DETAILS", + "someoneIsSellingTitle": "Someone is Selling Sats", + "someoneIsBuyingTitle": "Someone is Buying Sats", + "youCreatedOfferMessage": "You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the 'Cancel' button.", + "youAreSellingTitle": "You are selling{sats} sats", + "youAreBuyingTitle": "You are buying{sats} sats", + "forAmount": "for {amount}", + "timeLeftLabel": "Time Left: {time}", + "@timeLeftLabel": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "cancelPendingButton": "CANCEL PENDING", + "acceptCancelButton": "ACCEPT CANCEL", + "disputeButton": "DISPUTE", + "fiatSentButton": "FIAT SENT", + "completePurchaseButton": "COMPLETE PURCHASE", + "sellBitcoinButton": "SELL BITCOIN", + "buyBitcoinButton": "BUY BITCOIN", + "paymentMethodLabel": "Payment Method", + "createdOnLabel": "Created On", + "orderIdLabel": "Order ID", + "creatorReputationLabel": "Creator's Reputation", + "ratingLabel": "Rating", + "reviewsLabel": "Reviews", + "daysLabel": "Days", + "cancelTradeDialogTitle": "Cancel Trade", + "cooperativeCancelDialogMessage": "If you confirm, you will start a cooperative cancellation with your counterparty.", + "acceptCancelDialogMessage": "If you confirm, you will accept the cooperative cancellation initiated by your counterparty.", + "orderIdCopiedMessage": "Order ID copied to clipboard", "typeToAdd": "Type to add...", "noneSelected": "None selected", "fiatCurrencies": "Fiat currencies", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 71238fc7..c47a0e44 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -399,5 +399,42 @@ "share": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", "openWallet": "ABRIR BILLETERA", - "done": "HECHO" + "done": "HECHO", + + "@_comment_trade_detail_screen_new": "Nuevas cadenas de Pantalla de Detalles de Intercambio", + "orderDetailsTitle": "DETALLES DE ORDEN", + "buyOrderDetailsTitle": "DETALLES DE ORDEN DE COMPRA", + "sellOrderDetailsTitle": "DETALLES DE ORDEN DE VENTA", + "someoneIsSellingTitle": "Alguien está vendiendo Sats", + "someoneIsBuyingTitle": "Alguien está comprando Sats", + "youCreatedOfferMessage": "Creaste esta oferta. A continuación se muestran los detalles de tu oferta. Espera a que otro usuario la tome. Se publicará durante 24 horas. Puedes cancelarla en cualquier momento usando el botón 'Cancelar'.", + "youAreSellingTitle": "Estás vendiendo{sats} sats", + "youAreBuyingTitle": "Estás comprando{sats} sats", + "forAmount": "por {amount}", + "timeLeftLabel": "Tiempo restante: {time}", + "@timeLeftLabel": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "cancelPendingButton": "CANCELACIÓN PENDIENTE", + "acceptCancelButton": "ACEPTAR CANCELACIÓN", + "disputeButton": "DISPUTA", + "fiatSentButton": "FIAT ENVIADO", + "completePurchaseButton": "COMPLETAR COMPRA", + "sellBitcoinButton": "VENDER BITCOIN", + "buyBitcoinButton": "COMPRAR BITCOIN", + "paymentMethodLabel": "Método de Pago", + "createdOnLabel": "Creado el", + "orderIdLabel": "ID de Orden", + "creatorReputationLabel": "Reputación del Creador", + "ratingLabel": "Calificación", + "reviewsLabel": "Reseñas", + "daysLabel": "Días", + "cancelTradeDialogTitle": "Cancelar Intercambio", + "cooperativeCancelDialogMessage": "Si confirmas, iniciarás una cancelación cooperativa con tu contraparte.", + "acceptCancelDialogMessage": "Si confirmas, aceptarás la cancelación cooperativa iniciada por tu contraparte.", + "orderIdCopiedMessage": "ID de orden copiado al portapapeles" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index aab46113..d26623ee 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -399,5 +399,42 @@ "share": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", "openWallet": "APRI PORTAFOGLIO", - "done": "FATTO" + "done": "FATTO", + + "@_comment_trade_detail_screen_new": "Nuove Stringhe Schermata Dettagli Scambio", + "orderDetailsTitle": "DETTAGLI ORDINE", + "buyOrderDetailsTitle": "DETTAGLI ORDINE DI ACQUISTO", + "sellOrderDetailsTitle": "DETTAGLI ORDINE DI VENDITA", + "someoneIsSellingTitle": "Qualcuno sta vendendo Sats", + "someoneIsBuyingTitle": "Qualcuno sta comprando Sats", + "youCreatedOfferMessage": "Hai creato questa offerta. Di seguito sono riportati i dettagli della tua offerta. Aspetta che un altro utente la prenda. Sarà pubblicata per 24 ore. Puoi cancellarla in qualsiasi momento utilizzando il pulsante 'Annulla'.", + "youAreSellingTitle": "Stai vendendo{sats} sats", + "youAreBuyingTitle": "Stai comprando{sats} sats", + "forAmount": "per {amount}", + "timeLeftLabel": "Tempo rimasto: {time}", + "@timeLeftLabel": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "cancelPendingButton": "ANNULLAMENTO IN SOSPESO", + "acceptCancelButton": "ACCETTA ANNULLAMENTO", + "disputeButton": "DISPUTA", + "fiatSentButton": "FIAT INVIATO", + "completePurchaseButton": "COMPLETA ACQUISTO", + "sellBitcoinButton": "VENDI BITCOIN", + "buyBitcoinButton": "COMPRA BITCOIN", + "paymentMethodLabel": "Metodo di Pagamento", + "createdOnLabel": "Creato il", + "orderIdLabel": "ID Ordine", + "creatorReputationLabel": "Reputazione del Creatore", + "ratingLabel": "Valutazione", + "reviewsLabel": "Recensioni", + "daysLabel": "Giorni", + "cancelTradeDialogTitle": "Annulla Scambio", + "cooperativeCancelDialogMessage": "Se confermi, inizierai un annullamento cooperativo con la tua controparte.", + "acceptCancelDialogMessage": "Se confermi, accetterai l'annullamento cooperativo iniziato dalla tua controparte.", + "orderIdCopiedMessage": "ID ordine copiato negli appunti" } diff --git a/lib/shared/widgets/order_cards.dart b/lib/shared/widgets/order_cards.dart index bf488334..4d0a270c 100644 --- a/lib/shared/widgets/order_cards.dart +++ b/lib/shared/widgets/order_cards.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; /// Card that displays the order amount information (selling/buying sats for amount) class OrderAmountCard extends StatelessWidget { @@ -13,13 +14,13 @@ class OrderAmountCard extends StatelessWidget { final String? premiumText; const OrderAmountCard({ - Key? key, + super.key, required this.title, required this.amount, required this.currency, this.priceText, this.premiumText, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -43,7 +44,7 @@ class OrderAmountCard extends StatelessWidget { Row( children: [ Text( - 'for $amountString', + S.of(context)!.forAmount(amountString), style: const TextStyle( color: Colors.white70, fontSize: 16, @@ -81,7 +82,7 @@ class OrderAmountCard extends StatelessWidget { class PaymentMethodCard extends StatelessWidget { final String paymentMethod; - const PaymentMethodCard({Key? key, required this.paymentMethod}) : super(key: key); + const PaymentMethodCard({super.key, required this.paymentMethod}); @override Widget build(BuildContext context) { @@ -99,8 +100,8 @@ class PaymentMethodCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Payment Method', + Text( + S.of(context)!.paymentMethodLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -127,7 +128,7 @@ class PaymentMethodCard extends StatelessWidget { class CreatedDateCard extends StatelessWidget { final String createdDate; - const CreatedDateCard({Key? key, required this.createdDate}) : super(key: key); + const CreatedDateCard({super.key, required this.createdDate}); @override Widget build(BuildContext context) { @@ -145,8 +146,8 @@ class CreatedDateCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Created On', + Text( + S.of(context)!.createdOnLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -173,7 +174,10 @@ class CreatedDateCard extends StatelessWidget { class OrderIdCard extends StatelessWidget { final String orderId; - const OrderIdCard({Key? key, required this.orderId}) : super(key: key); + const OrderIdCard({ + super.key, + required this.orderId, + }); @override Widget build(BuildContext context) { @@ -182,8 +186,8 @@ class OrderIdCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Order ID', + Text( + S.of(context)!.orderIdLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -210,8 +214,8 @@ class OrderIdCard extends StatelessWidget { onPressed: () { Clipboard.setData(ClipboardData(text: orderId)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Order ID copied to clipboard'), + SnackBar( + content: Text(S.of(context)!.orderIdCopiedMessage), duration: Duration(seconds: 2), ), ); @@ -232,11 +236,11 @@ class CreatorReputationCard extends StatelessWidget { final int days; const CreatorReputationCard({ - Key? key, + super.key, required this.rating, required this.reviews, required this.days, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -246,8 +250,8 @@ class CreatorReputationCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Creator\'s Reputation', + Text( + S.of(context)!.creatorReputationLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -281,8 +285,8 @@ class CreatorReputationCard extends StatelessWidget { ], ), const SizedBox(height: 4), - const Text( - 'Rating', + Text( + S.of(context)!.ratingLabel.toString(), style: TextStyle( color: Colors.white70, fontSize: 12, @@ -315,8 +319,8 @@ class CreatorReputationCard extends StatelessWidget { ], ), const SizedBox(height: 4), - const Text( - 'Reviews', + Text( + S.of(context)!.reviewsLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -349,8 +353,8 @@ class CreatorReputationCard extends StatelessWidget { ], ), const SizedBox(height: 4), - const Text( - 'Days', + Text( + S.of(context)!.daysLabel, style: TextStyle( color: Colors.white70, fontSize: 12, @@ -375,11 +379,11 @@ class NotificationMessageCard extends StatelessWidget { final Color iconColor; const NotificationMessageCard({ - Key? key, + super.key, required this.message, this.icon = Icons.info_outline, this.iconColor = Colors.white70, - }) : super(key: key); + }); @override Widget build(BuildContext context) { From 1cbe9433cebdc41dc635c17d35a5ea89fa1624ec Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Tue, 8 Jul 2025 00:54:36 -0300 Subject: [PATCH 14/24] refactor: migrate colors to AppTheme and add fixed sats amount titles in trade screens --- lib/core/app_theme.dart | 25 ++ .../order/screens/take_order_screen.dart | 95 +++++--- .../trades/screens/trade_detail_screen.dart | 61 +++-- .../trades/widgets/trades_list_item.dart | 222 ++++++++++-------- lib/l10n/intl_en.arb | 6 +- lib/l10n/intl_es.arb | 9 +- lib/l10n/intl_it.arb | 9 +- lib/shared/widgets/order_cards.dart | 4 +- 8 files changed, 258 insertions(+), 173 deletions(-) diff --git a/lib/core/app_theme.dart b/lib/core/app_theme.dart index 167dab99..433cb195 100644 --- a/lib/core/app_theme.dart +++ b/lib/core/app_theme.dart @@ -41,6 +41,31 @@ class AppTheme { static const Color statusWarning = Color(0xFFF3CA29); static const Color statusError = Color(0xFFEF6A6A); static const Color statusActive = Color(0xFF9CD651); + + // Colors for role chips + static const Color createdByYouChip = Color(0xFF1565C0); // Colors.blue.shade700 + static const Color takenByYouChip = Color(0xFF00796B); // Colors.teal.shade700 + static const Color premiumPositiveChip = Color(0xFF388E3C); // Colors.green.shade700 + static const Color premiumNegativeChip = Color(0xFFC62828); // Colors.red.shade700 + + // Colors for status chips + static const Color statusPendingBackground = Color(0xFF854D0E); + static const Color statusPendingText = Color(0xFFFCD34D); + static const Color statusWaitingBackground = Color(0xFF7C2D12); + static const Color statusWaitingText = Color(0xFFFED7AA); + static const Color statusActiveBackground = Color(0xFF1E3A8A); + static const Color statusActiveText = Color(0xFF93C5FD); + static const Color statusSuccessBackground = Color(0xFF065F46); + static const Color statusSuccessText = Color(0xFF6EE7B7); + static const Color statusDisputeBackground = Color(0xFF7F1D1D); + static const Color statusDisputeText = Color(0xFFFCA5A5); + static const Color statusSettledBackground = Color(0xFF581C87); + static const Color statusSettledText = Color(0xFFC084FC); + static const Color statusInactiveBackground = Color(0xFF1F2937); // Colors.grey.shade800 + static const Color statusInactiveText = Color(0xFFD1D5DB); // Colors.grey.shade300 + + // Text colors + static const Color secondaryText = Color(0xFFBDBDBD); // Colors.grey.shade400 // Padding and margin constants static const EdgeInsets smallPadding = EdgeInsets.all(8.0); diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index a64d7995..5d718df9 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -61,49 +61,59 @@ class TakeOrderScreen extends ConsumerWidget { Widget _buildSellerAmount(WidgetRef ref, NostrEvent order) { return Builder( builder: (context) { - final selling = orderType == OrderType.sell ? S.of(context)!.selling : S.of(context)!.buying; final currencyFlag = CurrencyUtils.getFlagFromCurrency(order.currency!); - final amountString = '${order.fiatAmount} ${order.currency} $currencyFlag'; - final priceText = order.amount == '0' ? S.of(context)!.atMarketPrice : ''; - + final amountString = + '${order.fiatAmount} ${order.currency} $currencyFlag'; + final priceText = + order.amount == '0' ? S.of(context)!.atMarketPrice : ''; + + final hasFixedSatsAmount = order.amount != null && order.amount != '0'; + return CustomCard( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - selling == S.of(context)!.selling ? S.of(context)!.someoneIsSellingTitle : S.of(context)!.someoneIsBuyingTitle, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Row( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - S.of(context)!.forAmount(amountString), + hasFixedSatsAmount + ? (orderType == OrderType.sell + ? "${S.of(context)!.someoneIsSellingTitle.replaceAll(' Sats', '')} ${order.amount} Sats" + : "${S.of(context)!.someoneIsBuyingTitle.replaceAll(' Sats', '')} ${order.amount} Sats") + : (orderType == OrderType.sell + ? S.of(context)!.someoneIsSellingTitle + : S.of(context)!.someoneIsBuyingTitle), style: const TextStyle( - color: Colors.white70, - fontSize: 16, + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, ), ), - if (priceText.isNotEmpty) ...[ - const SizedBox(width: 8), - Text( - priceText, - style: const TextStyle( - color: Colors.white60, - fontSize: 14, + const SizedBox(height: 8), + Row( + children: [ + Text( + S.of(context)!.forAmount(amountString), + style: const TextStyle( + color: Colors.white70, + fontSize: 16, + ), ), - ), - ], + if (priceText.isNotEmpty) ...[ + // Fixed [...] brackets + const SizedBox(width: 8), + Text( + priceText, + style: const TextStyle( + color: Colors.white60, + fontSize: 14, + ), + ), + ], + ], + ), ], ), - ], - ), - ); + ); }, ); } @@ -168,8 +178,9 @@ class TakeOrderScreen extends ConsumerWidget { final orderDetailsNotifier = ref.read(orderNotifierProvider(orderId).notifier); - final buttonText = - orderType == OrderType.buy ? S.of(context)!.sellBitcoinButton : S.of(context)!.buyBitcoinButton; + final buttonText = orderType == OrderType.buy + ? S.of(context)!.sellBitcoinButton + : S.of(context)!.buyBitcoinButton; return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -201,7 +212,9 @@ class TakeOrderScreen extends ConsumerWidget { controller: _fiatAmountController, keyboardType: TextInputType.number, decoration: InputDecoration( - hintText: S.of(context)!.enterAmountBetween(order.fiatAmount.minimum.toString(), order.fiatAmount.maximum.toString()), + hintText: S.of(context)!.enterAmountBetween( + order.fiatAmount.minimum.toString(), + order.fiatAmount.maximum.toString()), errorText: errorText, ), ), @@ -217,7 +230,8 @@ class TakeOrderScreen extends ConsumerWidget { _fiatAmountController.text.trim()); if (inputAmount == null) { setState(() { - errorText = S.of(context)!.pleaseEnterValidNumber; + errorText = + S.of(context)!.pleaseEnterValidNumber; }); } else if (inputAmount < order.fiatAmount.minimum || @@ -225,7 +239,12 @@ class TakeOrderScreen extends ConsumerWidget { inputAmount > order.fiatAmount.maximum!)) { setState(() { - errorText = S.of(context)!.amountMustBeBetween(order.fiatAmount.minimum.toString(), order.fiatAmount.maximum.toString()); + errorText = S + .of(context)! + .amountMustBeBetween( + order.fiatAmount.minimum.toString(), + order.fiatAmount.maximum + .toString()); }); } else { Navigator.of(context).pop(inputAmount); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index b6fe25a7..46e71da9 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -66,9 +66,11 @@ class TradeDetailScreen extends ConsumerWidget { MostroMessageDetail(orderId: orderId), ], const SizedBox(height: 24), - _buildCountDownTime(context, orderPayload.expiresAt != null - ? orderPayload.expiresAt! * 1000 - : null), + _buildCountDownTime( + context, + orderPayload.expiresAt != null + ? orderPayload.expiresAt! * 1000 + : null), const SizedBox(height: 36), Wrap( alignment: WrapAlignment.center, @@ -92,7 +94,8 @@ class TradeDetailScreen extends ConsumerWidget { } /// Builds a card showing the user is "selling/buying X sats for Y fiat" etc. - Widget _buildSellerAmount(BuildContext context, WidgetRef ref, OrderState tradeState) { + Widget _buildSellerAmount( + BuildContext context, WidgetRef ref, OrderState tradeState) { final session = ref.watch(sessionProvider(orderId)); final isPending = tradeState.status == Status.pending; @@ -101,7 +104,9 @@ class TradeDetailScreen extends ConsumerWidget { // For pending orders created by the user, show a notification message if (isPending && isCreator) { - final selling = session?.role == Role.seller ? S.of(context)!.selling : S.of(context)!.buying; + final selling = session?.role == Role.seller + ? S.of(context)!.selling + : S.of(context)!.buying; // Currency information is now handled by OrderAmountCard // If `orderPayload.amount` is 0, the trade is "at market price" @@ -116,6 +121,10 @@ class TradeDetailScreen extends ConsumerWidget { : session?.startTime ?? DateTime.now(), ); + final hasFixedSatsAmount = tradeState.order!.amount != 0; + final satAmount = + hasFixedSatsAmount ? ' ${tradeState.order!.amount}' : ''; + return Column( children: [ NotificationMessageCard( @@ -124,8 +133,13 @@ class TradeDetailScreen extends ConsumerWidget { ), const SizedBox(height: 16), OrderAmountCard( - title: selling == S.of(context)!.selling ? S.of(context)!.someoneIsSellingTitle : S.of(context)!.someoneIsBuyingTitle, - amount: tradeState.order!.fiatAmount.toString(), + title: + "${selling == S.of(context)!.selling ? 'You are Selling' : 'You are Buying'}$satAmount Sats", + amount: (tradeState.order!.minAmount != null && + tradeState.order!.maxAmount != null && + tradeState.order!.minAmount != tradeState.order!.maxAmount) + ? "${tradeState.order!.minAmount} - ${tradeState.order!.maxAmount}" + : tradeState.order!.fiatAmount.toString(), currency: tradeState.order!.fiatCode, priceText: priceText, ), @@ -142,12 +156,13 @@ class TradeDetailScreen extends ConsumerWidget { } // For non-pending orders or orders not created by the user, use the original display - final selling = session?.role == Role.seller ? S.of(context)!.selling : S.of(context)!.buying; + final selling = session?.role == Role.seller + ? S.of(context)!.selling + : S.of(context)!.buying; - // If `orderPayload.amount` is 0, the trade is "at market price" - final isZeroAmount = (tradeState.order!.amount == 0); - final satText = isZeroAmount ? '' : ' ${tradeState.order!.amount}'; - final priceText = isZeroAmount ? 'at market price' : ''; + final hasFixedSatsAmount = tradeState.order!.amount != 0; + final satAmount = hasFixedSatsAmount ? ' ${tradeState.order!.amount}' : ''; + final priceText = !hasFixedSatsAmount ? S.of(context)!.atMarketPrice : ''; final premium = tradeState.order!.premium; final premiumText = premium == 0 @@ -168,8 +183,14 @@ class TradeDetailScreen extends ConsumerWidget { return Column( children: [ OrderAmountCard( - title: selling == S.of(context)!.selling ? S.of(context)!.youAreSellingTitle(satText) : S.of(context)!.youAreBuyingTitle(satText), - amount: tradeState.order!.fiatAmount.toString(), + title: selling == S.of(context)!.selling + ? "You are Selling$satAmount Sats" + : "You are Buying$satAmount Sats", + amount: (tradeState.order!.minAmount != null && + tradeState.order!.maxAmount != null && + tradeState.order!.minAmount != tradeState.order!.maxAmount) + ? "${tradeState.order!.minAmount} - ${tradeState.order!.maxAmount}" + : tradeState.order!.fiatAmount.toString(), currency: tradeState.order!.fiatCode, priceText: priceText, premiumText: premiumText, @@ -214,7 +235,9 @@ class TradeDetailScreen extends ConsumerWidget { countdownRemaining: hoursLeft, ), const SizedBox(height: 16), - Text(S.of(context)!.timeLeftLabel(difference.toString().split('.').first)), + Text(S + .of(context)! + .timeLeftLabel(difference.toString().split('.').first)), ], ); } @@ -246,14 +269,12 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.status == Status.fiatSent) { if (tradeState.action == actions.Action.cooperativeCancelInitiatedByPeer) { - cancelMessage = - 'If you confirm, you will accept the cooperative cancellation initiated by your counterparty.'; + cancelMessage = S.of(context)!.acceptCancelMessage; } else { - cancelMessage = - 'If you confirm, you will start a cooperative cancellation with your counterparty.'; + cancelMessage = S.of(context)!.cooperativeCancelMessage; } } else { - cancelMessage = 'Are you sure you want to cancel this trade?'; + cancelMessage = S.of(context)!.areYouSureCancel; } widgets.add(_buildNostrButton( diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 0762669b..bc146d45 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'; -// package:mostro_mobile/core/app_theme.dart is not used +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'; @@ -38,7 +38,7 @@ class TradesListItem extends ConsumerWidget { 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 + color: AppTheme.dark1, borderRadius: BorderRadius.circular(12.0), ), child: Padding( @@ -55,9 +55,11 @@ class TradesListItem extends ConsumerWidget { Row( children: [ Text( - isBuying ? S.of(context)!.buyingBitcoin : S.of(context)!.sellingBitcoin, + isBuying + ? S.of(context)!.buyingBitcoin + : S.of(context)!.sellingBitcoin, style: const TextStyle( - color: Colors.white, + color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), @@ -82,9 +84,13 @@ class TradesListItem extends ConsumerWidget { ), const SizedBox(width: 4), Text( - '${trade.fiatAmount.minimum} ${trade.currency ?? ''}', + trade.fiatAmount.maximum != null && + trade.fiatAmount.maximum != + trade.fiatAmount.minimum + ? '${trade.fiatAmount.minimum} - ${trade.fiatAmount.maximum} ${trade.currency ?? ''}' + : '${trade.fiatAmount.minimum} ${trade.currency ?? ''}', style: const TextStyle( - color: Colors.white, + color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.bold, ), @@ -100,14 +106,14 @@ class TradesListItem extends ConsumerWidget { color: double.tryParse(trade.premium!) != null && double.parse(trade.premium!) > 0 - ? Colors.green.shade700 - : Colors.red.shade700, + ? AppTheme.premiumPositiveChip + : AppTheme.premiumNegativeChip, borderRadius: BorderRadius.circular(8), ), child: Text( '${double.tryParse(trade.premium!) != null && double.parse(trade.premium!) > 0 ? '+' : ''}${trade.premium}%', style: const TextStyle( - color: Colors.white, + color: AppTheme.textPrimary, fontSize: 11, fontWeight: FontWeight.w500, ), @@ -117,19 +123,19 @@ class TradesListItem extends ConsumerWidget { ], ), 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, + style: const TextStyle( + color: AppTheme.secondaryText, fontSize: 14, ), ) : Text( S.of(context)!.bankTransfer, - style: TextStyle( - color: Colors.grey.shade400, + style: const TextStyle( + color: AppTheme.secondaryText, fontSize: 14, ), ), @@ -139,7 +145,7 @@ class TradesListItem extends ConsumerWidget { // Right side - Arrow icon const Icon( Icons.chevron_right, - color: Colors.white, + color: AppTheme.textPrimary, size: 24, ), ], @@ -153,13 +159,13 @@ class TradesListItem extends ConsumerWidget { 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 + color: isCreator ? AppTheme.createdByYouChip : AppTheme.takenByYouChip, + borderRadius: BorderRadius.circular(12), ), child: Text( isCreator ? S.of(context)!.createdByYou : S.of(context)!.takenByYou, style: const TextStyle( - color: Colors.white, + color: AppTheme.textPrimary, fontSize: 12, fontWeight: FontWeight.w500, ), @@ -168,91 +174,103 @@ class TradesListItem extends ConsumerWidget { } Widget _buildStatusChip(BuildContext context, Status status) { - Color backgroundColor; - Color textColor; - String label; + Color backgroundColor; + Color textColor; + String label; - switch (status) { - case Status.active: - backgroundColor = const Color(0xFF1E3A8A).withValues(alpha: 0.3); // Azul oscuro con transparencia - textColor = const Color(0xFF93C5FD); // Azul claro - label = S.of(context)!.active; - break; - case Status.pending: - backgroundColor = const Color(0xFF854D0E).withValues(alpha: 0.3); // Ámbar oscuro con transparencia - textColor = const Color(0xFFFCD34D); // Ámbar claro - label = S.of(context)!.pending; - break; - // ✅ SOLUCION PROBLEMA 1: Agregar casos específicos para waitingPayment y waitingBuyerInvoice - case Status.waitingPayment: - backgroundColor = const Color(0xFF7C2D12).withValues(alpha: 0.3); // Naranja oscuro con transparencia - textColor = const Color(0xFFFED7AA); // Naranja claro - label = S.of(context)!.waitingPayment; // En lugar de "Pending" - break; - case Status.waitingBuyerInvoice: - backgroundColor = const Color(0xFF7C2D12).withValues(alpha: 0.3); // Naranja oscuro con transparencia - textColor = const Color(0xFFFED7AA); // Naranja claro - label = S.of(context)!.waitingInvoice; // En lugar de "Pending" - break; - case Status.fiatSent: - backgroundColor = const Color(0xFF065F46).withValues(alpha: 0.3); // Verde oscuro con transparencia - textColor = const Color(0xFF6EE7B7); // Verde claro - label = S.of(context)!.fiatSent; - break; - case Status.canceled: - case Status.canceledByAdmin: - case Status.cooperativelyCanceled: - backgroundColor = Colors.grey.shade800.withValues(alpha: 0.3); - textColor = Colors.grey.shade300; - label = S.of(context)!.cancel; - break; - case Status.settledByAdmin: - case Status.settledHoldInvoice: - backgroundColor = const Color(0xFF581C87).withValues(alpha: 0.3); // Morado oscuro con transparencia - textColor = const Color(0xFFC084FC); // Morado claro - label = S.of(context)!.settled; - break; - case Status.completedByAdmin: - backgroundColor = const Color(0xFF065F46).withValues(alpha: 0.3); // Verde oscuro con transparencia - textColor = const Color(0xFF6EE7B7); // Verde claro - label = S.of(context)!.completed; - break; - case Status.dispute: - backgroundColor = const Color(0xFF7F1D1D).withValues(alpha: 0.3); // Rojo oscuro con transparencia - textColor = const Color(0xFFFCA5A5); // Rojo claro - label = S.of(context)!.dispute; - break; - case Status.expired: - backgroundColor = Colors.grey.shade800.withValues(alpha: 0.3); - textColor = Colors.grey.shade300; - label = S.of(context)!.expired; - break; - case Status.success: - backgroundColor = const Color(0xFF065F46).withValues(alpha: 0.3); // Verde oscuro con transparencia - textColor = const Color(0xFF6EE7B7); // Verde claro - label = S.of(context)!.success; - break; - default: - backgroundColor = Colors.grey.shade800.withValues(alpha: 0.3); - textColor = Colors.grey.shade300; - label = status.toString(); // Fallback para mostrar el status real - break; - } + switch (status) { + case Status.active: + backgroundColor = + AppTheme.statusActiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusActiveText; + label = S.of(context)!.active; + break; + case Status.pending: + backgroundColor = + AppTheme.statusPendingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusPendingText; + label = S.of(context)!.pending; + break; - 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, + case Status.waitingPayment: + backgroundColor = + AppTheme.statusWaitingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusWaitingText; + label = S.of(context)!.waitingPayment; + break; + case Status.waitingBuyerInvoice: + backgroundColor = + AppTheme.statusWaitingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusWaitingText; + label = S.of(context)!.waitingInvoice; + break; + case Status.fiatSent: + backgroundColor = + AppTheme.statusSuccessBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSuccessText; + label = S.of(context)!.fiatSent; + break; + case Status.canceled: + case Status.canceledByAdmin: + case Status.cooperativelyCanceled: + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; + label = S.of(context)!.cancel; + break; + case Status.settledByAdmin: + case Status.settledHoldInvoice: + backgroundColor = + AppTheme.statusSettledBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSettledText; + label = S.of(context)!.settled; + break; + case Status.completedByAdmin: + backgroundColor = + AppTheme.statusSuccessBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSuccessText; + label = S.of(context)!.completed; + break; + case Status.dispute: + backgroundColor = + AppTheme.statusDisputeBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusDisputeText; + label = S.of(context)!.dispute; + break; + case Status.expired: + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; + label = S.of(context)!.expired; + break; + case Status.success: + backgroundColor = + AppTheme.statusSuccessBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSuccessText; + label = S.of(context)!.success; + break; + default: + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; + label = status.toString(); + break; + } + + 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, + ), + ), + ); + } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fe00996b..f59cfc12 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -130,6 +130,8 @@ "sellOrderDetailsTitle": "SELL ORDER DETAILS", "someoneIsSellingTitle": "Someone is Selling Sats", "someoneIsBuyingTitle": "Someone is Buying Sats", + "someoneIsSellingFixedTitle": "Someone is Selling {sats} Sats", + "someoneIsBuyingFixedTitle": "Someone is Buying {sats} Sats", "youCreatedOfferMessage": "You created this offer. Below are the details of your offer. Wait for another user to take it. It will be published for 24 hours. You can cancel it at any time using the 'Cancel' button.", "youAreSellingTitle": "You are selling{sats} sats", "youAreBuyingTitle": "You are buying{sats} sats", @@ -153,7 +155,7 @@ "createdOnLabel": "Created On", "orderIdLabel": "Order ID", "creatorReputationLabel": "Creator's Reputation", - "ratingLabel": "Rating", + "ratingTitleLabel": "Rating", "reviewsLabel": "Reviews", "daysLabel": "Days", "cancelTradeDialogTitle": "Cancel Trade", @@ -319,8 +321,6 @@ "@_comment_take_order_screen": "Take Order Screen Strings", "orderDetails": "ORDER DETAILS", - "selling": "selling", - "buying": "buying", "atMarketPrice": "at market price", "withPremiumPercent": "with a +{premium}% premium", "@withPremiumPercent": { diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index c47a0e44..cc1fdf50 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -399,14 +399,15 @@ "share": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", "openWallet": "ABRIR BILLETERA", - "done": "HECHO", "@_comment_trade_detail_screen_new": "Nuevas cadenas de Pantalla de Detalles de Intercambio", "orderDetailsTitle": "DETALLES DE ORDEN", "buyOrderDetailsTitle": "DETALLES DE ORDEN DE COMPRA", "sellOrderDetailsTitle": "DETALLES DE ORDEN DE VENTA", - "someoneIsSellingTitle": "Alguien está vendiendo Sats", - "someoneIsBuyingTitle": "Alguien está comprando Sats", + "someoneIsSellingTitle": "Alguien está Vendiendo Sats", + "someoneIsBuyingTitle": "Alguien está Comprando Sats", + "someoneIsSellingFixedTitle": "Alguien está Vendiendo {sats} Sats", + "someoneIsBuyingFixedTitle": "Alguien está Comprando {sats} Sats", "youCreatedOfferMessage": "Creaste esta oferta. A continuación se muestran los detalles de tu oferta. Espera a que otro usuario la tome. Se publicará durante 24 horas. Puedes cancelarla en cualquier momento usando el botón 'Cancelar'.", "youAreSellingTitle": "Estás vendiendo{sats} sats", "youAreBuyingTitle": "Estás comprando{sats} sats", @@ -430,7 +431,7 @@ "createdOnLabel": "Creado el", "orderIdLabel": "ID de Orden", "creatorReputationLabel": "Reputación del Creador", - "ratingLabel": "Calificación", + "ratingTitleLabel": "Calificación", "reviewsLabel": "Reseñas", "daysLabel": "Días", "cancelTradeDialogTitle": "Cancelar Intercambio", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index d26623ee..4991a641 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -399,14 +399,15 @@ "share": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", "openWallet": "APRI PORTAFOGLIO", - "done": "FATTO", "@_comment_trade_detail_screen_new": "Nuove Stringhe Schermata Dettagli Scambio", "orderDetailsTitle": "DETTAGLI ORDINE", "buyOrderDetailsTitle": "DETTAGLI ORDINE DI ACQUISTO", "sellOrderDetailsTitle": "DETTAGLI ORDINE DI VENDITA", - "someoneIsSellingTitle": "Qualcuno sta vendendo Sats", - "someoneIsBuyingTitle": "Qualcuno sta comprando Sats", + "someoneIsSellingTitle": "Qualcuno sta Vendendo Sats", + "someoneIsBuyingTitle": "Qualcuno sta Comprando Sats", + "someoneIsSellingFixedTitle": "Qualcuno sta Vendendo {sats} Sats", + "someoneIsBuyingFixedTitle": "Qualcuno sta Comprando {sats} Sats", "youCreatedOfferMessage": "Hai creato questa offerta. Di seguito sono riportati i dettagli della tua offerta. Aspetta che un altro utente la prenda. Sarà pubblicata per 24 ore. Puoi cancellarla in qualsiasi momento utilizzando il pulsante 'Annulla'.", "youAreSellingTitle": "Stai vendendo{sats} sats", "youAreBuyingTitle": "Stai comprando{sats} sats", @@ -430,7 +431,7 @@ "createdOnLabel": "Creato il", "orderIdLabel": "ID Ordine", "creatorReputationLabel": "Reputazione del Creatore", - "ratingLabel": "Valutazione", + "ratingTitleLabel": "Valutazione", "reviewsLabel": "Recensioni", "daysLabel": "Giorni", "cancelTradeDialogTitle": "Annulla Scambio", diff --git a/lib/shared/widgets/order_cards.dart b/lib/shared/widgets/order_cards.dart index 4d0a270c..5845844b 100644 --- a/lib/shared/widgets/order_cards.dart +++ b/lib/shared/widgets/order_cards.dart @@ -275,7 +275,7 @@ class CreatorReputationCard extends StatelessWidget { ), const SizedBox(width: 4), Text( - rating.toString(), + rating.toStringAsFixed(1), style: const TextStyle( color: Colors.white, fontSize: 16, @@ -286,7 +286,7 @@ class CreatorReputationCard extends StatelessWidget { ), const SizedBox(height: 4), Text( - S.of(context)!.ratingLabel.toString(), + S.of(context)!.ratingTitleLabel, style: TextStyle( color: Colors.white70, fontSize: 12, From afc5938a77ceed3c4319289240bdb1edffbf115f Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Tue, 8 Jul 2025 01:05:37 -0300 Subject: [PATCH 15/24] feat: improve countdown timer display with hours:minutes:seconds format --- .../order/screens/take_order_screen.dart | 15 ++++++++++--- .../trades/screens/trade_detail_screen.dart | 22 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 5d718df9..39386a3b 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -127,18 +127,27 @@ class TakeOrderScreen extends ConsumerWidget { Widget _buildCountDownTime(BuildContext context, DateTime expiration) { Duration countdown = Duration(hours: 0); final now = DateTime.now(); + if (expiration.isAfter(now)) { countdown = expiration.difference(now); } + final int maxOrderHours = 24; + final hoursLeft = countdown.inHours.clamp(0, maxOrderHours); + final minutesLeft = countdown.inMinutes % 60; + final secondsLeft = countdown.inSeconds % 60; + + final formattedTime = + '${hoursLeft.toString().padLeft(2, '0')}:${minutesLeft.toString().padLeft(2, '0')}:${secondsLeft.toString().padLeft(2, '0')}'; + return Column( children: [ CircularCountdown( - countdownTotal: 24, - countdownRemaining: countdown.inHours, + countdownTotal: maxOrderHours, + countdownRemaining: hoursLeft, ), const SizedBox(height: 16), - Text(S.of(context)!.timeLeftLabel(countdown.toString().split('.')[0])), + Text(S.of(context)!.timeLeftLabel(formattedTime)), ], ); } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 46e71da9..d2e00d88 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -217,27 +217,31 @@ class TradeDetailScreen extends ConsumerWidget { /// Build a circular countdown to show how many hours are left until expiration. Widget _buildCountDownTime(BuildContext context, int? expiresAtTimestamp) { // Convert timestamp to DateTime + final now = DateTime.now(); + final expiration = expiresAtTimestamp != null && expiresAtTimestamp > 0 ? DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp) - : DateTime.now().add(const Duration(hours: 24)); + : now.add(const Duration(hours: 24)); - // If expiration has passed, the difference is negative => zero. - final now = DateTime.now(); final Duration difference = expiration.isAfter(now) ? expiration.difference(now) : const Duration(); - // Display hours left - final hoursLeft = difference.inHours.clamp(0, 9999); + final int maxOrderHours = 24; + final hoursLeft = difference.inHours.clamp(0, maxOrderHours); + final minutesLeft = difference.inMinutes % 60; + final secondsLeft = difference.inSeconds % 60; + + final formattedTime = + '${hoursLeft.toString().padLeft(2, '0')}:${minutesLeft.toString().padLeft(2, '0')}:${secondsLeft.toString().padLeft(2, '0')}'; + return Column( children: [ CircularCountdown( - countdownTotal: 24, + countdownTotal: maxOrderHours, countdownRemaining: hoursLeft, ), const SizedBox(height: 16), - Text(S - .of(context)! - .timeLeftLabel(difference.toString().split('.').first)), + Text(S.of(context)!.timeLeftLabel(formattedTime)), ], ); } From 2ffced2d081258e1ca83eaabdaada282b80dafb5 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 9 Jul 2025 16:13:08 -0300 Subject: [PATCH 16/24] refactor: remove timezone offset from date formatting in trade and order screens --- lib/features/order/screens/take_order_screen.dart | 8 ++------ lib/features/trades/screens/trade_detail_screen.dart | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index 39386a3b..e0db29d2 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -313,11 +313,7 @@ class TakeOrderScreen extends ConsumerWidget { final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - - final offset = dt.timeZoneOffset; - final sign = offset.isNegative ? '-' : '+'; - final hours = offset.inHours.abs().toString().padLeft(2, '0'); - - return '$formattedDate at $formattedTime (GMT$sign$hours)'; + + return '$formattedDate at $formattedTime'; } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index d2e00d88..bce1b78b 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -548,17 +548,13 @@ class TradeDetailScreen extends ConsumerWidget { : tradeState.order!.kind == OrderType.sell; } - /// Format the date time to a user-friendly string with UTC offset + /// Format the date time to a user-friendly string without timezone String formatDateTime(DateTime dt) { final dateFormatter = DateFormat('EEE, MMM dd yyyy'); final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - final offset = dt.timeZoneOffset; - final sign = offset.isNegative ? '-' : '+'; - final hours = offset.inHours.abs().toString().padLeft(2, '0'); - - return '$formattedDate at $formattedTime (GMT$sign$hours)'; + return '$formattedDate at $formattedTime'; } } From 9ddfb5f18ad196aa45a60895a178d3a7a87dbb70 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 9 Jul 2025 16:31:27 -0300 Subject: [PATCH 17/24] fix: add missing comma in Spanish and Italian translation files --- lib/l10n/intl_es.arb | 2 +- lib/l10n/intl_it.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index bfa2321f..c91a2082 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -438,7 +438,7 @@ "cancelTradeDialogTitle": "Cancelar Intercambio", "cooperativeCancelDialogMessage": "Si confirmas, iniciarás una cancelación cooperativa con tu contraparte.", "acceptCancelDialogMessage": "Si confirmas, aceptarás la cancelación cooperativa iniciada por tu contraparte.", - "orderIdCopiedMessage": "ID de orden copiado al portapapeles" + "orderIdCopiedMessage": "ID de orden copiado al portapapeles", "language": "Idioma", "systemDefault": "Predeterminado del sistema", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 80a166d7..4dfc9224 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -438,7 +438,7 @@ "cancelTradeDialogTitle": "Annulla Scambio", "cooperativeCancelDialogMessage": "Se confermi, inizierai un annullamento cooperativo con la tua controparte.", "acceptCancelDialogMessage": "Se confermi, accetterai l'annullamento cooperativo iniziato dalla tua controparte.", - "orderIdCopiedMessage": "ID ordine copiato negli appunti" + "orderIdCopiedMessage": "ID ordine copiato negli appunti", "language": "Lingua", "systemDefault": "Predefinito di sistema", From 80991b9910da435c50f5818f66d77ee291a33633 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 9 Jul 2025 16:44:17 -0300 Subject: [PATCH 18/24] fix: add missing placeholder metadata and spaces in Italian and Spanish translations --- lib/l10n/intl_es.arb | 29 +++++++++++++++++++++++++++-- lib/l10n/intl_it.arb | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index c91a2082..53ca6161 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -408,11 +408,36 @@ "someoneIsSellingTitle": "Alguien está Vendiendo Sats", "someoneIsBuyingTitle": "Alguien está Comprando Sats", "someoneIsSellingFixedTitle": "Alguien está Vendiendo {sats} Sats", + "@someoneIsSellingFixedTitle": { + "placeholders": { + "sats": {} + } + }, "someoneIsBuyingFixedTitle": "Alguien está Comprando {sats} Sats", + "@someoneIsBuyingFixedTitle": { + "placeholders": { + "sats": {} + } + }, "youCreatedOfferMessage": "Creaste esta oferta. A continuación se muestran los detalles de tu oferta. Espera a que otro usuario la tome. Se publicará durante 24 horas. Puedes cancelarla en cualquier momento usando el botón 'Cancelar'.", - "youAreSellingTitle": "Estás vendiendo{sats} sats", - "youAreBuyingTitle": "Estás comprando{sats} sats", + "youAreSellingTitle": "Estás vendiendo {sats} sats", + "@youAreSellingTitle": { + "placeholders": { + "sats": {} + } + }, + "youAreBuyingTitle": "Estás comprando {sats} sats", + "@youAreBuyingTitle": { + "placeholders": { + "sats": {} + } + }, "forAmount": "por {amount}", + "@forAmount": { + "placeholders": { + "amount": {} + } + }, "timeLeftLabel": "Tiempo restante: {time}", "@timeLeftLabel": { "placeholders": { diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 4dfc9224..3e6057c7 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -408,11 +408,51 @@ "someoneIsSellingTitle": "Qualcuno sta Vendendo Sats", "someoneIsBuyingTitle": "Qualcuno sta Comprando Sats", "someoneIsSellingFixedTitle": "Qualcuno sta Vendendo {sats} Sats", + "@someoneIsSellingFixedTitle": { + "placeholders": { + "sats": { + "type": "String", + "description": "Fixed amount of satoshis being sold" + } + } + }, "someoneIsBuyingFixedTitle": "Qualcuno sta Comprando {sats} Sats", + "@someoneIsBuyingFixedTitle": { + "placeholders": { + "sats": { + "type": "String", + "description": "Fixed amount of satoshis being bought" + } + } + }, "youCreatedOfferMessage": "Hai creato questa offerta. Di seguito sono riportati i dettagli della tua offerta. Aspetta che un altro utente la prenda. Sarà pubblicata per 24 ore. Puoi cancellarla in qualsiasi momento utilizzando il pulsante 'Annulla'.", - "youAreSellingTitle": "Stai vendendo{sats} sats", - "youAreBuyingTitle": "Stai comprando{sats} sats", + "youAreSellingTitle": "Stai vendendo {sats} sats", + "@youAreSellingTitle": { + "placeholders": { + "sats": { + "type": "String", + "description": "Amount of satoshis being sold" + } + } + }, + "youAreBuyingTitle": "Stai comprando {sats} sats", + "@youAreBuyingTitle": { + "placeholders": { + "sats": { + "type": "String", + "description": "Amount of satoshis being bought" + } + } + }, "forAmount": "per {amount}", + "@forAmount": { + "placeholders": { + "amount": { + "type": "String", + "description": "Fiat amount for the transaction" + } + } + }, "timeLeftLabel": "Tempo rimasto: {time}", "@timeLeftLabel": { "placeholders": { From 85bdaee883e7382379c4b8874900430a72efca32 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Wed, 9 Jul 2025 17:26:40 -0300 Subject: [PATCH 19/24] refactor: remove redundant type declarations from Italian translation placeholders --- lib/l10n/intl_it.arb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 3e6057c7..b5e0c036 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -411,7 +411,6 @@ "@someoneIsSellingFixedTitle": { "placeholders": { "sats": { - "type": "String", "description": "Fixed amount of satoshis being sold" } } @@ -420,7 +419,6 @@ "@someoneIsBuyingFixedTitle": { "placeholders": { "sats": { - "type": "String", "description": "Fixed amount of satoshis being bought" } } @@ -430,7 +428,6 @@ "@youAreSellingTitle": { "placeholders": { "sats": { - "type": "String", "description": "Amount of satoshis being sold" } } @@ -439,7 +436,6 @@ "@youAreBuyingTitle": { "placeholders": { "sats": { - "type": "String", "description": "Amount of satoshis being bought" } } @@ -448,7 +444,6 @@ "@forAmount": { "placeholders": { "amount": { - "type": "String", "description": "Fiat amount for the transaction" } } From f1829c58c3cef07fe062841fc4d93394aaf937b8 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 10 Jul 2025 02:42:02 -0300 Subject: [PATCH 20/24] fix: update trade detail screen to use localized strings for buy/sell titles --- lib/features/trades/screens/trade_detail_screen.dart | 8 +++++--- test/mocks.mocks.dart | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index bce1b78b..96a22ebf 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -134,7 +134,9 @@ class TradeDetailScreen extends ConsumerWidget { const SizedBox(height: 16), OrderAmountCard( title: - "${selling == S.of(context)!.selling ? 'You are Selling' : 'You are Buying'}$satAmount Sats", + selling == S.of(context)!.selling + ? S.of(context)!.youAreSellingTitle(satAmount) + : S.of(context)!.youAreBuyingTitle(satAmount), amount: (tradeState.order!.minAmount != null && tradeState.order!.maxAmount != null && tradeState.order!.minAmount != tradeState.order!.maxAmount) @@ -184,8 +186,8 @@ class TradeDetailScreen extends ConsumerWidget { children: [ OrderAmountCard( title: selling == S.of(context)!.selling - ? "You are Selling$satAmount Sats" - : "You are Buying$satAmount Sats", + ? S.of(context)!.youAreSellingTitle(satAmount) + : S.of(context)!.youAreBuyingTitle(satAmount), amount: (tradeState.order!.minAmount != null && tradeState.order!.maxAmount != null && tradeState.order!.minAmount != tradeState.order!.maxAmount) diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index 760793a9..e2757ae2 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -401,6 +401,7 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock implements _i11.SharedPreferencesAsync { MockSharedPreferencesAsync() { From 272cd574a9257f816d9fbd7c812aa0566ba17e62 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 10 Jul 2025 02:57:37 -0300 Subject: [PATCH 21/24] feat: add auto-sizing text to custom buttons with configurable width and font size --- lib/shared/widgets/custom_button.dart | 43 ++++++++++++------- .../widgets/custom_elevated_button.dart | 20 +++++++-- pubspec.lock | 8 ++++ pubspec.yaml | 1 + 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/lib/shared/widgets/custom_button.dart b/lib/shared/widgets/custom_button.dart index 1c1c82c0..cc75fbab 100644 --- a/lib/shared/widgets/custom_button.dart +++ b/lib/shared/widgets/custom_button.dart @@ -1,37 +1,48 @@ import 'package:flutter/material.dart'; +import 'package:auto_size_text/auto_size_text.dart'; import '../../core/app_theme.dart'; class CustomButton extends StatelessWidget { final String text; final VoidCallback onPressed; final bool isEnabled; + final double minFontSize; + final double width; const CustomButton({ super.key, required this.text, required this.onPressed, this.isEnabled = true, + this.minFontSize = 12.0, + this.width = 200.0, }); @override Widget build(BuildContext context) { - return ElevatedButton( - onPressed: isEnabled ? onPressed : null, - style: ElevatedButton.styleFrom( - backgroundColor: isEnabled - ? AppTheme.mostroGreen - : AppTheme.mostroGreen.withValues(alpha: .5), - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + return SizedBox( + width: width, + child: ElevatedButton( + onPressed: isEnabled ? onPressed : null, + style: ElevatedButton.styleFrom( + backgroundColor: isEnabled + ? AppTheme.mostroGreen + : AppTheme.mostroGreen.withValues(alpha: .5), + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), - ), - child: Text( - text, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: AutoSizeText( + text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + minFontSize: minFontSize, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ); diff --git a/lib/shared/widgets/custom_elevated_button.dart b/lib/shared/widgets/custom_elevated_button.dart index 4af76766..5b5eefaf 100644 --- a/lib/shared/widgets/custom_elevated_button.dart +++ b/lib/shared/widgets/custom_elevated_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:mostro_mobile/core/app_theme.dart'; class CustomElevatedButton extends StatelessWidget { @@ -7,6 +8,9 @@ class CustomElevatedButton extends StatelessWidget { final EdgeInsetsGeometry? padding; final Color? backgroundColor; final Color? foregroundColor; + final double minFontSize; + final double? width; + final TextStyle? textStyle; const CustomElevatedButton({ super.key, @@ -15,20 +19,30 @@ class CustomElevatedButton extends StatelessWidget { this.padding, this.backgroundColor, this.foregroundColor, + this.minFontSize = 12.0, + this.width, + this.textStyle, }); @override Widget build(BuildContext context) { - return ElevatedButton( + final buttonWidget = ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( foregroundColor: foregroundColor ?? AppTheme.cream1, backgroundColor: backgroundColor ?? AppTheme.mostroGreen, - textStyle: Theme.of(context).textTheme.labelLarge, padding: padding ?? const EdgeInsets.symmetric(vertical: 15, horizontal: 30), ), - child: Text(text), + child: AutoSizeText( + text, + style: textStyle ?? Theme.of(context).textTheme.labelLarge, + minFontSize: minFontSize, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ); + + return width != null ? SizedBox(width: width, child: buttonWidget) : buttonWidget; } } diff --git a/pubspec.lock b/pubspec.lock index 26ce8fb4..2106fc66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" base58check: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 791838fa..e370e2a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: flutter_background_service: ^5.1.0 path_provider: ^2.1.5 permission_handler: ^12.0.0+1 + auto_size_text: ^3.0.0 dev_dependencies: flutter_test: From aca0f033e98391462c34c4cfff8e180a1bfa2c6c Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 10 Jul 2025 20:15:57 -0300 Subject: [PATCH 22/24] feat: add internationalization support for date formatting in order and trade screens --- .../order/screens/take_order_screen.dart | 34 ++++++++++++++----- .../trades/screens/trade_detail_screen.dart | 30 +++++++++++----- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lib/features/order/screens/take_order_screen.dart b/lib/features/order/screens/take_order_screen.dart index e0db29d2..d1af337a 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -163,8 +163,12 @@ class TakeOrderScreen extends ConsumerWidget { } Widget _buildCreatedOn(NostrEvent order) { - return CreatedDateCard( - createdDate: formatDateTime(order.createdAt!), + return Builder( + builder: (context) { + return CreatedDateCard( + createdDate: formatDateTime(order.createdAt!, context), + ); + }, ); } @@ -308,12 +312,24 @@ class TakeOrderScreen extends ConsumerWidget { ); } - String formatDateTime(DateTime dt) { - final dateFormatter = DateFormat('EEE, MMM dd yyyy'); - final timeFormatter = DateFormat('HH:mm'); - final formattedDate = dateFormatter.format(dt); - final formattedTime = timeFormatter.format(dt); - - return '$formattedDate at $formattedTime'; + String formatDateTime(DateTime dt, [BuildContext? context]) { + if (context != null) { + // Use internationalized date format + final dateFormatter = DateFormat.yMMMd(Localizations.localeOf(context).languageCode); + final timeFormatter = DateFormat.Hm(Localizations.localeOf(context).languageCode); + final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + // Use the internationalized string for "Created on: date" + return S.of(context)!.createdOnDate('$formattedDate $formattedTime'); + } else { + // Fallback if context is not available + final dateFormatter = DateFormat('EEE, MMM dd yyyy'); + final timeFormatter = DateFormat('HH:mm'); + final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + return '$formattedDate at $formattedTime'; + } } } diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 96a22ebf..f585882e 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -119,6 +119,7 @@ class TradeDetailScreen extends ConsumerWidget { ? DateTime.fromMillisecondsSinceEpoch( tradeState.order!.createdAt! * 1000) : session?.startTime ?? DateTime.now(), + context, ); final hasFixedSatsAmount = tradeState.order!.amount != 0; @@ -180,6 +181,7 @@ class TradeDetailScreen extends ConsumerWidget { ? DateTime.fromMillisecondsSinceEpoch( tradeState.order!.createdAt! * 1000) : session?.startTime ?? DateTime.now(), + context, ); return Column( @@ -550,13 +552,25 @@ class TradeDetailScreen extends ConsumerWidget { : tradeState.order!.kind == OrderType.sell; } - /// Format the date time to a user-friendly string without timezone - String formatDateTime(DateTime dt) { - final dateFormatter = DateFormat('EEE, MMM dd yyyy'); - final timeFormatter = DateFormat('HH:mm'); - final formattedDate = dateFormatter.format(dt); - final formattedTime = timeFormatter.format(dt); - - return '$formattedDate at $formattedTime'; + /// Format the date time to a user-friendly string with internationalization + String formatDateTime(DateTime dt, [BuildContext? context]) { + if (context != null) { + // Use internationalized date format + final dateFormatter = DateFormat.yMMMd(Localizations.localeOf(context).languageCode); + final timeFormatter = DateFormat.Hm(Localizations.localeOf(context).languageCode); + final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + // Use the internationalized string for "Created on: date" + return S.of(context)!.createdOnDate('$formattedDate $formattedTime'); + } else { + // Fallback if context is not available + final dateFormatter = DateFormat('EEE, MMM dd yyyy'); + final timeFormatter = DateFormat('HH:mm'); + final formattedDate = dateFormatter.format(dt); + final formattedTime = timeFormatter.format(dt); + + return '$formattedDate at $formattedTime'; + } } } From c3998ce4aa735dbb9a38a11ae1affbb1789ea29e Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 10 Jul 2025 20:50:52 -0300 Subject: [PATCH 23/24] refactor: clean up duplicate code and improve formatting in trade detail screen --- .../trades/screens/trade_detail_screen.dart | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index b8576868..0fde481a 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -134,10 +134,9 @@ class TradeDetailScreen extends ConsumerWidget { ), const SizedBox(height: 16), OrderAmountCard( - title: - selling == S.of(context)!.selling - ? S.of(context)!.youAreSellingTitle(satAmount) - : S.of(context)!.youAreBuyingTitle(satAmount), + title: selling == S.of(context)!.selling + ? S.of(context)!.youAreSellingTitle(satAmount) + : S.of(context)!.youAreBuyingTitle(satAmount), amount: (tradeState.order!.minAmount != null && tradeState.order!.maxAmount != null && tradeState.order!.minAmount != tradeState.order!.maxAmount) @@ -167,20 +166,10 @@ class TradeDetailScreen extends ConsumerWidget { final satAmount = hasFixedSatsAmount ? ' ${tradeState.order!.amount}' : ''; final priceText = !hasFixedSatsAmount ? S.of(context)!.atMarketPrice : ''; - - final amountString = - '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; - - // If `orderPayload.amount` is 0, the trade is "at market price" - final isZeroAmount = (tradeState.order!.amount == 0); - final satText = isZeroAmount ? '' : ' ${tradeState.order!.amount}'; - final priceText = isZeroAmount ? ' ${S.of(context)!.atMarketPrice}' : ''; - final premium = tradeState.order!.premium; final premiumText = premium == 0 ? '' : (premium > 0) - ? S.of(context)!.withPremiumPercent(premium.toString()) : S.of(context)!.withDiscountPercent(premium.abs().toString()); @@ -194,13 +183,12 @@ class TradeDetailScreen extends ConsumerWidget { context, ); - return Column( children: [ OrderAmountCard( title: selling == S.of(context)!.selling - ? S.of(context)!.youAreSellingTitle(satAmount) - : S.of(context)!.youAreBuyingTitle(satAmount), + ? S.of(context)!.youAreSellingTitle(satAmount) + : S.of(context)!.youAreBuyingTitle(satAmount), amount: (tradeState.order!.minAmount != null && tradeState.order!.maxAmount != null && tradeState.order!.minAmount != tradeState.order!.maxAmount) @@ -219,7 +207,6 @@ class TradeDetailScreen extends ConsumerWidget { createdDate: timestamp, ), ], - ); } @@ -568,11 +555,13 @@ class TradeDetailScreen extends ConsumerWidget { String formatDateTime(DateTime dt, [BuildContext? context]) { if (context != null) { // Use internationalized date format - final dateFormatter = DateFormat.yMMMd(Localizations.localeOf(context).languageCode); - final timeFormatter = DateFormat.Hm(Localizations.localeOf(context).languageCode); + final dateFormatter = + DateFormat.yMMMd(Localizations.localeOf(context).languageCode); + final timeFormatter = + DateFormat.Hm(Localizations.localeOf(context).languageCode); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - + // Use the internationalized string for "Created on: date" return S.of(context)!.createdOnDate('$formattedDate $formattedTime'); } else { @@ -581,7 +570,7 @@ class TradeDetailScreen extends ConsumerWidget { final timeFormatter = DateFormat('HH:mm'); final formattedDate = dateFormatter.format(dt); final formattedTime = timeFormatter.format(dt); - + return '$formattedDate at $formattedTime'; } } From 72ec413a5269cf229ad4f2cc9578c684c5a79f11 Mon Sep 17 00:00:00 2001 From: Andrea Diaz Correia Date: Thu, 10 Jul 2025 21:00:08 -0300 Subject: [PATCH 24/24] chore: remove unnecessary ignore comment for MockSharedPreferencesAsync class --- test/mocks.mocks.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index e2757ae2..760793a9 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -401,7 +401,6 @@ class MockOpenOrdersRepository extends _i1.Mock /// A class which mocks [SharedPreferencesAsync]. /// /// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable class MockSharedPreferencesAsync extends _i1.Mock implements _i11.SharedPreferencesAsync { MockSharedPreferencesAsync() {