diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/personal_data_screen.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/personal_data_screen.dart index d4c12c2e..cad13029 100644 --- a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/personal_data_screen.dart +++ b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/personal_data_screen.dart @@ -1,11 +1,12 @@ -import 'package:account/src/features/personal_data/presentation/state/personal_data_view_model.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:account/src/features/personal_data/presentation/providers/personal_data_controller.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_ui/legacy_ui.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; class PersonalDataScreen extends ConsumerWidget { @@ -13,36 +14,112 @@ class PersonalDataScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isLoading = ref.watch( - personalDataViewModelProvider.select((s) => s.isLoading), + final userAsync = ref.watch(userInfoProvider); + return userAsync.when( + data: (user) => _PersonalDataForm(user: user), + loading: () => Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: const Center(child: CircularProgressIndicator()), + ), + error: (_, __) => LegacyPageLayout( + title: context.translate(I18n.personalData), + body: Center(child: Text(context.translate(I18n.errorGeneric))), + ), ); + } +} - ref.listen(personalDataViewModelProvider.select((s) => s.errorMessage), ( - _, - errorMessage, - ) { - if (errorMessage.isNotEmpty) { - showTopSnackbar( - context, - message: context.translate(errorMessage), - type: MessageType.error, +class _PersonalDataForm extends ConsumerStatefulWidget { + final UserEntity user; + + const _PersonalDataForm({required this.user}); + + @override + ConsumerState<_PersonalDataForm> createState() => _PersonalDataFormState(); +} + +class _PersonalDataFormState extends ConsumerState<_PersonalDataForm> { + late final TextEditingController _firstNameController; + late final TextEditingController _lastNameController; + late final TextEditingController _phoneController; + late String _isoCode; + String? _localError; + + @override + void initState() { + super.initState(); + _firstNameController = TextEditingController(text: widget.user.firstName); + _lastNameController = TextEditingController(text: widget.user.lastName); + final parsed = SfPhoneNumber.tryParse(widget.user.phone); + _phoneController = TextEditingController( + text: parsed?.nationalNumber ?? widget.user.phone, + ); + _isoCode = parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode; + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + bool get _hasChanges { + final user = widget.user; + final parsed = SfPhoneNumber.tryParse(user.phone); + final originalPhone = parsed?.nationalNumber ?? user.phone; + return _firstNameController.text.trim() != user.firstName || + _lastNameController.text.trim() != user.lastName || + _phoneController.text.trim() != originalPhone; + } + + void _onSubmit() { + if (!_hasChanges) return; + + final phoneText = _phoneController.text.trim(); + String? fullPhone; + if (phoneText.isNotEmpty) { + final parsed = SfPhoneNumber.tryParse( + _phoneController.text, + defaultIsoCode: _isoCode, + ); + if (parsed == null) { + setState(() => _localError = I18n.errorMessagePhoneIsInvalid); + return; + } + fullPhone = parsed.e164; + } + setState(() => _localError = null); + + final first = _firstNameController.text.trim().isNotEmpty + ? _firstNameController.text.trim() + : widget.user.firstName; + final last = _lastNameController.text.trim().isNotEmpty + ? _lastNameController.text.trim() + : widget.user.lastName; + + ref.read(personalDataControllerProvider.notifier).submit( + firstName: first, + lastName: last, + phone: fullPhone ?? widget.user.phone, ); + } + + @override + Widget build(BuildContext context) { + ref.listen(personalDataControllerProvider, (prev, next) async { + next.showErrorOn(context); + if (prev != null && + prev.isLoading && + !next.isLoading && + !next.hasError) { + await showSuccessDialog(context, I18n.personalDataUpdatedSuccess); + if (context.mounted) Navigator.of(context).pop(); } }); - ref.listen(personalDataViewModelProvider.select((s) => s.isComplete), ( - _, - isComplete, - ) { - if (isComplete) Navigator.pop(context); - }); - - if (isLoading) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: const Center(child: CircularProgressIndicator()), - ); - } + final submitState = ref.watch(personalDataControllerProvider); return LegacyPageLayout( title: context.translate(I18n.personalData), @@ -57,13 +134,57 @@ class PersonalDataScreen extends ConsumerWidget { children: [ const _ProfilePicture(), SizedBox(height: SizeUtils.getByScreen(small: 18, big: 16)), - const _EmailLabel(), + _EmailLabel(email: widget.user.email), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), - const _FirstNameField(), + CustomTextField( + controller: _firstNameController, + hint: widget.user.firstName, + label: context.translate(I18n.firstNameLabel), + ), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), - const _LastNameField(), + CustomTextField( + controller: _lastNameController, + hint: widget.user.lastName, + label: context.translate(I18n.lastNameLabel), + ), SizedBox(height: SizeUtils.getByScreen(small: 24, big: 22)), - const _PhoneField(), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + CountryPrefixPicker( + headerText: context.translate(I18n.selectYourCountry), + initialSelection: _isoCode, + onChanged: (country) { + final code = country.code; + if (code != null && code != _isoCode) { + setState(() => _isoCode = code); + } + }, + width: 80, + ), + const SizedBox(width: 8), + Expanded( + child: CustomTextField( + controller: _phoneController, + hint: widget.user.phone, + label: context.translate(I18n.phoneLabel), + keyboardType: TextInputType.phone, + ), + ), + ], + ), + if (_localError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + context.translate(_localError!), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), ], ), ), @@ -77,7 +198,17 @@ class PersonalDataScreen extends ConsumerWidget { children: [ Text(context.translate(I18n.personalDataMessage)), const SizedBox(height: 4), - const _SaveButton(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 10, + ), + child: PrimaryButton( + onPressed: submitState.isLoading ? null : _onSubmit, + text: context.translate(I18n.submit), + color: context.sfColors.legacyPrimary, + ), + ), ], ), ), @@ -99,15 +230,13 @@ class _ProfilePicture extends StatelessWidget { } } -class _EmailLabel extends ConsumerWidget { - const _EmailLabel(); +class _EmailLabel extends StatelessWidget { + final String email; + + const _EmailLabel({required this.email}); @override - Widget build(BuildContext context, WidgetRef ref) { - final email = ref.watch( - personalDataViewModelProvider.select((s) => s.user?.email ?? ''), - ); - + Widget build(BuildContext context) { return Column( children: [ Text( @@ -122,96 +251,3 @@ class _EmailLabel extends ConsumerWidget { ); } } - -class _FirstNameField extends ConsumerWidget { - const _FirstNameField(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(personalDataViewModelProvider.notifier); - final hint = ref.watch( - personalDataViewModelProvider.select((s) => s.user?.firstName ?? ''), - ); - - return CustomTextField( - controller: vm.firstNameController, - hint: hint, - label: context.translate(I18n.firstNameLabel), - ); - } -} - -class _LastNameField extends ConsumerWidget { - const _LastNameField(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(personalDataViewModelProvider.notifier); - final hint = ref.watch( - personalDataViewModelProvider.select((s) => s.user?.lastName ?? ''), - ); - - return CustomTextField( - controller: vm.lastNameController, - hint: hint, - label: context.translate(I18n.lastNameLabel), - ); - } -} - -class _PhoneField extends ConsumerWidget { - const _PhoneField(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(personalDataViewModelProvider.notifier); - final hint = ref.watch( - personalDataViewModelProvider.select((s) => s.user?.phone ?? ''), - ); - final isoCode = ref.watch( - personalDataViewModelProvider.select((s) => s.isoCode), - ); - - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - CountryPrefixPicker( - headerText: context.translate(I18n.selectYourCountry), - initialSelection: isoCode, - onChanged: (country) { - final code = country.code; - if (code != null) vm.updateCountry(code); - }, - width: 80, - ), - SizedBox(width: 8), - Expanded( - child: CustomTextField( - controller: vm.phoneController, - hint: hint, - label: context.translate(I18n.phoneLabel), - keyboardType: TextInputType.phone, - ), - ), - ], - ); - } -} - -class _SaveButton extends ConsumerWidget { - const _SaveButton(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(personalDataViewModelProvider.notifier); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: PrimaryButton( - onPressed: vm.updateUser, - text: context.translate(I18n.submit), - color: context.sfColors.legacyPrimary, - ), - ); - } -} diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.dart new file mode 100644 index 00000000..e085ad98 --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:account/src/core/providers/users_repository_provider.dart'; +import 'package:account/src/features/personal_data/domain/entities/update_user_request_entity.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'personal_data_controller.g.dart'; + +@riverpod +class PersonalDataController extends _$PersonalDataController { + @override + FutureOr build() {} + + Future submit({ + required String firstName, + required String lastName, + required String phone, + }) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + final user = await ref.read(userInfoProvider.future); + final request = UpdateUserRequestEntity( + id: user.id, + firstName: firstName, + lastName: lastName, + phone: phone, + language: user.language, + ); + await ref + .read(usersRepositoryProvider) + .updateUser(userId: user.id, request: request); + ref + .read(userInfoProvider.notifier) + .setUser( + user.copyWith( + firstName: firstName, + lastName: lastName, + phone: phone, + ), + ); + unawaited(ref.read(sfTrackingProvider).legacyAccountPersonalDataEdited()); + }); + } +} diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.g.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.g.dart new file mode 100644 index 00000000..fa8ba6d5 --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/providers/personal_data_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'personal_data_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(PersonalDataController) +const personalDataControllerProvider = PersonalDataControllerProvider._(); + +final class PersonalDataControllerProvider + extends $AsyncNotifierProvider { + const PersonalDataControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'personalDataControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$personalDataControllerHash(); + + @$internal + @override + PersonalDataController create() => PersonalDataController(); +} + +String _$personalDataControllerHash() => + r'c43c4076390f42d2f498fd748feea3b78a68ae20'; + +abstract class _$PersonalDataController extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_model.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_model.dart deleted file mode 100644 index f8af9a8d..00000000 --- a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_model.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:async'; - -import 'package:account/src/features/personal_data/domain/entities/update_user_request_entity.dart'; -import 'package:account/src/features/personal_data/presentation/state/personal_data_view_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_localizations/sf_localizations.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -import '../../../../core/domain/repositories/users_repository.dart'; -import '../../../../core/providers/users_repository_provider.dart'; - -final personalDataViewModelProvider = - NotifierProvider.autoDispose( - PersonalDataViewModel.new, - ); - -class PersonalDataViewModel extends Notifier { - late final UsersRepository _usersRepository; - late final SfTrackingRepository _tracking; - - late final TextEditingController firstNameController; - late final TextEditingController lastNameController; - late final TextEditingController phoneController; - - @override - PersonalDataViewState build() { - _usersRepository = ref.read(usersRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - firstNameController = TextEditingController(); - lastNameController = TextEditingController(); - phoneController = TextEditingController(); - - Future.microtask(_init); - - ref.onDispose(_disposeControllers); - - return const PersonalDataViewState(); - } - - Future _init() async { - try { - final user = await ref.read(userInfoProvider.future); - if (!ref.mounted) return; - - firstNameController.text = user.firstName; - lastNameController.text = user.lastName; - - final parsed = SfPhoneNumber.tryParse(user.phone); - phoneController.text = parsed?.nationalNumber ?? user.phone; - - state = state.copyWith( - user: user, - isoCode: parsed?.isoCode ?? SfPhoneNumber.defaultIsoCode, - isLoading: false, - ); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith(isLoading: false, errorMessage: I18n.errorGeneric); - } - } - - void updateCountry(String isoCode) { - if (isoCode == state.isoCode) return; - state = state.copyWith(isoCode: isoCode, errorMessage: ''); - } - - bool get _hasChanges { - final user = state.user; - if (user == null) return false; - final parsed = SfPhoneNumber.tryParse(user.phone); - final originalPhone = parsed?.nationalNumber ?? user.phone; - return firstNameController.text.trim() != user.firstName || - lastNameController.text.trim() != user.lastName || - phoneController.text.trim() != originalPhone; - } - - Future updateUser() async { - final user = state.user; - if (user == null || state.isLoading || !_hasChanges) return; - - String? fullPhone; - if (phoneController.text.trim().isNotEmpty) { - final parsed = SfPhoneNumber.tryParse( - phoneController.text, - defaultIsoCode: state.isoCode, - ); - if (parsed == null) { - state = state.copyWith(errorMessage: I18n.errorMessagePhoneIsInvalid); - return; - } - fullPhone = parsed.e164; - } - - try { - state = state.copyWith( - isLoading: true, - isComplete: false, - errorMessage: '', - ); - - final request = UpdateUserRequestEntity( - id: user.id, - firstName: firstNameController.text.trim().isNotEmpty - ? firstNameController.text.trim() - : user.firstName, - lastName: lastNameController.text.trim().isNotEmpty - ? lastNameController.text.trim() - : user.lastName, - phone: fullPhone ?? user.phone, - language: user.language, - ); - - await _usersRepository.updateUser(userId: user.id, request: request); - if (!ref.mounted) return; - - final updatedUser = user.copyWith( - firstName: request.firstName, - lastName: request.lastName, - phone: request.phone, - ); - ref.read(userInfoProvider.notifier).setUser(updatedUser); - - unawaited(_tracking.legacyAccountPersonalDataEdited()); - - state = state.copyWith(isLoading: false, isComplete: true); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith( - isLoading: false, - isComplete: false, - errorMessage: I18n.errorGeneric, - ); - } - } - - void _disposeControllers() { - firstNameController.dispose(); - lastNameController.dispose(); - phoneController.dispose(); - } -} diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.dart deleted file mode 100644 index d58ac78c..00000000 --- a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:sf_shared/sf_shared.dart'; - -part 'personal_data_view_state.freezed.dart'; - -@freezed -abstract class PersonalDataViewState with _$PersonalDataViewState { - const factory PersonalDataViewState({ - @Default(true) bool isLoading, - @Default(false) bool isComplete, - @Default('ES') String isoCode, - UserEntity? user, - @Default('') String errorMessage, - }) = _PersonalDataViewState; -} diff --git a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.freezed.dart b/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.freezed.dart deleted file mode 100644 index 7a4b60d1..00000000 --- a/modules/legacy/modules/account/lib/src/features/personal_data/presentation/state/personal_data_view_state.freezed.dart +++ /dev/null @@ -1,307 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'personal_data_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$PersonalDataViewState { - - bool get isLoading; bool get isComplete; String get isoCode; UserEntity? get user; String get errorMessage; -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$PersonalDataViewStateCopyWith get copyWith => _$PersonalDataViewStateCopyWithImpl(this as PersonalDataViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PersonalDataViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.user, user) || other.user == user)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,isoCode,user,errorMessage); - -@override -String toString() { - return 'PersonalDataViewState(isLoading: $isLoading, isComplete: $isComplete, isoCode: $isoCode, user: $user, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $PersonalDataViewStateCopyWith<$Res> { - factory $PersonalDataViewStateCopyWith(PersonalDataViewState value, $Res Function(PersonalDataViewState) _then) = _$PersonalDataViewStateCopyWithImpl; -@useResult -$Res call({ - bool isLoading, bool isComplete, String isoCode, UserEntity? user, String errorMessage -}); - - -$UserEntityCopyWith<$Res>? get user; - -} -/// @nodoc -class _$PersonalDataViewStateCopyWithImpl<$Res> - implements $PersonalDataViewStateCopyWith<$Res> { - _$PersonalDataViewStateCopyWithImpl(this._self, this._then); - - final PersonalDataViewState _self; - final $Res Function(PersonalDataViewState) _then; - -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? isComplete = null,Object? isoCode = null,Object? user = freezed,Object? errorMessage = null,}) { - return _then(_self.copyWith( -isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable -as bool,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable -as UserEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$UserEntityCopyWith<$Res>? get user { - if (_self.user == null) { - return null; - } - - return $UserEntityCopyWith<$Res>(_self.user!, (value) { - return _then(_self.copyWith(user: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [PersonalDataViewState]. -extension PersonalDataViewStatePatterns on PersonalDataViewState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _PersonalDataViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _PersonalDataViewState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _PersonalDataViewState value) $default,){ -final _that = this; -switch (_that) { -case _PersonalDataViewState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PersonalDataViewState value)? $default,){ -final _that = this; -switch (_that) { -case _PersonalDataViewState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( bool isLoading, bool isComplete, String isoCode, UserEntity? user, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _PersonalDataViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.isoCode,_that.user,_that.errorMessage);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( bool isLoading, bool isComplete, String isoCode, UserEntity? user, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _PersonalDataViewState(): -return $default(_that.isLoading,_that.isComplete,_that.isoCode,_that.user,_that.errorMessage);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isLoading, bool isComplete, String isoCode, UserEntity? user, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _PersonalDataViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.isoCode,_that.user,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _PersonalDataViewState implements PersonalDataViewState { - const _PersonalDataViewState({this.isLoading = true, this.isComplete = false, this.isoCode = 'ES', this.user, this.errorMessage = ''}); - - -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isComplete; -@override@JsonKey() final String isoCode; -@override final UserEntity? user; -@override@JsonKey() final String errorMessage; - -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$PersonalDataViewStateCopyWith<_PersonalDataViewState> get copyWith => __$PersonalDataViewStateCopyWithImpl<_PersonalDataViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersonalDataViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.user, user) || other.user == user)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,isoCode,user,errorMessage); - -@override -String toString() { - return 'PersonalDataViewState(isLoading: $isLoading, isComplete: $isComplete, isoCode: $isoCode, user: $user, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$PersonalDataViewStateCopyWith<$Res> implements $PersonalDataViewStateCopyWith<$Res> { - factory _$PersonalDataViewStateCopyWith(_PersonalDataViewState value, $Res Function(_PersonalDataViewState) _then) = __$PersonalDataViewStateCopyWithImpl; -@override @useResult -$Res call({ - bool isLoading, bool isComplete, String isoCode, UserEntity? user, String errorMessage -}); - - -@override $UserEntityCopyWith<$Res>? get user; - -} -/// @nodoc -class __$PersonalDataViewStateCopyWithImpl<$Res> - implements _$PersonalDataViewStateCopyWith<$Res> { - __$PersonalDataViewStateCopyWithImpl(this._self, this._then); - - final _PersonalDataViewState _self; - final $Res Function(_PersonalDataViewState) _then; - -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? isComplete = null,Object? isoCode = null,Object? user = freezed,Object? errorMessage = null,}) { - return _then(_PersonalDataViewState( -isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable -as bool,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,user: freezed == user ? _self.user : user // ignore: cast_nullable_to_non_nullable -as UserEntity?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -/// Create a copy of PersonalDataViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$UserEntityCopyWith<$Res>? get user { - if (_self.user == null) { - return null; - } - - return $UserEntityCopyWith<$Res>(_self.user!, (value) { - return _then(_self.copyWith(user: value)); - }); -} -} - -// dart format on diff --git a/modules/legacy/modules/account/test/features/personal_data/personal_data_controller_test.dart b/modules/legacy/modules/account/test/features/personal_data/personal_data_controller_test.dart new file mode 100644 index 00000000..106a79aa --- /dev/null +++ b/modules/legacy/modules/account/test/features/personal_data/personal_data_controller_test.dart @@ -0,0 +1,130 @@ +import 'package:account/src/core/domain/repositories/users_repository.dart'; +import 'package:account/src/core/providers/users_repository_provider.dart'; +import 'package:account/src/features/personal_data/domain/entities/update_user_request_entity.dart'; +import 'package:account/src/features/personal_data/presentation/providers/personal_data_controller.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockUsersRepository extends Mock implements UsersRepository {} + +class _FakeUserInfoNotifier extends UserInfoNotifier { + _FakeUserInfoNotifier(this._user); + final UserEntity _user; + + @override + Future build() async => _user; +} + +const _user = UserEntity( + id: 'user-1', + email: 'user1@test.com', + createdAt: 0, + status: 'active', + role: 'parent', + lastLogin: 0, + currentLogin: 0, + language: 'es', + firstName: 'Ada', + lastName: 'Lovelace', + hasApiKey: false, + phone: '+34600000000', +); + +void main() { + setUpAll(() { + registerFallbackValue( + const UpdateUserRequestEntity( + id: '', + firstName: '', + lastName: '', + phone: '', + language: 'es', + ), + ); + }); + + group('PersonalDataController.submit', () { + test('transitions to AsyncData and updates userInfoProvider on success', + () async { + final repo = MockUsersRepository(); + when( + () => repo.updateUser( + userId: any(named: 'userId'), + request: any(named: 'request'), + ), + ).thenAnswer((_) async {}); + + final container = makeContainer( + overrides: [ + userInfoProvider.overrideWith(() => _FakeUserInfoNotifier(_user)), + usersRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + addTearDown(container.dispose); + await container.read(userInfoProvider.future); + + await container + .read(personalDataControllerProvider.notifier) + .submit(firstName: 'Ada', lastName: 'King', phone: '+34600111222'); + + final state = container.read(personalDataControllerProvider); + expect(state, isA>()); + expect(state.error, isNull); + + verify( + () => repo.updateUser( + userId: _user.id, + request: const UpdateUserRequestEntity( + id: 'user-1', + firstName: 'Ada', + lastName: 'King', + phone: '+34600111222', + language: 'es', + ), + ), + ).called(1); + + final updated = await container.read(userInfoProvider.future); + expect(updated.lastName, 'King'); + expect(updated.phone, '+34600111222'); + }); + + test('exposes AsyncError when the repository fails', () async { + final repo = MockUsersRepository(); + when( + () => repo.updateUser( + userId: any(named: 'userId'), + request: any(named: 'request'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = makeContainer( + overrides: [ + userInfoProvider.overrideWith(() => _FakeUserInfoNotifier(_user)), + usersRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + addTearDown(container.dispose); + await container.read(userInfoProvider.future); + + await container + .read(personalDataControllerProvider.notifier) + .submit(firstName: 'Ada', lastName: 'King', phone: '+34600111222'); + + final state = container.read(personalDataControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + }); + }); +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 9d35a9c2..c981be2e 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -56,6 +56,7 @@ "passwordSpecial": "Ein Sonderzeichen enthalten", "passwordMatch": "Passwörter stimmen überein", "passwordChangedSuccess": "Passwort erfolgreich aktualisiert", + "personalDataUpdatedSuccess": "Persönliche Daten aktualisiert", "accept": "Akzeptieren", "errorMessageUnequalPasswords": "Passwörter stimmen nicht überein. versuchen Sie es erneut", "errorMessagePasswordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index a33b1a4a..139d9bb9 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -56,6 +56,7 @@ "passwordSpecial": "One special character", "passwordMatch": "Passwords match", "passwordChangedSuccess": "Password updated successfully", + "personalDataUpdatedSuccess": "Personal data updated successfully", "accept": "Accept", "errorMessageUnequalPasswords": "Passwords don't match. Try again", "errorMessagePasswordTooShort": "Password must include at least 8 characters", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index 40c194b5..76968a16 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -56,6 +56,7 @@ "passwordSpecial": "Una carácter especial", "passwordMatch": "Las contraseñas coinciden", "passwordChangedSuccess": "Contraseña actualizada correctamente", + "personalDataUpdatedSuccess": "Datos actualizados correctamente", "accept": "Aceptar", "errorMessageUnequalPasswords": "Las contraseñas no coinciden. Inténtalo de nuevo", "errorMessagePasswordTooShort": "La contraseña debe tener al menos 8 caracteres", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index 7257066a..a957f56a 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -56,6 +56,7 @@ "passwordSpecial": "Un caractère particulier", "passwordMatch": "Les mots de passe correspondent", "passwordChangedSuccess": "Mot de passe mis à jour", + "personalDataUpdatedSuccess": "Données personnelles mises à jour", "accept": "Accepter", "errorMessageUnequalPasswords": "Les mots de passe ne correspondent pas. essayer à nouveau", "errorMessagePasswordTooShort": "Le mot de passe doit contenir au moins 8 caractères", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index c33795d7..4a4ef198 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -56,6 +56,7 @@ "passwordSpecial": "Un carattere speciale", "passwordMatch": "Le password corrispondono", "passwordChangedSuccess": "Password aggiornata correttamente", + "personalDataUpdatedSuccess": "Dati personali aggiornati", "accept": "Accettare", "errorMessageUnequalPasswords": "Le password non corrispondono. riprova", "errorMessagePasswordTooShort": "La password deve contenere almeno 8 caratteri", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 7f17605f..4391ff0a 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -56,6 +56,7 @@ "passwordSpecial": "Um caráter especial", "passwordMatch": "As palavras-passe coincidem", "passwordChangedSuccess": "Palavra-passe atualizada com sucesso", + "personalDataUpdatedSuccess": "Dados pessoais atualizados", "accept": "Aceitar", "errorMessageUnequalPasswords": "Las contraseñas não é coincidência.", "errorMessagePasswordTooShort": "A senha deve ter pelo menos 8 caracteres", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 4868589f..03c2be85 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -678,6 +678,7 @@ class I18n { static const String payoutTitle = 'payoutTitle'; static const String personalData = 'personalData'; static const String personalDataMessage = 'personalDataMessage'; + static const String personalDataUpdatedSuccess = 'personalDataUpdatedSuccess'; static const String phone = 'phone'; static const String phoneHint = 'phoneHint'; static const String phoneLabel = 'phoneLabel';