From 29e9564e30b37e9f7e66eab170ce8a886968329b Mon Sep 17 00:00:00 2001 From: grunch Date: Fri, 3 Apr 2026 16:08:14 -0300 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20implement=20V1=20flow=20gaps=20?= =?UTF-8?q?=E2=80=94=20dispute,=20invoice,=20countdown,=20UI=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wire DISPUTE button to disputes_api.openDispute() in all 3 trade status contexts (active buyer, active seller, fiat-sent seller); upsert result into DisputeNotifier and navigate to dispute chat - Add waitingInvoice/waitingPayment TradeStatus values; map waitingBuyerInvoice → ADD INVOICE (buyer) and waitingPayment → PAY INVOICE (seller) action buttons in TradeDetailScreen - Replace hardcoded hold invoice mock with real TradeInfo.holdInvoice and TradeInfo.order.amountSats in PayLightningInvoiceScreen; show loading state while invoice is not yet available from the daemon - Fetch real expiresAt from getOrder() on screen open instead of hardcoded 900s countdown; falls back to default on error - Add tradeInfoProvider (FutureProvider.family) to trades_providers for looking up TradeInfo by orderId across the app - ChatRoomsScreen mobile AppBar: hamburger drawer toggle, Mostro logo, notification bell — matching HomeScreen pattern - Compact time format in trades list: "4m"/"2h"/"3d" instead of verbose - Redirect /relays to /settings (remove _Stub placeholder) --- lib/core/app_routes.dart | 19 +- .../chat/screens/chat_rooms_screen.dart | 67 +++- .../screens/pay_lightning_invoice_screen.dart | 356 +++++++++--------- .../trades/providers/trades_providers.dart | 12 + .../trades/screens/trade_detail_screen.dart | 127 ++++++- .../trades/widgets/trades_list_item.dart | 17 +- 6 files changed, 375 insertions(+), 223 deletions(-) diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart index 640fbdab..9a89c4e3 100644 --- a/lib/core/app_routes.dart +++ b/lib/core/app_routes.dart @@ -189,7 +189,7 @@ final GoRouter appRouter = GoRouter( ), GoRoute( path: AppRoute.relays, - builder: (_, __) => const _Stub('Relays'), + redirect: (_, __) => AppRoute.settings, ), GoRoute( path: AppRoute.walletSettings, @@ -228,20 +228,3 @@ final GoRouter appRouter = GoRouter( ], ); -// ── Placeholder screen ───────────────────────────────────────────────────────── - -class _Stub extends StatelessWidget { - const _Stub(this.name); - - final String name; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(name)), - body: Center( - child: Text(name, style: Theme.of(context).textTheme.bodyLarge), - ), - ); - } -} diff --git a/lib/features/chat/screens/chat_rooms_screen.dart b/lib/features/chat/screens/chat_rooms_screen.dart index 3f85b3ac..f146aba6 100644 --- a/lib/features/chat/screens/chat_rooms_screen.dart +++ b/lib/features/chat/screens/chat_rooms_screen.dart @@ -9,24 +9,41 @@ import 'package:mostro/features/chat/widgets/chat_list_item.dart'; import 'package:mostro/features/disputes/widgets/disputes_list.dart'; import 'package:mostro/features/drawer/screens/drawer_menu.dart'; import 'package:mostro/shared/widgets/bottom_nav_bar.dart'; +import 'package:mostro/shared/widgets/notification_bell.dart'; /// Route: /chat_list /// /// Top-level chat screen with two tabs: Messages and Disputes. -class ChatRoomsScreen extends ConsumerWidget { +class ChatRoomsScreen extends ConsumerStatefulWidget { const ChatRoomsScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _ChatRoomsScreenState(); +} + +class _ChatRoomsScreenState extends ConsumerState { + bool _drawerOpen = false; + + @override + Widget build(BuildContext context) { final colors = Theme.of(context).extension(); if (colors == null) throw StateError('AppColors theme extension must be registered'); final textTheme = Theme.of(context).textTheme; + final green = colors.mostroGreen; final isDesktop = MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop; final mainContent = Column( children: [ + if (!isDesktop) + SafeArea( + bottom: false, + child: _ChatAppBar( + green: green, + onMenuTap: () => setState(() => _drawerOpen = true), + ), + ), TabBar( indicatorColor: colors.mostroGreen, labelColor: colors.mostroGreen, @@ -55,14 +72,19 @@ class ChatRoomsScreen extends ConsumerWidget { Expanded(child: SafeArea(child: mainContent)), ], ) - : mainContent; + : Stack( + children: [ + mainContent, + if (_drawerOpen) + DrawerMenu( + onClose: () => setState(() => _drawerOpen = false), + ), + ], + ); return DefaultTabController( length: 2, child: Scaffold( - appBar: isDesktop - ? null - : AppBar(title: const Text('Chat')), body: body, bottomNavigationBar: const BottomNavBar(), ), @@ -70,6 +92,39 @@ class ChatRoomsScreen extends ConsumerWidget { } } +// ── Mobile app bar ───────────────────────────────────────────────────────────── + +class _ChatAppBar extends StatelessWidget { + const _ChatAppBar({required this.green, required this.onMenuTap}); + + final Color green; + final VoidCallback onMenuTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.sm, + vertical: AppSpacing.xs, + ), + child: Row( + children: [ + IconButton( + onPressed: onMenuTap, + icon: const Icon(Icons.menu, size: 24), + tooltip: 'Menu', + ), + const Spacer(), + Icon(Icons.psychology, size: 28, color: green), + const Spacer(), + const NotificationBell(), + const SizedBox(width: AppSpacing.sm), + ], + ), + ); + } +} + // ── Messages tab ────────────────────────────────────────────────────────────── class _MessagesTab extends ConsumerWidget { diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index 35a91837..9c4fa664 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/features/settings/providers/nwc_provider.dart'; +import 'package:mostro/features/trades/providers/trades_providers.dart'; import 'package:mostro/shared/widgets/nwc_payment_widget.dart'; /// Pay Lightning Invoice screen — Route `/pay_invoice/:orderId`. @@ -26,19 +27,11 @@ class PayLightningInvoiceScreen extends ConsumerStatefulWidget { class _PayLightningInvoiceScreenState extends ConsumerState { - // TODO(bridge): Replace with real invoice from trade provider once - // Dart bridge exposes TradeInfo.hold_invoice for widget.orderId. - // Subscribe to trade status stream and navigate on payment confirmation. - final _mockInvoice = - 'lnbc1500n1pj9nr7mpp5xz80dm6k5tqasn3nyh3e6fqzmtqpy0xf5h9m7y0yr5' - 'n4dqwk4esdqqcqzzsxqyz5vqsp5usyc4lg3dxp3skyhw5e8vy5w6v7kw6mxhf' - 'jyzpnpryz4jns7qs9qyyssqjrvz0waerp2g3kx6k2neqfmfp2sxlm0n3m'; - bool _waiting = false; /// `true` when NWC is connected but payment failed → show QR fallback. bool _manualMode = false; - void _simulatePaymentDetected() { + void _onPaymentDetected() { setState(() => _waiting = true); Future.delayed(const Duration(seconds: 2), () { if (!mounted) return; @@ -54,194 +47,213 @@ class _PayLightningInvoiceScreenState final cardBg = colors?.backgroundCard ?? const Color(0xFF1E2230); final isWalletConnected = ref.watch(isWalletConnectedProvider); + final tradeAsync = ref.watch(tradeInfoProvider(widget.orderId)); - // If NWC wallet is connected and payment hasn't failed yet, show auto-pay. - if (isWalletConnected && !_manualMode) { - return Scaffold( + return tradeAsync.when( + loading: () => Scaffold( + appBar: AppBar(title: const Text('Pay Lightning Invoice')), + body: const Center(child: CircularProgressIndicator()), + ), + error: (e, _) => Scaffold( appBar: AppBar(title: const Text('Pay Lightning Invoice')), - body: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Center( - child: NwcPaymentWidget( - bolt11: _mockInvoice, - // TODO(bridge): pass real sats amount from trade provider. - amountSats: 0, - onPaymentSuccess: _simulatePaymentDetected, - onFallbackToManual: () => setState(() => _manualMode = true), + body: Center(child: Text('Error loading trade: $e')), + ), + data: (trade) { + final invoice = trade?.holdInvoice ?? ''; + final amountSats = trade?.order.amountSats?.toInt() ?? 0; + + if (invoice.isEmpty) { + // Hold invoice not yet available — waiting for Mostro daemon. + return Scaffold( + appBar: AppBar(title: const Text('Pay Lightning Invoice')), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator(color: green), + const SizedBox(height: 16), + Text( + 'Waiting for hold invoice...', + style: TextStyle(color: colors?.textSecondary), + ), + ], + ), ), - ), - ), - ); - } + ); + } - return Scaffold( - appBar: AppBar(title: const Text('Pay Lightning Invoice')), - body: Padding( - padding: const EdgeInsets.all(AppSpacing.lg), - child: Column( - children: [ - // Info card with QR - Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(AppSpacing.lg), - decoration: BoxDecoration( - color: cardBg, - borderRadius: BorderRadius.circular(AppRadius.card), + // If NWC wallet is connected and payment hasn't failed yet, show auto-pay. + if (isWalletConnected && !_manualMode) { + return Scaffold( + appBar: AppBar(title: const Text('Pay Lightning Invoice')), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Center( + child: NwcPaymentWidget( + bolt11: invoice, + amountSats: amountSats, + onPaymentSuccess: _onPaymentDetected, + onFallbackToManual: () => setState(() => _manualMode = true), ), - child: Column( - children: [ - Row( - children: [ - Icon(Icons.bolt, color: green, size: 24), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: Text( - 'Pay this hold invoice to start the trade', - style: theme.textTheme.bodyMedium, - ), - ), - ], - ), - const SizedBox(height: AppSpacing.xl), + ), + ), + ); + } - // QR Code - Expanded( - child: Center( - child: Container( - padding: const EdgeInsets.all(AppSpacing.md), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(AppRadius.card), - ), - child: QrImageView( - data: _mockInvoice, - size: 200, - backgroundColor: Colors.white, - semanticsLabel: 'Lightning invoice QR code', - ), - ), - ), + return Scaffold( + appBar: AppBar(title: const Text('Pay Lightning Invoice')), + body: Padding( + padding: const EdgeInsets.all(AppSpacing.lg), + child: Column( + children: [ + // Info card with QR + Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.lg), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(AppRadius.card), ), - const SizedBox(height: AppSpacing.lg), - - // Copy + Share buttons - Row( + child: Column( children: [ + Row( + children: [ + Icon(Icons.bolt, color: green, size: 24), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: Text( + 'Pay this hold invoice to start the trade', + style: theme.textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.xl), + + // QR Code Expanded( - child: FilledButton.icon( - onPressed: () async { - await Clipboard.setData( - ClipboardData(text: _mockInvoice), - ); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invoice copied'), - duration: Duration(seconds: 1), - ), - ); - }, - icon: const Icon(Icons.copy, size: 16), - label: const Text('Copy'), - style: FilledButton.styleFrom( - backgroundColor: green, - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( + child: Center( + child: Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Colors.white, borderRadius: - BorderRadius.circular(AppRadius.button), + BorderRadius.circular(AppRadius.card), + ), + child: QrImageView( + data: invoice, + size: 200, + backgroundColor: Colors.white, + semanticsLabel: 'Lightning invoice QR code', ), ), ), ), - const SizedBox(width: AppSpacing.sm), - Expanded( - child: FilledButton.icon( - onPressed: () async { - try { - await SharePlus.instance - .share(ShareParams(text: _mockInvoice)); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Share failed: $e'), + const SizedBox(height: AppSpacing.lg), + + // Copy + Share buttons + Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: invoice), + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invoice copied'), + duration: Duration(seconds: 1), + ), + ); + }, + icon: const Icon(Icons.copy, size: 16), + label: const Text('Copy'), + style: FilledButton.styleFrom( + backgroundColor: green, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppRadius.button), ), - ); - } - }, - icon: const Icon(Icons.share, size: 16), - label: const Text('Share'), - style: FilledButton.styleFrom( - backgroundColor: green, - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(AppRadius.button), + ), ), ), - ), + const SizedBox(width: AppSpacing.sm), + Expanded( + child: FilledButton.icon( + onPressed: () async { + try { + await SharePlus.instance + .share(ShareParams(text: invoice)); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Share failed: $e'), + ), + ); + } + }, + icon: const Icon(Icons.share, size: 16), + label: const Text('Share'), + style: FilledButton.styleFrom( + backgroundColor: green, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(AppRadius.button), + ), + ), + ), + ), + ], ), ], ), - ], - ), - ), - ), - const SizedBox(height: AppSpacing.lg), - - // Waiting indicator or Cancel button - if (_waiting) - Column( - children: [ - CircularProgressIndicator(color: green), - const SizedBox(height: AppSpacing.sm), - Text( - 'Waiting for payment confirmation...', - style: TextStyle(color: colors?.textSecondary), - ), - ], - ) - else - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: () => context.pop(), - style: OutlinedButton.styleFrom( - foregroundColor: - colors?.destructiveRed ?? const Color(0xFFD84D4D), - side: BorderSide( - color: - colors?.destructiveRed ?? const Color(0xFFD84D4D), - ), - minimumSize: const Size(0, 48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), ), - child: const Text('Cancel'), ), - ), + const SizedBox(height: AppSpacing.lg), - // Hidden dev button to simulate payment (TODO: remove when wired) - if (!_waiting) - Padding( - padding: const EdgeInsets.only(top: AppSpacing.sm), - child: TextButton( - onPressed: _simulatePaymentDetected, - child: Text( - 'Simulate payment (dev)', - style: TextStyle( - color: colors?.textSubtle, - fontSize: 11, + // Waiting indicator or Cancel button + if (_waiting) + Column( + children: [ + CircularProgressIndicator(color: green), + const SizedBox(height: AppSpacing.sm), + Text( + 'Waiting for payment confirmation...', + style: TextStyle(color: colors?.textSecondary), + ), + ], + ) + else + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => context.pop(), + style: OutlinedButton.styleFrom( + foregroundColor: + colors?.destructiveRed ?? const Color(0xFFD84D4D), + side: BorderSide( + color: + colors?.destructiveRed ?? const Color(0xFFD84D4D), + ), + minimumSize: const Size(0, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + child: const Text('Cancel'), ), ), - ), - ), - ], - ), - ), + ], + ), + ), + ); + }, ); } } diff --git a/lib/features/trades/providers/trades_providers.dart b/lib/features/trades/providers/trades_providers.dart index 1512e7b3..b40db68a 100644 --- a/lib/features/trades/providers/trades_providers.dart +++ b/lib/features/trades/providers/trades_providers.dart @@ -144,6 +144,18 @@ final rawTradesProvider = FutureProvider>((ref) { return orders_api.listTrades(); }); +/// Returns the [rust_types.TradeInfo] for a given [orderId], or null if not found. +/// +/// Used by screens that need trade-level fields (e.g. [holdInvoice], [timeoutAt]) +/// that are not present on the order-book [OrderInfo]. +final tradeInfoProvider = + FutureProvider.autoDispose.family( + (ref, orderId) async { + final trades = await ref.watch(rawTradesProvider.future); + return trades.where((t) => t.order.id == orderId).firstOrNull; + }, +); + /// Invalidates the raw trades cache, forcing a fresh DB fetch on next read. /// /// Call this after a trade is successfully saved (e.g. after [takeOrder]). diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index f5c6de7b..bf6d03c2 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/src/rust/api/disputes.dart' as disputes_api; import 'package:mostro/src/rust/api/orders.dart' as orders_api; import 'package:mostro/features/disputes/providers/disputes_providers.dart'; import 'package:mostro/features/home/providers/home_order_providers.dart'; @@ -36,6 +37,10 @@ const _kCountdownSeconds = 900; // 15 minutes enum TradeStatus { /// Status not yet resolved (initial loading state — no actions shown). loading('Loading'), + /// Buyer must submit Lightning invoice (waitingBuyerInvoice). + waitingInvoice('Waiting Invoice'), + /// Seller must pay hold invoice (waitingPayment). + waitingPayment('Waiting Payment'), active('Active'), fiatSent('Fiat Sent'), completed('Completed'), @@ -59,6 +64,7 @@ class _TradeDetailScreenState extends ConsumerState { @override void initState() { super.initState(); + _loadExpiresAt(); _startCountdown(); } @@ -68,6 +74,28 @@ class _TradeDetailScreenState extends ConsumerState { super.dispose(); } + /// Fetches the real `expiresAt` from the order and resets [_remaining]. + /// + /// Falls back to the default [_kCountdownSeconds] when the field is null or + /// the order is no longer available. + Future _loadExpiresAt() async { + try { + final info = await orders_api.getOrder(orderId: widget.orderId); + final raw = info?.expiresAt; + if (raw == null || !mounted) return; + // PlatformInt64 = int on native, BigInt on web. + final expiresAtSeconds = raw is BigInt ? raw.toInt() : raw; + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + final diff = expiresAtSeconds - now; + if (!mounted) return; + setState(() { + _remaining = diff > 0 ? Duration(seconds: diff) : Duration.zero; + }); + } catch (_) { + // Keep the default remaining time on error. + } + } + void _startCountdown() { _countdownTimer = Timer.periodic(const Duration(seconds: 1), (_) { if (!mounted) return; @@ -91,7 +119,12 @@ class _TradeDetailScreenState extends ConsumerState { static TradeStatus _mapOrderStatus(OrderStatus s) { switch (s) { + case OrderStatus.waitingBuyerInvoice: + return TradeStatus.waitingInvoice; + case OrderStatus.waitingPayment: + return TradeStatus.waitingPayment; case OrderStatus.active: + case OrderStatus.inProgress: return TradeStatus.active; case OrderStatus.fiatSent: return TradeStatus.fiatSent; @@ -102,6 +135,7 @@ class _TradeDetailScreenState extends ConsumerState { return TradeStatus.pendingRating; case OrderStatus.canceled: case OrderStatus.canceledByAdmin: + case OrderStatus.cooperativelyCanceled: case OrderStatus.expired: return TradeStatus.cancelled; case OrderStatus.dispute: @@ -147,6 +181,16 @@ class _TradeDetailScreenState extends ConsumerState { } String _getInstructionText(bool isBuyer, TradeStatus status) { + if (status == TradeStatus.waitingInvoice) { + return isBuyer + ? 'Submit your Lightning invoice so the seller can lock the funds.' + : 'Waiting for the buyer to submit their Lightning invoice.'; + } + if (status == TradeStatus.waitingPayment) { + return isBuyer + ? 'The seller is paying the hold invoice. Please wait.' + : 'Pay the hold invoice to lock the funds and start the trade.'; + } if (isBuyer) { if (status == TradeStatus.active) { return 'Send the fiat payment to the seller, then tap "Fiat Sent".'; @@ -185,6 +229,35 @@ class _TradeDetailScreenState extends ConsumerState { return '$h:$m:$s'; } + /// Open a dispute for this trade, upsert into the local dispute notifier, + /// and navigate to the dispute chat. + Future _openDispute() async { + try { + final dispute = await disputes_api.openDispute(tradeId: widget.orderId); + if (!context.mounted) return; + final raw = dispute.openedAt; + // PlatformInt64 = int on native, BigInt on web. + final openedAt = raw is BigInt ? raw.toInt() : raw; + ref.read(disputeNotifierProvider.notifier).upsert( + DisputeItem( + id: dispute.id, + tradeId: dispute.tradeId, + status: DisputeStatus.open, + initiatedByMe: true, + openedAt: openedAt, + ), + ); + if (!context.mounted) return; + context.push(AppRoute.disputeDetailsPath(dispute.id)); + } catch (e, st) { + debugPrint('[TradeDetailScreen] openDispute error: $e\n$st'); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not open dispute')), + ); + } + } + /// Shared style for destructive (cancel / dispute) outlined buttons. ButtonStyle _destructiveOutlineStyle(Color destructiveRed) => OutlinedButton.styleFrom( @@ -365,6 +438,42 @@ class _TradeDetailScreenState extends ConsumerState { const SizedBox(height: AppSpacing.xl), ], + // ── Buyer: Waiting Invoice — ADD INVOICE button ──────── + if (isBuyer && status == TradeStatus.waitingInvoice) ...[ + FilledButton.icon( + onPressed: () => + context.push(AppRoute.addInvoicePath(widget.orderId)), + icon: const Icon(Icons.receipt_long_outlined, size: 16), + label: const Text('ADD INVOICE'), + style: FilledButton.styleFrom( + backgroundColor: green, + foregroundColor: Colors.black, + minimumSize: const Size.fromHeight(40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + ), + ], + + // ── Seller: Waiting Payment — PAY INVOICE button ─────── + if (!isBuyer && status == TradeStatus.waitingPayment) ...[ + FilledButton.icon( + onPressed: () => + context.push(AppRoute.payInvoicePath(widget.orderId)), + icon: const Icon(Icons.bolt, size: 16), + label: const Text('PAY INVOICE'), + style: FilledButton.styleFrom( + backgroundColor: green, + foregroundColor: Colors.black, + minimumSize: const Size.fromHeight(40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + ), + ], + // Action buttons (buyer flow — T060) if (isBuyer && status == TradeStatus.active) ...[ MostroReactiveButton( @@ -398,11 +507,7 @@ class _TradeDetailScreenState extends ConsumerState { const SizedBox(width: AppSpacing.sm), Expanded( child: OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Coming soon')), - ); - }, + onPressed: _openDispute, icon: const Icon(Icons.gavel, size: 16), label: const Text('DISPUTE'), style: _destructiveOutlineStyle( @@ -461,11 +566,7 @@ class _TradeDetailScreenState extends ConsumerState { const SizedBox(width: AppSpacing.sm), Expanded( child: OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Coming soon')), - ); - }, + onPressed: _openDispute, icon: const Icon(Icons.gavel, size: 16), label: const Text('DISPUTE'), style: _destructiveOutlineStyle( @@ -618,11 +719,7 @@ class _TradeDetailScreenState extends ConsumerState { const SizedBox(width: AppSpacing.sm), Expanded( child: OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Coming soon')), - ); - }, + onPressed: _openDispute, icon: const Icon(Icons.gavel, size: 16), label: const Text('DISPUTE'), style: _destructiveOutlineStyle( diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index 89e38ce6..34af1604 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -163,22 +163,15 @@ class TradesListItem extends ConsumerWidget { }; } - /// Returns a human-readable "time ago" string from a unix timestamp. + /// Returns a compact "time ago" string from a unix timestamp (e.g. "4m", "2h", "3d"). static String _timeAgo(int unixSeconds) { final dt = DateTime.fromMillisecondsSinceEpoch(unixSeconds * 1000); final diff = DateTime.now().difference(dt); - if (diff.isNegative || diff.inSeconds < 60) return 'just now'; - if (diff.inMinutes < 60) { - final m = diff.inMinutes; - return '$m ${m == 1 ? 'minute' : 'minutes'} ago'; - } - if (diff.inHours < 24) { - final h = diff.inHours; - return '$h ${h == 1 ? 'hour' : 'hours'} ago'; - } - final d = diff.inDays; - return '$d ${d == 1 ? 'day' : 'days'} ago'; + if (diff.isNegative || diff.inSeconds < 60) return 'now'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m'; + if (diff.inHours < 24) return '${diff.inHours}h'; + return '${diff.inDays}d'; } } From 002f3d786ad554ab75d886ed6e3906df714e2357 Mon Sep 17 00:00:00 2001 From: grunch Date: Fri, 3 Apr 2026 16:15:06 -0300 Subject: [PATCH 2/4] fix: address CodeRabbit review findings - Hide raw exception details in PayLightningInvoiceScreen error state; log via debugPrint and show a generic user-facing message - Guard NwcPaymentWidget behind amountSats > 0 check so it is only instantiated when the hold invoice amount is known - Localize waiting-invoice/waiting-payment instruction strings in TradeDetailScreen (en/es/it/fr/de) - Localize "Could not open dispute" error snackbar in TradeDetailScreen - Remove unused flutter/material.dart import from app_routes.dart --- lib/core/app_routes.dart | 1 - .../screens/pay_lightning_invoice_screen.dart | 15 ++++++++++----- .../trades/screens/trade_detail_screen.dart | 11 ++++++----- lib/l10n/app_de.arb | 8 +++++++- lib/l10n/app_en.arb | 14 +++++++++++++- lib/l10n/app_es.arb | 8 +++++++- lib/l10n/app_fr.arb | 8 +++++++- lib/l10n/app_it.arb | 8 +++++++- 8 files changed, 57 insertions(+), 16 deletions(-) diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart index 9a89c4e3..afea6be1 100644 --- a/lib/core/app_routes.dart +++ b/lib/core/app_routes.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index 9c4fa664..ee11fc99 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -54,15 +54,20 @@ class _PayLightningInvoiceScreenState appBar: AppBar(title: const Text('Pay Lightning Invoice')), body: const Center(child: CircularProgressIndicator()), ), - error: (e, _) => Scaffold( - appBar: AppBar(title: const Text('Pay Lightning Invoice')), - body: Center(child: Text('Error loading trade: $e')), - ), + error: (e, st) { + debugPrint('[PayLightningInvoiceScreen] load error: $e\n$st'); + return Scaffold( + appBar: AppBar(title: const Text('Pay Lightning Invoice')), + body: const Center( + child: Text('An error occurred while loading the trade.'), + ), + ); + }, data: (trade) { final invoice = trade?.holdInvoice ?? ''; final amountSats = trade?.order.amountSats?.toInt() ?? 0; - if (invoice.isEmpty) { + if (invoice.isEmpty || amountSats <= 0) { // Hold invoice not yet available — waiting for Mostro daemon. return Scaffold( appBar: AppBar(title: const Text('Pay Lightning Invoice')), diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index bf6d03c2..e00e82fa 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -181,15 +181,16 @@ class _TradeDetailScreenState extends ConsumerState { } String _getInstructionText(bool isBuyer, TradeStatus status) { + final l10n = AppLocalizations.of(context); if (status == TradeStatus.waitingInvoice) { return isBuyer - ? 'Submit your Lightning invoice so the seller can lock the funds.' - : 'Waiting for the buyer to submit their Lightning invoice.'; + ? l10n.tradeWaitingInvoiceBuyerInstruction + : l10n.tradeWaitingInvoiceSellerInstruction; } if (status == TradeStatus.waitingPayment) { return isBuyer - ? 'The seller is paying the hold invoice. Please wait.' - : 'Pay the hold invoice to lock the funds and start the trade.'; + ? l10n.tradeWaitingPaymentBuyerInstruction + : l10n.tradeWaitingPaymentSellerInstruction; } if (isBuyer) { if (status == TradeStatus.active) { @@ -253,7 +254,7 @@ class _TradeDetailScreenState extends ConsumerState { debugPrint('[TradeDetailScreen] openDispute error: $e\n$st'); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not open dispute')), + SnackBar(content: Text(AppLocalizations.of(context).openDisputeFailed)), ); } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c2837190..1e457b39 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -303,5 +303,11 @@ "aboutLndNodeAliasExplanation": "Der lesbare Alias des LND-Knotens, wie vom Knotenbetreiber konfiguriert.", "aboutSupportedChainsExplanation": "Die vom LND-Knoten unterstützten Blockchain(s) (z.B. 'bitcoin').", "aboutSupportedNetworksExplanation": "Die Netzwerke, in denen der LND-Knoten betrieben wird (z.B. 'mainnet', 'testnet').", - "aboutLndNodeUriExplanation": "Die Verbindungs-URI des LND-Knotens im Format pubkey@host:port. Wird zum Öffnen direkter Zahlungskanäle verwendet." + "aboutLndNodeUriExplanation": "Die Verbindungs-URI des LND-Knotens im Format pubkey@host:port. Wird zum Öffnen direkter Zahlungskanäle verwendet.", + "openDisputeFailed": "Streit konnte nicht eröffnet werden. Bitte erneut versuchen.", + "tradeWaitingInvoiceBuyerInstruction": "Sende deine Lightning-Rechnung, damit der Verkäufer die Gelder sperren kann.", + "tradeWaitingInvoiceSellerInstruction": "Warte auf die Lightning-Rechnung des Käufers.", + "tradeWaitingPaymentBuyerInstruction": "Der Verkäufer bezahlt die Hold-Rechnung. Bitte warten.", + "tradeWaitingPaymentSellerInstruction": "Bezahle die Hold-Rechnung, um die Gelder zu sperren und den Handel zu starten.", + "tradeLoadError": "Beim Laden des Handels ist ein Fehler aufgetreten." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b7c9f27b..28cb31c5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -616,5 +616,17 @@ "aboutSupportedNetworksExplanation": "The network(s) the LND node operates on (e.g. 'mainnet', 'testnet').", "@aboutSupportedNetworksExplanation": {"description": "Info dialog explanation for the Supported Networks field"}, "aboutLndNodeUriExplanation": "The connection URI of the LND node in the format pubkey@host:port. Used to open direct payment channels.", - "@aboutLndNodeUriExplanation": {"description": "Info dialog explanation for the LND Node URI field"} + "@aboutLndNodeUriExplanation": {"description": "Info dialog explanation for the LND Node URI field"}, + "openDisputeFailed": "Could not open dispute. Please try again.", + "@openDisputeFailed": {"description": "Snackbar shown when opening a dispute fails"}, + "tradeWaitingInvoiceBuyerInstruction": "Submit your Lightning invoice so the seller can lock the funds.", + "@tradeWaitingInvoiceBuyerInstruction": {"description": "Instruction shown to the buyer while waiting to submit their Lightning invoice"}, + "tradeWaitingInvoiceSellerInstruction": "Waiting for the buyer to submit their Lightning invoice.", + "@tradeWaitingInvoiceSellerInstruction": {"description": "Instruction shown to the seller while waiting for the buyer's Lightning invoice"}, + "tradeWaitingPaymentBuyerInstruction": "The seller is paying the hold invoice. Please wait.", + "@tradeWaitingPaymentBuyerInstruction": {"description": "Instruction shown to the buyer while the seller pays the hold invoice"}, + "tradeWaitingPaymentSellerInstruction": "Pay the hold invoice to lock the funds and start the trade.", + "@tradeWaitingPaymentSellerInstruction": {"description": "Instruction shown to the seller prompting them to pay the hold invoice"}, + "tradeLoadError": "An error occurred while loading the trade.", + "@tradeLoadError": {"description": "Error message shown when a trade fails to load"} } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c7b8b008..fb944928 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -303,5 +303,11 @@ "aboutLndNodeAliasExplanation": "El alias legible por humanos del nodo LND configurado por el operador del nodo.", "aboutSupportedChainsExplanation": "La(s) blockchain(s) soportadas por el nodo LND (p.ej. 'bitcoin').", "aboutSupportedNetworksExplanation": "La(s) red(es) en la(s) que opera el nodo LND (p.ej. 'mainnet', 'testnet').", - "aboutLndNodeUriExplanation": "El URI de conexión del nodo LND en el formato pubkey@host:puerto. Se utiliza para abrir canales de pago directos." + "aboutLndNodeUriExplanation": "El URI de conexión del nodo LND en el formato pubkey@host:puerto. Se utiliza para abrir canales de pago directos.", + "openDisputeFailed": "No se pudo abrir la disputa. Por favor, inténtelo de nuevo.", + "tradeWaitingInvoiceBuyerInstruction": "Envía tu factura Lightning para que el vendedor pueda bloquear los fondos.", + "tradeWaitingInvoiceSellerInstruction": "Esperando a que el comprador envíe su factura Lightning.", + "tradeWaitingPaymentBuyerInstruction": "El vendedor está pagando la factura hold. Por favor, espera.", + "tradeWaitingPaymentSellerInstruction": "Paga la factura hold para bloquear los fondos e iniciar el intercambio.", + "tradeLoadError": "Ocurrió un error al cargar el intercambio." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7339bda7..9683b951 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -303,5 +303,11 @@ "aboutLndNodeAliasExplanation": "L'alias lisible par l'homme du nœud LND tel que configuré par l'opérateur du nœud.", "aboutSupportedChainsExplanation": "La ou les blockchain(s) supportée(s) par le nœud LND (ex. 'bitcoin').", "aboutSupportedNetworksExplanation": "Le ou les réseau(x) sur lesquels le nœud LND opère (ex. 'mainnet', 'testnet').", - "aboutLndNodeUriExplanation": "L'URI de connexion du nœud LND au format pubkey@hôte:port. Utilisée pour ouvrir des canaux de paiement directs." + "aboutLndNodeUriExplanation": "L'URI de connexion du nœud LND au format pubkey@hôte:port. Utilisée pour ouvrir des canaux de paiement directs.", + "openDisputeFailed": "Impossible d'ouvrir le litige. Veuillez réessayer.", + "tradeWaitingInvoiceBuyerInstruction": "Soumettez votre facture Lightning pour que le vendeur puisse bloquer les fonds.", + "tradeWaitingInvoiceSellerInstruction": "En attente de la facture Lightning de l'acheteur.", + "tradeWaitingPaymentBuyerInstruction": "Le vendeur est en train de payer la facture hold. Veuillez patienter.", + "tradeWaitingPaymentSellerInstruction": "Payez la facture hold pour bloquer les fonds et démarrer l'échange.", + "tradeLoadError": "Une erreur s'est produite lors du chargement de l'échange." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 32e769fe..4ce48175 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -303,5 +303,11 @@ "aboutLndNodeAliasExplanation": "L'alias leggibile dall'uomo del nodo LND configurato dall'operatore del nodo.", "aboutSupportedChainsExplanation": "La/le blockchain supportata/e dal nodo LND (es. 'bitcoin').", "aboutSupportedNetworksExplanation": "La/le rete/i su cui opera il nodo LND (es. 'mainnet', 'testnet').", - "aboutLndNodeUriExplanation": "L'URI di connessione del nodo LND nel formato pubkey@host:porta. Utilizzato per aprire canali di pagamento diretti." + "aboutLndNodeUriExplanation": "L'URI di connessione del nodo LND nel formato pubkey@host:porta. Utilizzato per aprire canali di pagamento diretti.", + "openDisputeFailed": "Impossibile aprire la disputa. Riprovare.", + "tradeWaitingInvoiceBuyerInstruction": "Invia la tua fattura Lightning per permettere al venditore di bloccare i fondi.", + "tradeWaitingInvoiceSellerInstruction": "In attesa che il compratore invii la propria fattura Lightning.", + "tradeWaitingPaymentBuyerInstruction": "Il venditore sta pagando la fattura hold. Attendere.", + "tradeWaitingPaymentSellerInstruction": "Paga la fattura hold per bloccare i fondi e avviare lo scambio.", + "tradeLoadError": "Si è verificato un errore durante il caricamento dello scambio." } From 7d4525d059855c5e3fd3bfd54a0a4ade60d309d8 Mon Sep 17 00:00:00 2001 From: grunch Date: Fri, 3 Apr 2026 16:25:12 -0300 Subject: [PATCH 3/4] chore(l10n): regenerate localizations for new trade and dispute strings --- lib/l10n/app_localizations.dart | 36 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_de.dart | 24 ++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 22 ++++++++++++++++++ lib/l10n/app_localizations_es.dart | 23 +++++++++++++++++++ lib/l10n/app_localizations_fr.dart | 24 ++++++++++++++++++++ lib/l10n/app_localizations_it.dart | 23 +++++++++++++++++++ 6 files changed, 152 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 97405dc2..dcf7fb21 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1789,6 +1789,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'The connection URI of the LND node in the format pubkey@host:port. Used to open direct payment channels.'** String get aboutLndNodeUriExplanation; + + /// Snackbar shown when opening a dispute fails + /// + /// In en, this message translates to: + /// **'Could not open dispute. Please try again.'** + String get openDisputeFailed; + + /// Instruction shown to the buyer while waiting to submit their Lightning invoice + /// + /// In en, this message translates to: + /// **'Submit your Lightning invoice so the seller can lock the funds.'** + String get tradeWaitingInvoiceBuyerInstruction; + + /// Instruction shown to the seller while waiting for the buyer's Lightning invoice + /// + /// In en, this message translates to: + /// **'Waiting for the buyer to submit their Lightning invoice.'** + String get tradeWaitingInvoiceSellerInstruction; + + /// Instruction shown to the buyer while the seller pays the hold invoice + /// + /// In en, this message translates to: + /// **'The seller is paying the hold invoice. Please wait.'** + String get tradeWaitingPaymentBuyerInstruction; + + /// Instruction shown to the seller prompting them to pay the hold invoice + /// + /// In en, this message translates to: + /// **'Pay the hold invoice to lock the funds and start the trade.'** + String get tradeWaitingPaymentSellerInstruction; + + /// Error message shown when a trade fails to load + /// + /// In en, this message translates to: + /// **'An error occurred while loading the trade.'** + String get tradeLoadError; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 15e3d42b..8ceaabef 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -927,4 +927,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutLndNodeUriExplanation => 'Die Verbindungs-URI des LND-Knotens im Format pubkey@host:port. Wird zum Öffnen direkter Zahlungskanäle verwendet.'; + + @override + String get openDisputeFailed => + 'Streit konnte nicht eröffnet werden. Bitte erneut versuchen.'; + + @override + String get tradeWaitingInvoiceBuyerInstruction => + 'Sende deine Lightning-Rechnung, damit der Verkäufer die Gelder sperren kann.'; + + @override + String get tradeWaitingInvoiceSellerInstruction => + 'Warte auf die Lightning-Rechnung des Käufers.'; + + @override + String get tradeWaitingPaymentBuyerInstruction => + 'Der Verkäufer bezahlt die Hold-Rechnung. Bitte warten.'; + + @override + String get tradeWaitingPaymentSellerInstruction => + 'Bezahle die Hold-Rechnung, um die Gelder zu sperren und den Handel zu starten.'; + + @override + String get tradeLoadError => + 'Beim Laden des Handels ist ein Fehler aufgetreten.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index cfa15290..ad9e3f04 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -915,4 +915,26 @@ class AppLocalizationsEn extends AppLocalizations { @override String get aboutLndNodeUriExplanation => 'The connection URI of the LND node in the format pubkey@host:port. Used to open direct payment channels.'; + + @override + String get openDisputeFailed => 'Could not open dispute. Please try again.'; + + @override + String get tradeWaitingInvoiceBuyerInstruction => + 'Submit your Lightning invoice so the seller can lock the funds.'; + + @override + String get tradeWaitingInvoiceSellerInstruction => + 'Waiting for the buyer to submit their Lightning invoice.'; + + @override + String get tradeWaitingPaymentBuyerInstruction => + 'The seller is paying the hold invoice. Please wait.'; + + @override + String get tradeWaitingPaymentSellerInstruction => + 'Pay the hold invoice to lock the funds and start the trade.'; + + @override + String get tradeLoadError => 'An error occurred while loading the trade.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index d721f563..03c485e4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -928,4 +928,27 @@ class AppLocalizationsEs extends AppLocalizations { @override String get aboutLndNodeUriExplanation => 'El URI de conexión del nodo LND en el formato pubkey@host:puerto. Se utiliza para abrir canales de pago directos.'; + + @override + String get openDisputeFailed => + 'No se pudo abrir la disputa. Por favor, inténtelo de nuevo.'; + + @override + String get tradeWaitingInvoiceBuyerInstruction => + 'Envía tu factura Lightning para que el vendedor pueda bloquear los fondos.'; + + @override + String get tradeWaitingInvoiceSellerInstruction => + 'Esperando a que el comprador envíe su factura Lightning.'; + + @override + String get tradeWaitingPaymentBuyerInstruction => + 'El vendedor está pagando la factura hold. Por favor, espera.'; + + @override + String get tradeWaitingPaymentSellerInstruction => + 'Paga la factura hold para bloquear los fondos e iniciar el intercambio.'; + + @override + String get tradeLoadError => 'Ocurrió un error al cargar el intercambio.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 49b7752d..40c56246 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -931,4 +931,28 @@ class AppLocalizationsFr extends AppLocalizations { @override String get aboutLndNodeUriExplanation => 'L\'URI de connexion du nœud LND au format pubkey@hôte:port. Utilisée pour ouvrir des canaux de paiement directs.'; + + @override + String get openDisputeFailed => + 'Impossible d\'ouvrir le litige. Veuillez réessayer.'; + + @override + String get tradeWaitingInvoiceBuyerInstruction => + 'Soumettez votre facture Lightning pour que le vendeur puisse bloquer les fonds.'; + + @override + String get tradeWaitingInvoiceSellerInstruction => + 'En attente de la facture Lightning de l\'acheteur.'; + + @override + String get tradeWaitingPaymentBuyerInstruction => + 'Le vendeur est en train de payer la facture hold. Veuillez patienter.'; + + @override + String get tradeWaitingPaymentSellerInstruction => + 'Payez la facture hold pour bloquer les fonds et démarrer l\'échange.'; + + @override + String get tradeLoadError => + 'Une erreur s\'est produite lors du chargement de l\'échange.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e3c49c93..8be544da 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -925,4 +925,27 @@ class AppLocalizationsIt extends AppLocalizations { @override String get aboutLndNodeUriExplanation => 'L\'URI di connessione del nodo LND nel formato pubkey@host:porta. Utilizzato per aprire canali di pagamento diretti.'; + + @override + String get openDisputeFailed => 'Impossibile aprire la disputa. Riprovare.'; + + @override + String get tradeWaitingInvoiceBuyerInstruction => + 'Invia la tua fattura Lightning per permettere al venditore di bloccare i fondi.'; + + @override + String get tradeWaitingInvoiceSellerInstruction => + 'In attesa che il compratore invii la propria fattura Lightning.'; + + @override + String get tradeWaitingPaymentBuyerInstruction => + 'Il venditore sta pagando la fattura hold. Attendere.'; + + @override + String get tradeWaitingPaymentSellerInstruction => + 'Paga la fattura hold per bloccare i fondi e avviare lo scambio.'; + + @override + String get tradeLoadError => + 'Si è verificato un errore durante il caricamento dello scambio.'; } From 85bcee6628a31326ea943e638cd7dbd9871cfebb Mon Sep 17 00:00:00 2001 From: grunch Date: Fri, 3 Apr 2026 16:27:38 -0300 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20apply=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20localization,=20countdown=20ratio,=20widget=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Localize error and waiting-for-invoice strings in PayLightningInvoiceScreen - Add tradeWaitingForHoldInvoice key to all 5 language files - Fix countdown CircularProgressIndicator ratio: track actual total duration in _totalCountdownSeconds (set from server expiresAt) instead of the hardcoded constant so the arc is correct after _loadExpiresAt - Downgrade ChatRoomsScreen from ConsumerStatefulWidget to StatefulWidget since the state class does not consume any Riverpod providers directly --- lib/features/chat/screens/chat_rooms_screen.dart | 6 +++--- .../order/screens/pay_lightning_invoice_screen.dart | 7 ++++--- lib/features/trades/screens/trade_detail_screen.dart | 8 ++++++-- lib/l10n/app_de.arb | 3 ++- lib/l10n/app_en.arb | 4 +++- lib/l10n/app_es.arb | 3 ++- lib/l10n/app_fr.arb | 3 ++- lib/l10n/app_it.arb | 3 ++- 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/features/chat/screens/chat_rooms_screen.dart b/lib/features/chat/screens/chat_rooms_screen.dart index f146aba6..ab1117fa 100644 --- a/lib/features/chat/screens/chat_rooms_screen.dart +++ b/lib/features/chat/screens/chat_rooms_screen.dart @@ -14,14 +14,14 @@ import 'package:mostro/shared/widgets/notification_bell.dart'; /// Route: /chat_list /// /// Top-level chat screen with two tabs: Messages and Disputes. -class ChatRoomsScreen extends ConsumerStatefulWidget { +class ChatRoomsScreen extends StatefulWidget { const ChatRoomsScreen({super.key}); @override - ConsumerState createState() => _ChatRoomsScreenState(); + State createState() => _ChatRoomsScreenState(); } -class _ChatRoomsScreenState extends ConsumerState { +class _ChatRoomsScreenState extends State { bool _drawerOpen = false; @override diff --git a/lib/features/order/screens/pay_lightning_invoice_screen.dart b/lib/features/order/screens/pay_lightning_invoice_screen.dart index ee11fc99..509364a7 100644 --- a/lib/features/order/screens/pay_lightning_invoice_screen.dart +++ b/lib/features/order/screens/pay_lightning_invoice_screen.dart @@ -9,6 +9,7 @@ import 'package:mostro/core/app_routes.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/features/settings/providers/nwc_provider.dart'; import 'package:mostro/features/trades/providers/trades_providers.dart'; +import 'package:mostro/l10n/app_localizations.dart'; import 'package:mostro/shared/widgets/nwc_payment_widget.dart'; /// Pay Lightning Invoice screen — Route `/pay_invoice/:orderId`. @@ -58,8 +59,8 @@ class _PayLightningInvoiceScreenState debugPrint('[PayLightningInvoiceScreen] load error: $e\n$st'); return Scaffold( appBar: AppBar(title: const Text('Pay Lightning Invoice')), - body: const Center( - child: Text('An error occurred while loading the trade.'), + body: Center( + child: Text(AppLocalizations.of(context).tradeLoadError), ), ); }, @@ -78,7 +79,7 @@ class _PayLightningInvoiceScreenState CircularProgressIndicator(color: green), const SizedBox(height: 16), Text( - 'Waiting for hold invoice...', + AppLocalizations.of(context).tradeWaitingForHoldInvoice, style: TextStyle(color: colors?.textSecondary), ), ], diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index e00e82fa..adb03da3 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -60,6 +60,7 @@ enum TradeStatus { class _TradeDetailScreenState extends ConsumerState { Timer? _countdownTimer; Duration _remaining = const Duration(seconds: _kCountdownSeconds); + int _totalCountdownSeconds = _kCountdownSeconds; @override void initState() { @@ -89,6 +90,7 @@ class _TradeDetailScreenState extends ConsumerState { final diff = expiresAtSeconds - now; if (!mounted) return; setState(() { + _totalCountdownSeconds = diff > 0 ? diff : _kCountdownSeconds; _remaining = diff > 0 ? Duration(seconds: diff) : Duration.zero; }); } catch (_) { @@ -418,8 +420,10 @@ class _TradeDetailScreenState extends ConsumerState { width: 80, height: 80, child: CircularProgressIndicator( - value: (_remaining.inSeconds / _kCountdownSeconds) - .clamp(0.0, 1.0), + value: _totalCountdownSeconds > 0 + ? (_remaining.inSeconds / _totalCountdownSeconds) + .clamp(0.0, 1.0) + : 1.0, strokeWidth: 4, color: _remaining.inMinutes < 5 ? colors?.destructiveRed ?? const Color(0xFFD84D4D) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1e457b39..e02f9fba 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -309,5 +309,6 @@ "tradeWaitingInvoiceSellerInstruction": "Warte auf die Lightning-Rechnung des Käufers.", "tradeWaitingPaymentBuyerInstruction": "Der Verkäufer bezahlt die Hold-Rechnung. Bitte warten.", "tradeWaitingPaymentSellerInstruction": "Bezahle die Hold-Rechnung, um die Gelder zu sperren und den Handel zu starten.", - "tradeLoadError": "Beim Laden des Handels ist ein Fehler aufgetreten." + "tradeLoadError": "Beim Laden des Handels ist ein Fehler aufgetreten.", + "tradeWaitingForHoldInvoice": "Warte auf Hold-Rechnung..." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 28cb31c5..71d6db45 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -628,5 +628,7 @@ "tradeWaitingPaymentSellerInstruction": "Pay the hold invoice to lock the funds and start the trade.", "@tradeWaitingPaymentSellerInstruction": {"description": "Instruction shown to the seller prompting them to pay the hold invoice"}, "tradeLoadError": "An error occurred while loading the trade.", - "@tradeLoadError": {"description": "Error message shown when a trade fails to load"} + "@tradeLoadError": {"description": "Error message shown when a trade fails to load"}, + "tradeWaitingForHoldInvoice": "Waiting for hold invoice...", + "@tradeWaitingForHoldInvoice": {"description": "Loading message shown while the Mostro daemon has not yet sent the hold invoice"} } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index fb944928..ad0eff7e 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -309,5 +309,6 @@ "tradeWaitingInvoiceSellerInstruction": "Esperando a que el comprador envíe su factura Lightning.", "tradeWaitingPaymentBuyerInstruction": "El vendedor está pagando la factura hold. Por favor, espera.", "tradeWaitingPaymentSellerInstruction": "Paga la factura hold para bloquear los fondos e iniciar el intercambio.", - "tradeLoadError": "Ocurrió un error al cargar el intercambio." + "tradeLoadError": "Ocurrió un error al cargar el intercambio.", + "tradeWaitingForHoldInvoice": "Esperando la factura hold..." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9683b951..7fe8eb46 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -309,5 +309,6 @@ "tradeWaitingInvoiceSellerInstruction": "En attente de la facture Lightning de l'acheteur.", "tradeWaitingPaymentBuyerInstruction": "Le vendeur est en train de payer la facture hold. Veuillez patienter.", "tradeWaitingPaymentSellerInstruction": "Payez la facture hold pour bloquer les fonds et démarrer l'échange.", - "tradeLoadError": "Une erreur s'est produite lors du chargement de l'échange." + "tradeLoadError": "Une erreur s'est produite lors du chargement de l'échange.", + "tradeWaitingForHoldInvoice": "En attente de la facture hold..." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4ce48175..a2570b6f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -309,5 +309,6 @@ "tradeWaitingInvoiceSellerInstruction": "In attesa che il compratore invii la propria fattura Lightning.", "tradeWaitingPaymentBuyerInstruction": "Il venditore sta pagando la fattura hold. Attendere.", "tradeWaitingPaymentSellerInstruction": "Paga la fattura hold per bloccare i fondi e avviare lo scambio.", - "tradeLoadError": "Si è verificato un errore durante il caricamento dello scambio." + "tradeLoadError": "Si è verificato un errore durante il caricamento dello scambio.", + "tradeWaitingForHoldInvoice": "In attesa della fattura hold..." }