180 lines
6.2 KiB
Dart
180 lines
6.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_multi_formatter/flutter_multi_formatter.dart';
|
|
|
|
import 'package:pshared/models/payment/methods/card.dart';
|
|
|
|
import 'package:pweb/utils/text_field_styles.dart';
|
|
|
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
|
|
|
|
class CardFormMinimal extends StatefulWidget {
|
|
final void Function(CardPaymentMethod) onChanged;
|
|
final CardPaymentMethod? initialData;
|
|
final bool isEditable;
|
|
|
|
const CardFormMinimal({
|
|
super.key,
|
|
required this.onChanged,
|
|
this.initialData,
|
|
required this.isEditable,
|
|
});
|
|
|
|
@override
|
|
State<CardFormMinimal> createState() => _CardFormMinimalState();
|
|
}
|
|
|
|
class _CardFormMinimalState extends State<CardFormMinimal> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late TextEditingController _panController;
|
|
late TextEditingController _firstNameController;
|
|
late TextEditingController _lastNameController;
|
|
late TextEditingController _expiryController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_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,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant CardFormMinimal oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
final newData = widget.initialData;
|
|
final oldData = oldWidget.initialData;
|
|
|
|
if (newData == null && oldData != null) {
|
|
_panController.clear();
|
|
_firstNameController.clear();
|
|
_lastNameController.clear();
|
|
_expiryController.clear();
|
|
return;
|
|
}
|
|
|
|
if (newData != null && newData != oldData) {
|
|
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 || hasExpiryChange) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _emitIfValid());
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return Form(
|
|
key: _formKey,
|
|
onChanged: _emitIfValid,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
readOnly: !widget.isEditable,
|
|
controller: _panController,
|
|
decoration: getInputDecoration(context, l10n.cardNumber, widget.isEditable),
|
|
style: getTextFieldStyle(context, widget.isEditable),
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [CreditCardNumberInputFormatter()],
|
|
validator: (v) => (v == null || v.replaceAll(' ', '').length < 12) ? l10n.enterCardNumber : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
readOnly: !widget.isEditable,
|
|
controller: _firstNameController,
|
|
decoration: getInputDecoration(context, l10n.firstName, widget.isEditable),
|
|
style: getTextFieldStyle(context, widget.isEditable),
|
|
validator: (v) => (v == null || v.isEmpty) ? l10n.enterFirstName : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
readOnly: !widget.isEditable,
|
|
controller: _lastNameController,
|
|
decoration: getInputDecoration(context, l10n.lastName, widget.isEditable),
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_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});
|
|
}
|