diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index 0a3a7409..9b5eda64 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -28,7 +28,6 @@ class AddOrderScreen extends ConsumerStatefulWidget { class _AddOrderScreenState extends ConsumerState { final _formKey = GlobalKey(); - final _fiatAmountController = TextEditingController(); final _lightningAddressController = TextEditingController(); final _scrollController = ScrollController(); final _customPaymentMethodController = TextEditingController(); @@ -71,28 +70,17 @@ class _AddOrderScreenState extends ConsumerState { @override void dispose() { _scrollController.dispose(); - _fiatAmountController.dispose(); _lightningAddressController.dispose(); _customPaymentMethodController.dispose(); _satsAmountController.dispose(); super.dispose(); } - void _parseFiatAmount(String input) { - if (input.contains('-')) { - final parts = input.split('-'); - if (parts.length == 2) { - setState(() { - _minFiatAmount = int.tryParse(parts[0].trim()); - _maxFiatAmount = int.tryParse(parts[1].trim()); - }); - } - } else { - setState(() { - _minFiatAmount = int.tryParse(input); - _maxFiatAmount = null; - }); - } + void _onAmountChanged(int? minAmount, int? maxAmount) { + setState(() { + _minFiatAmount = minAmount; + _maxFiatAmount = maxAmount; + }); } @override @@ -143,8 +131,7 @@ class _AddOrderScreenState extends ConsumerState { const SizedBox(height: 16), AmountSection( orderType: _orderType, - controller: _fiatAmountController, - onAmountChanged: _parseFiatAmount, + onAmountChanged: _onAmountChanged, ), const SizedBox(height: 16), PaymentMethodsSection( diff --git a/lib/features/order/widgets/amount_section.dart b/lib/features/order/widgets/amount_section.dart index 0ac926b4..fa755edb 100644 --- a/lib/features/order/widgets/amount_section.dart +++ b/lib/features/order/widgets/amount_section.dart @@ -1,67 +1,215 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/features/order/widgets/form_section.dart'; import 'package:mostro_mobile/generated/l10n.dart'; -class AmountSection extends StatelessWidget { +class AmountSection extends StatefulWidget { final OrderType orderType; - final TextEditingController controller; - final Function(String) onAmountChanged; + final Function(int? minAmount, int? maxAmount) onAmountChanged; const AmountSection({ super.key, required this.orderType, - required this.controller, required this.onAmountChanged, }); + @override + State createState() => _AmountSectionState(); +} + +class _AmountSectionState extends State { + final TextEditingController _minAmountController = TextEditingController(); + final TextEditingController _maxAmountController = TextEditingController(); + final FocusNode _maxAmountFocusNode = FocusNode(); + + bool _showSecondInput = false; + bool _isRangeMode = false; + bool _hasUserInteractedWithSecondField = false; + + @override + void initState() { + super.initState(); + + // Listen to min amount changes to show/hide second input + _minAmountController.addListener(_onMinAmountChanged); + + // Listen to focus changes on max amount field to enter range mode + _maxAmountFocusNode.addListener(_onMaxAmountFocusChanged); + } + + @override + void dispose() { + _minAmountController.dispose(); + _maxAmountController.dispose(); + _maxAmountFocusNode.dispose(); + super.dispose(); + } + + void _onMinAmountChanged() { + final hasContent = _minAmountController.text.isNotEmpty; + if (hasContent != _showSecondInput) { + setState(() { + _showSecondInput = hasContent; + if (!hasContent) { + // Reset everything when min amount is cleared + _isRangeMode = false; + _hasUserInteractedWithSecondField = false; + _maxAmountController.clear(); + } + }); + } + _notifyAmountChanged(); + } + + void _onMaxAmountFocusChanged() { + if (_maxAmountFocusNode.hasFocus && !_hasUserInteractedWithSecondField) { + setState(() { + _isRangeMode = true; + _hasUserInteractedWithSecondField = true; + }); + } + } + + void _notifyAmountChanged() { + final minAmount = int.tryParse(_minAmountController.text); + final maxAmount = int.tryParse(_maxAmountController.text); + widget.onAmountChanged(minAmount, maxAmount); + } + + String _getTitle() { + if (_isRangeMode) { + return widget.orderType == OrderType.buy + ? S.of(context)!.creatingRangeOrderBuySend + : S.of(context)!.creatingRangeOrder; + } + return widget.orderType == OrderType.buy + ? S.of(context)!.enterAmountYouWantToSend + : S.of(context)!.enterAmountYouWantToReceive; + } + + Widget? _getTopRightWidget() { + if (_isRangeMode) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF8CC63F), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + S.of(context)!.rangeOrder, + style: const TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ); + } + return null; + } + + String? _validateMinAmount(String? value) { + if (value == null || value.isEmpty) { + return S.of(context)!.pleaseEnterAmount; + } + if (int.tryParse(value) == null) { + return S.of(context)!.pleaseEnterValidAmount; + } + return null; + } + + String? _validateMaxAmount(String? value) { + if (value == null || value.isEmpty) { + return null; // Max amount is optional + } + if (int.tryParse(value) == null) { + return S.of(context)!.pleaseEnterValidAmount; + } + + final minAmount = int.tryParse(_minAmountController.text); + final maxAmount = int.tryParse(value); + if (minAmount != null && maxAmount != null && maxAmount <= minAmount) { + return S.of(context)!.maxMustBeGreaterThanMin; + } + return null; + } + @override Widget build(BuildContext context) { return FormSection( - title: orderType == OrderType.buy - ? S.of(context)!.enterFiatAmountBuy - : S.of(context)!.enterFiatAmountSell, - icon: const Icon(Icons.credit_card, color: Color(0xFF8CC63F), size: 18), + title: _getTitle(), + topRightWidget: _getTopRightWidget(), + icon: const Icon(Icons.money, color: Color(0xFF8CC63F), size: 18), iconBackgroundColor: const Color(0xFF8CC63F).withValues(alpha: 0.3), - child: TextFormField( - key: const Key('fiatAmountField'), - controller: controller, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - border: InputBorder.none, - hintText: S.of(context)!.enterAmountHint, - hintStyle: const TextStyle(color: Colors.grey), - ), - keyboardType: const TextInputType.numberWithOptions(signed: true), - onChanged: onAmountChanged, - validator: (value) { - if (value == null || value.isEmpty) { - return S.of(context)!.pleaseEnterAmount; - } - - // Regex to match either a single number or a range format (number-number) - // The regex allows optional spaces around the hyphen - final regex = RegExp(r'^\d+$|^\d+\s*-\s*\d+$'); - - if (!regex.hasMatch(value)) { - return S.of(context)!.pleaseEnterValidAmount; - } - - // If it's a range, check that the first number is less than the second - if (value.contains('-')) { - final parts = value.split('-'); - final firstNum = int.tryParse(parts[0].trim()); - final secondNum = int.tryParse(parts[1].trim()); - - if (firstNum != null && - secondNum != null && - firstNum >= secondNum) { - return S.of(context)!.rangeFirstLowerThanSecond; - } - } - - return null; - }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Input row (min amount + optional max amount) + Row( + children: [ + // Min amount input + Expanded( + flex: _showSecondInput ? 2 : 1, + child: TextFormField( + key: const Key('minAmountField'), + controller: _minAmountController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + border: InputBorder.none, + hintText: S.of(context)!.enterAmountHint, + hintStyle: const TextStyle(color: Colors.grey), + ), + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + validator: _validateMinAmount, + onChanged: (_) => _notifyAmountChanged(), + ), + ), + + // "to" label and max amount input (shown after first digit) + if (_showSecondInput) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + S.of(context)!.to, + style: const TextStyle(color: Colors.grey, fontSize: 16), + ), + ), + Expanded( + flex: 2, + child: TextFormField( + key: const Key('maxAmountField'), + controller: _maxAmountController, + focusNode: _maxAmountFocusNode, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + border: InputBorder.none, + hintText: S.of(context)!.maxAmount, + hintStyle: const TextStyle(color: Colors.grey), + ), + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + validator: _validateMaxAmount, + onChanged: (_) => _notifyAmountChanged(), + ), + ), + ], + ], + ), + + // Tip text (shown when second input is visible but user hasn't tapped it) + if (_showSecondInput && !_hasUserInteractedWithSecondField) ...[ + const SizedBox(height: 8), + Text( + S.of(context)!.tapSecondFieldForRange, + style: const TextStyle( + color: Color(0xFF8CC63F), + fontSize: 12, + ), + ), + ], + ], ), ); } diff --git a/lib/features/order/widgets/form_section.dart b/lib/features/order/widgets/form_section.dart index b526d99f..96c7b07d 100644 --- a/lib/features/order/widgets/form_section.dart +++ b/lib/features/order/widgets/form_section.dart @@ -10,6 +10,7 @@ class FormSection extends StatelessWidget { final Widget? extraContent; final String? infoTooltip; final String? infoTitle; + final Widget? topRightWidget; const FormSection({ super.key, @@ -20,6 +21,7 @@ class FormSection extends StatelessWidget { this.extraContent, this.infoTooltip, this.infoTitle, + this.topRightWidget, }); @override @@ -62,6 +64,10 @@ class FormSection extends StatelessWidget { ), ), ], + if (topRightWidget != null) ...[ + const SizedBox(width: 8), + topRightWidget!, + ], ], ), ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 51b9599a..1f883963 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -353,12 +353,18 @@ "youWantToSellBitcoin": "You want to sell Bitcoin", "lightningAddressOptional": "Lightning Address (optional)", "enterLightningAddress": "Enter lightning address", - "enterFiatAmountBuy": "Enter the fiat amount you want to pay (you can set a range)", - "enterFiatAmountSell": "Enter the fiat amount you want to receive (you can set a range)", - "enterAmountHint": "Enter amount (example: 100 or 100-500)", + "enterAmountHint": "Enter amount", "pleaseEnterAmount": "Please enter an amount", - "pleaseEnterValidAmount": "Please enter a valid amount (e.g., 100) or range (e.g., 100-500)", - "rangeFirstLowerThanSecond": "In a range, the first number must be less than the second", + "pleaseEnterValidAmount": "Please enter a valid amount", + "enterAmountYouWantToReceive": "Enter amount you want to receive", + "enterAmountYouWantToSend": "Enter amount you want to send", + "creatingRangeOrder": "Creating a range order (you can receive between min and max amounts)", + "creatingRangeOrderBuySend": "Creating a range order (you can send between min and max amounts)", + "rangeOrder": "Range Order", + "maxAmount": "Max amount", + "tapSecondFieldForRange": "💡 Tap the second field to create a range order", + "to": "to", + "maxMustBeGreaterThanMin": "Maximum amount must be greater than minimum amount", "paymentMethodsForCurrency": "Payment methods for {currency}", "enterCustomPaymentMethod": "Enter custom payment method", "loadingPaymentMethods": "Loading payment methods...", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 12e9c782..c2f4eb27 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -309,12 +309,18 @@ "youWantToSellBitcoin": "Quieres vender Bitcoin", "lightningAddressOptional": "Lightning Address (opcional)", "enterLightningAddress": "Introduzca una lightning address", - "enterFiatAmountBuy": "Ingresa la cantidad fiat que quieres pagar (puedes establecer un rango)", - "enterFiatAmountSell": "Ingresa la cantidad fiat que quieres recibir (puedes establecer un rango)", - "enterAmountHint": "Ingresa cantidad (ejemplo: 100 o 100-500)", + "enterAmountHint": "Ingresa cantidad", "pleaseEnterAmount": "Por favor ingresa una cantidad", - "pleaseEnterValidAmount": "Por favor ingresa una cantidad válida (ej., 100) o rango (ej., 100-500)", - "rangeFirstLowerThanSecond": "En un rango, el primer número debe ser menor que el segundo", + "pleaseEnterValidAmount": "Por favor ingresa una cantidad válida", + "enterAmountYouWantToReceive": "Ingresa la cantidad que quieres recibir", + "enterAmountYouWantToSend": "Ingresa la cantidad que quieres enviar", + "creatingRangeOrder": "Creando orden de rango (puedes recibir entre cantidades mínima y máxima)", + "creatingRangeOrderBuySend": "Creando orden de rango (puedes enviar entre cantidades mínima y máxima)", + "rangeOrder": "Orden de Rango", + "maxAmount": "Cantidad máxima", + "tapSecondFieldForRange": "💡 Toca el segundo campo para crear una orden de rango", + "to": "a", + "maxMustBeGreaterThanMin": "La cantidad máxima debe ser mayor que la cantidad mínima", "paymentMethodsForCurrency": "Métodos de pago para {currency}", "enterCustomPaymentMethod": "Ingresa método de pago personalizado", "loadingPaymentMethods": "Cargando métodos de pago...", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 2112213c..dec2ee53 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -314,12 +314,18 @@ "youWantToSellBitcoin": "Vuoi vendere Bitcoin", "lightningAddressOptional": "Lightning Address (opzionale)", "enterLightningAddress": "Inserisci lightning address", - "enterFiatAmountBuy": "Inserisci l'importo fiat che vuoi pagare (puoi impostare un intervallo)", - "enterFiatAmountSell": "Inserisci l'importo fiat che vuoi ricevere (puoi impostare un intervallo)", - "enterAmountHint": "Inserisci importo (esempio: 100 o 100-500)", + "enterAmountHint": "Inserisci importo", "pleaseEnterAmount": "Per favore inserisci un importo", - "pleaseEnterValidAmount": "Per favore inserisci un importo valido (es., 100) o intervallo (es., 100-500)", - "rangeFirstLowerThanSecond": "In un intervallo, il primo numero deve essere minore del secondo", + "pleaseEnterValidAmount": "Per favore inserisci un importo valido", + "enterAmountYouWantToReceive": "Inserisci l'importo che vuoi ricevere", + "enterAmountYouWantToSend": "Inserisci l'importo che vuoi inviare", + "creatingRangeOrder": "Creazione ordine intervallo (puoi ricevere tra importi minimo e massimo)", + "creatingRangeOrderBuySend": "Creazione ordine intervallo (puoi inviare tra importi minimo e massimo)", + "rangeOrder": "Ordine Intervallo", + "maxAmount": "Importo massimo", + "tapSecondFieldForRange": "💡 Tocca il secondo campo per creare un ordine intervallo", + "to": "a", + "maxMustBeGreaterThanMin": "L'importo massimo deve essere maggiore dell'importo minimo", "paymentMethodsForCurrency": "Metodi di pagamento per {currency}", "enterCustomPaymentMethod": "Inserisci metodo di pagamento personalizzato", "loadingPaymentMethods": "Caricamento metodi di pagamento...", diff --git a/lib/shared/widgets/currency_text_field.dart b/lib/shared/widgets/currency_text_field.dart deleted file mode 100644 index 3a45754b..00000000 --- a/lib/shared/widgets/currency_text_field.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; - -class CurrencyTextField extends StatefulWidget { - final TextEditingController controller; - final String label; - final ValueChanged<(int?, int?)>? onChanged; - - /// If a single integer is entered, do we treat min and max as the same number (true) - /// or do we treat max as null (false)? - final bool singleValueSetsMaxSameAsMin; - - const CurrencyTextField({ - super.key, - required this.controller, - required this.label, - this.singleValueSetsMaxSameAsMin = false, - this.onChanged, - }); - - @override - State createState() => CurrencyTextFieldState(); -} - -class CurrencyTextFieldState extends State { - (int?, int?) _parseInput() { - final text = widget.controller.text.trim(); - if (text.isEmpty) return (null, null); - if (text.contains('-')) { - final parts = text.split('-'); - if (parts.length == 2) { - final minVal = int.tryParse(parts[0].trim()); - final maxVal = int.tryParse(parts[1].trim()); - return (minVal, maxVal); - } else { - return (null, null); - } - } else { - final value = int.tryParse(text); - return (value, widget.singleValueSetsMaxSameAsMin ? value : null); - } - } - - @override - Widget build(BuildContext context) { - return TextFormField( - controller: widget.controller, - style: const TextStyle(color: AppTheme.cream1), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^[0-9]*-?[0-9]*$')), - ], - decoration: InputDecoration( - labelText: widget.label, - ), - onChanged: (value) { - final parsed = _parseInput(); - if (widget.onChanged != null) widget.onChanged!(parsed); - }, - validator: (value) { - final (minVal, maxVal) = _parseInput(); - if (minVal == null && maxVal == null) { - return 'Invalid number or range'; - } - if (minVal != null && maxVal != null && minVal > maxVal) { - return 'Minimum cannot exceed maximum'; - } - return null; - }, - ); - } -}