diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart index 89912022..8f26ccca 100644 --- a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart +++ b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart @@ -1,12 +1,13 @@ -import 'package:account/src/features/account_settings/presentation/state/account_settings_view_model.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:account/src/features/account_settings/presentation/providers/account_settings_controller.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_ui/legacy_ui.dart'; -import 'package:sf_shared/sf_shared.dart'; import 'package:navigation/navigation.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:utils/utils.dart'; @@ -14,7 +15,7 @@ import 'widgets/reg_code_dialog.dart'; class AccountSettingsScreen extends ConsumerWidget { final NavigationContract navigationContract; - static final _privacyUrl = + static const _privacyUrl = 'https://savefamilygps.com/pages/politica-de-privacidad-reloj-gps-infantil-localizador-savefamily'; const AccountSettingsScreen({super.key, required this.navigationContract}); @@ -24,15 +25,15 @@ class AccountSettingsScreen extends ConsumerWidget { final color = context.sfColors.legacyPrimary; final selectedDevice = ref.watch(selectedDeviceProvider).value; final isLoggingOut = ref.watch( - accountSettingsViewModelProvider.select((s) => s.isLoggingOut), + accountSettingsControllerProvider.select((s) => s.isLoading), ); - ref.listen(accountSettingsViewModelProvider.select((s) => s.isLoggingOut), ( - prev, - isLoggingOut, - ) { - if (prev == true && !isLoggingOut) { - navigationContract.goTo(AppRoutes.legacyLogin); + ref.listen(accountSettingsControllerProvider, (prev, next) async { + if (prev != null && prev.isLoading && !next.isLoading) { + await clearSessionData(); + ref.invalidate(legacyDevicesProvider); + ref.invalidate(selectedDeviceProvider); + if (context.mounted) navigationContract.goTo(AppRoutes.legacyLogin); } }); @@ -41,8 +42,8 @@ class AccountSettingsScreen extends ConsumerWidget { body: SingleChildScrollView( child: Padding( padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(horizontal: 22, vertical: 10), - big: EdgeInsets.symmetric(horizontal: 21, vertical: 8), + small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10), + big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8), ), child: Column( children: [ @@ -101,9 +102,9 @@ class AccountSettingsScreen extends ConsumerWidget { _item( context, onPressed: () { - showDialog( + showDialog( context: context, - builder: (context) => Dialog( + builder: (_) => Dialog( backgroundColor: Colors.transparent, child: RegCodeDialog( regCode: selectedDevice?.id ?? '', @@ -131,8 +132,8 @@ class AccountSettingsScreen extends ConsumerWidget { ), footer: Container( padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(vertical: 12, horizontal: 30), - big: EdgeInsets.symmetric(vertical: 10, horizontal: 28), + small: const EdgeInsets.symmetric(vertical: 12, horizontal: 30), + big: const EdgeInsets.symmetric(vertical: 10, horizontal: 28), ), child: PrimaryButton( text: context.translate(I18n.logOut), @@ -149,7 +150,7 @@ class AccountSettingsScreen extends ConsumerWidget { : null, onPressed: isLoggingOut ? () {} - : ref.read(accountSettingsViewModelProvider.notifier).logout, + : ref.read(accountSettingsControllerProvider.notifier).logout, ), ), ); diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.dart new file mode 100644 index 00000000..500a23c8 --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:legacy_auth/legacy_auth.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'account_settings_controller.g.dart'; + +@riverpod +class AccountSettingsController extends _$AccountSettingsController { + @override + FutureOr build() {} + + Future logout() async { + state = const AsyncLoading(); + try { + await ref.read(legacyAuthRepositoryProvider).logout(); + } catch (_) {} + unawaited(ref.read(sfTrackingProvider).legacyAuthLogout()); + state = const AsyncData(null); + } +} diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.g.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.g.dart new file mode 100644 index 00000000..90bb651e --- /dev/null +++ b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/providers/account_settings_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_settings_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(AccountSettingsController) +const accountSettingsControllerProvider = AccountSettingsControllerProvider._(); + +final class AccountSettingsControllerProvider + extends $AsyncNotifierProvider { + const AccountSettingsControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'accountSettingsControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$accountSettingsControllerHash(); + + @$internal + @override + AccountSettingsController create() => AccountSettingsController(); +} + +String _$accountSettingsControllerHash() => + r'8ca0c05ca6f2d5696126f5ab7ade83419c544afe'; + +abstract class _$AccountSettingsController 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/account_settings/presentation/state/account_settings_view_model.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_model.dart deleted file mode 100644 index c6677e47..00000000 --- a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_model.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:async'; - -import 'package:account/src/features/account_settings/presentation/state/account_settings_view_state.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:legacy_auth/legacy_auth.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_infrastructure/sf_infrastructure.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -final accountSettingsViewModelProvider = - NotifierProvider.autoDispose< - AccountSettingsViewModel, - AccountSettingsViewState - >(AccountSettingsViewModel.new); - -class AccountSettingsViewModel extends Notifier { - late final SfTrackingRepository _tracking; - - @override - AccountSettingsViewState build() { - _tracking = ref.read(sfTrackingProvider); - return const AccountSettingsViewState(); - } - - Future logout() async { - if (state.isLoggingOut) return; - - state = state.copyWith(isLoggingOut: true, errorMessage: ''); - - try { - await ref.read(legacyAuthRepositoryProvider).logout(); - } catch (_) {} - - await clearSessionData(); - ref.invalidate(legacyDevicesProvider); - ref.invalidate(selectedDeviceProvider); - - unawaited(_tracking.legacyAuthLogout()); - - state = state.copyWith(isLoggingOut: false); - } -} diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.dart deleted file mode 100644 index cf7b60df..00000000 --- a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'account_settings_view_state.freezed.dart'; - -@freezed -abstract class AccountSettingsViewState with _$AccountSettingsViewState { - const factory AccountSettingsViewState({ - @Default(false) bool isLoggingOut, - @Default('') String errorMessage, - }) = _AccountSettingsViewState; -} diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.freezed.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.freezed.dart deleted file mode 100644 index 611e444c..00000000 --- a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/state/account_settings_view_state.freezed.dart +++ /dev/null @@ -1,274 +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 'account_settings_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$AccountSettingsViewState { - - bool get isLoggingOut; String get errorMessage; -/// Create a copy of AccountSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$AccountSettingsViewStateCopyWith get copyWith => _$AccountSettingsViewStateCopyWithImpl(this as AccountSettingsViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is AccountSettingsViewState&&(identical(other.isLoggingOut, isLoggingOut) || other.isLoggingOut == isLoggingOut)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoggingOut,errorMessage); - -@override -String toString() { - return 'AccountSettingsViewState(isLoggingOut: $isLoggingOut, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $AccountSettingsViewStateCopyWith<$Res> { - factory $AccountSettingsViewStateCopyWith(AccountSettingsViewState value, $Res Function(AccountSettingsViewState) _then) = _$AccountSettingsViewStateCopyWithImpl; -@useResult -$Res call({ - bool isLoggingOut, String errorMessage -}); - - - - -} -/// @nodoc -class _$AccountSettingsViewStateCopyWithImpl<$Res> - implements $AccountSettingsViewStateCopyWith<$Res> { - _$AccountSettingsViewStateCopyWithImpl(this._self, this._then); - - final AccountSettingsViewState _self; - final $Res Function(AccountSettingsViewState) _then; - -/// Create a copy of AccountSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isLoggingOut = null,Object? errorMessage = null,}) { - return _then(_self.copyWith( -isLoggingOut: null == isLoggingOut ? _self.isLoggingOut : isLoggingOut // ignore: cast_nullable_to_non_nullable -as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -} - - -/// Adds pattern-matching-related methods to [AccountSettingsViewState]. -extension AccountSettingsViewStatePatterns on AccountSettingsViewState { -/// 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( _AccountSettingsViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _AccountSettingsViewState() 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( _AccountSettingsViewState value) $default,){ -final _that = this; -switch (_that) { -case _AccountSettingsViewState(): -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( _AccountSettingsViewState value)? $default,){ -final _that = this; -switch (_that) { -case _AccountSettingsViewState() 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 isLoggingOut, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _AccountSettingsViewState() when $default != null: -return $default(_that.isLoggingOut,_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 isLoggingOut, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _AccountSettingsViewState(): -return $default(_that.isLoggingOut,_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 isLoggingOut, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _AccountSettingsViewState() when $default != null: -return $default(_that.isLoggingOut,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _AccountSettingsViewState implements AccountSettingsViewState { - const _AccountSettingsViewState({this.isLoggingOut = false, this.errorMessage = ''}); - - -@override@JsonKey() final bool isLoggingOut; -@override@JsonKey() final String errorMessage; - -/// Create a copy of AccountSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$AccountSettingsViewStateCopyWith<_AccountSettingsViewState> get copyWith => __$AccountSettingsViewStateCopyWithImpl<_AccountSettingsViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _AccountSettingsViewState&&(identical(other.isLoggingOut, isLoggingOut) || other.isLoggingOut == isLoggingOut)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoggingOut,errorMessage); - -@override -String toString() { - return 'AccountSettingsViewState(isLoggingOut: $isLoggingOut, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$AccountSettingsViewStateCopyWith<$Res> implements $AccountSettingsViewStateCopyWith<$Res> { - factory _$AccountSettingsViewStateCopyWith(_AccountSettingsViewState value, $Res Function(_AccountSettingsViewState) _then) = __$AccountSettingsViewStateCopyWithImpl; -@override @useResult -$Res call({ - bool isLoggingOut, String errorMessage -}); - - - - -} -/// @nodoc -class __$AccountSettingsViewStateCopyWithImpl<$Res> - implements _$AccountSettingsViewStateCopyWith<$Res> { - __$AccountSettingsViewStateCopyWithImpl(this._self, this._then); - - final _AccountSettingsViewState _self; - final $Res Function(_AccountSettingsViewState) _then; - -/// Create a copy of AccountSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isLoggingOut = null,Object? errorMessage = null,}) { - return _then(_AccountSettingsViewState( -isLoggingOut: null == isLoggingOut ? _self.isLoggingOut : isLoggingOut // ignore: cast_nullable_to_non_nullable -as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/account/lib/src/features/delete_account/presentation/providers/delete_account_controller.g.dart b/modules/legacy/modules/account/lib/src/features/delete_account/presentation/providers/delete_account_controller.g.dart index aa4c7cef..a2974668 100644 --- a/modules/legacy/modules/account/lib/src/features/delete_account/presentation/providers/delete_account_controller.g.dart +++ b/modules/legacy/modules/account/lib/src/features/delete_account/presentation/providers/delete_account_controller.g.dart @@ -34,7 +34,7 @@ final class DeleteAccountControllerProvider } String _$deleteAccountControllerHash() => - r'fec69b905e8020c9357500bdd4c75e70b2b0797d'; + r'293d3eacd0a4189e8849dd9f825a0a04c80cb0ff'; abstract class _$DeleteAccountController extends $AsyncNotifier { FutureOr build(); diff --git a/modules/legacy/modules/account/test/features/account_settings/account_settings_controller_test.dart b/modules/legacy/modules/account/test/features/account_settings/account_settings_controller_test.dart new file mode 100644 index 00000000..9595f2d0 --- /dev/null +++ b/modules/legacy/modules/account/test/features/account_settings/account_settings_controller_test.dart @@ -0,0 +1,57 @@ +import 'package:account/src/features/account_settings/presentation/providers/account_settings_controller.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:legacy_auth/legacy_auth.dart'; +import 'package:legacy_auth/src/core/domain/repositories/auth_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockLegacyAuthRepository extends Mock implements LegacyAuthRepository {} + +void main() { + ProviderContainer buildContainer(LegacyAuthRepository authRepo) { + return makeContainer( + overrides: [ + legacyAuthRepositoryProvider.overrideWithValue(authRepo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('AccountSettingsController.logout', () { + test('transitions to AsyncData when backend logout succeeds', () async { + final auth = MockLegacyAuthRepository(); + when(() => auth.logout()).thenAnswer((_) async {}); + + final container = buildContainer(auth); + addTearDown(container.dispose); + + await container.read(accountSettingsControllerProvider.notifier).logout(); + + final state = container.read(accountSettingsControllerProvider); + expect(state, isA>()); + expect(state.isLoading, isFalse); + + verify(() => auth.logout()).called(1); + }); + + test('still completes when backend logout throws', () async { + final auth = MockLegacyAuthRepository(); + when(() => auth.logout()).thenThrow(Exception('server down')); + + final container = buildContainer(auth); + addTearDown(container.dispose); + + await container.read(accountSettingsControllerProvider.notifier).logout(); + + final state = container.read(accountSettingsControllerProvider); + expect(state, isA>()); + expect(state.isLoading, isFalse); + + verify(() => auth.logout()).called(1); + }); + }); +}