diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart index b76cff97..0fdf796f 100644 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart @@ -1,89 +1,99 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:legacy_theme/legacy_theme.dart'; import 'package:legacy_ui/legacy_ui.dart'; import 'package:navigation/navigation.dart'; -import 'package:settings/src/features/language/presentation/state/language_view_model.dart'; +import 'package:settings/src/features/language/presentation/providers/language_controller.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; -class LanguageScreen extends ConsumerWidget { +const _languageOptions = { + 'es': 'español', + 'en': 'English', + 'pt': 'português', + 'it': 'italiano', + 'fr': 'français', + 'de': 'Deutsch', +}; + +class LanguageScreen extends ConsumerStatefulWidget { final NavigationContract navigationContract; const LanguageScreen({super.key, required this.navigationContract}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _LanguageScreenState(); +} - final vm = ref.read(languageViewModelProvider.notifier); - final language = ref.watch( - languageViewModelProvider.select((s) => s.language), - ); +class _LanguageScreenState extends ConsumerState { + String? _selectedLanguage; - const Map languageOptions = { - 'es': 'español', - 'en': 'English', - 'pt': 'português', - 'it': 'italiano', - 'fr': 'français', - 'de': 'Deutsch', - }; - final languageCodes = languageOptions.keys.toList(growable: false); - final languageLabels = languageOptions.values.toList(growable: false); - - ref.listen(languageViewModelProvider.select((s) => s.errorMessage), ( - _, - errorMessage, - ) { - if (errorMessage.isNotEmpty) { - showTopSnackbar( - context, - message: errorMessage, - type: MessageType.error, - ); + @override + Widget build(BuildContext context) { + ref.listen(languageControllerProvider, (prev, next) async { + next.showErrorOn(context); + if (prev != null && + prev.isLoading && + !next.isLoading && + !next.hasError) { + await showSuccessDialog(context, I18n.deviceUpdatedSuccess); + if (context.mounted) widget.navigationContract.goBack(); } }); - ref.listen(languageViewModelProvider.select((s) => s.isComplete), ( - _, - isComplete, - ) { - if (isComplete) navigationContract.goBack(); - }); + final device = ref.watch(selectedDeviceProvider).value; + final currentLanguage = device?.settings.language ?? 'es'; + final selectedLanguage = _selectedLanguage ?? currentLanguage; + final isLoading = ref.watch( + languageControllerProvider.select((s) => s.isLoading), + ); return LegacyPageLayout( title: context.translate(I18n.languageTitle), body: SingleChildScrollView( child: RadioGroup( - groupValue: language, + groupValue: selectedLanguage, onChanged: (value) { - vm.selectLanguage(value.toString()); + if (value != null) setState(() => _selectedLanguage = value); }, child: Column( - children: List.generate(languageOptions.length, (int i) { - return _Option( - value: languageCodes.elementAt(i), - label: languageLabels.elementAt(i), - ); - }), + children: _languageOptions.entries + .map((e) => _Option(value: e.key, label: e.value)) + .toList(growable: false), ), ), ), - footer: const _SaveSection(), + footer: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: isLoading + ? const Center(child: CircularProgressIndicator()) + : PrimaryButton( + onPressed: () async { + if (device == null) return; + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + ref + .read(languageControllerProvider.notifier) + .save(device: device, newLanguage: selectedLanguage); + }, + text: context.translate(I18n.save), + color: context.sfColors.legacyPrimary, + ), + ), ); } } -class _Option extends ConsumerWidget { +class _Option extends StatelessWidget { final String value; final String label; const _Option({required this.value, required this.label}); @override - Widget build(BuildContext context, WidgetRef ref) { - + Widget build(BuildContext context) { return RadioListTile( title: Text(label), value: value, @@ -92,29 +102,3 @@ class _Option extends ConsumerWidget { ); } } - -class _SaveSection extends ConsumerWidget { - const _SaveSection(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(languageViewModelProvider.notifier); - final isLoading = ref.watch( - languageViewModelProvider.select((s) => s.isLoading), - ); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: isLoading - ? const Center(child: CircularProgressIndicator()) - : PrimaryButton( - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - vm.submit(); - }, - text: context.translate(I18n.save), - color: context.sfColors.legacyPrimary, - ), - ); - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.dart new file mode 100644 index 00000000..18e5d39a --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/providers/language_repository_provider.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'language_controller.g.dart'; + +@riverpod +class LanguageController extends _$LanguageController { + @override + FutureOr build() {} + + Future save({ + required DeviceEntity device, + required String newLanguage, + }) async { + if (newLanguage == device.settings.language) return; + + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref + .read(languageRepositoryProvider) + .updateDeviceLanguage(device: device, newLanguage: newLanguage); + ref.syncDeviceSettings( + device, + device.settings.copyWith(language: newLanguage), + ); + unawaited( + ref.read(sfTrackingProvider).legacySettingsLanguageChanged(newLanguage), + ); + }); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.g.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.g.dart new file mode 100644 index 00000000..43ec1cc9 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/providers/language_controller.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'language_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(LanguageController) +const languageControllerProvider = LanguageControllerProvider._(); + +final class LanguageControllerProvider + extends $AsyncNotifierProvider { + const LanguageControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'languageControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$languageControllerHash(); + + @$internal + @override + LanguageController create() => LanguageController(); +} + +String _$languageControllerHash() => + r'54d18f53bea0cdb8f7346cf66d4c1e190cfd7d75'; + +abstract class _$LanguageController 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/settings/lib/src/features/language/presentation/state/language_view_model.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart deleted file mode 100644 index b288f147..00000000 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:settings/src/core/domain/repositories/language_repository.dart'; -import 'package:settings/src/core/providers/language_repository_provider.dart'; -import 'package:sf_tracking/sf_tracking.dart'; -import 'language_view_state.dart'; - -final languageViewModelProvider = - NotifierProvider.autoDispose( - LanguageViewModel.new, - ); - -class LanguageViewModel extends Notifier { - late final LanguageRepository _languageRepository; - late final SfTrackingRepository _tracking; - - @override - LanguageViewState build() { - _languageRepository = ref.read(languageRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - Future.microtask(() => load()); - return const LanguageViewState(isLoading: true); - } - - Future load() async { - state = state.copyWith(isLoading: true, errorMessage: ''); - try { - final device = ref.read(selectedDeviceProvider).value; - state = state.copyWith( - isLoading: false, - device: device, - language: device?.settings.language ?? 'es', - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith(isLoading: false, errorMessage: e.toString()); - } - } - - void selectLanguage(String value) { - if (value == state.language) return; - state = state.copyWith(language: value); - } - - Future submit() async { - final device = state.device; - if (device == null) return; - - if (state.language == device.settings.language) { - state = state.copyWith(isComplete: true); - return; - } - - try { - state = state.copyWith(isLoading: true, isComplete: false); - - await _languageRepository.updateDeviceLanguage( - device: device, - newLanguage: state.language, - ); - - if (!ref.mounted) return; - ref.syncDeviceSettings( - device, - device.settings.copyWith(language: state.language), - ); - - unawaited(_tracking.legacySettingsLanguageChanged(state.language)); - - state = state.copyWith(isLoading: false, isComplete: true); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith(isLoading: false, errorMessage: e.toString()); - } - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_state.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_state.dart deleted file mode 100644 index f1fd123d..00000000 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_state.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:sf_shared/sf_shared.dart'; - -part 'language_view_state.freezed.dart'; - -@freezed -abstract class LanguageViewState with _$LanguageViewState { - const LanguageViewState._(); - - const factory LanguageViewState({ - @Default(false) bool isLoading, - @Default(false) bool isComplete, - DeviceEntity? device, - @Default('es') String language, - @Default('') String errorMessage, - }) = _LanguageViewState; -} diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_state.freezed.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_state.freezed.dart deleted file mode 100644 index 469fc12a..00000000 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_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 'language_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$LanguageViewState { - - bool get isLoading; bool get isComplete; DeviceEntity? get device; String get language; String get errorMessage; -/// Create a copy of LanguageViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$LanguageViewStateCopyWith get copyWith => _$LanguageViewStateCopyWithImpl(this as LanguageViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is LanguageViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.device, device) || other.device == device)&&(identical(other.language, language) || other.language == language)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,device,language,errorMessage); - -@override -String toString() { - return 'LanguageViewState(isLoading: $isLoading, isComplete: $isComplete, device: $device, language: $language, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $LanguageViewStateCopyWith<$Res> { - factory $LanguageViewStateCopyWith(LanguageViewState value, $Res Function(LanguageViewState) _then) = _$LanguageViewStateCopyWithImpl; -@useResult -$Res call({ - bool isLoading, bool isComplete, DeviceEntity? device, String language, String errorMessage -}); - - -$DeviceEntityCopyWith<$Res>? get device; - -} -/// @nodoc -class _$LanguageViewStateCopyWithImpl<$Res> - implements $LanguageViewStateCopyWith<$Res> { - _$LanguageViewStateCopyWithImpl(this._self, this._then); - - final LanguageViewState _self; - final $Res Function(LanguageViewState) _then; - -/// Create a copy of LanguageViewState -/// 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? device = freezed,Object? language = null,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,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable -as DeviceEntity?,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} -/// Create a copy of LanguageViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$DeviceEntityCopyWith<$Res>? get device { - if (_self.device == null) { - return null; - } - - return $DeviceEntityCopyWith<$Res>(_self.device!, (value) { - return _then(_self.copyWith(device: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [LanguageViewState]. -extension LanguageViewStatePatterns on LanguageViewState { -/// 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( _LanguageViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _LanguageViewState() 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( _LanguageViewState value) $default,){ -final _that = this; -switch (_that) { -case _LanguageViewState(): -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( _LanguageViewState value)? $default,){ -final _that = this; -switch (_that) { -case _LanguageViewState() 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, DeviceEntity? device, String language, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _LanguageViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.device,_that.language,_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, DeviceEntity? device, String language, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _LanguageViewState(): -return $default(_that.isLoading,_that.isComplete,_that.device,_that.language,_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, DeviceEntity? device, String language, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _LanguageViewState() when $default != null: -return $default(_that.isLoading,_that.isComplete,_that.device,_that.language,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _LanguageViewState extends LanguageViewState { - const _LanguageViewState({this.isLoading = false, this.isComplete = false, this.device, this.language = 'es', this.errorMessage = ''}): super._(); - - -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isComplete; -@override final DeviceEntity? device; -@override@JsonKey() final String language; -@override@JsonKey() final String errorMessage; - -/// Create a copy of LanguageViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$LanguageViewStateCopyWith<_LanguageViewState> get copyWith => __$LanguageViewStateCopyWithImpl<_LanguageViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _LanguageViewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.device, device) || other.device == device)&&(identical(other.language, language) || other.language == language)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,isLoading,isComplete,device,language,errorMessage); - -@override -String toString() { - return 'LanguageViewState(isLoading: $isLoading, isComplete: $isComplete, device: $device, language: $language, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$LanguageViewStateCopyWith<$Res> implements $LanguageViewStateCopyWith<$Res> { - factory _$LanguageViewStateCopyWith(_LanguageViewState value, $Res Function(_LanguageViewState) _then) = __$LanguageViewStateCopyWithImpl; -@override @useResult -$Res call({ - bool isLoading, bool isComplete, DeviceEntity? device, String language, String errorMessage -}); - - -@override $DeviceEntityCopyWith<$Res>? get device; - -} -/// @nodoc -class __$LanguageViewStateCopyWithImpl<$Res> - implements _$LanguageViewStateCopyWith<$Res> { - __$LanguageViewStateCopyWithImpl(this._self, this._then); - - final _LanguageViewState _self; - final $Res Function(_LanguageViewState) _then; - -/// Create a copy of LanguageViewState -/// 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? device = freezed,Object? language = null,Object? errorMessage = null,}) { - return _then(_LanguageViewState( -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,device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable -as DeviceEntity?,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable -as String,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -/// Create a copy of LanguageViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$DeviceEntityCopyWith<$Res>? get device { - if (_self.device == null) { - return null; - } - - return $DeviceEntityCopyWith<$Res>(_self.device!, (value) { - return _then(_self.copyWith(device: value)); - }); -} -} - -// dart format on diff --git a/modules/legacy/modules/settings/pubspec.yaml b/modules/legacy/modules/settings/pubspec.yaml index c6f8ba1a..82de74c4 100644 --- a/modules/legacy/modules/settings/pubspec.yaml +++ b/modules/legacy/modules/settings/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: get_it: ^9.0.5 go_router: ^17.0.0 flutter_riverpod: ^3.0.3 + riverpod_annotation: ^3.0.3 freezed_annotation: ^3.1.0 freezed: ^3.2.3 dio: ^5.9.2 @@ -72,6 +73,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + riverpod_generator: ^3.0.3 + build_runner: ^2.7.1 + mocktail: ^1.0.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/modules/legacy/modules/settings/test/features/language/language_controller_test.dart b/modules/legacy/modules/settings/test/features/language/language_controller_test.dart new file mode 100644 index 00000000..046922e0 --- /dev/null +++ b/modules/legacy/modules/settings/test/features/language/language_controller_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:settings/src/core/domain/repositories/language_repository.dart'; +import 'package:settings/src/core/providers/language_repository_provider.dart'; +import 'package:settings/src/features/language/presentation/providers/language_controller.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 MockLanguageRepository extends Mock implements LanguageRepository {} + +const _device = DeviceEntity( + id: 'device-1', + identificator: 'imei-1', + carrierName: 'Watch', + settings: DeviceSettingsEntity(language: 'es'), +); + +void main() { + setUpAll(() { + registerFallbackValue(_device); + }); + + ProviderContainer buildContainer(LanguageRepository repo) { + return makeContainer( + overrides: [ + languageRepositoryProvider.overrideWithValue(repo), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('LanguageController.save', () { + test('transitions to AsyncData when repository succeeds', () async { + final repo = MockLanguageRepository(); + when( + () => repo.updateDeviceLanguage( + device: any(named: 'device'), + newLanguage: any(named: 'newLanguage'), + ), + ).thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(languageControllerProvider.notifier) + .save(device: _device, newLanguage: 'en'); + + final state = container.read(languageControllerProvider); + expect(state, isA>()); + expect(state.error, isNull); + + verify( + () => repo.updateDeviceLanguage(device: _device, newLanguage: 'en'), + ).called(1); + }); + + test('no-ops when new language equals current', () async { + final repo = MockLanguageRepository(); + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(languageControllerProvider.notifier) + .save(device: _device, newLanguage: 'es'); + + verifyNever( + () => repo.updateDeviceLanguage( + device: any(named: 'device'), + newLanguage: any(named: 'newLanguage'), + ), + ); + }); + + test('exposes AsyncError when the repository fails', () async { + final repo = MockLanguageRepository(); + when( + () => repo.updateDeviceLanguage( + device: any(named: 'device'), + newLanguage: any(named: 'newLanguage'), + ), + ).thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + + await container + .read(languageControllerProvider.notifier) + .save(device: _device, newLanguage: 'en'); + + final state = container.read(languageControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + }); + }); +}