207 lines
5.8 KiB
Dart
207 lines
5.8 KiB
Dart
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<String> onChanged;
|
|
final bool enabled;
|
|
|
|
@override
|
|
State<BirthField> createState() => _BirthFieldState();
|
|
}
|
|
|
|
class _BirthFieldState extends State<BirthField> {
|
|
final _day = TextEditingController();
|
|
final _year = TextEditingController();
|
|
final _monthField = GlobalKey<FormFieldState<int>>();
|
|
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<int>(
|
|
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(),
|
|
);
|
|
}
|