Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions lib/features/order/screens/add_order_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class AddOrderScreen extends ConsumerStatefulWidget {

class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
final _formKey = GlobalKey<FormState>();
final _fiatAmountController = TextEditingController();
final _lightningAddressController = TextEditingController();
final _scrollController = ScrollController();
final _customPaymentMethodController = TextEditingController();
Expand Down Expand Up @@ -71,28 +70,17 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
@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
Expand Down Expand Up @@ -143,8 +131,7 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
const SizedBox(height: 16),
AmountSection(
orderType: _orderType,
controller: _fiatAmountController,
onAmountChanged: _parseFiatAmount,
onAmountChanged: _onAmountChanged,
),
const SizedBox(height: 16),
PaymentMethodsSection(
Expand Down
242 changes: 195 additions & 47 deletions lib/features/order/widgets/amount_section.dart
Original file line number Diff line number Diff line change
@@ -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<AmountSection> createState() => _AmountSectionState();
}

class _AmountSectionState extends State<AmountSection> {
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,
),
),
],
],
),
);
}
Expand Down
6 changes: 6 additions & 0 deletions lib/features/order/widgets/form_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class FormSection extends StatelessWidget {
final Widget? extraContent;
final String? infoTooltip;
final String? infoTitle;
final Widget? topRightWidget;

const FormSection({
super.key,
Expand All @@ -20,6 +21,7 @@ class FormSection extends StatelessWidget {
this.extraContent,
this.infoTooltip,
this.infoTitle,
this.topRightWidget,
});

@override
Expand Down Expand Up @@ -62,6 +64,10 @@ class FormSection extends StatelessWidget {
),
),
],
if (topRightWidget != null) ...[
const SizedBox(width: 8),
topRightWidget!,
],
],
),
),
Expand Down
16 changes: 11 additions & 5 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down
16 changes: 11 additions & 5 deletions lib/l10n/intl_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down
Loading