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 c58bc03b..60aad67c 100644 --- a/lib/features/order/screens/take_order_screen.dart +++ b/lib/features/order/screens/take_order_screen.dart @@ -1,15 +1,15 @@ 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: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'; @@ -29,20 +29,28 @@ class TakeOrderScreen extends ConsumerWidget { final order = ref.watch(eventProvider(orderId)); return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: S.of(context)!.orderDetails), + backgroundColor: AppTheme.backgroundDark, + appBar: OrderAppBar( + title: orderType == OrderType.buy + ? 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(context, 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, context), + _buildCountDownTime(context, order.expirationDate), const SizedBox(height: 36), - // Pass the full order to the action buttons widget. _buildActionButtons(context, ref, order), ], ), @@ -50,238 +58,282 @@ class TakeOrderScreen extends ConsumerWidget { ); } - Widget _buildSellerAmount( - WidgetRef ref, NostrEvent order, BuildContext context) { - final selling = orderType == OrderType.sell - ? S.of(context)!.selling - : S.of(context)!.buying; - final amountString = - '${order.fiatAmount} ${order.currency} ${CurrencyUtils.getFlagFromCurrency(order.currency!)}'; - final satAmount = order.amount == '0' ? '' : ' ${order.amount}'; - final price = order.amount != '0' ? '' : S.of(context)!.atMarketPrice; - final premium = int.parse(order.premium ?? '0'); - final premiumText = premium >= 0 - ? premium == 0 - ? '' - : S.of(context)!.withPremiumPercent(premium.toString()) - : S.of(context)!.withDiscountPercent(premium.abs().toString()); - final methods = order.paymentMethods.isNotEmpty - ? order.paymentMethods.join(', ') - : S.of(context)!.noPaymentMethod; - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: Column( - spacing: 2, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context)!.someoneIsSellingBuying( - selling, satAmount, amountString, price, premiumText), - style: AppTheme.theme.textTheme.bodyLarge, - softWrap: true, - ), - const SizedBox(height: 16), - Text( - S - .of(context)! - .createdOnDate(formatDateTime(order.createdAt!)), - style: textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Text( - S.of(context)!.paymentMethodsAre(methods), - style: textTheme.bodyLarge, + Widget _buildSellerAmount(WidgetRef ref, NostrEvent order) { + return Builder( + builder: (context) { + final currencyFlag = CurrencyUtils.getFlagFromCurrency(order.currency!); + 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( + 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.white, + fontSize: 18, + fontWeight: FontWeight.w600, + ), - ], - ), + ), + 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, + ), + ), + ], + ], + ), + ], ), - ], - ), + ); + }, ); } 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( - SnackBar( - content: Text(S.of(context)!.orderIdCopied), - 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, ); } - 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)) { 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)!.timeLeft(countdown.toString().split('.')[0])), + Text(S.of(context)!.timeLeftLabel(formattedTime)), ], ); } + Widget _buildPaymentMethod(BuildContext context, NostrEvent order) { + final methods = order.paymentMethods.isNotEmpty + ? order.paymentMethods.join(', ') + : S.of(context)!.noPaymentMethod; + + return PaymentMethodCard( + paymentMethod: methods, + ); + } + + Widget _buildCreatedOn(NostrEvent order) { + return Builder( + builder: (context) { + return CreatedDateCard( + createdDate: formatDateTime(order.createdAt!, context), + ); + }, + ); + } + + Widget _buildCreatorReputation(NostrEvent order) { + final ratingInfo = order.rating; + + 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) { - final orderDetailsNotifier = ref.watch( - orderNotifierProvider(order.orderId!).notifier, - ); + final orderDetailsNotifier = + ref.read(orderNotifierProvider(orderId).notifier); + + final buttonText = orderType == OrderType.buy + ? S.of(context)!.sellBitcoinButton + : S.of(context)!.buyBitcoinButton; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - OutlinedButton( - onPressed: () { - context.pop(); - }, - style: AppTheme.theme.outlinedButtonTheme.style, - child: Text(S.of(context)!.close), + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + style: AppTheme.theme.outlinedButtonTheme.style, + child: Text(S.of(context)!.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: Text(S.of(context)!.enterAmount), - content: TextField( - controller: _fiatAmountController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: S.of(context)!.enterAmountBetween( - order.fiatAmount.minimum.toString(), - order.fiatAmount.maximum.toString()), - errorText: errorText, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: Text(S.of(context)!.cancel), - ), - ElevatedButton( - key: const Key('submitAmountButton'), - onPressed: () { - final inputAmount = int.tryParse( - _fiatAmountController.text.trim()); - if (inputAmount == null) { - setState(() { - errorText = - S.of(context)!.pleaseEnterValidNumber; - }); - } else if (inputAmount < - order.fiatAmount.minimum || - inputAmount > order.fiatAmount.maximum!) { - setState(() { - errorText = S - .of(context)! - .amountMustBeBetween( - order.fiatAmount.minimum.toString(), - order.fiatAmount.maximum.toString()); - }); - } else { - Navigator.of(context).pop(inputAmount); - } - }, - child: Text(S.of(context)!.submit), + + Expanded( + child: ElevatedButton( + onPressed: () async { + // Check if this is a range order + if (order.fiatAmount.maximum != null && + 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: Text(S.of(context)!.enterAmount), + content: TextField( + controller: _fiatAmountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: S.of(context)!.enterAmountBetween( + order.fiatAmount.minimum.toString(), + order.fiatAmount.maximum.toString()), + errorText: errorText, + ), ), - ], - ); - }, - ); - }, - ); + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: Text(S.of(context)!.cancel), + ), + ElevatedButton( + key: const Key('submitAmountButton'), + onPressed: () { + final inputAmount = int.tryParse( + _fiatAmountController.text.trim()); + if (inputAmount == null) { + setState(() { + errorText = + S.of(context)!.pleaseEnterValidNumber; + }); + } else if (inputAmount < + order.fiatAmount.minimum || + (order.fiatAmount.maximum != null && + inputAmount > + order.fiatAmount.maximum!)) { + setState(() { + errorText = S + .of(context)! + .amountMustBeBetween( + order.fiatAmount.minimum.toString(), + order.fiatAmount.maximum + .toString()); + }); + } else { + Navigator.of(context).pop(inputAmount); + } + }, + child: Text(S.of(context)!.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: Text(S.of(context)!.take), ), ], ); } - 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)'; + 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/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 d337dd37..0fde481a 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -1,21 +1,21 @@ 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'; 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'; 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'; -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'; import 'package:mostro_mobile/generated/l10n.dart'; class TradeDetailScreen extends ConsumerWidget { @@ -31,14 +31,19 @@ class TradeDetailScreen extends ConsumerWidget { final orderPayload = tradeState.order; if (orderPayload == null) { return const Scaffold( - backgroundColor: AppTheme.dark1, + backgroundColor: AppTheme.backgroundDark, body: Center(child: CircularProgressIndicator()), ); } + // 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 = _isUserCreator(session, tradeState); + return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: S.of(context)!.orderDetails), + backgroundColor: AppTheme.backgroundDark, + appBar: OrderAppBar(title: S.of(context)!.orderDetailsTitle), body: Builder( builder: (context) { return SingleChildScrollView( @@ -51,8 +56,15 @@ 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(context, tradeState), + 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( context, @@ -85,27 +97,81 @@ class TradeDetailScreen extends ConsumerWidget { Widget _buildSellerAmount( BuildContext context, 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 = _isUserCreator(session, tradeState); + + // 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; + // 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 ? S.of(context)!.atMarketPrice : ''; + + final paymentMethod = tradeState.order!.paymentMethod; + final createdOn = formatDateTime( + tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 + ? DateTime.fromMillisecondsSinceEpoch( + tradeState.order!.createdAt! * 1000) + : session?.startTime ?? DateTime.now(), + context, + ); - final currencyFlag = CurrencyUtils.getFlagFromCurrency( - tradeState.order!.fiatCode, - ); + final hasFixedSatsAmount = tradeState.order!.amount != 0; + final satAmount = + hasFixedSatsAmount ? ' ${tradeState.order!.amount}' : ''; - final amountString = - '${tradeState.order!.fiatAmount} ${tradeState.order!.fiatCode} $currencyFlag'; + return Column( + children: [ + NotificationMessageCard( + message: S.of(context)!.youCreatedOfferMessage, + icon: Icons.info_outline, + ), + const SizedBox(height: 16), + OrderAmountCard( + 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) + ? "${tradeState.order!.minAmount} - ${tradeState.order!.maxAmount}" + : tradeState.order!.fiatAmount.toString(), + currency: tradeState.order!.fiatCode, + priceText: priceText, + ), + const SizedBox(height: 16), + PaymentMethodCard( + paymentMethod: paymentMethod, + ), + const SizedBox(height: 16), + CreatedDateCard( + createdDate: createdOn, + ), + ], + ); + } - // 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}' : ''; + // 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 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 ? '' : (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; @@ -113,101 +179,72 @@ class TradeDetailScreen extends ConsumerWidget { tradeState.order!.createdAt != null && tradeState.order!.createdAt! > 0 ? DateTime.fromMillisecondsSinceEpoch( tradeState.order!.createdAt! * 1000) - : session.startTime, + : session?.startTime ?? DateTime.now(), + context, ); - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: Column( - // Using Column with spacing = 2 isn't standard; using SizedBoxes for spacing is fine. - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isSellingRole - ? S.of(context)!.youAreSellingText( - amountString, premiumText, priceText, satText) - : S.of(context)!.youAreBuyingText( - amountString, premiumText, priceText, satText), - style: AppTheme.theme.textTheme.bodyLarge, - softWrap: true, - ), - const SizedBox(height: 16), - Text( - '${S.of(context)!.createdOn}: $timestamp', - style: textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Text( - '${S.of(context)!.paymentMethodsLabel}: $method', - style: textTheme.bodyLarge, - ), - ], - ), - ), - ], - ), + + return Column( + children: [ + OrderAmountCard( + 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) + ? "${tradeState.order!.minAmount} - ${tradeState.order!.maxAmount}" + : 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, + ), + ], ); } /// 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( - SnackBar( - content: Text(S.of(context)!.orderIdCopied), - 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, ); } /// 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)!.timeLeft(difference.toString().split('.').first)), + Text(S.of(context)!.timeLeftLabel(formattedTime)), ], ); } @@ -248,14 +285,14 @@ class TradeDetailScreen extends ConsumerWidget { } 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( @@ -307,7 +344,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 @@ -325,7 +362,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 @@ -338,7 +375,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 @@ -370,11 +407,9 @@ 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( - S.of(context)!.cancelPending, + S.of(context)!.cancelPendingButton, action: actions.Action.cooperativeCancelInitiatedByYou, backgroundColor: Colors.grey, onPressed: null, @@ -383,7 +418,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: () => @@ -396,7 +431,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 @@ -413,7 +448,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'), @@ -489,15 +524,54 @@ 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'); - 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)'; + /// Build a card showing the creator's reputation with rating, reviews and days + 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 + const rating = 3.1; + const reviews = 15; + const days = 7; + + return CreatorReputationCard( + rating: rating, + reviews: reviews, + days: days, + ); + } + + /// 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 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'; + } } } diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 503f5b23..387d2b7d 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,8 +38,9 @@ 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( @@ -60,7 +61,7 @@ class TradesListItem extends ConsumerWidget { ? S.of(context)!.buyingBitcoin : S.of(context)!.sellingBitcoin, style: const TextStyle( - color: Colors.white, + color: AppTheme.textPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), @@ -85,9 +86,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, ), @@ -103,14 +108,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, ), @@ -120,19 +125,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, ), ), @@ -142,7 +147,7 @@ class TradesListItem extends ConsumerWidget { // Right side - Arrow icon const Icon( Icons.chevron_right, - color: Colors.white, + color: AppTheme.textPrimary, size: 24, ), ], @@ -156,16 +161,15 @@ 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, ), @@ -180,77 +184,81 @@ class TradesListItem extends ConsumerWidget { switch (status) { case Status.active: - backgroundColor = const Color(0xFF1E3A8A) - .withValues(alpha: 0.3); // Azul oscuro con transparencia - textColor = const Color(0xFF93C5FD); // Azul claro + + backgroundColor = + AppTheme.statusActiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusActiveText; 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 + backgroundColor = + AppTheme.statusPendingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusPendingText; 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" + backgroundColor = + AppTheme.statusWaitingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusWaitingText; + label = S.of(context)!.waitingPayment; 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" + backgroundColor = + AppTheme.statusWaitingBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusWaitingText; + label = S.of(context)!.waitingInvoice; break; case Status.fiatSent: - backgroundColor = const Color(0xFF065F46) - .withValues(alpha: 0.3); // Verde oscuro con transparencia - textColor = const Color(0xFF6EE7B7); // Verde claro + 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 = Colors.grey.shade800.withValues(alpha: 0.3); - textColor = Colors.grey.shade300; + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; 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 + backgroundColor = + AppTheme.statusSettledBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSettledText; 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 + backgroundColor = + AppTheme.statusSuccessBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSuccessText; 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 + backgroundColor = + AppTheme.statusDisputeBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusDisputeText; label = S.of(context)!.dispute; break; case Status.expired: - backgroundColor = Colors.grey.shade800.withValues(alpha: 0.3); - textColor = Colors.grey.shade300; + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; 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 + backgroundColor = + AppTheme.statusSuccessBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusSuccessText; 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 + backgroundColor = + AppTheme.statusInactiveBackground.withValues(alpha: 0.3); + textColor = AppTheme.statusInactiveText; + label = status.toString(); break; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 49f74416..749e0ef8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -129,6 +129,45 @@ "createYourOwnOffer": "You can also create your own offer and wait for someone to take it.\n\nSet the amount and preferred payment method — Mostro handles the rest.", "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", + "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", + "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", + "ratingTitleLabel": "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", @@ -288,8 +327,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 a78963f6..0edfbf22 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -405,16 +405,86 @@ "share": "Compartir", "failedToShareInvoice": "Error al compartir factura. Por favor intenta copiarla en su lugar.", "openWallet": "ABRIR BILLETERA", + + + "@_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", + "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", + "@youAreSellingTitle": { + "placeholders": { + "sats": {} + } + }, + "youAreBuyingTitle": "Estás comprando {sats} sats", + "@youAreBuyingTitle": { + "placeholders": { + "sats": {} + } + }, + "forAmount": "por {amount}", + "@forAmount": { + "placeholders": { + "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", + "ratingTitleLabel": "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", + "language": "Idioma", "systemDefault": "Predeterminado del sistema", "english": "Inglés", "spanish": "Español", "italian": "Italiano", "chooseLanguageDescription": "Elige tu idioma preferido o usa el predeterminado del sistema", + + + "loadingOrder": "Cargando orden...", "done": "HECHO", "unsupportedLinkFormat": "Formato de enlace no compatible", "failedToOpenLink": "Error al abrir enlace", "failedToLoadOrder": "Error al cargar orden", "failedToOpenOrder": "Error al abrir orden" + } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 151603e9..317ee750 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -405,16 +405,94 @@ "share": "Condividi", "failedToShareInvoice": "Errore nel condividere la fattura. Per favore prova a copiarla invece.", "openWallet": "APRI PORTAFOGLIO", + + + "@_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", + "someoneIsSellingFixedTitle": "Qualcuno sta Vendendo {sats} Sats", + "@someoneIsSellingFixedTitle": { + "placeholders": { + "sats": { + "description": "Fixed amount of satoshis being sold" + } + } + }, + "someoneIsBuyingFixedTitle": "Qualcuno sta Comprando {sats} Sats", + "@someoneIsBuyingFixedTitle": { + "placeholders": { + "sats": { + "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", + "@youAreSellingTitle": { + "placeholders": { + "sats": { + "description": "Amount of satoshis being sold" + } + } + }, + "youAreBuyingTitle": "Stai comprando {sats} sats", + "@youAreBuyingTitle": { + "placeholders": { + "sats": { + "description": "Amount of satoshis being bought" + } + } + }, + "forAmount": "per {amount}", + "@forAmount": { + "placeholders": { + "amount": { + "description": "Fiat amount for the transaction" + } + } + }, + "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", + "ratingTitleLabel": "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", + "language": "Lingua", "systemDefault": "Predefinito di sistema", "english": "Inglese", "spanish": "Spagnolo", "italian": "Italiano", "chooseLanguageDescription": "Scegli la tua lingua preferita o usa il predefinito di sistema", + "loadingOrder": "Caricamento ordine...", "done": "FATTO", "unsupportedLinkFormat": "Formato di link non supportato", "failedToOpenLink": "Impossibile aprire il link", "failedToLoadOrder": "Impossibile caricare l'ordine", "failedToOpenOrder": "Impossibile aprire l'ordine" + } 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_card.dart b/lib/shared/widgets/custom_card.dart index d8288f98..d1db9928 100644 --- a/lib/shared/widgets/custom_card.dart +++ b/lib/shared/widgets/custom_card.dart @@ -20,11 +20,11 @@ class CustomCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - color: color ?? AppTheme.dark2, + color: color ?? AppTheme.dark1, margin: margin, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), - side: borderSide ?? BorderSide(color: AppTheme.dark2), + side: borderSide ?? BorderSide(color: AppTheme.dark1), ), child: Padding( padding: padding, 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/lib/shared/widgets/order_cards.dart b/lib/shared/widgets/order_cards.dart new file mode 100644 index 00000000..5845844b --- /dev/null +++ b/lib/shared/widgets/order_cards.dart @@ -0,0 +1,414 @@ +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'; +import 'package:mostro_mobile/generated/l10n.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({ + super.key, + required this.title, + required this.amount, + required this.currency, + this.priceText, + this.premiumText, + }); + + @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( + S.of(context)!.forAmount(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({super.key, required this.paymentMethod}); + + @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: [ + Text( + S.of(context)!.paymentMethodLabel, + 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({super.key, required this.createdDate}); + + @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: [ + Text( + S.of(context)!.createdOnLabel, + 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({ + super.key, + required this.orderId, + }); + + @override + Widget build(BuildContext context) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.orderIdLabel, + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + 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( + SnackBar( + content: Text(S.of(context)!.orderIdCopiedMessage), + 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({ + super.key, + required this.rating, + required this.reviews, + required this.days, + }); + + @override + Widget build(BuildContext context) { + return CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.creatorReputationLabel, + 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.toStringAsFixed(1), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + S.of(context)!.ratingTitleLabel, + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + // Reviews section + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.person_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), + Text( + S.of(context)!.reviewsLabel, + 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), + Text( + S.of(context)!.daysLabel, + 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({ + super.key, + required this.message, + this.icon = Icons.info_outline, + this.iconColor = Colors.white70, + }); + + @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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 62a5a7de..a16613e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,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 240fb64c..97a1a061 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,8 +77,12 @@ dependencies: flutter_background_service: ^5.1.0 path_provider: ^2.1.5 permission_handler: ^12.0.0+1 + + auto_size_text: ^3.0.0 + app_links: ^6.4.0 + dev_dependencies: flutter_test: sdk: flutter