import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lba/res/colors.dart'; import 'package:lba/widgets/button.dart'; class AddCardBottomSheet extends StatefulWidget { const AddCardBottomSheet({super.key}); @override State createState() => _AddCardBottomSheetState(); } class _AddCardBottomSheetState extends State { final TextEditingController _cardNumberController = TextEditingController(); final TextEditingController _expiryDateController = TextEditingController(); bool _isCardNumberValid = false; bool _isExpiryDateValid = false; final FocusNode _cardNumberFocus = FocusNode(); final FocusNode _expiryDateFocus = FocusNode(); Color _borderColor = Colors.grey.shade400; @override void initState() { super.initState(); _cardNumberController.addListener(_validateCardNumber); _expiryDateController.addListener(_validateExpiryDate); // Change border color on focus _cardNumberFocus.addListener(() { setState(() { _borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus ? Theme.of(context).primaryColor : Colors.grey.shade400; }); }); _expiryDateFocus.addListener(() { setState(() { _borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus ? Theme.of(context).primaryColor : Colors.grey.shade400; }); }); } @override void dispose() { _cardNumberController.removeListener(_validateCardNumber); _expiryDateController.removeListener(_validateExpiryDate); _cardNumberController.dispose(); _expiryDateController.dispose(); _cardNumberFocus.dispose(); _expiryDateFocus.dispose(); super.dispose(); } void _validateCardNumber() { final text = _cardNumberController.text.replaceAll('-', ''); setState(() { _isCardNumberValid = text.length == 16; }); } void _validateExpiryDate() { final text = _expiryDateController.text; if (text.length == 5) { final parts = text.split('/'); if (parts.length == 2) { final month = int.tryParse(parts[0]); final year = int.tryParse(parts[1]); if (month != null && year != null) { final now = DateTime.now(); final currentYear = now.year % 100; final currentMonth = now.month; if (month >= 1 && month <= 12 && (year > currentYear || (year == currentYear && month >= currentMonth))) { setState(() { _isExpiryDateValid = true; }); return; } } } } setState(() { _isExpiryDateValid = false; }); } bool get _isFormValid => _isCardNumberValid && _isExpiryDateValid; void _addCard() { if (_isFormValid) { Navigator.pop(context, _cardNumberController.text); } } @override Widget build(BuildContext context) { return SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( padding: const EdgeInsets.all(24.0), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Add card', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87, ), ), const SizedBox(height: 8), const Divider(), const SizedBox(height: 24), AnimatedContainer( duration: const Duration(milliseconds: 200), height: 55, decoration: BoxDecoration( border: Border.all(color: _borderColor, width: 1.5), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Padding( padding: EdgeInsets.symmetric(horizontal: 12.0), child: Icon(Icons.credit_card, color: Colors.grey), ), Expanded( child: TextFormField( controller: _cardNumberController, focusNode: _cardNumberFocus, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(16), _CardNumberInputFormatter(), ], decoration: const InputDecoration( hintText: '1234-5678-1234-5678', border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 15), ), ), ), SizedBox( width: 80, child: TextFormField( controller: _expiryDateController, focusNode: _expiryDateFocus, textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(4), _ExpiryDateInputFormatter(), ], decoration: const InputDecoration( hintText: 'MM/YY', border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, errorBorder: InputBorder.none, disabledBorder: InputBorder.none, ), ), ), ], ), ), const SizedBox(height: 32), SizedBox( width: double.infinity, height: 50, child: Button( text: 'Add card', onPressed: _isFormValid ? _addCard : () {}, color: _isFormValid ? const Color(0xFFE53935) : Colors.red.shade200, ), ), ], ), ), ); } } class _CardNumberInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { var text = newValue.text; if (newValue.selection.baseOffset == 0) { return newValue; } var buffer = StringBuffer(); for (int i = 0; i < text.length; i++) { buffer.write(text[i]); var nonZeroIndex = i + 1; if (nonZeroIndex % 4 == 0 && nonZeroIndex != text.length) { buffer.write('-'); } } var string = buffer.toString(); return newValue.copyWith( text: string, selection: TextSelection.collapsed(offset: string.length)); } } class _ExpiryDateInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { var newText = newValue.text; if (newValue.selection.baseOffset == 0) { return newValue; } if (oldValue.text.endsWith('/') && oldValue.text.length > newValue.text.length) { newText = newText.substring(0, newText.length - 1); } var buffer = StringBuffer(); for (int i = 0; i < newText.length; i++) { buffer.write(newText[i]); if (i == 1 && newText.length > 2 && !newText.contains('/')) { buffer.write('/'); } } var string = buffer.toString(); return newValue.copyWith( text: string.substring(0, string.length > 5 ? 5 : string.length), selection: TextSelection.collapsed( offset: string.length > 5 ? 5 : string.length, ), ); } }