From 0cb5101ed91fd59bd0094c02b7c60b413a2d7280 Mon Sep 17 00:00:00 2001 From: Arseni Date: Mon, 12 Jan 2026 21:24:58 +0300 Subject: [PATCH] Card expiry date and few small fixes --- .../lib/models/payment/methods/card.dart | 4 +- .../pshared/lib/provider/permissions.dart | 1 + frontend/pweb/lib/l10n/en.arb | 1 + frontend/pweb/lib/l10n/ru.arb | 1 + .../lib/pages/payment_methods/add/card.dart | 51 ++++++++++++++++++- .../payment_methods/payment_page/page.dart | 2 +- frontend/pweb/lib/utils/payment/dropdown.dart | 2 +- 7 files changed, 57 insertions(+), 5 deletions(-) diff --git a/frontend/pshared/lib/models/payment/methods/card.dart b/frontend/pshared/lib/models/payment/methods/card.dart index ca7b164..8daf69c 100644 --- a/frontend/pshared/lib/models/payment/methods/card.dart +++ b/frontend/pshared/lib/models/payment/methods/card.dart @@ -16,8 +16,8 @@ class CardPaymentMethod implements PaymentMethodData { const CardPaymentMethod({ required this.pan, - this.expMonth, - this.expYear, + required this.expMonth, + required this.expYear, required this.firstName, required this.lastName, this.country, diff --git a/frontend/pshared/lib/provider/permissions.dart b/frontend/pshared/lib/provider/permissions.dart index 3e8e20f..3d2e29f 100644 --- a/frontend/pshared/lib/provider/permissions.dart +++ b/frontend/pshared/lib/provider/permissions.dart @@ -165,6 +165,7 @@ class PermissionsProvider extends ChangeNotifier { perm.Action? action, Object? objectRef, }) { + if (!_organizations.isOrganizationSet) return false; final orgId = _organizations.current.id; final pd = policyDescriptions.firstWhereOrNull( (policy) => diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 91d4d02..5131500 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -360,6 +360,7 @@ "enterBik": "Enter BIK", "add": "Add", "expiryDate": "Expiry (MM/YY)", + "enterExpiryDate": "Enter card expiry date", "firstName": "First Name", "enterFirstName": "Enter First Name", "lastName": "Last Name", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index a53bf96..95b69f3 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -360,6 +360,7 @@ "enterBik": "Введите БИК", "add": "Добавить", "expiryDate": "Срок действия (ММ/ГГ)", + "enterExpiryDate": "Введите срок действия карты", "firstName": "Имя", "enterFirstName": "Введите имя", "lastName": "Фамилия", diff --git a/frontend/pweb/lib/pages/payment_methods/add/card.dart b/frontend/pweb/lib/pages/payment_methods/add/card.dart index 6f27397..ecb5d19 100644 --- a/frontend/pweb/lib/pages/payment_methods/add/card.dart +++ b/frontend/pweb/lib/pages/payment_methods/add/card.dart @@ -30,6 +30,7 @@ class _CardFormMinimalState extends State { late TextEditingController _panController; late TextEditingController _firstNameController; late TextEditingController _lastNameController; + late TextEditingController _expiryController; @override void initState() { @@ -37,17 +38,23 @@ class _CardFormMinimalState extends State { _panController = TextEditingController(text: widget.initialData?.pan ?? ''); _firstNameController = TextEditingController(text: widget.initialData?.firstName ?? ''); _lastNameController = TextEditingController(text: widget.initialData?.lastName ?? ''); + _expiryController = TextEditingController(text: _formatExpiry(widget.initialData)); WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); } void _emitIfValid() { if (_formKey.currentState?.validate() ?? false) { + final expiry = _parseExpiry(_expiryController.text); + if (expiry == null) return; + widget.onChanged( CardPaymentMethod( pan: _panController.text.replaceAll(' ', ''), firstName: _firstNameController.text, lastName: _lastNameController.text, + expMonth: expiry.month, + expYear: expiry.year, ), ); } @@ -63,6 +70,7 @@ class _CardFormMinimalState extends State { _panController.clear(); _firstNameController.clear(); _lastNameController.clear(); + _expiryController.clear(); return; } @@ -70,12 +78,14 @@ class _CardFormMinimalState extends State { final hasPanChange = newData.pan != _panController.text; final hasFirstNameChange = newData.firstName != _firstNameController.text; final hasLastNameChange = newData.lastName != _lastNameController.text; + final hasExpiryChange = _formatExpiry(newData) != _expiryController.text; if (hasPanChange) _panController.text = newData.pan; if (hasFirstNameChange) _firstNameController.text = newData.firstName; if (hasLastNameChange) _lastNameController.text = newData.lastName; + if (hasExpiryChange) _expiryController.text = _formatExpiry(newData); - if (hasPanChange || hasFirstNameChange || hasLastNameChange) { + if (hasPanChange || hasFirstNameChange || hasLastNameChange || hasExpiryChange) { WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid()); } } @@ -115,6 +125,16 @@ class _CardFormMinimalState extends State { style: getTextFieldStyle(context, widget.isEditable), validator: (v) => (v == null || v.isEmpty) ? l10n.enterLastName : null, ), + const SizedBox(height: 12), + TextFormField( + readOnly: !widget.isEditable, + controller: _expiryController, + decoration: getInputDecoration(context, l10n.expiryDate, widget.isEditable), + style: getTextFieldStyle(context, widget.isEditable), + keyboardType: TextInputType.number, + inputFormatters: [CreditCardExpirationDateFormatter()], + validator: (v) => _parseExpiry(v ?? '') == null ? l10n.enterExpiryDate : null, + ), ], ), ); @@ -125,6 +145,35 @@ class _CardFormMinimalState extends State { _panController.dispose(); _firstNameController.dispose(); _lastNameController.dispose(); + _expiryController.dispose(); super.dispose(); } + + String _formatExpiry(CardPaymentMethod? data) { + if (data == null) return ''; + if (data.expMonth == null || data.expYear == null) return ''; + final month = data.expMonth!.toString().padLeft(2, '0'); + final year = (data.expYear! % 100).toString().padLeft(2, '0'); + return '$month/$year'; + } + + _CardExpiry? _parseExpiry(String value) { + final normalized = value.trim(); + final match = RegExp(r'^(\d{2})\s*/\s*(\d{2})$').firstMatch(normalized); + if (match == null) return null; + + final month = int.tryParse(match.group(1)!); + final year = int.tryParse(match.group(2)!); + if (month == null || year == null || month < 1 || month > 12) return null; + + final normalizedYear = 2000 + year; + return _CardExpiry(month: month, year: normalizedYear); + } +} + +class _CardExpiry { + final int month; + final int year; + + const _CardExpiry({required this.month, required this.year}); } diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart index 3096cb8..c94c363 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/page.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/recipient/provider.dart'; -import 'package:pweb/pages/dashboard/payouts/form.dart'; +import 'package:pweb/pages/dashboard/payouts/form.dart'; import 'package:pweb/pages/payment_methods/payment_page/back_button.dart'; import 'package:pweb/pages/payment_methods/payment_page/header.dart'; import 'package:pweb/pages/payment_methods/payment_page/method_selector.dart'; diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart index eb237d0..291b30c 100644 --- a/frontend/pweb/lib/utils/payment/dropdown.dart +++ b/frontend/pweb/lib/utils/payment/dropdown.dart @@ -23,7 +23,7 @@ class PaymentMethodDropdown extends StatelessWidget { @override Widget build(BuildContext context) => DropdownButtonFormField( dropdownColor: Theme.of(context).colorScheme.onSecondary, - value: _getSelectedMethod(), + initialValue: _getSelectedMethod(), decoration: InputDecoration( labelText: AppLocalizations.of(context)!.whereGetMoney, border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),