From 026ee6317c368e3893c3c9859546d62809870097 Mon Sep 17 00:00:00 2001 From: Vlad Sumin <46864104+Merculiar@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:45:21 +0300 Subject: [PATCH] feat: onboarding flow changes (#1584) --- packages/espressocash_app/lib/app.dart | 6 - .../screens/country_picker_screen.dart | 58 +++++- .../widgets/country_picker.dart | 13 +- .../screens/onboarding_flow_screen.dart | 65 +++++- .../onboarding/screens/profile_screen.dart | 196 ------------------ .../profile/data/profile_repository.dart | 6 +- 6 files changed, 117 insertions(+), 227 deletions(-) delete mode 100644 packages/espressocash_app/lib/features/onboarding/screens/profile_screen.dart diff --git a/packages/espressocash_app/lib/app.dart b/packages/espressocash_app/lib/app.dart index 68ef5c6a62..8d3139317a 100644 --- a/packages/espressocash_app/lib/app.dart +++ b/packages/espressocash_app/lib/app.dart @@ -45,12 +45,6 @@ class _EspressoCashAppState extends State { OnboardingFlowScreen.open( context, navigator: _navigator.currentState, - onConfirmed: () { - AuthenticatedFlowScreen.open( - context, - navigator: _navigator.currentState, - ); - }, ); } }); diff --git a/packages/espressocash_app/lib/features/country_picker/screens/country_picker_screen.dart b/packages/espressocash_app/lib/features/country_picker/screens/country_picker_screen.dart index ef989911e4..7417fb2cb8 100644 --- a/packages/espressocash_app/lib/features/country_picker/screens/country_picker_screen.dart +++ b/packages/espressocash_app/lib/features/country_picker/screens/country_picker_screen.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:flutter/material.dart'; import '../../../l10n/l10n.dart'; @@ -8,13 +9,52 @@ import '../../../ui/text_field.dart'; import '../../../ui/theme.dart'; import '../models/country.dart'; +typedef CountryOnTap = Future Function( + Country updatedCountry, + BuildContext context, +); + class CountryPickerScreen extends StatelessWidget { const CountryPickerScreen({ super.key, this.initial, + this.onTap, }); + static Future open( + BuildContext context, { + Country? initial, + CountryOnTap? onTap, + NavigatorState? navigator, + }) => + (navigator ?? Navigator.of(context, rootNavigator: true)) + .pushAndRemoveUntil( + PageRouteBuilder( + pageBuilder: (context, _, __) => CountryPickerScreen( + initial: initial, + onTap: onTap, + ), + transitionDuration: Duration.zero, + ), + F, + ); + + static Future push( + BuildContext context, { + Country? initial, + CountryOnTap? onTap, + }) => + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => CountryPickerScreen( + initial: initial, + onTap: onTap, + ), + ), + ); + final Country? initial; + final CountryOnTap? onTap; @override Widget build(BuildContext context) => CpTheme.dark( @@ -23,15 +63,21 @@ class CountryPickerScreen extends StatelessWidget { appBar: CpAppBar( title: Text(context.l10n.selectCountryTitle.toUpperCase()), ), - body: _Wrapper(child: _Content(initial: initial)), + body: _Wrapper( + child: _Content( + initial: initial, + onTap: onTap, + ), + ), ), ); } class _Content extends StatefulWidget { - const _Content({this.initial}); + const _Content({this.initial, required this.onTap}); final Country? initial; + final CountryOnTap? onTap; @override State<_Content> createState() => _ContentState(); @@ -143,7 +189,13 @@ class _ContentState extends State<_Content> { ), selectedColor: Colors.white, shape: selected ? const StadiumBorder() : null, - onTap: () => Navigator.pop(context, country), + onTap: () async { + await widget.onTap + ?.let((onTap) => onTap(country, context)); + + if (!context.mounted) return; + Navigator.pop(context); + }, ), ); }, diff --git a/packages/espressocash_app/lib/features/country_picker/widgets/country_picker.dart b/packages/espressocash_app/lib/features/country_picker/widgets/country_picker.dart index 171d60c2e5..321befa46b 100644 --- a/packages/espressocash_app/lib/features/country_picker/widgets/country_picker.dart +++ b/packages/espressocash_app/lib/features/country_picker/widgets/country_picker.dart @@ -1,3 +1,4 @@ +import 'package:dfunc/dfunc.dart'; import 'package:flutter/material.dart'; import '../../../l10n/l10n.dart'; @@ -25,15 +26,15 @@ class CountryPicker extends StatelessWidget { child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), onTap: () async { - final Country? updated = await Navigator.push( + Country? country; + await CountryPickerScreen.push( context, - MaterialPageRoute( - builder: (context) => CountryPickerScreen(initial: country), - ), + initial: country, + onTap: (updated, _) async => country = updated, ); - if (context.mounted && updated != null) { - onSubmitted(updated); + if (context.mounted) { + country?.let(onSubmitted); } }, title: Text( diff --git a/packages/espressocash_app/lib/features/onboarding/screens/onboarding_flow_screen.dart b/packages/espressocash_app/lib/features/onboarding/screens/onboarding_flow_screen.dart index ba161469c8..939a9b75db 100644 --- a/packages/espressocash_app/lib/features/onboarding/screens/onboarding_flow_screen.dart +++ b/packages/espressocash_app/lib/features/onboarding/screens/onboarding_flow_screen.dart @@ -1,35 +1,78 @@ +import 'dart:async'; + +import 'package:dfunc/dfunc.dart'; import 'package:flutter/material.dart'; import '../../../di.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/dialogs.dart'; +import '../../../ui/loader.dart'; +import '../../authenticated/screens/authenticated_flow_screen.dart'; +import '../../country_picker/models/country.dart'; +import '../../country_picker/screens/country_picker_screen.dart'; +import '../../profile/service/update_profile.dart'; import '../data/onboarding_repository.dart'; -import 'profile_screen.dart'; import 'view_recovery_phrase_screen.dart'; class OnboardingFlowScreen { - static void open( + static Future open( BuildContext context, { - required VoidCallback onConfirmed, NavigatorState? navigator, - }) { + }) async { final hasConfirmedPassphrase = sl().hasConfirmedPassphrase; if (hasConfirmedPassphrase) { - OnboardingProfileScreen.open( + await CountryPickerScreen.open( context, navigator: navigator, - onConfirmed: onConfirmed, + onTap: _updateCountry, ); + + if (context.mounted) { + AuthenticatedFlowScreen.open( + context, + navigator: navigator, + ); + } } else { ViewRecoveryPhraseScreen.open( context, navigator: navigator, - onConfirmed: () => OnboardingProfileScreen.open( - context, - navigator: navigator, - onConfirmed: onConfirmed, - ), + onConfirmed: () async { + await CountryPickerScreen.open( + context, + navigator: navigator, + onTap: _updateCountry, + ); + + if (context.mounted) { + AuthenticatedFlowScreen.open( + context, + navigator: navigator, + ); + } + }, ); } } } + +Future _updateCountry(Country country, BuildContext context) => + runWithLoader( + context, + () async { + await sl() + .call( + countryCode: country.code, + ) + .foldAsync((e) => throw e, ignore); + + if (!context.mounted) return; + }, + onError: (error) => showErrorDialog( + context, + context.l10n.lblProfileUpdateFailed, + error, + ), + ); diff --git a/packages/espressocash_app/lib/features/onboarding/screens/profile_screen.dart b/packages/espressocash_app/lib/features/onboarding/screens/profile_screen.dart deleted file mode 100644 index d78a9fba3c..0000000000 --- a/packages/espressocash_app/lib/features/onboarding/screens/profile_screen.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'dart:async'; - -import 'package:dfunc/dfunc.dart'; -import 'package:flutter/material.dart'; - -import '../../../di.dart'; -import '../../../gen/assets.gen.dart'; -import '../../../l10n/l10n.dart'; -import '../../../ui/button.dart'; -import '../../../ui/dialogs.dart'; -import '../../../ui/form_page.dart'; -import '../../../ui/loader.dart'; -import '../../../ui/text_field.dart'; -import '../../../utils/email.dart'; -import '../../country_picker/models/country.dart'; -import '../../country_picker/widgets/country_picker.dart'; -import '../../profile/data/profile_repository.dart'; -import '../../profile/service/update_profile.dart'; - -class OnboardingProfileScreen extends StatefulWidget { - const OnboardingProfileScreen({ - super.key, - required this.onConfirmed, - }); - - final VoidCallback onConfirmed; - - static void open( - BuildContext context, { - required VoidCallback onConfirmed, - NavigatorState? navigator, - }) => - (navigator ?? Navigator.of(context, rootNavigator: true)) - .pushAndRemoveUntil( - PageRouteBuilder( - pageBuilder: (context, _, __) => - OnboardingProfileScreen(onConfirmed: onConfirmed), - transitionDuration: Duration.zero, - ), - F, - ); - - @override - State createState() => - _OnboardingProfileScreenState(); -} - -class _OnboardingProfileScreenState extends State { - final _firstNameController = TextEditingController(); - final _lastNameController = TextEditingController(); - final _emailController = TextEditingController(); - Country? _country; - - @override - void initState() { - super.initState(); - - final repository = sl(); - - _firstNameController.text = repository.firstName; - _lastNameController.text = repository.lastName; - _emailController.text = repository.email; - - final country = repository.country; - if (country != null) { - _country = Country.findByCode(country); - } - } - - @override - void dispose() { - _firstNameController.dispose(); - _lastNameController.dispose(); - _emailController.dispose(); - super.dispose(); - } - - void _handleSubmitted() => runWithLoader( - context, - () async { - await sl() - .call( - firstName: _firstNameController.text, - lastName: _lastNameController.text, - // ignore: avoid-non-null-assertion, should not be null - countryCode: _country!.code, - photoPath: null, - email: _emailController.text, - ) - .foldAsync((e) => throw e, ignore); - - unawaited(Future.microtask(() => widget.onConfirmed())); - }, - onError: (error) => showErrorDialog( - context, - context.l10n.lblProfileUpdateFailed, - error, - ), - ); - - bool get _isValid => - _firstNameController.text.isNotEmpty && - _lastNameController.text.isNotEmpty && - _emailController.text.isValidEmail && - _country != null; - - @override - Widget build(BuildContext context) => FormPage( - title: Text(context.l10n.onboardingProfileTitle.toUpperCase()), - backgroundImage: Assets.images.blank, - colorTheme: FormPageColorTheme.gold, - header: FormPageHeader( - title: const SizedBox.shrink(), - description: Text( - context.l10n.yourEmailDisclaimer, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400), - ), - icon: Assets.images.profileGraphic, - ), - child: Column( - children: [ - _ProfileTextField( - emailController: _firstNameController, - inputType: TextInputType.name, - placeholder: context.l10n.yourFirstNamePlaceholder, - textCapitalization: TextCapitalization.words, - ), - const SizedBox(height: 14), - _ProfileTextField( - emailController: _lastNameController, - inputType: TextInputType.name, - placeholder: context.l10n.yourLastNamePlaceholder, - textCapitalization: TextCapitalization.words, - ), - const SizedBox(height: 14), - _ProfileTextField( - emailController: _emailController, - inputType: TextInputType.emailAddress, - placeholder: context.l10n.yourEmailPlaceholder, - ), - const SizedBox(height: 14), - CountryPicker( - country: _country, - onSubmitted: (country) => setState(() => _country = country), - ), - const SizedBox(height: 28), - const Spacer(), - ListenableBuilder( - listenable: Listenable.merge([ - _firstNameController, - _lastNameController, - _emailController, - ]), - builder: (context, child) => CpButton( - width: double.infinity, - text: context.l10n.next, - onPressed: _isValid ? _handleSubmitted : null, - ), - ), - ], - ), - ); -} - -class _ProfileTextField extends StatelessWidget { - const _ProfileTextField({ - required this.emailController, - required this.inputType, - required this.placeholder, - this.textCapitalization = TextCapitalization.none, - }); - - final TextEditingController emailController; - final TextInputType inputType; - final TextCapitalization textCapitalization; - final String placeholder; - - @override - Widget build(BuildContext context) => CpTextField( - padding: const EdgeInsets.only( - top: 18, - bottom: 16, - left: 26, - right: 26, - ), - controller: emailController, - inputType: inputType, - textInputAction: TextInputAction.next, - textCapitalization: textCapitalization, - backgroundColor: const Color(0xFF9D8A59), - placeholder: placeholder, - placeholderColor: Colors.white, - textColor: Colors.white, - fontSize: 16, - ); -} diff --git a/packages/espressocash_app/lib/features/profile/data/profile_repository.dart b/packages/espressocash_app/lib/features/profile/data/profile_repository.dart index 3d61b46e90..e802599239 100644 --- a/packages/espressocash_app/lib/features/profile/data/profile_repository.dart +++ b/packages/espressocash_app/lib/features/profile/data/profile_repository.dart @@ -11,11 +11,7 @@ class ProfileRepository extends ChangeNotifier { final SharedPreferences _sharedPreferences; - bool get hasAllRequiredFields => - firstName.isNotEmpty && - lastName.isNotEmpty && - email.isNotEmpty && - country != null; + bool get hasAllRequiredFields => country != null; String get fullName => [firstName, lastName].where((it) => it.isNotEmpty).join(' ');