263 lines
8.4 KiB
Dart
263 lines
8.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:lba/gen/assets.gen.dart';
|
|
import 'package:lba/widgets/button.dart';
|
|
import 'package:lba/res/colors.dart';
|
|
|
|
class AddCardBottomSheet extends StatefulWidget {
|
|
const AddCardBottomSheet({super.key});
|
|
|
|
@override
|
|
State<AddCardBottomSheet> createState() => _AddCardBottomSheetState();
|
|
}
|
|
|
|
class _AddCardBottomSheetState extends State<AddCardBottomSheet> {
|
|
final TextEditingController _cardNumberController = TextEditingController();
|
|
final TextEditingController _expiryDateController = TextEditingController();
|
|
|
|
bool _isCardNumberValid = false;
|
|
bool _isExpiryDateValid = false;
|
|
final FocusNode _cardNumberFocus = FocusNode();
|
|
final FocusNode _expiryDateFocus = FocusNode();
|
|
Color _borderColor = AppColors.greyBorder;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_cardNumberController.addListener(_validateCardNumber);
|
|
_expiryDateController.addListener(_validateExpiryDate);
|
|
|
|
_cardNumberFocus.addListener(() {
|
|
setState(() {
|
|
_borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus
|
|
? Theme.of(context).primaryColor
|
|
: AppColors.greyBorder;
|
|
});
|
|
});
|
|
_expiryDateFocus.addListener(() {
|
|
setState(() {
|
|
_borderColor = _cardNumberFocus.hasFocus || _expiryDateFocus.hasFocus
|
|
? Theme.of(context).primaryColor
|
|
: AppColors.greyBorder;
|
|
});
|
|
});
|
|
}
|
|
|
|
@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: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Add card',
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppColors.textPrice,
|
|
),
|
|
),
|
|
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: [
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.0),
|
|
child: SvgPicture.asset(Assets.icons.card.path, width: 24, height: 24),
|
|
),
|
|
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
|
|
? AppColors.errorColor
|
|
: AppColors.errorColor.withOpacity(0.5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
} |