Merge remote-tracking branch 'origin/main' into wallet-72
This commit is contained in:
commit
9370eacd9c
26 files changed
+975
-82
No files matched your search
@@ -99,10 +99,11 @@ class _AuthFormState extends State<AuthForm> {
|
||||
return;
|
||||
}
|
||||
final email = _email.text.trim();
|
||||
final delivery = signup.state.result!.delivery;
|
||||
TextInput.finishAutofillContext();
|
||||
_password.clear();
|
||||
_confirmation.clear();
|
||||
context.goNamed(AppRoutes.verificationSent, extra: email);
|
||||
context.goNamed(AppRoutes.verificationSent, extra: delivery ?? email);
|
||||
} else {
|
||||
final login = context.read<LoginProvider>();
|
||||
await login.login(
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
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(),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
||||
|
||||
import 'birth.dart';
|
||||
import 'country.dart';
|
||||
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
@@ -37,17 +38,11 @@ class ProfileForm extends StatelessWidget {
|
||||
onChanged: (value) => update(name: value),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
BirthField(
|
||||
key: const ValueKey('kyc-birth'),
|
||||
initialValue: p.profile.dateOfBirth,
|
||||
value: p.profile.dateOfBirth,
|
||||
enabled: !p.busy,
|
||||
keyboardType: TextInputType.datetime,
|
||||
autofillHints: const [AutofillHints.birthday],
|
||||
decoration: InputDecoration(
|
||||
labelText: l.kycBirth,
|
||||
helperText: l.kycBirthHelp,
|
||||
),
|
||||
onChanged: (value) => update(birth: value.trim()),
|
||||
onChanged: (value) => update(birth: value),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CountryField(
|
||||
|
||||
@@ -272,7 +272,11 @@
|
||||
"kycProfileHelp": "Enter your details exactly as shown on your identity document.",
|
||||
"kycFullName": "Full legal name",
|
||||
"kycBirth": "Date of birth",
|
||||
"kycBirthHelp": "YYYY-MM-DD, for example 1990-04-23",
|
||||
"kycBirthDay": "Day",
|
||||
"kycBirthMonth": "Month",
|
||||
"kycBirthYear": "Year",
|
||||
"kycBirthInvalid": "Enter a valid date of birth.",
|
||||
"kycBirthFuture": "Date of birth cannot be in the future.",
|
||||
"kycCountry": "Country of residence",
|
||||
"kycCountrySelect": "Select a country",
|
||||
"kycCountrySearch": "Search countries",
|
||||
|
||||
@@ -272,7 +272,11 @@
|
||||
"kycProfileHelp": "Укажите данные точно как в документе, удостоверяющем личность.",
|
||||
"kycFullName": "Полное имя по документу",
|
||||
"kycBirth": "Дата рождения",
|
||||
"kycBirthHelp": "ГГГГ-ММ-ДД, например 1990-04-23",
|
||||
"kycBirthDay": "День",
|
||||
"kycBirthMonth": "Месяц",
|
||||
"kycBirthYear": "Год",
|
||||
"kycBirthInvalid": "Укажите корректную дату рождения.",
|
||||
"kycBirthFuture": "Дата рождения не может быть в будущем.",
|
||||
"kycCountry": "Страна проживания",
|
||||
"kycCountrySelect": "Выберите страну",
|
||||
"kycCountrySearch": "Поиск страны",
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
|
||||
import 'package:sxchnge_wallet/sxchnge_wallet.dart';
|
||||
|
||||
import '../auth/page.dart';
|
||||
@@ -144,6 +146,9 @@ GoRouter createAppRouter({bool Function()? authenticated}) {
|
||||
name: AppRoutes.verificationSent,
|
||||
builder: (context, state) => VerificationPage(
|
||||
email: state.extra is String ? state.extra as String : null,
|
||||
delivery: state.extra is VerificationDelivery
|
||||
? state.extra as VerificationDelivery
|
||||
: null,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
@@ -15,9 +15,10 @@ import '../routing/routes.dart';
|
||||
import '../generated/i18n/app_localizations.dart';
|
||||
|
||||
class VerificationPage extends StatefulWidget {
|
||||
const VerificationPage({super.key, this.email, this.token});
|
||||
const VerificationPage({super.key, this.email, this.token, this.delivery});
|
||||
final String? email;
|
||||
final String? token;
|
||||
final VerificationDelivery? delivery;
|
||||
|
||||
@override
|
||||
State<VerificationPage> createState() => _VerificationPageState();
|
||||
@@ -25,9 +26,12 @@ class VerificationPage extends StatefulWidget {
|
||||
|
||||
class _VerificationPageState extends State<VerificationPage> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
late final _email = TextEditingController(text: widget.email);
|
||||
late final _email = TextEditingController(
|
||||
text: widget.delivery?.email ?? widget.email,
|
||||
);
|
||||
late final _provider = VerificationProvider(
|
||||
context.read<VerificationService>(),
|
||||
delivery: widget.delivery,
|
||||
);
|
||||
final _emailFocus = FocusNode();
|
||||
String? _recipient;
|
||||
@@ -44,7 +48,7 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
if (!mounted) return;
|
||||
if (widget.token != null) {
|
||||
_provider.confirm(widget.token!);
|
||||
} else if (_email.text.isNotEmpty) {
|
||||
} else if (widget.delivery == null && _email.text.isNotEmpty) {
|
||||
_send();
|
||||
}
|
||||
});
|
||||
@@ -58,14 +62,26 @@ class _VerificationPageState extends State<VerificationPage> {
|
||||
@override
|
||||
void didUpdateWidget(covariant VerificationPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.token == widget.token) return;
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
_provider.reset();
|
||||
if (oldWidget.token == widget.token &&
|
||||
oldWidget.email == widget.email &&
|
||||
identical(oldWidget.delivery, widget.delivery)) {
|
||||
return;
|
||||
}
|
||||
if (oldWidget.token != widget.token) {
|
||||
context.read<JourneyRecorder>().abandon(JourneyExit.navigation);
|
||||
}
|
||||
_provider.reset(delivery: widget.delivery);
|
||||
_lastWasSend = false;
|
||||
_validated = false;
|
||||
_email.clear();
|
||||
_recipient = null;
|
||||
_email.text = widget.delivery?.email ?? widget.email ?? '';
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && widget.token != null) _provider.confirm(widget.token!);
|
||||
if (!mounted) return;
|
||||
if (widget.token != null) {
|
||||
_provider.confirm(widget.token!);
|
||||
} else if (widget.delivery == null && _email.text.isNotEmpty) {
|
||||
_send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user