Skip to content

Commit e943b8f

Browse files
authored
Improve payment methods: expand list, always-visible custom input, and fix submit validation (#455)
* add news payment methods * feat: always show custom payment method input field - Remove Other checkbox requirement to show custom input - Custom text field always visible below payment method selector - Remove showCustomField parameter and _showCustomPaymentMethod state - Simplify onMethodsChanged callback signature - Match custom field UI style with amount input (fontSize, borders, margins) * text size * fix: disable submit button when amount field is empty in order creation - Add _minFiatAmount null check in _getSubmitCallback() - Prevents null check error when submitting without entering an amount - Button stays disabled until currency, amount, and payment method are filled * improve payment methods * add crc payment methods * fix: allow order submission with only custom payment method - Check custom text field in addition to selected methods for submit validation - Add listener to update button state as user types custom payment method * Remove the dead Other injection
1 parent e45a26a commit e943b8f

File tree

4 files changed

+81
-119
lines changed

4 files changed

+81
-119
lines changed

assets/data/payment_methods.json

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
{
2-
"USD": [
3-
"Cash App",
4-
"Venmo",
5-
"Zelle",
6-
"Bank Transfer",
7-
"PayPal"
8-
],
9-
"EUR": ["SEPA", "Revolut", "N26", "Bank Transfer"],
10-
"GBP": ["Bank Transfer", "Revolut", "Monzo"],
11-
"JPY": ["Bank Transfer", "PayPay"],
12-
"VES": ["Pago Móvil", "Binance P2P", "Efectivo"],
13-
"ARS": ["Mercado Pago", "MODO", "CVU", "Belo", "Lemon"],
14-
"CUP": ["Transfermovil", "EnZona", "Efectivo"],
15-
"CLP": ["Transferencia bancaria", "MACH", "Cuenta RUT"],
16-
"COP": ["Nequi", "Daviplata", "PSE", "Bancolombia"],
17-
"MXN": ["SPEI", "CoDi", "Efectivo"],
18-
"default": ["Bank Transfer", "Cash in person", "Other"]
2+
"ARS": ["Mercado Pago", "MODO", "CVU", "Belo", "Lemon", "CBU", "Efectivo"],
3+
"AUD": ["PayID", "Alipay", "Cash deposit", "Revolut", "Cash"],
4+
"BOB": ["QR", "Transferencia Bancaria", "Efectivo"],
5+
"BRL": ["PIX", "TED", "PicPay", "Depósito", "Cash"],
6+
"CAD": ["Interac e-Transfer", "National Bank of Canada", "Any national bank", "Wise", "Revolut", "Cash"],
7+
"CHF": ["TWINT", "Cash"],
8+
"CLP": ["Banco de Chile", "MACH", "Cuenta RUT", "Mercado Pago", "Banco Falabella", "Transferencia bancaria", "Efectivo"],
9+
"CRC": ["Bank transfer (IBAN)", "SINPE Móvil", "Cash"],
10+
"COP": ["Nequi", "Daviplata", "PSE", "Bancolombia", "Llaves BRE-B", "Davivienda", "Efectivo"],
11+
"CUP": ["Transfermovil", "EnZona", "Tarjeta Clásica", "Saldo móvil", "MiTransfer", "Efectivo"],
12+
"EUR": ["SEPA", "Revolut", "N26", "Bank Transfer", "Wise", "SEPA instant", "Bizum", "SEPA bank transfer", "Payoneer", "Cash"],
13+
"GBP": ["Revolut", "Monzo", "Wise", "Lloyds Bank", "Any national bank", "Bank Transfer", "Cash"],
14+
"JPY": ["PayPay", "Bank Transfer", "Cash"],
15+
"MLC": ["Transfermovil", "EnZona"],
16+
"MXN": ["SPEI", "CoDi", "Retiro Cajero BBVA", "Efectivo"],
17+
"NGN": ["Zenith Bank", "Access Bank", "Bank Transfer", "Cash"],
18+
"PEN": ["Yape", "Plin", "BCP", "QR Yape", "Interbank", "Cash"],
19+
"PHP": ["Any national bank", "GCash", "Cash"],
20+
"PYG": ["SIPAP", "Transferencia bancaria", "Efectivo"],
21+
"USD": ["Cash App", "Venmo", "Zelle", "PayPal", "Wise", "Payoneer", "Strike", "Revolut", "Banco Guayaquil", "Banco Pichincha", "Banco Davivienda", "N1CO", "Transfer365", "Cash"],
22+
"VES": ["Pago Móvil", "Binance P2P", "BDV", "Mercantil", "Banesco", "Efectivo"],
23+
"default": ["Bank Transfer", "Cash in person"]
1924
}

lib/features/order/providers/payment_methods_provider.dart

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@ final paymentMethodsForCurrencyProvider =
1717
return paymentMethodsData.when(
1818
data: (data) {
1919
if (data.containsKey(currencyCode)) {
20-
final methods = List<String>.from(data[currencyCode]);
21-
22-
if (!methods.contains('Other')) {
23-
methods.add('Other');
24-
}
25-
return methods;
20+
return List<String>.from(data[currencyCode]);
2621
} else {
2722
return List<String>.from(
28-
data['default'] ?? ['Bank Transfer', 'Cash in person', 'Other']);
23+
data['default'] ?? ['Bank Transfer', 'Cash in person']);
2924
}
3025
},
3126
loading: () => ['Loading...'],
32-
error: (_, __) => ['Bank Transfer', 'Cash in person', 'Other'],
27+
error: (_, __) => ['Bank Transfer', 'Cash in person'],
3328
);
3429
});
3530

lib/features/order/screens/add_order_screen.dart

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
5252
Timer? _fixedPriceRangeErrorTimer;
5353

5454
List<String> _selectedPaymentMethods = [];
55-
bool _showCustomPaymentMethod = false;
5655

5756
@override
5857
void initState() {
5958
super.initState();
59+
_customPaymentMethodController.addListener(_onCustomPaymentMethodChanged);
6060

6161
WidgetsBinding.instance.addPostFrameCallback((_) {
6262
final GoRouterState state = GoRouterState.of(context);
@@ -89,11 +89,16 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
8989
_fixedPriceRangeErrorTimer?.cancel();
9090
_scrollController.dispose();
9191
_lightningAddressController.dispose();
92+
_customPaymentMethodController.removeListener(_onCustomPaymentMethodChanged);
9293
_customPaymentMethodController.dispose();
9394
_satsAmountController.dispose();
9495
super.dispose();
9596
}
9697

98+
void _onCustomPaymentMethodChanged() {
99+
setState(() {});
100+
}
101+
97102
void _onAmountChanged(int? minAmount, int? maxAmount) {
98103
setState(() {
99104
_minFiatAmount = minAmount;
@@ -240,7 +245,6 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
240245
if (previous != null && previous != next && context.mounted) {
241246
setState(() {
242247
_selectedPaymentMethods = [];
243-
_showCustomPaymentMethod = false;
244248
_customPaymentMethodController.clear();
245249
});
246250
}
@@ -303,12 +307,10 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
303307
const SizedBox(height: 16),
304308
PaymentMethodsSection(
305309
selectedMethods: _selectedPaymentMethods,
306-
showCustomField: _showCustomPaymentMethod,
307310
customController: _customPaymentMethodController,
308-
onMethodsChanged: (methods, showCustom) {
311+
onMethodsChanged: (methods) {
309312
setState(() {
310313
_selectedPaymentMethods = methods;
311-
_showCustomPaymentMethod = showCustom;
312314
});
313315
},
314316
),
@@ -420,13 +422,19 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
420422
return null; // Disables button, prevents loading state
421423
}
422424

425+
// Ensure at least a minimum amount is entered
426+
if (_minFiatAmount == null) {
427+
return null;
428+
}
429+
423430
// Check other basic conditions that would prevent submission
424431
final selectedFiatCode = ref.read(selectedFiatCodeProvider);
425432
if (selectedFiatCode == null || selectedFiatCode.isEmpty) {
426433
return null;
427434
}
428435

429-
if (_selectedPaymentMethods.isEmpty) {
436+
if (_selectedPaymentMethods.isEmpty &&
437+
_customPaymentMethodController.text.trim().isEmpty) {
430438
return null;
431439
}
432440

@@ -448,7 +456,8 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
448456
// Now we know selectedFiatCode is non-null and non-empty
449457
final fiatCode = selectedFiatCode;
450458

451-
if (_selectedPaymentMethods.isEmpty) {
459+
if (_selectedPaymentMethods.isEmpty &&
460+
_customPaymentMethodController.text.trim().isEmpty) {
452461
SnackBarHelper.showTopSnackBar(
453462
context,
454463
S.of(context)!.pleaseSelectPaymentMethod,
@@ -502,14 +511,7 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
502511

503512
List<String> paymentMethods =
504513
List<String>.from(_selectedPaymentMethods);
505-
if (_showCustomPaymentMethod &&
506-
_customPaymentMethodController.text.isNotEmpty) {
507-
// Remove localized "Other" (case-insensitive, trimmed) from the list
508-
final localizedOther = S.of(context)!.other.trim().toLowerCase();
509-
paymentMethods.removeWhere(
510-
(method) => method.trim().toLowerCase() == localizedOther,
511-
);
512-
514+
if (_customPaymentMethodController.text.isNotEmpty) {
513515
String sanitizedPaymentMethod = _customPaymentMethodController.text;
514516

515517
final problematicChars = RegExp(r'[,"\\\[\]{}]');

lib/features/order/widgets/payment_methods_section.dart

Lines changed: 39 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import 'package:mostro_mobile/core/app_theme.dart';
88

99
class PaymentMethodsSection extends ConsumerWidget {
1010
final List<String> selectedMethods;
11-
final bool showCustomField;
1211
final TextEditingController customController;
13-
final Function(List<String>, bool) onMethodsChanged;
12+
final Function(List<String>) onMethodsChanged;
1413

1514
/// Helper function to translate payment method names
1615
String _translatePaymentMethod(String method, BuildContext context) {
@@ -29,7 +28,6 @@ class PaymentMethodsSection extends ConsumerWidget {
2928
const PaymentMethodsSection({
3029
super.key,
3130
required this.selectedMethods,
32-
required this.showCustomField,
3331
required this.customController,
3432
required this.onMethodsChanged,
3533
});
@@ -44,26 +42,19 @@ class PaymentMethodsSection extends ConsumerWidget {
4442
title: S.of(context)!.paymentMethodsForCurrency(selectedFiatCode ?? ''),
4543
icon: const Icon(Icons.credit_card, color: AppTheme.mostroGreen, size: 18),
4644
iconBackgroundColor: AppTheme.mostroGreen.withValues(alpha: 0.3),
47-
extraContent: showCustomField
48-
? Padding(
49-
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
50-
child: TextField(
51-
key: const Key('paymentMethodField'),
52-
controller: customController,
53-
style: const TextStyle(color: Colors.white),
54-
decoration: InputDecoration(
55-
hintText: S.of(context)!.enterCustomPaymentMethod,
56-
hintStyle: TextStyle(color: Colors.grey),
57-
enabledBorder: UnderlineInputBorder(
58-
borderSide: BorderSide(color: Colors.white24),
59-
),
60-
focusedBorder: UnderlineInputBorder(
61-
borderSide: BorderSide(color: AppTheme.mostroGreen),
62-
),
63-
),
64-
),
65-
)
66-
: null,
45+
extraContent: Padding(
46+
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
47+
child: TextField(
48+
key: const Key('paymentMethodField'),
49+
controller: customController,
50+
style: const TextStyle(color: Colors.white, fontSize: 15),
51+
decoration: InputDecoration(
52+
border: InputBorder.none,
53+
hintText: S.of(context)!.enterCustomPaymentMethod,
54+
hintStyle: TextStyle(color: Colors.grey, fontSize: 13),
55+
),
56+
),
57+
),
6758
child: paymentMethodsData.when(
6859
loading: () => Text(S.of(context)!.loadingPaymentMethods,
6960
style: const TextStyle(color: Colors.white)),
@@ -92,9 +83,7 @@ class PaymentMethodsSection extends ConsumerWidget {
9283
context,
9384
availableMethods,
9485
selectedMethods,
95-
showCustomField,
9686
onMethodsChanged,
97-
customController,
9887
);
9988
},
10089
child: Row(
@@ -107,6 +96,7 @@ class PaymentMethodsSection extends ConsumerWidget {
10796
style: TextStyle(
10897
color:
10998
selectedMethods.isEmpty ? Colors.grey : Colors.white,
99+
fontSize: 16,
110100
),
111101
),
112102
),
@@ -123,21 +113,20 @@ class PaymentMethodsSection extends ConsumerWidget {
123113
BuildContext context,
124114
List<String> availableMethods,
125115
List<String> selectedMethods,
126-
bool showCustomField,
127-
Function(List<String>, bool) onMethodsChanged,
128-
TextEditingController customController,
116+
Function(List<String>) onMethodsChanged,
129117
) {
118+
// Remove "Other" from available methods since custom field is always visible
130119
final translatedOther = _translatePaymentMethod('Other', context);
131-
if (!availableMethods.contains(translatedOther)) {
132-
availableMethods = [...availableMethods, translatedOther];
133-
}
120+
availableMethods = availableMethods
121+
.where((m) => m != translatedOther)
122+
.toList();
134123

135124
// Normalize to current locale so checkbox states align with localized labels
136125
final localizedSelected = selectedMethods
137126
.map((m) => _translatePaymentMethod(m, context))
127+
.where((m) => m != translatedOther)
138128
.toList();
139129
List<String> dialogSelectedMethods = List<String>.from(localizedSelected);
140-
bool dialogShowOtherField = dialogSelectedMethods.contains(translatedOther);
141130

142131
showDialog(
143132
context: context,
@@ -155,51 +144,23 @@ class PaymentMethodsSection extends ConsumerWidget {
155144
child: SingleChildScrollView(
156145
child: Column(
157146
mainAxisSize: MainAxisSize.min,
158-
children: [
159-
...availableMethods.map((method) => CheckboxListTile(
160-
title: Text(method,
161-
style: const TextStyle(color: Colors.white)),
162-
value: dialogSelectedMethods.contains(method),
163-
activeColor: AppTheme.mostroGreen,
164-
checkColor: Colors.black,
165-
contentPadding: EdgeInsets.zero,
166-
onChanged: (selected) {
167-
setDialogState(() {
168-
if (selected == true) {
169-
dialogSelectedMethods.add(method);
170-
if (method == translatedOther) {
171-
dialogShowOtherField = true;
172-
}
173-
} else {
174-
dialogSelectedMethods.remove(method);
175-
if (method == translatedOther) {
176-
dialogShowOtherField = false;
177-
}
178-
}
179-
});
180-
},
181-
)),
182-
if (dialogShowOtherField) ...[
183-
const SizedBox(height: 16),
184-
// Use the existing controller directly to avoid memory leaks and cursor position loss
185-
TextField(
186-
controller: customController,
187-
style: const TextStyle(color: Colors.white),
188-
decoration: InputDecoration(
189-
hintText: S.of(context)!.enterCustomPaymentMethod,
190-
hintStyle: TextStyle(color: Colors.grey),
191-
enabledBorder: UnderlineInputBorder(
192-
borderSide: BorderSide(color: Colors.white24),
193-
),
194-
focusedBorder: UnderlineInputBorder(
195-
borderSide: BorderSide(color: AppTheme.mostroGreen),
196-
),
197-
),
198-
// No need for an onChanged handler that updates the controller
199-
// as the controller will automatically update its text
200-
),
201-
],
202-
],
147+
children: availableMethods.map((method) => CheckboxListTile(
148+
title: Text(method,
149+
style: const TextStyle(color: Colors.white)),
150+
value: dialogSelectedMethods.contains(method),
151+
activeColor: AppTheme.mostroGreen,
152+
checkColor: Colors.black,
153+
contentPadding: EdgeInsets.zero,
154+
onChanged: (selected) {
155+
setDialogState(() {
156+
if (selected == true) {
157+
dialogSelectedMethods.add(method);
158+
} else {
159+
dialogSelectedMethods.remove(method);
160+
}
161+
});
162+
},
163+
)).toList(),
203164
),
204165
),
205166
),
@@ -217,8 +178,7 @@ class PaymentMethodsSection extends ConsumerWidget {
217178
),
218179
ElevatedButton(
219180
onPressed: () {
220-
onMethodsChanged(
221-
dialogSelectedMethods, dialogShowOtherField);
181+
onMethodsChanged(dialogSelectedMethods);
222182
Navigator.of(context).pop();
223183
},
224184
style: ElevatedButton.styleFrom(

0 commit comments

Comments
 (0)