From 734bd79af7580509ac9d1f29634013987be56301 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 22 Apr 2026 22:21:35 +0200 Subject: [PATCH] refactor(device_management): migrate remote_connection to Riverpod --- .../providers/remote_camera_controller.dart | 95 ++++++ .../providers/remote_camera_controller.g.dart | 64 ++++ .../remote_picture_index_provider.dart | 43 +++ .../remote_picture_index_provider.g.dart | 64 ++++ .../providers/remote_pictures_provider.dart | 15 + .../providers/remote_pictures_provider.g.dart | 87 +++++ .../providers/spy_call_controller.dart | 38 +++ .../providers/spy_call_controller.g.dart | 55 ++++ .../providers/spy_call_form_provider.dart | 14 + .../providers/spy_call_form_provider.g.dart | 62 ++++ .../presentation/remote_camera_screen.dart | 139 ++++---- .../state/remote_connection_view_model.dart | 220 ------------- .../state/remote_connection_view_state.dart | 26 -- .../remote_connection_view_state.freezed.dart | 310 ------------------ .../widgets/show_picture_dialog.dart | 35 +- .../presentation/widgets/spy_call_dialog.dart | 244 ++++++-------- .../spy_call_controller_test.dart | 78 +++++ 17 files changed, 807 insertions(+), 782 deletions(-) create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.g.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart create mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.g.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.dart delete mode 100644 modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.freezed.dart create mode 100644 modules/legacy/modules/device_management/test/features/remote_connection/spy_call_controller_test.dart diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.dart new file mode 100644 index 00000000..24cee7f6 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart'; +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'remote_camera_controller.g.dart'; + +const int _photoWaitSeconds = 5; + +class RemoteCameraState { + const RemoteCameraState({ + this.isTakingPicture = false, + this.isWaitingForPhoto = false, + this.photoCountdown = 0, + }); + + final bool isTakingPicture; + final bool isWaitingForPhoto; + final int photoCountdown; + + RemoteCameraState copyWith({ + bool? isTakingPicture, + bool? isWaitingForPhoto, + int? photoCountdown, + }) { + return RemoteCameraState( + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isWaitingForPhoto: isWaitingForPhoto ?? this.isWaitingForPhoto, + photoCountdown: photoCountdown ?? this.photoCountdown, + ); + } +} + +@riverpod +class RemoteCameraController extends _$RemoteCameraController { + Timer? _timer; + + @override + RemoteCameraState build() { + ref.onDispose(() => _timer?.cancel()); + return const RemoteCameraState(); + } + + Future takePicture(String deviceIdentificator) async { + state = state.copyWith( + isTakingPicture: true, + isWaitingForPhoto: false, + photoCountdown: 0, + ); + + try { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.requestPhoto, + ), + ); + } catch (e) { + state = const RemoteCameraState(); + rethrow; + } + + unawaited( + ref.read(sfTrackingProvider).legacyDeviceRemoteConnectionPhotoTaken(), + ); + + state = state.copyWith( + isTakingPicture: false, + isWaitingForPhoto: true, + photoCountdown: _photoWaitSeconds, + ); + + _timer?.cancel(); + final completer = Completer(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + final remaining = state.photoCountdown - 1; + if (remaining <= 0) { + timer.cancel(); + _timer = null; + state = state.copyWith( + isWaitingForPhoto: false, + photoCountdown: 0, + ); + if (!completer.isCompleted) completer.complete(); + } else { + state = state.copyWith(photoCountdown: remaining); + } + }); + + await completer.future; + ref.invalidate(remotePicturesProvider(deviceIdentificator)); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.g.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.g.dart new file mode 100644 index 00000000..31c66877 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_camera_controller.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_camera_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(RemoteCameraController) +const remoteCameraControllerProvider = RemoteCameraControllerProvider._(); + +final class RemoteCameraControllerProvider + extends $NotifierProvider { + const RemoteCameraControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'remoteCameraControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$remoteCameraControllerHash(); + + @$internal + @override + RemoteCameraController create() => RemoteCameraController(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(RemoteCameraState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$remoteCameraControllerHash() => + r'2f947b277991072a2d95072528931fcb13569af8'; + +abstract class _$RemoteCameraController extends $Notifier { + RemoteCameraState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + RemoteCameraState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart new file mode 100644 index 00000000..d86778b0 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'remote_picture_index_provider.g.dart'; + +@riverpod +class RemotePictureIndex extends _$RemotePictureIndex { + @override + int build() => 0; + + void prev({required int total}) { + if (total <= 0) return; + unawaited( + ref + .read(sfTrackingProvider) + .legacyDeviceRemoteConnectionPictureViewed('prev'), + ); + final next = state - 1; + state = next < 0 ? total - 1 : next; + } + + void next({required int total}) { + if (total <= 0) return; + unawaited( + ref + .read(sfTrackingProvider) + .legacyDeviceRemoteConnectionPictureViewed('next'), + ); + state = (state + 1) % total; + } + + void set(int value) { + if (value == state) return; + unawaited( + ref + .read(sfTrackingProvider) + .legacyDeviceRemoteConnectionPictureViewed('direct'), + ); + state = value; + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.g.dart new file mode 100644 index 00000000..03147bb6 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_picture_index_provider.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_picture_index_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(RemotePictureIndex) +const remotePictureIndexProvider = RemotePictureIndexProvider._(); + +final class RemotePictureIndexProvider + extends $NotifierProvider { + const RemotePictureIndexProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'remotePictureIndexProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$remotePictureIndexHash(); + + @$internal + @override + RemotePictureIndex create() => RemotePictureIndex(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$remotePictureIndexHash() => + r'239044ffbde1be75d554b1a41acc887214b965df'; + +abstract class _$RemotePictureIndex extends $Notifier { + int build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + int, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart new file mode 100644 index 00000000..d1f4a0e3 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart @@ -0,0 +1,15 @@ +import 'package:device_management/src/core/providers/pictures_repository_provider.dart'; +import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'remote_pictures_provider.g.dart'; + +@riverpod +Future> remotePictures( + Ref ref, + String deviceIdentificator, +) async { + return ref + .read(picturesRepositoryProvider) + .getPictures(deviceId: deviceIdentificator); +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.g.dart new file mode 100644 index 00000000..0cde3e81 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/remote_pictures_provider.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_pictures_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(remotePictures) +const remotePicturesProvider = RemotePicturesFamily._(); + +final class RemotePicturesProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + const RemotePicturesProvider._({ + required RemotePicturesFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'remotePicturesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$remotePicturesHash(); + + @override + String toString() { + return r'remotePicturesProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as String; + return remotePictures(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is RemotePicturesProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$remotePicturesHash() => r'b6a70ae302081c6be2f829fe3a13dc2107cfaee8'; + +final class RemotePicturesFamily extends $Family + with $FunctionalFamilyOverride>, String> { + const RemotePicturesFamily._() + : super( + retry: null, + name: r'remotePicturesProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + RemotePicturesProvider call(String deviceIdentificator) => + RemotePicturesProvider._(argument: deviceIdentificator, from: this); + + @override + String toString() => r'remotePicturesProvider'; +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.dart new file mode 100644 index 00000000..201aae0e --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +part 'spy_call_controller.g.dart'; + +class SpyCallInvalidPhoneException implements Exception { + const SpyCallInvalidPhoneException(); +} + +@riverpod +class SpyCallController extends _$SpyCallController { + @override + FutureOr build() {} + + Future call({ + required String deviceIdentificator, + required String phoneE164, + }) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.callCenter, + data: {'phone_number': phoneE164}, + ), + ); + unawaited( + ref + .read(sfTrackingProvider) + .legacyDeviceRemoteConnectionCallInitiated(), + ); + }); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.g.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.g.dart new file mode 100644 index 00000000..b7ff36eb --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_controller.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spy_call_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SpyCallController) +const spyCallControllerProvider = SpyCallControllerProvider._(); + +final class SpyCallControllerProvider + extends $AsyncNotifierProvider { + const SpyCallControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'spyCallControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$spyCallControllerHash(); + + @$internal + @override + SpyCallController create() => SpyCallController(); +} + +String _$spyCallControllerHash() => r'f5e88f8b050d5461d60836f0c288f987f9ed3604'; + +abstract class _$SpyCallController 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/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart new file mode 100644 index 00000000..39e96b7b --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart @@ -0,0 +1,14 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'spy_call_form_provider.g.dart'; + +@riverpod +class SpyCallForm extends _$SpyCallForm { + @override + String build() => 'ES'; + + void setIsoCode(String code) { + if (code == state) return; + state = code; + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.g.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.g.dart new file mode 100644 index 00000000..eadc2df0 --- /dev/null +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/providers/spy_call_form_provider.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spy_call_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SpyCallForm) +const spyCallFormProvider = SpyCallFormProvider._(); + +final class SpyCallFormProvider extends $NotifierProvider { + const SpyCallFormProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'spyCallFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$spyCallFormHash(); + + @$internal + @override + SpyCallForm create() => SpyCallForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$spyCallFormHash() => r'5e883cd4c326b9ed1aa17f26a24930b3dc64ba65'; + +abstract class _$SpyCallForm extends $Notifier { + String build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart index f2147041..6a027ba8 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart @@ -1,16 +1,19 @@ import 'package:design_system/design_system.dart'; -import 'package:legacy_theme/legacy_theme.dart'; +import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_camera_controller.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart'; +import 'package:device_management/src/features/remote_connection/presentation/widgets/show_picture_dialog.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:lottie/lottie.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart'; -import 'package:device_management/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart'; import 'package:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import 'package:legacy_device_state/legacy_device_state.dart'; -import 'package:legacy_ui/legacy_ui.dart'; class RemoteCameraScreen extends ConsumerWidget { final NavigationContract navigationContract; @@ -19,88 +22,66 @@ class RemoteCameraScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen(remoteConnectionViewModelProvider.select((s) => s.errorEvent), ( - _, - next, - ) { - if (next != null) { - final message = switch (next) { - RemoteConnectionErrorEvent.takePicture => context.translate( - I18n.errorTakePicture, - ), - RemoteConnectionErrorEvent.fetchPhotos => context.translate( - I18n.errorFetchPhotos, - ), - RemoteConnectionErrorEvent.call => context.translate(I18n.errorCall), - RemoteConnectionErrorEvent.invalidPhone => context.translate( - I18n.errorMessagePhoneIsInvalid, - ), - }; - showTopSnackbar(context, message: message, type: MessageType.error); - } - }); + final device = ref.watch(selectedDeviceProvider).value; - ref.listen( - remoteConnectionViewModelProvider.select((s) => s.successEvent), - (_, next) { - if (next != null) { - final message = switch (next) { - RemoteConnectionSuccessEvent.photoTaken => context.translate( - I18n.photoTaken, - ), - }; - showTopSnackbar(context, message: message, type: MessageType.success); - ref.read(remoteConnectionViewModelProvider.notifier).clearSuccess(); - } - }, - ); + if (device == null) { + return LegacyPageLayout( + title: context.translate(I18n.remoteCamera), + body: const Center(child: CircularProgressIndicator()), + ); + } - final isLoading = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.isLoadingPictures), - ); - final isTaking = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.isTakingPicture), - ); - final isWaiting = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.isWaitingForPhoto), - ); - final countdown = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.photoCountdown), - ); + final identificator = device.identificator; + final cameraState = ref.watch(remoteCameraControllerProvider); + final picturesAsync = ref.watch(remotePicturesProvider(identificator)); Widget body; - if (isLoading || isTaking) { + if (cameraState.isTakingPicture) { body = const Center(child: CircularProgressIndicator()); - } else if (isWaiting) { - body = _WaitingForPhotoOverlay(remainingSeconds: countdown); + } else if (cameraState.isWaitingForPhoto) { + body = _WaitingForPhotoOverlay( + remainingSeconds: cameraState.photoCountdown, + ); } else { - body = const _GallerySection(); + body = picturesAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => + Center(child: Text(context.translate(I18n.errorFetchPhotos))), + data: (pictures) => _GallerySection(pictures: pictures), + ); } return LegacyPageLayout( title: context.translate(I18n.remoteCamera), body: body, - footer: isWaiting ? const SizedBox.shrink() : const _TakePictureSection(), + footer: cameraState.isWaitingForPhoto + ? const SizedBox.shrink() + : _TakePictureSection(deviceIdentificator: identificator), ); } } class _GallerySection extends ConsumerWidget { - const _GallerySection(); + final List pictures; + + const _GallerySection({required this.pictures}); @override Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(remoteConnectionViewModelProvider.notifier); - - final pictures = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.pictures), - ); + if (pictures.isEmpty) { + return Center( + child: Text( + context.translate(I18n.noPhotosAvailable), + style: const TextStyle(color: Colors.grey), + ), + ); + } return GridView.count( primary: false, padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(horizontal: 24, vertical: 12), - big: EdgeInsets.symmetric(horizontal: 23, vertical: 11), + small: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + big: const EdgeInsets.symmetric(horizontal: 23, vertical: 11), ), crossAxisSpacing: 11, mainAxisSpacing: 11, @@ -110,10 +91,10 @@ class _GallerySection extends ConsumerWidget { pictures.length, (int index) => TextButton( onPressed: () { - vm.setPictureIndex(index); - showDialog( + ref.read(remotePictureIndexProvider.notifier).set(index); + showDialog( context: context, - builder: (context) => Dialog(child: ShowPictureDialog()), + builder: (_) => const Dialog(child: ShowPictureDialog()), ); }, child: Container( @@ -135,21 +116,31 @@ class _GallerySection extends ConsumerWidget { } class _TakePictureSection extends ConsumerWidget { - const _TakePictureSection(); + final String deviceIdentificator; + + const _TakePictureSection({required this.deviceIdentificator}); @override Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(remoteConnectionViewModelProvider.notifier); - return Padding( padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(vertical: 12, horizontal: 26), - big: EdgeInsets.symmetric(vertical: 10, horizontal: 25), + small: const EdgeInsets.symmetric(vertical: 12, horizontal: 26), + big: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), child: PrimaryButton( onPressed: () async { if (!await guardDeviceCommand(context, ref)) return; - vm.takePicture(); + if (!context.mounted) return; + try { + await ref + .read(remoteCameraControllerProvider.notifier) + .takePicture(deviceIdentificator); + if (!context.mounted) return; + await showSuccessDialog(context, I18n.photoTaken); + } catch (_) { + if (!context.mounted) return; + await showErrorDialog(context, I18n.errorTakePicture); + } }, text: context.translate(I18n.takePicture), color: context.sfColors.legacyPrimary, diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart deleted file mode 100644 index d93a4777..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'dart:async'; - -import 'package:device_management/src/core/providers/pictures_repository_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.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 '../../../../core/domain/repositories/pictures_repository.dart'; - -final remoteConnectionViewModelProvider = - NotifierProvider.autoDispose< - RemoteConnectionViewModel, - RemoteConnectionViewState - >(RemoteConnectionViewModel.new); - -class RemoteConnectionViewModel extends Notifier { - late final TextEditingController phoneController; - late final CommandsRepository _commandsRepository; - late final PicturesRepository _picturesRepository; - late final SfTrackingRepository _tracking; - Timer? _photoTimer; - - static const int _photoWaitSeconds = 5; - - @override - RemoteConnectionViewState build() { - _commandsRepository = ref.read(commandsRepositoryProvider); - _picturesRepository = ref.read(picturesRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - - unawaited(_tracking.legacyDeviceRemoteConnectionStarted()); - - phoneController = TextEditingController(); - phoneController.addListener(_onPhoneChanged); - - ref.onDispose(disposeControllers); - - Future.microtask(load); - - return const RemoteConnectionViewState(); - } - - Future load() async { - final device = ref.read(selectedDeviceProvider).value; - if (device == null) return; - - state = state.copyWith(deviceId: device.identificator); - - try { - final pictures = await _picturesRepository.getPictures( - deviceId: device.identificator, - ); - if (!ref.mounted) return; - - state = state.copyWith(pictures: pictures, isLoadingPictures: false); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith(isLoadingPictures: false); - } - } - - void _onPhoneChanged() { - final text = phoneController.text; - if (text == state.phone) return; - - state = state.copyWith(phone: text, errorEvent: null); - } - - void updateCountry(String isoCode) { - if (isoCode == state.isoCode) return; - state = state.copyWith(isoCode: isoCode, errorEvent: null); - } - - void prevPicture() { - int pictureIndex = state.pictureIndex - 1; - - if (pictureIndex < 0) { - pictureIndex = state.pictures.length - 1; - } - - unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('prev')); - state = state.copyWith(pictureIndex: pictureIndex); - } - - void nextPicture() { - int pictureIndex = (state.pictureIndex + 1) % state.pictures.length; - - unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('next')); - state = state.copyWith(pictureIndex: pictureIndex); - } - - void setPictureIndex(int value) { - unawaited(_tracking.legacyDeviceRemoteConnectionPictureViewed('direct')); - state = state.copyWith(pictureIndex: value); - } - - void clearSuccess() { - state = state.copyWith(successEvent: null); - } - - Future takePicture() async { - try { - state = state.copyWith( - isTakingPicture: true, - successEvent: null, - errorEvent: null, - ); - - final request = SendCommandRequestModel( - device: state.deviceId, - command: DeviceCommand.requestPhoto, - ); - - await _commandsRepository.send(request: request); - if (!ref.mounted) return; - - unawaited(_tracking.legacyDeviceRemoteConnectionPhotoTaken()); - - state = state.copyWith( - isTakingPicture: false, - isWaitingForPhoto: true, - photoCountdown: _photoWaitSeconds, - ); - - _startPhotoCountdown(); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isTakingPicture: false, - errorEvent: RemoteConnectionErrorEvent.takePicture, - ); - } - } - - void _startPhotoCountdown() { - _photoTimer?.cancel(); - _photoTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (!ref.mounted) { - timer.cancel(); - return; - } - final remaining = state.photoCountdown - 1; - if (remaining <= 0) { - timer.cancel(); - _photoTimer = null; - _fetchPhotosAfterCapture(); - } else { - state = state.copyWith(photoCountdown: remaining); - } - }); - } - - Future _fetchPhotosAfterCapture() async { - try { - final pictures = await _picturesRepository.getPictures( - deviceId: state.deviceId, - ); - if (!ref.mounted) return; - - state = state.copyWith( - isWaitingForPhoto: false, - photoCountdown: 0, - pictures: pictures, - successEvent: RemoteConnectionSuccessEvent.photoTaken, - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isWaitingForPhoto: false, - photoCountdown: 0, - errorEvent: RemoteConnectionErrorEvent.fetchPhotos, - ); - } - } - - Future call() async { - final parsed = SfPhoneNumber.tryParse( - phoneController.text, - defaultIsoCode: state.isoCode, - ); - if (parsed == null) { - state = state.copyWith( - errorEvent: RemoteConnectionErrorEvent.invalidPhone, - ); - return; - } - - try { - state = state.copyWith(isCalling: true); - - final request = SendCommandRequestModel( - device: state.deviceId, - command: DeviceCommand.callCenter, - data: {'phone_number': parsed.e164}, - ); - await _commandsRepository.send(request: request); - - if (!ref.mounted) return; - - unawaited(_tracking.legacyDeviceRemoteConnectionCallInitiated()); - - state = state.copyWith(isCalling: false); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isCalling: false, - errorEvent: RemoteConnectionErrorEvent.call, - ); - } - } - - void disposeControllers() { - phoneController.removeListener(_onPhoneChanged); - - phoneController.dispose(); - } -} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.dart deleted file mode 100644 index 53b8ea86..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart'; - -part 'remote_connection_view_state.freezed.dart'; - -enum RemoteConnectionErrorEvent { takePicture, fetchPhotos, call, invalidPhone } - -enum RemoteConnectionSuccessEvent { photoTaken } - -@freezed -abstract class RemoteConnectionViewState with _$RemoteConnectionViewState { - const factory RemoteConnectionViewState({ - @Default('') String deviceId, - @Default('ES') String isoCode, - @Default('') String phone, - @Default([]) List pictures, - @Default(0) int pictureIndex, - @Default(true) bool isLoadingPictures, - @Default(false) bool isTakingPicture, - @Default(false) bool isWaitingForPhoto, - @Default(0) int photoCountdown, - @Default(false) bool isCalling, - RemoteConnectionErrorEvent? errorEvent, - RemoteConnectionSuccessEvent? successEvent, - }) = _RemoteConnectionViewState; -} diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.freezed.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.freezed.dart deleted file mode 100644 index 468c0497..00000000 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_state.freezed.dart +++ /dev/null @@ -1,310 +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_connection_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$RemoteConnectionViewState { - - String get deviceId; String get isoCode; String get phone; List get pictures; int get pictureIndex; bool get isLoadingPictures; bool get isTakingPicture; bool get isWaitingForPhoto; int get photoCountdown; bool get isCalling; RemoteConnectionErrorEvent? get errorEvent; RemoteConnectionSuccessEvent? get successEvent; -/// Create a copy of RemoteConnectionViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$RemoteConnectionViewStateCopyWith get copyWith => _$RemoteConnectionViewStateCopyWithImpl(this as RemoteConnectionViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other.pictures, pictures)&&(identical(other.pictureIndex, pictureIndex) || other.pictureIndex == pictureIndex)&&(identical(other.isLoadingPictures, isLoadingPictures) || other.isLoadingPictures == isLoadingPictures)&&(identical(other.isTakingPicture, isTakingPicture) || other.isTakingPicture == isTakingPicture)&&(identical(other.isWaitingForPhoto, isWaitingForPhoto) || other.isWaitingForPhoto == isWaitingForPhoto)&&(identical(other.photoCountdown, photoCountdown) || other.photoCountdown == photoCountdown)&&(identical(other.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); -} - - -@override -int get hashCode => Object.hash(runtimeType,deviceId,isoCode,phone,const DeepCollectionEquality().hash(pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent); - -@override -String toString() { - return 'RemoteConnectionViewState(deviceId: $deviceId, isoCode: $isoCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)'; -} - - -} - -/// @nodoc -abstract mixin class $RemoteConnectionViewStateCopyWith<$Res> { - factory $RemoteConnectionViewStateCopyWith(RemoteConnectionViewState value, $Res Function(RemoteConnectionViewState) _then) = _$RemoteConnectionViewStateCopyWithImpl; -@useResult -$Res call({ - String deviceId, String isoCode, String phone, List pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent -}); - - - - -} -/// @nodoc -class _$RemoteConnectionViewStateCopyWithImpl<$Res> - implements $RemoteConnectionViewStateCopyWith<$Res> { - _$RemoteConnectionViewStateCopyWithImpl(this._self, this._then); - - final RemoteConnectionViewState _self; - final $Res Function(RemoteConnectionViewState) _then; - -/// Create a copy of RemoteConnectionViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? isoCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isWaitingForPhoto = null,Object? photoCountdown = null,Object? isCalling = null,Object? errorEvent = freezed,Object? successEvent = freezed,}) { - return _then(_self.copyWith( -deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable -as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable -as String,pictures: null == pictures ? _self.pictures : pictures // ignore: cast_nullable_to_non_nullable -as List,pictureIndex: null == pictureIndex ? _self.pictureIndex : pictureIndex // ignore: cast_nullable_to_non_nullable -as int,isLoadingPictures: null == isLoadingPictures ? _self.isLoadingPictures : isLoadingPictures // ignore: cast_nullable_to_non_nullable -as bool,isTakingPicture: null == isTakingPicture ? _self.isTakingPicture : isTakingPicture // ignore: cast_nullable_to_non_nullable -as bool,isWaitingForPhoto: null == isWaitingForPhoto ? _self.isWaitingForPhoto : isWaitingForPhoto // ignore: cast_nullable_to_non_nullable -as bool,photoCountdown: null == photoCountdown ? _self.photoCountdown : photoCountdown // ignore: cast_nullable_to_non_nullable -as int,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable -as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable -as RemoteConnectionErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable -as RemoteConnectionSuccessEvent?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [RemoteConnectionViewState]. -extension RemoteConnectionViewStatePatterns on RemoteConnectionViewState { -/// 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( _RemoteConnectionViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _RemoteConnectionViewState() 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( _RemoteConnectionViewState value) $default,){ -final _that = this; -switch (_that) { -case _RemoteConnectionViewState(): -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( _RemoteConnectionViewState value)? $default,){ -final _that = this; -switch (_that) { -case _RemoteConnectionViewState() 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, String isoCode, String phone, List pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _RemoteConnectionViewState() when $default != null: -return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);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, String isoCode, String phone, List pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent) $default,) {final _that = this; -switch (_that) { -case _RemoteConnectionViewState(): -return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);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, String isoCode, String phone, List pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent)? $default,) {final _that = this; -switch (_that) { -case _RemoteConnectionViewState() when $default != null: -return $default(_that.deviceId,_that.isoCode,_that.phone,_that.pictures,_that.pictureIndex,_that.isLoadingPictures,_that.isTakingPicture,_that.isWaitingForPhoto,_that.photoCountdown,_that.isCalling,_that.errorEvent,_that.successEvent);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _RemoteConnectionViewState implements RemoteConnectionViewState { - const _RemoteConnectionViewState({this.deviceId = '', this.isoCode = 'ES', this.phone = '', final List pictures = const [], this.pictureIndex = 0, this.isLoadingPictures = true, this.isTakingPicture = false, this.isWaitingForPhoto = false, this.photoCountdown = 0, this.isCalling = false, this.errorEvent, this.successEvent}): _pictures = pictures; - - -@override@JsonKey() final String deviceId; -@override@JsonKey() final String isoCode; -@override@JsonKey() final String phone; - final List _pictures; -@override@JsonKey() List get pictures { - if (_pictures is EqualUnmodifiableListView) return _pictures; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_pictures); -} - -@override@JsonKey() final int pictureIndex; -@override@JsonKey() final bool isLoadingPictures; -@override@JsonKey() final bool isTakingPicture; -@override@JsonKey() final bool isWaitingForPhoto; -@override@JsonKey() final int photoCountdown; -@override@JsonKey() final bool isCalling; -@override final RemoteConnectionErrorEvent? errorEvent; -@override final RemoteConnectionSuccessEvent? successEvent; - -/// Create a copy of RemoteConnectionViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$RemoteConnectionViewStateCopyWith<_RemoteConnectionViewState> get copyWith => __$RemoteConnectionViewStateCopyWithImpl<_RemoteConnectionViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _RemoteConnectionViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.isoCode, isoCode) || other.isoCode == isoCode)&&(identical(other.phone, phone) || other.phone == phone)&&const DeepCollectionEquality().equals(other._pictures, _pictures)&&(identical(other.pictureIndex, pictureIndex) || other.pictureIndex == pictureIndex)&&(identical(other.isLoadingPictures, isLoadingPictures) || other.isLoadingPictures == isLoadingPictures)&&(identical(other.isTakingPicture, isTakingPicture) || other.isTakingPicture == isTakingPicture)&&(identical(other.isWaitingForPhoto, isWaitingForPhoto) || other.isWaitingForPhoto == isWaitingForPhoto)&&(identical(other.photoCountdown, photoCountdown) || other.photoCountdown == photoCountdown)&&(identical(other.isCalling, isCalling) || other.isCalling == isCalling)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successEvent, successEvent) || other.successEvent == successEvent)); -} - - -@override -int get hashCode => Object.hash(runtimeType,deviceId,isoCode,phone,const DeepCollectionEquality().hash(_pictures),pictureIndex,isLoadingPictures,isTakingPicture,isWaitingForPhoto,photoCountdown,isCalling,errorEvent,successEvent); - -@override -String toString() { - return 'RemoteConnectionViewState(deviceId: $deviceId, isoCode: $isoCode, phone: $phone, pictures: $pictures, pictureIndex: $pictureIndex, isLoadingPictures: $isLoadingPictures, isTakingPicture: $isTakingPicture, isWaitingForPhoto: $isWaitingForPhoto, photoCountdown: $photoCountdown, isCalling: $isCalling, errorEvent: $errorEvent, successEvent: $successEvent)'; -} - - -} - -/// @nodoc -abstract mixin class _$RemoteConnectionViewStateCopyWith<$Res> implements $RemoteConnectionViewStateCopyWith<$Res> { - factory _$RemoteConnectionViewStateCopyWith(_RemoteConnectionViewState value, $Res Function(_RemoteConnectionViewState) _then) = __$RemoteConnectionViewStateCopyWithImpl; -@override @useResult -$Res call({ - String deviceId, String isoCode, String phone, List pictures, int pictureIndex, bool isLoadingPictures, bool isTakingPicture, bool isWaitingForPhoto, int photoCountdown, bool isCalling, RemoteConnectionErrorEvent? errorEvent, RemoteConnectionSuccessEvent? successEvent -}); - - - - -} -/// @nodoc -class __$RemoteConnectionViewStateCopyWithImpl<$Res> - implements _$RemoteConnectionViewStateCopyWith<$Res> { - __$RemoteConnectionViewStateCopyWithImpl(this._self, this._then); - - final _RemoteConnectionViewState _self; - final $Res Function(_RemoteConnectionViewState) _then; - -/// Create a copy of RemoteConnectionViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? isoCode = null,Object? phone = null,Object? pictures = null,Object? pictureIndex = null,Object? isLoadingPictures = null,Object? isTakingPicture = null,Object? isWaitingForPhoto = null,Object? photoCountdown = null,Object? isCalling = null,Object? errorEvent = freezed,Object? successEvent = freezed,}) { - return _then(_RemoteConnectionViewState( -deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable -as String,isoCode: null == isoCode ? _self.isoCode : isoCode // ignore: cast_nullable_to_non_nullable -as String,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable -as String,pictures: null == pictures ? _self._pictures : pictures // ignore: cast_nullable_to_non_nullable -as List,pictureIndex: null == pictureIndex ? _self.pictureIndex : pictureIndex // ignore: cast_nullable_to_non_nullable -as int,isLoadingPictures: null == isLoadingPictures ? _self.isLoadingPictures : isLoadingPictures // ignore: cast_nullable_to_non_nullable -as bool,isTakingPicture: null == isTakingPicture ? _self.isTakingPicture : isTakingPicture // ignore: cast_nullable_to_non_nullable -as bool,isWaitingForPhoto: null == isWaitingForPhoto ? _self.isWaitingForPhoto : isWaitingForPhoto // ignore: cast_nullable_to_non_nullable -as bool,photoCountdown: null == photoCountdown ? _self.photoCountdown : photoCountdown // ignore: cast_nullable_to_non_nullable -as int,isCalling: null == isCalling ? _self.isCalling : isCalling // ignore: cast_nullable_to_non_nullable -as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable -as RemoteConnectionErrorEvent?,successEvent: freezed == successEvent ? _self.successEvent : successEvent // ignore: cast_nullable_to_non_nullable -as RemoteConnectionSuccessEvent?, - )); -} - - -} - -// dart format on diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart index 4802a099..50379d1b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/show_picture_dialog.dart @@ -1,8 +1,10 @@ +import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_picture_index_provider.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/remote_pictures_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sf_localizations/sf_localizations.dart'; -import 'package:device_management/src/features/remote_connection/domain/entities/picture_entity.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; class ShowPictureDialog extends ConsumerWidget { @@ -10,15 +12,15 @@ class ShowPictureDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final viewModel = ref.read(remoteConnectionViewModelProvider.notifier); - final pictures = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.pictures), - ); - final pictureIndex = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.pictureIndex), - ); + final device = ref.watch(selectedDeviceProvider).value; + if (device == null) return const SizedBox.shrink(); - if (pictures.isEmpty) { + final picturesAsync = + ref.watch(remotePicturesProvider(device.identificator)); + final pictureIndex = ref.watch(remotePictureIndexProvider); + final pictures = picturesAsync.value ?? const []; + + if (pictures.isEmpty || pictureIndex >= pictures.length) { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, @@ -47,8 +49,12 @@ class ShowPictureDialog extends ConsumerWidget { Expanded(child: _PictureSection(picture: picture)), _MetadataSection(picture: picture), _ControlsSection( - prev: viewModel.prevPicture, - next: viewModel.nextPicture, + prev: () => ref + .read(remotePictureIndexProvider.notifier) + .prev(total: pictures.length), + next: () => ref + .read(remotePictureIndexProvider.notifier) + .next(total: pictures.length), ), ], ), @@ -90,7 +96,10 @@ class _MetadataSection extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8), child: Text( dateStr, - style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ); } diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart index 0685f2c2..7a2d0e75 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart @@ -1,57 +1,67 @@ import 'package:design_system/design_system.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_controller.dart'; +import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_form_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart'; -import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart'; import 'package:legacy_device_state/legacy_device_state.dart'; import 'package:legacy_theme/legacy_theme.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -class SpyCallDialog extends ConsumerWidget { +class SpyCallDialog extends ConsumerStatefulWidget { const SpyCallDialog({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(remoteConnectionViewModelProvider.notifier); + ConsumerState createState() => _SpyCallDialogState(); +} - ref.listen(remoteConnectionViewModelProvider.select((s) => s.errorEvent), ( - _, - next, - ) { - if (next != null) { - final message = switch (next) { - RemoteConnectionErrorEvent.invalidPhone => context.translate( - I18n.errorMessagePhoneIsInvalid, - ), - RemoteConnectionErrorEvent.call => context.translate(I18n.errorCall), - RemoteConnectionErrorEvent.takePicture => context.translate( - I18n.errorTakePicture, - ), - RemoteConnectionErrorEvent.fetchPhotos => context.translate( - I18n.errorFetchPhotos, - ), - }; - showTopSnackbar(context, message: message, type: MessageType.error); - } - }); +class _SpyCallDialogState extends ConsumerState { + final _phoneController = TextEditingController(); - ref.listen(remoteConnectionViewModelProvider.select((s) => s.isCalling), ( - prev, - isCalling, - ) { - if (prev == true && !isCalling) { - final error = ref.read(remoteConnectionViewModelProvider).errorEvent; - if (error == null && context.mounted) { - Navigator.pop(context); - showTopSnackbar( - context, - message: context.translate(I18n.remoteListening), - type: MessageType.success, - ); - } - } - }); + @override + void dispose() { + _phoneController.dispose(); + super.dispose(); + } + + Future _submit() async { + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; + + final isoCode = ref.read(spyCallFormProvider); + final parsed = SfPhoneNumber.tryParse( + _phoneController.text, + defaultIsoCode: isoCode, + ); + if (parsed == null) { + await showErrorDialog(context, I18n.errorMessagePhoneIsInvalid); + return; + } + + final device = ref.read(selectedDeviceProvider).value; + if (device == null) return; + + await ref.read(spyCallControllerProvider.notifier).call( + deviceIdentificator: device.identificator, + phoneE164: parsed.e164, + ); + if (!mounted) return; + final state = ref.read(spyCallControllerProvider); + if (state.hasError) { + await showErrorDialog(context, I18n.errorCall); + return; + } + Navigator.pop(context); + await showSuccessDialog(context, I18n.remoteListening); + } + + @override + Widget build(BuildContext context) { + final primaryColor = context.sfColors.legacyPrimary; + final isoCode = ref.watch(spyCallFormProvider); + final isCalling = + ref.watch(spyCallControllerProvider.select((s) => s.isLoading)); return Container( padding: SizeUtils.getByScreen( @@ -66,7 +76,26 @@ class SpyCallDialog extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const _Header(), + Stack( + children: [ + Center( + child: Text( + context.translate(I18n.remoteListening), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 19, big: 18), + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.close, color: primaryColor), + ), + ), + ], + ), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 7)), Text( context.translate(I18n.spyCallSubtitle), @@ -77,106 +106,43 @@ class SpyCallDialog extends ConsumerWidget { ), ), SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)), - _PhoneSection(onSubmit: () async { - if (!await guardDeviceCommand(context, ref)) return; - vm.call(); - }), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CountryPrefixPicker( + headerText: context.translate(I18n.selectYourCountry), + initialSelection: isoCode, + onChanged: (country) { + final code = country.code; + if (code != null) { + ref.read(spyCallFormProvider.notifier).setIsoCode(code); + } + }, + width: 80, + ), + SizedBox(width: SizeUtils.getByScreen(small: 8, big: 7)), + Expanded( + child: CustomTextField( + controller: _phoneController, + hint: context.translate(I18n.insertPhone), + keyboardType: TextInputType.phone, + onSubmitted: (_) => _submit(), + ), + ), + ], + ), SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)), - _CallSection(onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - vm.call(); - }), + isCalling + ? const Center(child: CircularProgressIndicator()) + : PrimaryButton( + onPressed: _submit, + text: context.translate(I18n.call), + color: primaryColor, + height: SizeUtils.getByScreen(small: 38, big: 36), + radius: SizeUtils.getByScreen(small: 32, big: 34), + ), ], ), ); } } - -class _Header extends StatelessWidget { - const _Header(); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Center( - child: Text( - context.translate(I18n.remoteListening), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 19, big: 18), - ), - ), - ), - Align( - alignment: Alignment.centerRight, - child: IconButton( - onPressed: () => Navigator.pop(context), - icon: Icon(Icons.close, color: context.sfColors.legacyPrimary), - ), - ), - ], - ); - } -} - -class _PhoneSection extends ConsumerWidget { - final VoidCallback onSubmit; - - const _PhoneSection({required this.onSubmit}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final vm = ref.read(remoteConnectionViewModelProvider.notifier); - final isoCode = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.isoCode), - ); - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - 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: SizeUtils.getByScreen(small: 8, big: 7)), - Expanded( - child: CustomTextField( - controller: vm.phoneController, - hint: context.translate(I18n.insertPhone), - keyboardType: TextInputType.phone, - onSubmitted: (_) => onSubmit(), - ), - ), - ], - ); - } -} - -class _CallSection extends ConsumerWidget { - final VoidCallback onPressed; - - const _CallSection({required this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isCalling = ref.watch( - remoteConnectionViewModelProvider.select((s) => s.isCalling), - ); - - return isCalling - ? const Center(child: CircularProgressIndicator()) - : PrimaryButton( - onPressed: onPressed, - text: context.translate(I18n.call), - color: context.sfColors.legacyPrimary, - height: SizeUtils.getByScreen(small: 38, big: 36), - radius: SizeUtils.getByScreen(small: 32, big: 34), - ); - } -} diff --git a/modules/legacy/modules/device_management/test/features/remote_connection/spy_call_controller_test.dart b/modules/legacy/modules/device_management/test/features/remote_connection/spy_call_controller_test.dart new file mode 100644 index 00000000..ed759cf8 --- /dev/null +++ b/modules/legacy/modules/device_management/test/features/remote_connection/spy_call_controller_test.dart @@ -0,0 +1,78 @@ +import 'package:device_management/src/features/remote_connection/presentation/providers/spy_call_controller.dart'; +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:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockCommandsRepository extends Mock implements CommandsRepository {} + +void main() { + setUpAll(() { + registerFallbackValue( + SendCommandRequestModel( + device: 'x', + command: DeviceCommand.callCenter, + ), + ); + }); + + ProviderContainer buildContainer(CommandsRepository commands) { + return makeContainer( + overrides: [ + commandsRepositoryProvider.overrideWithValue(commands), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('SpyCallController.call', () { + test('sends CALL_CENTER command and transitions to AsyncData', () async { + final commands = MockCommandsRepository(); + when(() => commands.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(commands); + addTearDown(container.dispose); + + await container.read(spyCallControllerProvider.notifier).call( + deviceIdentificator: 'imei-1', + phoneE164: '+34600000001', + ); + + expect( + container.read(spyCallControllerProvider), + isA>(), + ); + final sent = verify( + () => commands.send(request: captureAny(named: 'request')), + ).captured.single as SendCommandRequestModel; + expect(sent.command, DeviceCommand.callCenter); + expect(sent.device, 'imei-1'); + expect(sent.data, {'phone_number': '+34600000001'}); + }); + + test('exposes AsyncError when command fails', () async { + final commands = MockCommandsRepository(); + when(() => commands.send(request: any(named: 'request'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(commands); + addTearDown(container.dispose); + + await container.read(spyCallControllerProvider.notifier).call( + deviceIdentificator: 'imei-1', + phoneE164: '+34600000001', + ); + + expect( + container.read(spyCallControllerProvider), + isA>(), + ); + }); + }); +}