From eb2bde8d40287e5660a7fa6f1707f68326da17b8 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 22 Apr 2026 01:20:55 +0200 Subject: [PATCH] refactor(legacy-settings): migrate remote_management to AsyncNotifier --- .../remote_management_controller.dart | 50 ++++ .../remote_management_controller.g.dart | 57 ++++ .../remote_management_screen.dart | 170 ++++------- .../state/remote_management_view_model.dart | 69 ----- .../state/remote_management_view_state.dart | 13 - .../remote_management_view_state.freezed.dart | 280 ------------------ .../presentation/widgets/confirm_dialog.dart | 70 ++--- .../presentation/settings_screen.dart | 9 +- .../remote_management_controller_test.dart | 127 ++++++++ 9 files changed, 330 insertions(+), 515 deletions(-) create mode 100644 modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.dart create mode 100644 modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.g.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.dart delete mode 100644 modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.freezed.dart create mode 100644 modules/legacy/modules/settings/test/features/remote_management/remote_management_controller_test.dart diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.dart new file mode 100644 index 00000000..e297e7dd --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.dart @@ -0,0 +1,50 @@ +import 'dart:async'; + +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_shared/sf_shared.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'remote_management_controller.g.dart'; + +@riverpod +class RemoteManagementController extends _$RemoteManagementController { + @override + FutureOr build() {} + + Future shutdown() async { + unawaited( + ref.read(sfTrackingProvider).legacySettingsRemoteManagementShutdown(), + ); + await _send(DeviceCommand.shutdown); + } + + Future restart() async { + unawaited( + ref.read(sfTrackingProvider).legacySettingsRemoteManagementRestart(), + ); + await _send(DeviceCommand.restart); + } + + Future factoryReset() async { + unawaited( + ref.read(sfTrackingProvider).legacySettingsRemoteManagementFactoryReset(), + ); + await _send(DeviceCommand.factory); + } + + Future _send(DeviceCommand command) async { + final device = ref.read(selectedDeviceProvider).value; + if (device == null) return; + + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: device.identificator, + command: command, + ), + ); + }); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.g.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.g.dart new file mode 100644 index 00000000..ca5b3c83 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/providers/remote_management_controller.g.dart @@ -0,0 +1,57 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_management_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(RemoteManagementController) +const remoteManagementControllerProvider = + RemoteManagementControllerProvider._(); + +final class RemoteManagementControllerProvider + extends $AsyncNotifierProvider { + const RemoteManagementControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'remoteManagementControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$remoteManagementControllerHash(); + + @$internal + @override + RemoteManagementController create() => RemoteManagementController(); +} + +String _$remoteManagementControllerHash() => + r'7442ea1f5b7029ebe78c0ab97523e81dcb65a0df'; + +abstract class _$RemoteManagementController 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/remote_management/presentation/remote_management_screen.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart index 9ea5331f..946c85e9 100644 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart @@ -1,12 +1,12 @@ -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/remote_management/presentation/state/remote_management_view_model.dart'; +import 'package:settings/src/features/remote_management/presentation/providers/remote_management_controller.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'widgets/confirm_dialog.dart'; @@ -17,6 +17,34 @@ class RemoteManagementScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.listen(remoteManagementControllerProvider, (prev, next) async { + next.showErrorOn(context); + if (prev != null && + prev.isLoading && + !next.isLoading && + !next.hasError) { + await showSuccessDialog(context, I18n.deviceUpdatedSuccess); + } + }); + + Future runCommand({ + required String title, + required String message, + required Future Function() command, + }) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + await showDialog( + context: context, + builder: (_) => ConfirmDialog( + title: title, + message: message, + onConfirm: command, + ), + ); + } + + final controller = ref.read(remoteManagementControllerProvider.notifier); return LegacyPageLayout( title: context.translate(I18n.remoteManagement), @@ -29,113 +57,44 @@ class RemoteManagementScreen extends ConsumerWidget { size: 180, ), ), - SizedBox(height: 36), - _OptionsSection(), - ], - ), - ); - } -} - -class _OptionsSection extends ConsumerWidget { - const _OptionsSection(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(remoteManagementViewModelProvider.notifier); - - return SingleChildScrollView( - child: Column( - children: [ - _SectionButton( - title: context.translate(I18n.remoteTurnOff), - subtitle: context.translate(I18n.remoteTurnOffMessage), - icon: Icons.settings_power_outlined, - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) => Dialog( - child: ConfirmDialog( + const SizedBox(height: 36), + SingleChildScrollView( + child: Column( + children: [ + _CommandButton( + title: context.translate(I18n.remoteTurnOff), + subtitle: context.translate(I18n.remoteTurnOffMessage), + icon: Icons.settings_power_outlined, + onPressed: () => runCommand( title: context.translate(I18n.remoteTurnOff), message: context.translate(I18n.remoteTurnOffConfirm), - onConfirm: () async { - await vm.shutdown(); - if (!context.mounted) return; - - final errorMessage = ref.read( - remoteManagementViewModelProvider.select( - (s) => s.errorMessage, - ), - ); - if (errorMessage.isNotEmpty) { - showTopSnackbar( - context, - message: errorMessage, - type: MessageType.error, - ); - } - - final isComplete = ref.read( - remoteManagementViewModelProvider.select( - (s) => s.isComplete, - ), - ); - if (isComplete) { - Navigator.pop(context); - } - }, + command: controller.shutdown, ), ), - ); - }, - ), - SizedBox(height: 12), - _SectionButton( - title: context.translate(I18n.remoteRestart), - subtitle: context.translate(I18n.remoteRestartMessage), - icon: Icons.refresh_outlined, - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) => Dialog( - child: ConfirmDialog( + const SizedBox(height: 12), + _CommandButton( + title: context.translate(I18n.remoteRestart), + subtitle: context.translate(I18n.remoteRestartMessage), + icon: Icons.refresh_outlined, + onPressed: () => runCommand( title: context.translate(I18n.remoteRestart), message: context.translate(I18n.remoteRestartConfirm), - onConfirm: () { - vm.restart(); - Navigator.pop(context); - }, + command: controller.restart, ), ), - ); - }, - ), - SizedBox(height: 12), - _SectionButton( - title: context.translate(I18n.remoteFactoryReset), - subtitle: context.translate(I18n.remoteFactoryResetMessage), - icon: Icons.restart_alt_outlined, - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) => Dialog( - child: ConfirmDialog( + const SizedBox(height: 12), + _CommandButton( + title: context.translate(I18n.remoteFactoryReset), + subtitle: context.translate(I18n.remoteFactoryResetMessage), + icon: Icons.restart_alt_outlined, + onPressed: () => runCommand( title: context.translate(I18n.remoteFactoryReset), message: context.translate(I18n.remoteFactoryResetConfirm), - onConfirm: () { - vm.factoryReset(); - Navigator.pop(context); - }, + command: controller.factoryReset, ), ), - ); - }, + ], + ), ), ], ), @@ -143,13 +102,13 @@ class _OptionsSection extends ConsumerWidget { } } -class _SectionButton extends ConsumerWidget { +class _CommandButton extends StatelessWidget { final String title; final String subtitle; final IconData icon; final VoidCallback onPressed; - const _SectionButton({ + const _CommandButton({ required this.title, required this.subtitle, required this.icon, @@ -157,22 +116,17 @@ class _SectionButton extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - + Widget build(BuildContext context) { return SectionButton( onPressed: onPressed, - icon: Icon( - icon, - color: context.sfColors.legacyPrimary, - size: 48, - ), + icon: Icon(icon, color: context.sfColors.legacyPrimary, size: 48), body: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Text( title, - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16), ), Text( subtitle, diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart deleted file mode 100644 index d7052e5b..00000000 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:legacy_device_state/legacy_device_state.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_tracking/sf_tracking.dart'; - -import 'remote_management_view_state.dart'; - -final remoteManagementViewModelProvider = - NotifierProvider.autoDispose< - RemoteManagementViewModel, - RemoteManagementViewState - >(RemoteManagementViewModel.new); - -class RemoteManagementViewModel extends Notifier { - late final CommandsRepository _commandsRepository; - late final SfTrackingRepository _tracking; - - @override - RemoteManagementViewState build() { - _commandsRepository = ref.read(commandsRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - state = const RemoteManagementViewState(); - - _init(); - - return state; - } - - Future _init() async { - final device = ref.read(selectedDeviceProvider).value; - - state = state.copyWith(deviceId: device!.identificator, isLoading: false); - } - - Future tryCommand(DeviceCommand command) async { - try { - state = state.copyWith(isLoading: true, isComplete: false); - - final request = SendCommandRequestModel( - device: state.deviceId, - command: command, - ); - - _commandsRepository.send(request: request); - - state = state.copyWith(isLoading: false, isComplete: true); - } catch (e) { - state = state.copyWith(isLoading: false, errorMessage: e.toString()); - } - } - - Future shutdown() async { - unawaited(_tracking.legacySettingsRemoteManagementShutdown()); - await tryCommand(DeviceCommand.shutdown); - } - - Future restart() async { - unawaited(_tracking.legacySettingsRemoteManagementRestart()); - await tryCommand(DeviceCommand.restart); - } - - Future factoryReset() async { - unawaited(_tracking.legacySettingsRemoteManagementFactoryReset()); - await tryCommand(DeviceCommand.factory); - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.dart deleted file mode 100644 index 47ee1e18..00000000 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'remote_management_view_state.freezed.dart'; - -@freezed -abstract class RemoteManagementViewState with _$RemoteManagementViewState { - const factory RemoteManagementViewState({ - @Default('') String deviceId, - @Default(true) bool isLoading, - @Default(true) bool isComplete, - @Default('') String errorMessage, - }) = _RemoteManagementViewState; -} diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.freezed.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.freezed.dart deleted file mode 100644 index d0cf27fa..00000000 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_state.freezed.dart +++ /dev/null @@ -1,280 +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 'remote_management_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$RemoteManagementViewState { - - String get deviceId; bool get isLoading; bool get isComplete; String get errorMessage; -/// Create a copy of RemoteManagementViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$RemoteManagementViewStateCopyWith get copyWith => _$RemoteManagementViewStateCopyWithImpl(this as RemoteManagementViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteManagementViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,deviceId,isLoading,isComplete,errorMessage); - -@override -String toString() { - return 'RemoteManagementViewState(deviceId: $deviceId, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class $RemoteManagementViewStateCopyWith<$Res> { - factory $RemoteManagementViewStateCopyWith(RemoteManagementViewState value, $Res Function(RemoteManagementViewState) _then) = _$RemoteManagementViewStateCopyWithImpl; -@useResult -$Res call({ - String deviceId, bool isLoading, bool isComplete, String errorMessage -}); - - - - -} -/// @nodoc -class _$RemoteManagementViewStateCopyWithImpl<$Res> - implements $RemoteManagementViewStateCopyWith<$Res> { - _$RemoteManagementViewStateCopyWithImpl(this._self, this._then); - - final RemoteManagementViewState _self; - final $Res Function(RemoteManagementViewState) _then; - -/// Create a copy of RemoteManagementViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) { - return _then(_self.copyWith( -deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable -as String,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,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - -} - - -/// Adds pattern-matching-related methods to [RemoteManagementViewState]. -extension RemoteManagementViewStatePatterns on RemoteManagementViewState { -/// 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( _RemoteManagementViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _RemoteManagementViewState() 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( _RemoteManagementViewState value) $default,){ -final _that = this; -switch (_that) { -case _RemoteManagementViewState(): -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( _RemoteManagementViewState value)? $default,){ -final _that = this; -switch (_that) { -case _RemoteManagementViewState() 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( String deviceId, bool isLoading, bool isComplete, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _RemoteManagementViewState() when $default != null: -return $default(_that.deviceId,_that.isLoading,_that.isComplete,_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( String deviceId, bool isLoading, bool isComplete, String errorMessage) $default,) {final _that = this; -switch (_that) { -case _RemoteManagementViewState(): -return $default(_that.deviceId,_that.isLoading,_that.isComplete,_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( String deviceId, bool isLoading, bool isComplete, String errorMessage)? $default,) {final _that = this; -switch (_that) { -case _RemoteManagementViewState() when $default != null: -return $default(_that.deviceId,_that.isLoading,_that.isComplete,_that.errorMessage);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _RemoteManagementViewState implements RemoteManagementViewState { - const _RemoteManagementViewState({this.deviceId = '', this.isLoading = true, this.isComplete = true, this.errorMessage = ''}); - - -@override@JsonKey() final String deviceId; -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isComplete; -@override@JsonKey() final String errorMessage; - -/// Create a copy of RemoteManagementViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$RemoteManagementViewStateCopyWith<_RemoteManagementViewState> get copyWith => __$RemoteManagementViewStateCopyWithImpl<_RemoteManagementViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteManagementViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); -} - - -@override -int get hashCode => Object.hash(runtimeType,deviceId,isLoading,isComplete,errorMessage); - -@override -String toString() { - return 'RemoteManagementViewState(deviceId: $deviceId, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)'; -} - - -} - -/// @nodoc -abstract mixin class _$RemoteManagementViewStateCopyWith<$Res> implements $RemoteManagementViewStateCopyWith<$Res> { - factory _$RemoteManagementViewStateCopyWith(_RemoteManagementViewState value, $Res Function(_RemoteManagementViewState) _then) = __$RemoteManagementViewStateCopyWithImpl; -@override @useResult -$Res call({ - String deviceId, bool isLoading, bool isComplete, String errorMessage -}); - - - - -} -/// @nodoc -class __$RemoteManagementViewStateCopyWithImpl<$Res> - implements _$RemoteManagementViewStateCopyWith<$Res> { - __$RemoteManagementViewStateCopyWithImpl(this._self, this._then); - - final _RemoteManagementViewState _self; - final $Res Function(_RemoteManagementViewState) _then; - -/// Create a copy of RemoteManagementViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) { - return _then(_RemoteManagementViewState( -deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable -as String,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,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/widgets/confirm_dialog.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/widgets/confirm_dialog.dart index f8b6d0bb..9f3e43aa 100644 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/widgets/confirm_dialog.dart +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/widgets/confirm_dialog.dart @@ -1,10 +1,8 @@ -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sf_localizations/sf_localizations.dart'; import 'package:legacy_theme/legacy_theme.dart'; +import 'package:sf_localizations/sf_localizations.dart'; -class ConfirmDialog extends ConsumerWidget { +class ConfirmDialog extends StatelessWidget { final String title; final String message; final VoidCallback onConfirm; @@ -17,48 +15,32 @@ class ConfirmDialog extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.all(Radius.circular(10)), + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AlertDialog( + icon: Icon( + Icons.warning_amber_rounded, + color: theme.colorScheme.error, + size: 40, ), - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), - height: 180, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + title: Text(title), + content: Text(message, style: theme.textTheme.bodyMedium), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.translate(I18n.cancel)), + ), + FilledButton( + onPressed: () { + Navigator.pop(context); + onConfirm(); + }, + style: FilledButton.styleFrom( + backgroundColor: context.sfColors.legacyPrimary, ), - Text(message, textAlign: TextAlign.center), - Row( - children: [ - Expanded( - child: PrimaryButton( - onPressed: () { - Navigator.pop(context); - }, - text: context.translate(I18n.cancel), - color: context.sfColors.legacyPrimary, - ), - ), - SizedBox(width: 14), - Expanded( - child: PrimaryButton( - onPressed: onConfirm, - text: context.translate(I18n.accept), - color: context.sfColors.legacyPrimary, - ), - ), - ], - ), - ], - ), + child: Text(context.translate(I18n.accept)), + ), + ], ); } } diff --git a/modules/legacy/modules/settings/lib/src/features/settings/presentation/settings_screen.dart b/modules/legacy/modules/settings/lib/src/features/settings/presentation/settings_screen.dart index b5040368..36c44415 100644 --- a/modules/legacy/modules/settings/lib/src/features/settings/presentation/settings_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/settings/presentation/settings_screen.dart @@ -82,7 +82,14 @@ class SettingsScreen extends ConsumerWidget { text: I18n.batteryNightSavingMode, color: color, ), - // _item(context, onPressed: () => navigationContract.pushTo(AppRoutes.remoteManagement), icon: Icons.settings_remote_outlined, text: I18n.remoteManagement, color: color), + _item( + context, + onPressed: () => + navigationContract.pushTo(AppRoutes.remoteManagement), + icon: Icons.settings_remote_outlined, + text: I18n.remoteManagement, + color: color, + ), _item( context, onPressed: () => diff --git a/modules/legacy/modules/settings/test/features/remote_management/remote_management_controller_test.dart b/modules/legacy/modules/settings/test/features/remote_management/remote_management_controller_test.dart new file mode 100644 index 00000000..f275760e --- /dev/null +++ b/modules/legacy/modules/settings/test/features/remote_management/remote_management_controller_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:settings/src/features/remote_management/presentation/providers/remote_management_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 MockCommandsRepository extends Mock implements CommandsRepository {} + +class _FakeSelectedDeviceNotifier extends SelectedDeviceNotifier { + _FakeSelectedDeviceNotifier(this._device); + final DeviceEntity? _device; + + @override + Future build() async => _device; +} + +const _device = DeviceEntity( + id: 'device-1', + identificator: 'imei-1', + carrierName: 'Watch', +); + +void main() { + setUpAll(() { + registerFallbackValue( + const SendCommandRequestModel( + device: '', + command: DeviceCommand.shutdown, + ), + ); + }); + + ProviderContainer buildContainer(CommandsRepository repo) { + return makeContainer( + overrides: [ + commandsRepositoryProvider.overrideWithValue(repo), + selectedDeviceProvider.overrideWith( + () => _FakeSelectedDeviceNotifier(_device), + ), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('RemoteManagementController', () { + test('shutdown sends DeviceCommand.shutdown on success', () async { + final repo = MockCommandsRepository(); + when(() => repo.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + await container.read(selectedDeviceProvider.future); + + await container.read(remoteManagementControllerProvider.notifier).shutdown(); + + expect( + container.read(remoteManagementControllerProvider), + isA>(), + ); + + final captured = verify( + () => repo.send(request: captureAny(named: 'request')), + ).captured.single as SendCommandRequestModel; + expect(captured.command, DeviceCommand.shutdown); + expect(captured.device, 'imei-1'); + }); + + test('restart sends DeviceCommand.restart on success', () async { + final repo = MockCommandsRepository(); + when(() => repo.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + await container.read(selectedDeviceProvider.future); + + await container.read(remoteManagementControllerProvider.notifier).restart(); + + final captured = verify( + () => repo.send(request: captureAny(named: 'request')), + ).captured.single as SendCommandRequestModel; + expect(captured.command, DeviceCommand.restart); + }); + + test('factoryReset sends DeviceCommand.factory on success', () async { + final repo = MockCommandsRepository(); + when(() => repo.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo); + addTearDown(container.dispose); + await container.read(selectedDeviceProvider.future); + + await container + .read(remoteManagementControllerProvider.notifier) + .factoryReset(); + + final captured = verify( + () => repo.send(request: captureAny(named: 'request')), + ).captured.single as SendCommandRequestModel; + expect(captured.command, DeviceCommand.factory); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockCommandsRepository(); + when(() => repo.send(request: any(named: 'request'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo); + addTearDown(container.dispose); + await container.read(selectedDeviceProvider.future); + + await container.read(remoteManagementControllerProvider.notifier).shutdown(); + + final state = container.read(remoteManagementControllerProvider); + expect(state, isA>()); + expect(state.error, isA()); + }); + }); +}