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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract final class CardFormValidators {
return AppStrings.subscriptionsPaymentExpiryMonthHint;
}

final year = int.tryParse(_trimmed(yearValue));
final year = _parseFullYear(yearValue);
if (year != null) {
final currentDate = now ?? DateTime.now();
if (_isExpired(month: month, year: year, now: currentDate) ||
Expand All @@ -67,11 +67,11 @@ abstract final class CardFormValidators {
if (trimmed.isEmpty) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}
if (trimmed.length != 4) {
if (trimmed.length != 2) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}

final year = int.tryParse(trimmed);
final year = _parseFullYear(trimmed);
if (year == null) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}
Expand Down Expand Up @@ -107,6 +107,16 @@ abstract final class CardFormValidators {

static String _digitsOnly(String? value) => (value ?? '').replaceAll(RegExp(r'\D'), '');

static int? _parseFullYear(String? value) {
final trimmed = _trimmed(value);
if (trimmed.length != 2) return null;

final shortYear = int.tryParse(trimmed);
if (shortYear == null) return null;

return 2000 + shortYear;
}

static bool _isExpired({
required int month,
required int year,
Expand Down
37 changes: 35 additions & 2 deletions lib/features/cards/presentation/widgets/save_card_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ class _SaveCardDialogState extends State<SaveCardDialog> {
final form = _formKey.currentState;
if (form == null || !form.validate()) return;

final expiryYear = _expiryYearController.text.trim();

context.read<SaveCardCubit>().saveCard(
payload: SaveCardPayload(
cardNumber: _cardNumberController.text.replaceAll(RegExp(r'\D'), ''),
cardHolder: _normalizedCardHolder,
expiryMonth: _expiryMonthController.text.trim(),
expiryYear: _expiryYearController.text.trim(),
expiryYear: _buildBackendExpiryYear(expiryYear),
),
);
}
Expand Down Expand Up @@ -220,7 +222,7 @@ class _SaveCardDialogState extends State<SaveCardDialog> {
showLabel: false,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
const _ExpiryYearTextInputFormatter(),
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
validator: (value) => CardFormValidators.expiryYear(
value,
Expand Down Expand Up @@ -289,6 +291,8 @@ class _SaveCardDialogState extends State<SaveCardDialog> {
}
}

String _buildBackendExpiryYear(String value) => value.isEmpty ? value : '20$value';

final class _CardPreview extends StatelessWidget {
final TextEditingController previewCardNumberController;
final String cardHolderValue;
Expand Down Expand Up @@ -488,3 +492,32 @@ final class _CardHolderTextInputFormatter extends TextInputFormatter {
extension on String {
String ifEmpty(String fallback) => isEmpty ? fallback : this;
}

final class _ExpiryYearTextInputFormatter extends TextInputFormatter {
const _ExpiryYearTextInputFormatter();

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');

if (digits.length <= 2) {
return TextEditingValue(
text: digits,
selection: TextSelection.collapsed(offset: digits.length),
);
}

if (digits.length == 4 && digits.startsWith('20')) {
final shortYear = digits.substring(2);
return TextEditingValue(
text: shortYear,
selection: TextSelection.collapsed(offset: shortYear.length),
);
}

return oldValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract final class SubscriptionPaymentValidators {
return AppStrings.subscriptionsPaymentExpiryMonthHint;
}

final year = int.tryParse(_trimmed(yearValue));
final year = _parseFullYear(yearValue);
if (year != null) {
final currentDate = now ?? DateTime.now();
if (_isExpired(month: month, year: year, now: currentDate) ||
Expand All @@ -67,11 +67,11 @@ abstract final class SubscriptionPaymentValidators {
if (trimmed.isEmpty) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}
if (trimmed.length != 4) {
if (trimmed.length != 2) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}

final year = int.tryParse(trimmed);
final year = _parseFullYear(trimmed);
if (year == null) {
return AppStrings.subscriptionsPaymentExpiryYearHint;
}
Expand Down Expand Up @@ -107,6 +107,16 @@ abstract final class SubscriptionPaymentValidators {

static String _digitsOnly(String? value) => (value ?? '').replaceAll(RegExp(r'\D'), '');

static int? _parseFullYear(String? value) {
final trimmed = _trimmed(value);
if (trimmed.length != 2) return null;

final shortYear = int.tryParse(trimmed);
if (shortYear == null) return null;

return 2000 + shortYear;
}

static bool _isExpired({
required int month,
required int year,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ class _SubscriptionPaymentDialogState extends State<SubscriptionPaymentDialog> {
final form = _formKey.currentState;
if (form == null || !form.validate()) return;

final expiryYear = _expiryYearController.text.trim();

context.read<SubscriptionPaymentCubit>().pay(
payload: SubscriptionPaymentPayload(
subscriptionId: widget.item.id,
saveCard: _rememberData,
cardNumber: _cardNumberController.text.replaceAll(RegExp(r'\D'), ''),
cardHolder: _cardHolderController.text.trim(),
expiryMonth: _expiryMonthController.text.trim(),
expiryYear: _expiryYearController.text.trim(),
expiryYear: _buildBackendExpiryYear(expiryYear),
cvv: _cvvController.text.trim(),
),
);
Expand Down Expand Up @@ -232,7 +234,7 @@ class _SubscriptionPaymentDialogState extends State<SubscriptionPaymentDialog> {
showLabel: false,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
const _ExpiryYearTextInputFormatter(),
],
Comment thread
CowboyGH marked this conversation as resolved.
validator: (value) =>
SubscriptionPaymentValidators.expiryYear(
Expand Down Expand Up @@ -310,6 +312,8 @@ class _SubscriptionPaymentDialogState extends State<SubscriptionPaymentDialog> {
}
}

String _buildBackendExpiryYear(String value) => value.isEmpty ? value : '20$value';

final class _PaymentPreviewCard extends StatelessWidget {
final String previewCardNumber;
final String cardHolder;
Expand Down Expand Up @@ -535,3 +539,32 @@ final class _CardNumberTextInputFormatter extends TextInputFormatter {
);
}
}

final class _ExpiryYearTextInputFormatter extends TextInputFormatter {
const _ExpiryYearTextInputFormatter();

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final digits = newValue.text.replaceAll(RegExp(r'\D'), '');

if (digits.length <= 2) {
return TextEditingValue(
text: digits,
selection: TextSelection.collapsed(offset: digits.length),
);
}

if (digits.length == 4 && digits.startsWith('20')) {
final shortYear = digits.substring(2);
return TextEditingValue(
text: shortYear,
selection: TextSelection.collapsed(offset: shortYear.length),
);
}

return oldValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ void main() {
expect(
CardFormValidators.expiryMonth(
'3',
yearValue: '2026',
yearValue: '26',
now: fixedNow,
),
isNotNull,
Expand All @@ -84,15 +84,15 @@ void main() {
expect(
CardFormValidators.expiryMonth(
'4',
yearValue: '2026',
yearValue: '26',
now: fixedNow,
),
isNull,
);
expect(
CardFormValidators.expiryMonth(
'5',
yearValue: '2026',
yearValue: '26',
now: fixedNow,
),
isNull,
Expand All @@ -103,7 +103,7 @@ void main() {
expect(
CardFormValidators.expiryMonth(
'5',
yearValue: '9999',
yearValue: '99',
now: fixedNow,
),
isNotNull,
Expand All @@ -121,21 +121,21 @@ void main() {
expect(CardFormValidators.expiryYear(' '), invalidMessage);
});

test('returns invalid error when year length is not 4', () {
expect(CardFormValidators.expiryYear('24'), invalidMessage);
test('returns invalid error when year length is not 2', () {
expect(CardFormValidators.expiryYear('2'), invalidMessage);
expect(CardFormValidators.expiryYear('202'), invalidMessage);
expect(CardFormValidators.expiryYear('20245'), invalidMessage);
expect(CardFormValidators.expiryYear('2026'), invalidMessage);
});

test('returns null when year length is 4', () {
expect(CardFormValidators.expiryYear('2026'), isNull);
expect(CardFormValidators.expiryYear(' 2026 '), isNull);
test('returns null when year length is 2', () {
expect(CardFormValidators.expiryYear('26'), isNull);
expect(CardFormValidators.expiryYear(' 26 '), isNull);
});

test('returns hidden error when year is before current year', () {
expect(
CardFormValidators.expiryYear(
'2025',
'25',
now: fixedNow,
),
isNotNull,
Expand All @@ -145,7 +145,7 @@ void main() {
test('returns hidden error when month is already in the past for current year', () {
expect(
CardFormValidators.expiryYear(
'2026',
'26',
monthValue: '3',
now: fixedNow,
),
Expand All @@ -156,15 +156,15 @@ void main() {
test('returns null when current year is paired with current or future month', () {
expect(
CardFormValidators.expiryYear(
'2026',
'26',
monthValue: '4',
now: fixedNow,
),
isNull,
);
expect(
CardFormValidators.expiryYear(
'2026',
'26',
monthValue: '12',
now: fixedNow,
),
Expand All @@ -175,7 +175,7 @@ void main() {
test('returns hidden error when year is unrealistically far in the future', () {
expect(
CardFormValidators.expiryYear(
'9999',
'99',
now: fixedNow,
),
isNotNull,
Expand Down
Loading
Loading