import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import '../generated/i18n/app_localizations.dart'; /// Birthday entry with a canonical date string at the profile boundary. class BirthField extends StatefulWidget { const BirthField({ super.key, required this.value, required this.onChanged, this.enabled = true, }); final String value; final ValueChanged onChanged; final bool enabled; @override State createState() => _BirthFieldState(); } class _BirthFieldState extends State { final _day = TextEditingController(); final _year = TextEditingController(); final _monthField = GlobalKey>(); int? _month; late String _emitted; @override void initState() { super.initState(); _readValue(); } @override void didUpdateWidget(BirthField oldWidget) { super.didUpdateWidget(oldWidget); if (widget.value != _emitted) { _readValue(); _monthField.currentState?.didChange(_month); } } void _readValue() { _emitted = widget.value; final parts = RegExp(r'^(\d{4})-(\d{2})-(\d{2})$').firstMatch(widget.value); _year.text = parts?.group(1) ?? ''; final month = int.tryParse(parts?.group(2) ?? ''); _month = month != null && month >= 1 && month <= 12 ? month : null; _day.text = parts?.group(3) ?? ''; } DateTime? get _date { final day = int.tryParse(_day.text); final year = int.tryParse(_year.text); if (day == null || year == null || year < 1 || _year.text.length != 4 || _month == null) { return null; } final date = DateTime(year, _month!, day); return date.year == year && date.month == _month && date.day == day ? date : null; } bool _future(DateTime date) => date.isAfter(DateUtils.dateOnly(DateTime.now())); void _change() { setState(() {}); final date = _date; _emitted = date == null || _future(date) ? '' : '${_year.text}-${_month.toString().padLeft(2, '0')}-' '${_day.text.padLeft(2, '0')}'; widget.onChanged(_emitted); } @override void dispose() { _day.dispose(); _year.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; final date = _date; final complete = _day.text.isNotEmpty && _month != null && _year.text.length == 4; final error = !complete ? null : date == null ? l.kycBirthInvalid : _future(date) ? l.kycBirthFuture : null; final months = DateFormat.MMMM(Localizations.localeOf(context).toString()); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(l.kycBirth, style: Theme.of(context).textTheme.labelLarge), const SizedBox(height: 8), LayoutBuilder( builder: (context, constraints) { final day = _number( key: 'birth-day', controller: _day, label: l.kycBirthDay, length: 2, autofill: AutofillHints.birthdayDay, ); final month = DropdownButtonFormField( key: _monthField, initialValue: _month, isExpanded: true, decoration: InputDecoration(labelText: l.kycBirthMonth), items: [ for (var month = 1; month <= 12; month++) DropdownMenuItem( value: month, child: Text(months.format(DateTime(2000, month))), ), ], onChanged: !widget.enabled ? null : (value) { _month = value; _change(); }, ); final year = _number( key: 'birth-year', controller: _year, label: l.kycBirthYear, length: 4, autofill: AutofillHints.birthdayYear, ); final scale = MediaQuery.textScalerOf(context).scale(16) / 16; if (constraints.maxWidth < 400 * scale) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ day, const SizedBox(height: 16), month, const SizedBox(height: 16), year, ], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 2, child: day), const SizedBox(width: 12), Expanded(flex: 4, child: month), const SizedBox(width: 12), Expanded(flex: 3, child: year), ], ); }, ), if (error != null) Padding( padding: const EdgeInsets.only(top: 8), child: Semantics( liveRegion: true, child: Text( error, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ), ], ); } Widget _number({ required String key, required TextEditingController controller, required String label, required int length, required String autofill, }) => TextFormField( key: ValueKey(key), controller: controller, enabled: widget.enabled, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, autofillHints: [autofill], inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(length), ], decoration: InputDecoration(labelText: label), onChanged: (_) => _change(), ); }