diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart new file mode 100644 index 00000000..cd7515a3 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/providers/wifi_repository_provider.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart'; + +part 'saved_wifi_networks_provider.g.dart'; + +@riverpod +Future> savedWifiNetworks( + Ref ref, + String deviceIdentificator, +) async { + return ref + .read(wifiRepositoryProvider) + .getWifiNetworks(deviceIdentificator: deviceIdentificator); +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.g.dart new file mode 100644 index 00000000..9ec3ecb6 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'saved_wifi_networks_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(savedWifiNetworks) +const savedWifiNetworksProvider = SavedWifiNetworksFamily._(); + +final class SavedWifiNetworksProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + const SavedWifiNetworksProvider._({ + required SavedWifiNetworksFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'savedWifiNetworksProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$savedWifiNetworksHash(); + + @override + String toString() { + return r'savedWifiNetworksProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as String; + return savedWifiNetworks(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is SavedWifiNetworksProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$savedWifiNetworksHash() => r'421ac8fad874ad3bd90efdf6ea4a3c43a478cb88'; + +final class SavedWifiNetworksFamily extends $Family + with $FunctionalFamilyOverride>, String> { + const SavedWifiNetworksFamily._() + : super( + retry: null, + name: r'savedWifiNetworksProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + SavedWifiNetworksProvider call(String deviceIdentificator) => + SavedWifiNetworksProvider._(argument: deviceIdentificator, from: this); + + @override + String toString() => r'savedWifiNetworksProvider'; +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart new file mode 100644 index 00000000..43289946 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart @@ -0,0 +1,8 @@ +import 'package:get_it/get_it.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; + +part 'web_socket_service_provider.g.dart'; + +@Riverpod(keepAlive: true) +WebSocketService webSocketService(Ref ref) => GetIt.I(); diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.g.dart new file mode 100644 index 00000000..54c7a22c --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/web_socket_service_provider.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'web_socket_service_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(webSocketService) +const webSocketServiceProvider = WebSocketServiceProvider._(); + +final class WebSocketServiceProvider + extends + $FunctionalProvider< + WebSocketService, + WebSocketService, + WebSocketService + > + with $Provider { + const WebSocketServiceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'webSocketServiceProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$webSocketServiceHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + WebSocketService create(Ref ref) { + return webSocketService(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(WebSocketService value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$webSocketServiceHash() => r'4a522db698e3aced3c90dc3ea1c2bf9e85f88830'; diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.dart new file mode 100644 index 00000000..cb8c2f78 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.dart @@ -0,0 +1,35 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'wifi_connect_form_provider.g.dart'; + +class WifiConnectFormState { + const WifiConnectFormState({ + this.canSave = false, + this.obscurePassword = true, + }); + + final bool canSave; + final bool obscurePassword; + + WifiConnectFormState copyWith({bool? canSave, bool? obscurePassword}) { + return WifiConnectFormState( + canSave: canSave ?? this.canSave, + obscurePassword: obscurePassword ?? this.obscurePassword, + ); + } +} + +@riverpod +class WifiConnectForm extends _$WifiConnectForm { + @override + WifiConnectFormState build() => const WifiConnectFormState(); + + void setCanSave(bool value) { + if (value == state.canSave) return; + state = state.copyWith(canSave: value); + } + + void togglePasswordVisibility() { + state = state.copyWith(obscurePassword: !state.obscurePassword); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.g.dart new file mode 100644 index 00000000..44e9a76a --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'wifi_connect_form_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(WifiConnectForm) +const wifiConnectFormProvider = WifiConnectFormProvider._(); + +final class WifiConnectFormProvider + extends $NotifierProvider { + const WifiConnectFormProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'wifiConnectFormProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$wifiConnectFormHash(); + + @$internal + @override + WifiConnectForm create() => WifiConnectForm(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(WifiConnectFormState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$wifiConnectFormHash() => r'44d3409aa045e7a50174ce54e69a24beaf128566'; + +abstract class _$WifiConnectForm extends $Notifier { + WifiConnectFormState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + WifiConnectFormState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.dart new file mode 100644 index 00000000..2b6b3daa --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/core/providers/wifi_repository_provider.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart'; +import 'package:sf_tracking/sf_tracking.dart'; +import 'package:uuid/uuid.dart'; + +part 'wifi_controller.g.dart'; + +enum WifiControllerAction { connectAndSave, remove, set } + +@riverpod +class WifiController extends _$WifiController { + WifiControllerAction? _lastAction; + + WifiControllerAction? get lastAction => _lastAction; + + @override + FutureOr build() {} + + Future connectAndSave({ + required String deviceIdentificator, + required String ssid, + required String bssid, + required String password, + }) async { + _lastAction = WifiControllerAction.connectAndSave; + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.setWifi, + data: {'ssid': ssid, 'bssid': bssid, 'password': password}, + ), + ); + await ref.read(wifiRepositoryProvider).createWifiNetwork( + id: const Uuid().v4(), + deviceIdentificator: deviceIdentificator, + ssid: ssid, + bssid: bssid, + password: password, + ); + ref.invalidate(savedWifiNetworksProvider(deviceIdentificator)); + final networks = await ref + .read(savedWifiNetworksProvider(deviceIdentificator).future); + unawaited( + ref + .read(sfTrackingProvider) + .legacySettingsWifiAdded(totalCount: networks.length), + ); + }); + } + + Future removeNetwork({ + required String deviceIdentificator, + required String networkId, + }) async { + _lastAction = WifiControllerAction.remove; + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref + .read(wifiRepositoryProvider) + .deleteWifiNetwork(networkId: networkId); + ref.invalidate(savedWifiNetworksProvider(deviceIdentificator)); + final networks = await ref + .read(savedWifiNetworksProvider(deviceIdentificator).future); + unawaited( + ref + .read(sfTrackingProvider) + .legacySettingsWifiRemoved(totalCount: networks.length), + ); + }); + } + + Future setNetwork({ + required String deviceIdentificator, + required WifiNetworkEntity network, + }) async { + _lastAction = WifiControllerAction.set; + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.setWifi, + data: { + 'ssid': network.ssid, + 'bssid': network.bssid, + 'password': network.password ?? '', + }, + ), + ); + unawaited( + ref + .read(wifiCurrentNetworkProvider.notifier) + .requestCurrent(deviceIdentificator), + ); + }); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.g.dart new file mode 100644 index 00000000..e697893f --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_controller.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'wifi_controller.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(WifiController) +const wifiControllerProvider = WifiControllerProvider._(); + +final class WifiControllerProvider + extends $AsyncNotifierProvider { + const WifiControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'wifiControllerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$wifiControllerHash(); + + @$internal + @override + WifiController create() => WifiController(); +} + +String _$wifiControllerHash() => r'498786d32573636cb57284969a25f64936f91a6e'; + +abstract class _$WifiController 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/wifi_settings/presentation/providers/wifi_current_network_provider.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart new file mode 100644 index 00000000..1924f3de --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; + +part 'wifi_current_network_provider.g.dart'; + +class WifiCurrentNetworkState { + const WifiCurrentNetworkState({this.network, this.isLoading = false}); + final WifiNetworkEntity? network; + final bool isLoading; + + WifiCurrentNetworkState copyWith({ + WifiNetworkEntity? network, + bool? isLoading, + bool clearNetwork = false, + }) { + return WifiCurrentNetworkState( + network: clearNetwork ? null : (network ?? this.network), + isLoading: isLoading ?? this.isLoading, + ); + } +} + +@riverpod +class WifiCurrentNetwork extends _$WifiCurrentNetwork { + Timer? _timeout; + + @override + WifiCurrentNetworkState build() { + final sub = ref + .read(webSocketServiceProvider) + .events + .listen(_onEvent); + ref.onDispose(() { + sub.cancel(); + _timeout?.cancel(); + }); + return const WifiCurrentNetworkState(); + } + + void _onEvent(WebSocketEvent event) { + if (event is! WifiEvent) return; + _timeout?.cancel(); + state = state.copyWith( + network: WifiNetworkEntity( + id: '', + ssid: event.ssid, + bssid: event.bssid, + password: event.password, + ), + isLoading: false, + ); + } + + Future requestCurrent(String deviceIdentificator) async { + state = state.copyWith(isLoading: true); + try { + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.wifiCurrent, + ), + ); + } catch (_) { + state = state.copyWith(isLoading: false); + return; + } + _timeout?.cancel(); + _timeout = Timer(const Duration(seconds: 15), () { + if (state.isLoading) state = state.copyWith(isLoading: false); + }); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.g.dart new file mode 100644 index 00000000..f909b5e1 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'wifi_current_network_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(WifiCurrentNetwork) +const wifiCurrentNetworkProvider = WifiCurrentNetworkProvider._(); + +final class WifiCurrentNetworkProvider + extends $NotifierProvider { + const WifiCurrentNetworkProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'wifiCurrentNetworkProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$wifiCurrentNetworkHash(); + + @$internal + @override + WifiCurrentNetwork create() => WifiCurrentNetwork(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(WifiCurrentNetworkState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$wifiCurrentNetworkHash() => + r'308b925398753ccb641bd602f4efa61f8b5bc58f'; + +abstract class _$WifiCurrentNetwork extends $Notifier { + WifiCurrentNetworkState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + WifiCurrentNetworkState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.dart new file mode 100644 index 00000000..f3d885f3 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:legacy_device_state/legacy_device_state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/scanned_wifi_network.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; + +part 'wifi_scan_provider.g.dart'; + +class WifiScanTimeoutException implements Exception { + const WifiScanTimeoutException(); +} + +class WifiScanState { + const WifiScanState({ + this.networks = const [], + this.isScanning = false, + this.secondsRemaining = 0, + }); + + final List networks; + final bool isScanning; + final int secondsRemaining; + + WifiScanState copyWith({ + List? networks, + bool? isScanning, + int? secondsRemaining, + }) { + return WifiScanState( + networks: networks ?? this.networks, + isScanning: isScanning ?? this.isScanning, + secondsRemaining: secondsRemaining ?? this.secondsRemaining, + ); + } +} + +@riverpod +class WifiScan extends _$WifiScan { + static const _scanDurationSeconds = 30; + + Timer? _countdown; + Completer? _completer; + + @override + WifiScanState build() { + final sub = ref + .read(webSocketServiceProvider) + .events + .listen(_onEvent); + ref.onDispose(() { + sub.cancel(); + _countdown?.cancel(); + if (_completer != null && !_completer!.isCompleted) { + _completer!.complete(); + } + }); + return const WifiScanState(); + } + + void _onEvent(WebSocketEvent event) { + if (event is! WifiSearchEvent) return; + _countdown?.cancel(); + final networks = event.wifis + .map( + (wifi) => ScannedWifiNetwork( + ssid: wifi['ssid'] as String? ?? '', + bssid: wifi['bssid'] as String? ?? '', + ), + ) + .where((network) => network.ssid.isNotEmpty) + .toList(); + state = state.copyWith( + networks: networks, + isScanning: false, + secondsRemaining: 0, + ); + if (_completer != null && !_completer!.isCompleted) { + _completer!.complete(); + } + } + + Future scan(String deviceIdentificator) async { + state = state.copyWith( + isScanning: true, + secondsRemaining: _scanDurationSeconds, + ); + + await ref.read(commandsRepositoryProvider).send( + request: SendCommandRequestModel( + device: deviceIdentificator, + command: DeviceCommand.wifiSearch, + ), + ); + + _completer = Completer(); + _countdown?.cancel(); + _countdown = Timer.periodic(const Duration(seconds: 1), (_) { + final remaining = state.secondsRemaining - 1; + if (remaining <= 0) { + _countdown?.cancel(); + if (state.isScanning) { + state = state.copyWith(isScanning: false, secondsRemaining: 0); + if (!_completer!.isCompleted) { + _completer!.completeError(const WifiScanTimeoutException()); + } + } + } else { + state = state.copyWith(secondsRemaining: remaining); + } + }); + + return _completer!.future; + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.g.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.g.dart new file mode 100644 index 00000000..a8cb9ba2 --- /dev/null +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/providers/wifi_scan_provider.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'wifi_scan_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(WifiScan) +const wifiScanProvider = WifiScanProvider._(); + +final class WifiScanProvider + extends $NotifierProvider { + const WifiScanProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'wifiScanProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$wifiScanHash(); + + @$internal + @override + WifiScan create() => WifiScan(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(WifiScanState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$wifiScanHash() => r'8a0d4bbb2c7514505c6868691b32720846f0470a'; + +abstract class _$WifiScan extends $Notifier { + WifiScanState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + WifiScanState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart deleted file mode 100644 index 2b034111..00000000 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart +++ /dev/null @@ -1,356 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get_it/get_it.dart'; -import 'package:legacy_device_state/legacy_device_state.dart'; -import 'package:sf_shared/sf_shared.dart'; -import 'package:sf_infrastructure/sf_infrastructure.dart'; -import 'package:sf_tracking/sf_tracking.dart'; -import 'package:uuid/uuid.dart'; - -import '../../../../core/domain/repositories/wifi_repository.dart'; -import '../../../../core/providers/wifi_repository_provider.dart'; -import '../../domain/entities/scanned_wifi_network.dart'; -import '../../domain/entities/wifi_network_entity.dart'; -import 'wifi_settings_view_state.dart'; - -final wifiSettingsViewModelProvider = - NotifierProvider.autoDispose( - WifiSettingsViewModel.new, - ); - -class WifiSettingsViewModel extends Notifier { - late final WifiRepository _repository; - late final CommandsRepository _commands; - late final SfTrackingRepository _tracking; - late final WebSocketService _webSocket; - StreamSubscription? _webSocketSubscription; - Timer? _scanCountdown; - Timer? _currentNetworkTimeout; - - static const _scanDurationSeconds = 30; - - @override - WifiSettingsViewState build() { - _repository = ref.read(wifiRepositoryProvider); - _commands = ref.read(commandsRepositoryProvider); - _tracking = ref.read(sfTrackingProvider); - _webSocket = GetIt.I(); - - _webSocketSubscription = _webSocket.events.listen(_onWebSocketEvent); - ref.onDispose(() { - _webSocketSubscription?.cancel(); - _scanCountdown?.cancel(); - _currentNetworkTimeout?.cancel(); - }); - - Future.microtask(_load); - return const WifiSettingsViewState(); - } - - String? get _identificator => - ref.read(selectedDeviceProvider).value?.identificator; - - void _onWebSocketEvent(WebSocketEvent event) { - switch (event) { - case WifiEvent(): - debugPrint('[WiFi] WS received WIFI_CURRENT: ssid=${event.ssid} bssid=${event.bssid}'); - _currentNetworkTimeout?.cancel(); - final network = WifiNetworkEntity( - id: '', - ssid: event.ssid, - bssid: event.bssid, - password: event.password, - ); - state = state.copyWith( - currentNetwork: network, - isConnecting: false, - isLoadingCurrentNetwork: false, - ); - case WifiSearchEvent(): - debugPrint('[WiFi] WS received WIFI_SEARCH: ${event.wifis.length} networks'); - _scanCountdown?.cancel(); - final networks = event.wifis - .map( - (wifi) => ScannedWifiNetwork( - ssid: wifi['ssid'] as String? ?? '', - bssid: wifi['bssid'] as String? ?? '', - ), - ) - .where((network) => network.ssid.isNotEmpty) - .toList(); - state = state.copyWith( - availableNetworks: networks, - isScanning: false, - scanSecondsRemaining: 0, - ); - default: - break; - } - } - - Future _load() async { - final identificator = _identificator; - if (identificator == null) { - state = state.copyWith(isLoading: false); - return; - } - - try { - debugPrint('[WiFi] loading saved networks for $identificator'); - final networks = await _repository.getWifiNetworks( - deviceIdentificator: identificator, - ); - if (!ref.mounted) return; - debugPrint('[WiFi] loaded ${networks.length} saved networks'); - - state = state.copyWith( - savedNetworks: networks, - isLoading: false, - isLoadingCurrentNetwork: true, - ); - - debugPrint('[WiFi] sending WIFI_CURRENT command'); - unawaited( - _commands - .send( - request: SendCommandRequestModel( - device: identificator, - command: DeviceCommand.wifiCurrent, - ), - ) - .catchError((_) { - debugPrint('[WiFi] WIFI_CURRENT command failed'); - if (ref.mounted) { - state = state.copyWith(isLoadingCurrentNetwork: false); - } - }), - ); - - _currentNetworkTimeout?.cancel(); - _currentNetworkTimeout = Timer(const Duration(seconds: 15), () { - if (!ref.mounted) return; - if (state.isLoadingCurrentNetwork) { - state = state.copyWith(isLoadingCurrentNetwork: false); - } - }); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith( - isLoading: false, - error: WifiSettingsError.loadFailed, - ); - } - } - - Future scanNetworks() async { - final identificator = _identificator; - if (identificator == null) return; - - state = state.copyWith( - isScanning: true, - scanSecondsRemaining: _scanDurationSeconds, - error: null, - ); - - try { - debugPrint('[WiFi] sending WIFI_SEARCH command'); - await _commands.send( - request: SendCommandRequestModel( - device: identificator, - command: DeviceCommand.wifiSearch, - ), - ); - debugPrint('[WiFi] WIFI_SEARCH sent, waiting for WS response (${_scanDurationSeconds}s timeout)'); - - _scanCountdown?.cancel(); - - _scanCountdown = Timer.periodic(const Duration(seconds: 1), (_) { - if (!ref.mounted) { - _scanCountdown?.cancel(); - return; - } - final remaining = state.scanSecondsRemaining - 1; - if (remaining <= 0) { - _scanCountdown?.cancel(); - if (state.isScanning) { - state = state.copyWith( - isScanning: false, - scanSecondsRemaining: 0, - error: WifiSettingsError.scanFailed, - ); - } - } else { - state = state.copyWith(scanSecondsRemaining: remaining); - } - }); - } catch (_) { - if (!ref.mounted) return; - _scanCountdown?.cancel(); - state = state.copyWith( - isScanning: false, - scanSecondsRemaining: 0, - error: WifiSettingsError.scanFailed, - ); - } - } - - Future connectAndSave({ - required String ssid, - required String bssid, - required String password, - }) async { - final identificator = _identificator; - if (identificator == null) return; - - state = state.copyWith(isConnecting: true, error: null, success: null); - - try { - debugPrint('[WiFi] connecting to $ssid ($bssid)'); - await _commands.send( - request: SendCommandRequestModel( - device: identificator, - command: DeviceCommand.setWifi, - data: {'ssid': ssid, 'bssid': bssid, 'password': password}, - ), - ); - debugPrint('[WiFi] SET_WIFI sent, saving network'); - - final id = const Uuid().v4(); - await _repository.createWifiNetwork( - id: id, - deviceIdentificator: identificator, - ssid: ssid, - bssid: bssid, - password: password, - ); - if (!ref.mounted) return; - - final networks = await _repository.getWifiNetworks( - deviceIdentificator: identificator, - ); - if (!ref.mounted) return; - - debugPrint('[WiFi] network saved, total: ${networks.length}'); - unawaited(_tracking.legacySettingsWifiAdded(totalCount: networks.length)); - - state = state.copyWith( - savedNetworks: networks, - isConnecting: false, - success: WifiSettingsSuccess.networkSaved, - ); - } catch (e) { - debugPrint('[WiFi] connectAndSave failed: $e'); - if (!ref.mounted) return; - state = state.copyWith( - isConnecting: false, - error: WifiSettingsError.saveFailed, - ); - } - } - - Future removeNetwork(String networkId) async { - final identificator = _identificator; - if (identificator == null) return; - - state = state.copyWith(isSaving: true, error: null, success: null); - - try { - debugPrint('[WiFi] deleting network $networkId'); - await _repository.deleteWifiNetwork(networkId: networkId); - if (!ref.mounted) return; - - final networks = await _repository.getWifiNetworks( - deviceIdentificator: identificator, - ); - if (!ref.mounted) return; - - debugPrint('[WiFi] network deleted, remaining: ${networks.length}'); - unawaited( - _tracking.legacySettingsWifiRemoved(totalCount: networks.length), - ); - - state = state.copyWith( - savedNetworks: networks, - isSaving: false, - success: WifiSettingsSuccess.networkDeleted, - ); - } catch (e) { - debugPrint('[WiFi] delete failed: $e'); - if (!ref.mounted) return; - state = state.copyWith( - isSaving: false, - error: WifiSettingsError.deleteFailed, - ); - } - } - - Future setNetwork(WifiNetworkEntity network) async { - final identificator = _identificator; - if (identificator == null) return; - - state = state.copyWith(isConnecting: true, error: null, success: null); - - try { - debugPrint('[WiFi] setting saved network: ${network.ssid} (${network.bssid})'); - await _commands.send( - request: SendCommandRequestModel( - device: identificator, - command: DeviceCommand.setWifi, - data: { - 'ssid': network.ssid, - 'bssid': network.bssid, - 'password': network.password ?? '', - }, - ), - ); - if (!ref.mounted) return; - debugPrint('[WiFi] SET_WIFI sent, requesting WIFI_CURRENT'); - - state = state.copyWith( - isConnecting: false, - isLoadingCurrentNetwork: true, - success: WifiSettingsSuccess.networkSet, - ); - - unawaited( - _commands - .send( - request: SendCommandRequestModel( - device: identificator, - command: DeviceCommand.wifiCurrent, - ), - ) - .catchError((_) { - debugPrint('[WiFi] WIFI_CURRENT after set failed'); - if (ref.mounted) { - state = state.copyWith(isLoadingCurrentNetwork: false); - } - }), - ); - - _currentNetworkTimeout?.cancel(); - _currentNetworkTimeout = Timer(const Duration(seconds: 15), () { - if (!ref.mounted) return; - if (state.isLoadingCurrentNetwork) { - state = state.copyWith(isLoadingCurrentNetwork: false); - } - }); - } catch (_) { - if (!ref.mounted) return; - state = state.copyWith( - isConnecting: false, - error: WifiSettingsError.setFailed, - ); - } - } - - void clearError() { - if (state.error != null) state = state.copyWith(error: null); - } - - void clearSuccess() { - if (state.success != null) state = state.copyWith(success: null); - } -} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.dart deleted file mode 100644 index 684da661..00000000 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../../domain/entities/scanned_wifi_network.dart'; -import '../../domain/entities/wifi_network_entity.dart'; - -part 'wifi_settings_view_state.freezed.dart'; - -enum WifiSettingsError { loadFailed, scanFailed, connectFailed, saveFailed, deleteFailed, setFailed } - -enum WifiSettingsSuccess { networkSaved, networkDeleted, connected, networkSet } - -@freezed -abstract class WifiSettingsViewState with _$WifiSettingsViewState { - const factory WifiSettingsViewState({ - @Default([]) List savedNetworks, - @Default([]) List availableNetworks, - WifiNetworkEntity? currentNetwork, - @Default(true) bool isLoading, - @Default(false) bool isLoadingCurrentNetwork, - @Default(false) bool isScanning, - @Default(0) int scanSecondsRemaining, - @Default(false) bool isSaving, - @Default(false) bool isConnecting, - WifiSettingsError? error, - WifiSettingsSuccess? success, - }) = _WifiSettingsViewState; -} diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.freezed.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.freezed.dart deleted file mode 100644 index a2d87630..00000000 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_state.freezed.dart +++ /dev/null @@ -1,337 +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 'wifi_settings_view_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$WifiSettingsViewState { - - List get savedNetworks; List get availableNetworks; WifiNetworkEntity? get currentNetwork; bool get isLoading; bool get isLoadingCurrentNetwork; bool get isScanning; int get scanSecondsRemaining; bool get isSaving; bool get isConnecting; WifiSettingsError? get error; WifiSettingsSuccess? get success; -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$WifiSettingsViewStateCopyWith get copyWith => _$WifiSettingsViewStateCopyWithImpl(this as WifiSettingsViewState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is WifiSettingsViewState&&const DeepCollectionEquality().equals(other.savedNetworks, savedNetworks)&&const DeepCollectionEquality().equals(other.availableNetworks, availableNetworks)&&(identical(other.currentNetwork, currentNetwork) || other.currentNetwork == currentNetwork)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingCurrentNetwork, isLoadingCurrentNetwork) || other.isLoadingCurrentNetwork == isLoadingCurrentNetwork)&&(identical(other.isScanning, isScanning) || other.isScanning == isScanning)&&(identical(other.scanSecondsRemaining, scanSecondsRemaining) || other.scanSecondsRemaining == scanSecondsRemaining)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.error, error) || other.error == error)&&(identical(other.success, success) || other.success == success)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(savedNetworks),const DeepCollectionEquality().hash(availableNetworks),currentNetwork,isLoading,isLoadingCurrentNetwork,isScanning,scanSecondsRemaining,isSaving,isConnecting,error,success); - -@override -String toString() { - return 'WifiSettingsViewState(savedNetworks: $savedNetworks, availableNetworks: $availableNetworks, currentNetwork: $currentNetwork, isLoading: $isLoading, isLoadingCurrentNetwork: $isLoadingCurrentNetwork, isScanning: $isScanning, scanSecondsRemaining: $scanSecondsRemaining, isSaving: $isSaving, isConnecting: $isConnecting, error: $error, success: $success)'; -} - - -} - -/// @nodoc -abstract mixin class $WifiSettingsViewStateCopyWith<$Res> { - factory $WifiSettingsViewStateCopyWith(WifiSettingsViewState value, $Res Function(WifiSettingsViewState) _then) = _$WifiSettingsViewStateCopyWithImpl; -@useResult -$Res call({ - List savedNetworks, List availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success -}); - - -$WifiNetworkEntityCopyWith<$Res>? get currentNetwork; - -} -/// @nodoc -class _$WifiSettingsViewStateCopyWithImpl<$Res> - implements $WifiSettingsViewStateCopyWith<$Res> { - _$WifiSettingsViewStateCopyWithImpl(this._self, this._then); - - final WifiSettingsViewState _self; - final $Res Function(WifiSettingsViewState) _then; - -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? savedNetworks = null,Object? availableNetworks = null,Object? currentNetwork = freezed,Object? isLoading = null,Object? isLoadingCurrentNetwork = null,Object? isScanning = null,Object? scanSecondsRemaining = null,Object? isSaving = null,Object? isConnecting = null,Object? error = freezed,Object? success = freezed,}) { - return _then(_self.copyWith( -savedNetworks: null == savedNetworks ? _self.savedNetworks : savedNetworks // ignore: cast_nullable_to_non_nullable -as List,availableNetworks: null == availableNetworks ? _self.availableNetworks : availableNetworks // ignore: cast_nullable_to_non_nullable -as List,currentNetwork: freezed == currentNetwork ? _self.currentNetwork : currentNetwork // ignore: cast_nullable_to_non_nullable -as WifiNetworkEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isLoadingCurrentNetwork: null == isLoadingCurrentNetwork ? _self.isLoadingCurrentNetwork : isLoadingCurrentNetwork // ignore: cast_nullable_to_non_nullable -as bool,isScanning: null == isScanning ? _self.isScanning : isScanning // ignore: cast_nullable_to_non_nullable -as bool,scanSecondsRemaining: null == scanSecondsRemaining ? _self.scanSecondsRemaining : scanSecondsRemaining // ignore: cast_nullable_to_non_nullable -as int,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable -as bool,isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as WifiSettingsError?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as WifiSettingsSuccess?, - )); -} -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$WifiNetworkEntityCopyWith<$Res>? get currentNetwork { - if (_self.currentNetwork == null) { - return null; - } - - return $WifiNetworkEntityCopyWith<$Res>(_self.currentNetwork!, (value) { - return _then(_self.copyWith(currentNetwork: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [WifiSettingsViewState]. -extension WifiSettingsViewStatePatterns on WifiSettingsViewState { -/// 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( _WifiSettingsViewState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _WifiSettingsViewState() 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( _WifiSettingsViewState value) $default,){ -final _that = this; -switch (_that) { -case _WifiSettingsViewState(): -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( _WifiSettingsViewState value)? $default,){ -final _that = this; -switch (_that) { -case _WifiSettingsViewState() 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( List savedNetworks, List availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _WifiSettingsViewState() when $default != null: -return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);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( List savedNetworks, List availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success) $default,) {final _that = this; -switch (_that) { -case _WifiSettingsViewState(): -return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);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( List savedNetworks, List availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success)? $default,) {final _that = this; -switch (_that) { -case _WifiSettingsViewState() when $default != null: -return $default(_that.savedNetworks,_that.availableNetworks,_that.currentNetwork,_that.isLoading,_that.isLoadingCurrentNetwork,_that.isScanning,_that.scanSecondsRemaining,_that.isSaving,_that.isConnecting,_that.error,_that.success);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _WifiSettingsViewState implements WifiSettingsViewState { - const _WifiSettingsViewState({final List savedNetworks = const [], final List availableNetworks = const [], this.currentNetwork, this.isLoading = true, this.isLoadingCurrentNetwork = false, this.isScanning = false, this.scanSecondsRemaining = 0, this.isSaving = false, this.isConnecting = false, this.error, this.success}): _savedNetworks = savedNetworks,_availableNetworks = availableNetworks; - - - final List _savedNetworks; -@override@JsonKey() List get savedNetworks { - if (_savedNetworks is EqualUnmodifiableListView) return _savedNetworks; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_savedNetworks); -} - - final List _availableNetworks; -@override@JsonKey() List get availableNetworks { - if (_availableNetworks is EqualUnmodifiableListView) return _availableNetworks; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_availableNetworks); -} - -@override final WifiNetworkEntity? currentNetwork; -@override@JsonKey() final bool isLoading; -@override@JsonKey() final bool isLoadingCurrentNetwork; -@override@JsonKey() final bool isScanning; -@override@JsonKey() final int scanSecondsRemaining; -@override@JsonKey() final bool isSaving; -@override@JsonKey() final bool isConnecting; -@override final WifiSettingsError? error; -@override final WifiSettingsSuccess? success; - -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$WifiSettingsViewStateCopyWith<_WifiSettingsViewState> get copyWith => __$WifiSettingsViewStateCopyWithImpl<_WifiSettingsViewState>(this, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _WifiSettingsViewState&&const DeepCollectionEquality().equals(other._savedNetworks, _savedNetworks)&&const DeepCollectionEquality().equals(other._availableNetworks, _availableNetworks)&&(identical(other.currentNetwork, currentNetwork) || other.currentNetwork == currentNetwork)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingCurrentNetwork, isLoadingCurrentNetwork) || other.isLoadingCurrentNetwork == isLoadingCurrentNetwork)&&(identical(other.isScanning, isScanning) || other.isScanning == isScanning)&&(identical(other.scanSecondsRemaining, scanSecondsRemaining) || other.scanSecondsRemaining == scanSecondsRemaining)&&(identical(other.isSaving, isSaving) || other.isSaving == isSaving)&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.error, error) || other.error == error)&&(identical(other.success, success) || other.success == success)); -} - - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_savedNetworks),const DeepCollectionEquality().hash(_availableNetworks),currentNetwork,isLoading,isLoadingCurrentNetwork,isScanning,scanSecondsRemaining,isSaving,isConnecting,error,success); - -@override -String toString() { - return 'WifiSettingsViewState(savedNetworks: $savedNetworks, availableNetworks: $availableNetworks, currentNetwork: $currentNetwork, isLoading: $isLoading, isLoadingCurrentNetwork: $isLoadingCurrentNetwork, isScanning: $isScanning, scanSecondsRemaining: $scanSecondsRemaining, isSaving: $isSaving, isConnecting: $isConnecting, error: $error, success: $success)'; -} - - -} - -/// @nodoc -abstract mixin class _$WifiSettingsViewStateCopyWith<$Res> implements $WifiSettingsViewStateCopyWith<$Res> { - factory _$WifiSettingsViewStateCopyWith(_WifiSettingsViewState value, $Res Function(_WifiSettingsViewState) _then) = __$WifiSettingsViewStateCopyWithImpl; -@override @useResult -$Res call({ - List savedNetworks, List availableNetworks, WifiNetworkEntity? currentNetwork, bool isLoading, bool isLoadingCurrentNetwork, bool isScanning, int scanSecondsRemaining, bool isSaving, bool isConnecting, WifiSettingsError? error, WifiSettingsSuccess? success -}); - - -@override $WifiNetworkEntityCopyWith<$Res>? get currentNetwork; - -} -/// @nodoc -class __$WifiSettingsViewStateCopyWithImpl<$Res> - implements _$WifiSettingsViewStateCopyWith<$Res> { - __$WifiSettingsViewStateCopyWithImpl(this._self, this._then); - - final _WifiSettingsViewState _self; - final $Res Function(_WifiSettingsViewState) _then; - -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? savedNetworks = null,Object? availableNetworks = null,Object? currentNetwork = freezed,Object? isLoading = null,Object? isLoadingCurrentNetwork = null,Object? isScanning = null,Object? scanSecondsRemaining = null,Object? isSaving = null,Object? isConnecting = null,Object? error = freezed,Object? success = freezed,}) { - return _then(_WifiSettingsViewState( -savedNetworks: null == savedNetworks ? _self._savedNetworks : savedNetworks // ignore: cast_nullable_to_non_nullable -as List,availableNetworks: null == availableNetworks ? _self._availableNetworks : availableNetworks // ignore: cast_nullable_to_non_nullable -as List,currentNetwork: freezed == currentNetwork ? _self.currentNetwork : currentNetwork // ignore: cast_nullable_to_non_nullable -as WifiNetworkEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isLoadingCurrentNetwork: null == isLoadingCurrentNetwork ? _self.isLoadingCurrentNetwork : isLoadingCurrentNetwork // ignore: cast_nullable_to_non_nullable -as bool,isScanning: null == isScanning ? _self.isScanning : isScanning // ignore: cast_nullable_to_non_nullable -as bool,scanSecondsRemaining: null == scanSecondsRemaining ? _self.scanSecondsRemaining : scanSecondsRemaining // ignore: cast_nullable_to_non_nullable -as int,isSaving: null == isSaving ? _self.isSaving : isSaving // ignore: cast_nullable_to_non_nullable -as bool,isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable -as bool,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as WifiSettingsError?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as WifiSettingsSuccess?, - )); -} - -/// Create a copy of WifiSettingsViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$WifiNetworkEntityCopyWith<$Res>? get currentNetwork { - if (_self.currentNetwork == null) { - return null; - } - - return $WifiNetworkEntityCopyWith<$Res>(_self.currentNetwork!, (value) { - return _then(_self.copyWith(currentNetwork: value)); - }); -} -} - -// dart format on diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart index a5121064..02d79237 100644 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart @@ -1,13 +1,14 @@ -import 'package:legacy_theme/legacy_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_theme/legacy_theme.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_connect_form_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; -import '../state/wifi_settings_view_model.dart'; - void showConnectWifiSheet( BuildContext context, { + required String deviceIdentificator, String? ssid, String? bssid, }) { @@ -15,15 +16,24 @@ void showConnectWifiSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - builder: (_) => _ConnectWifiSheet(ssid: ssid, bssid: bssid), + builder: (_) => _ConnectWifiSheet( + deviceIdentificator: deviceIdentificator, + ssid: ssid, + bssid: bssid, + ), ); } class _ConnectWifiSheet extends ConsumerStatefulWidget { + final String deviceIdentificator; final String? ssid; final String? bssid; - const _ConnectWifiSheet({this.ssid, this.bssid}); + const _ConnectWifiSheet({ + required this.deviceIdentificator, + this.ssid, + this.bssid, + }); @override ConsumerState<_ConnectWifiSheet> createState() => _ConnectWifiSheetState(); @@ -33,46 +43,55 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { late final TextEditingController _ssidController; late final TextEditingController _bssidController; final _passwordController = TextEditingController(); - bool _obscurePassword = true; @override void initState() { super.initState(); _ssidController = TextEditingController(text: widget.ssid ?? ''); _bssidController = TextEditingController(text: widget.bssid ?? ''); + _ssidController.addListener(_refreshCanSave); + _bssidController.addListener(_refreshCanSave); + _passwordController.addListener(_refreshCanSave); + WidgetsBinding.instance.addPostFrameCallback((_) => _refreshCanSave()); } @override void dispose() { + _ssidController.removeListener(_refreshCanSave); + _bssidController.removeListener(_refreshCanSave); + _passwordController.removeListener(_refreshCanSave); _ssidController.dispose(); _bssidController.dispose(); _passwordController.dispose(); super.dispose(); } - bool get _canSave => - _ssidController.text.trim().isNotEmpty && - _bssidController.text.trim().isNotEmpty && - _passwordController.text.trim().isNotEmpty; + void _refreshCanSave() { + final canSave = _ssidController.text.trim().isNotEmpty && + _bssidController.text.trim().isNotEmpty && + _passwordController.text.trim().isNotEmpty; + ref.read(wifiConnectFormProvider.notifier).setCanSave(canSave); + } void _submit() { - if (!_canSave) return; + final formState = ref.read(wifiConnectFormProvider); + if (!formState.canSave) return; - final vm = ref.read(wifiSettingsViewModelProvider.notifier); - vm.connectAndSave( - ssid: _ssidController.text.trim(), - bssid: _bssidController.text.trim(), - password: _passwordController.text.trim(), - ); + ref.read(wifiControllerProvider.notifier).connectAndSave( + deviceIdentificator: widget.deviceIdentificator, + ssid: _ssidController.text.trim(), + bssid: _bssidController.text.trim(), + password: _passwordController.text.trim(), + ); Navigator.pop(context); } @override Widget build(BuildContext context) { final primaryColor = context.sfColors.legacyPrimary; - final isConnecting = ref.watch( - wifiSettingsViewModelProvider.select((s) => s.isConnecting), - ); + final formState = ref.watch(wifiConnectFormProvider); + final isSubmitting = ref + .watch(wifiControllerProvider.select((s) => s.isLoading)); final bottomInset = MediaQuery.of(context).viewInsets.bottom; final hasPrefilled = widget.ssid != null; @@ -81,7 +100,7 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( top: false, @@ -119,12 +138,13 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { ), ), TextButton( - onPressed: _canSave && !isConnecting ? _submit : null, - child: isConnecting + onPressed: formState.canSave && !isSubmitting + ? _submit + : null, + child: isSubmitting ? SizedBox( width: SizeUtils.getByScreen(small: 20, big: 22), - height: - SizeUtils.getByScreen(small: 20, big: 22), + height: SizeUtils.getByScreen(small: 20, big: 22), child: CircularProgressIndicator( strokeWidth: 2, color: primaryColor, @@ -133,7 +153,9 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { : Text( context.translate(I18n.save), style: TextStyle( - color: _canSave ? primaryColor : Colors.grey, + color: formState.canSave + ? primaryColor + : Colors.grey, fontWeight: FontWeight.w600, fontSize: SizeUtils.getByScreen( small: 16, @@ -152,11 +174,10 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { fontWeight: FontWeight.w500, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), TextField( controller: _ssidController, readOnly: hasPrefilled, - onChanged: (_) => setState(() {}), decoration: _inputDecoration( hintText: context.translate(I18n.wifiSsidHint), primaryColor: primaryColor, @@ -170,11 +191,10 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { fontWeight: FontWeight.w500, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), TextField( controller: _bssidController, readOnly: hasPrefilled, - onChanged: (_) => setState(() {}), decoration: _inputDecoration( hintText: context.translate(I18n.wifiBssidHint), primaryColor: primaryColor, @@ -188,20 +208,20 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { fontWeight: FontWeight.w500, ), ), - SizedBox(height: 8), + const SizedBox(height: 8), TextField( controller: _passwordController, - obscureText: _obscurePassword, - onChanged: (_) => setState(() {}), + obscureText: formState.obscurePassword, decoration: _inputDecoration( hintText: context.translate(I18n.wifiPasswordHint), primaryColor: primaryColor, ).copyWith( suffixIcon: IconButton( - onPressed: () => - setState(() => _obscurePassword = !_obscurePassword), + onPressed: ref + .read(wifiConnectFormProvider.notifier) + .togglePasswordVisibility, icon: Icon( - _obscurePassword + formState.obscurePassword ? Icons.visibility_off : Icons.visibility, color: Colors.grey, @@ -226,18 +246,21 @@ class _ConnectWifiSheetState extends ConsumerState<_ConnectWifiSheet> { return InputDecoration( hintText: hintText, border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + borderRadius: const BorderRadius.all(Radius.circular(12)), borderSide: BorderSide(color: primaryColor, width: 2), ), - contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 14, + ), ); } } diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart index ac3d68d8..a238f8c8 100644 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart @@ -1,93 +1,145 @@ -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:navigation/navigation.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/saved_wifi_networks_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_current_network_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_scan_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/widgets/add_wifi_network_sheet.dart'; +import 'package:settings/src/features/wifi_settings/presentation/widgets/available_wifi_network_card.dart'; +import 'package:settings/src/features/wifi_settings/presentation/widgets/wifi_network_card.dart'; import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; import 'package:utils/utils.dart'; -import 'state/wifi_settings_view_model.dart'; -import 'state/wifi_settings_view_state.dart'; -import 'widgets/available_wifi_network_card.dart'; -import 'widgets/add_wifi_network_sheet.dart'; -import 'widgets/wifi_network_card.dart'; - -class WifiSettingsScreen extends ConsumerWidget { +class WifiSettingsScreen extends ConsumerStatefulWidget { final NavigationContract navigationContract; const WifiSettingsScreen({super.key, required this.navigationContract}); @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(wifiSettingsViewModelProvider); - final vm = ref.read(wifiSettingsViewModelProvider.notifier); + ConsumerState createState() => _WifiSettingsScreenState(); +} + +class _WifiSettingsScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final identificator = + ref.read(selectedDeviceProvider).value?.identificator; + if (identificator == null) return; + ref + .read(wifiCurrentNetworkProvider.notifier) + .requestCurrent(identificator); + }); + } + + @override + Widget build(BuildContext context) { final primaryColor = context.sfColors.legacyPrimary; + final device = ref.watch(selectedDeviceProvider).value; + final identificator = device?.identificator; - ref.listen(wifiSettingsViewModelProvider.select((s) => s.error), ( - _, - error, - ) { - if (error == null) return; - final message = switch (error) { - WifiSettingsError.loadFailed => context.translate(I18n.wifiLoadError), - WifiSettingsError.scanFailed => context.translate(I18n.wifiScanFailed), - WifiSettingsError.connectFailed => - context.translate(I18n.wifiConnectFailed), - WifiSettingsError.saveFailed => context.translate(I18n.wifiSaveFailed), - WifiSettingsError.deleteFailed => - context.translate(I18n.wifiDeleteFailed), - WifiSettingsError.setFailed => - context.translate(I18n.wifiSetFailed), + ref.watch(wifiCurrentNetworkProvider); + ref.watch(wifiScanProvider); + + ref.listen(wifiControllerProvider, (prev, next) async { + if (prev == null || !prev.isLoading || next.isLoading) return; + if (next.hasError) { + await next.showErrorOn(context); + return; + } + final action = + ref.read(wifiControllerProvider.notifier).lastAction; + final message = switch (action) { + WifiControllerAction.connectAndSave => I18n.wifiNetworkSaved, + WifiControllerAction.remove => I18n.wifiNetworkDeleted, + WifiControllerAction.set => I18n.wifiNetworkSet, + null => I18n.deviceUpdatedSuccess, }; - showTopSnackbar(context, message: message, type: MessageType.error); - vm.clearError(); + await showSuccessDialog(context, message); }); - ref.listen(wifiSettingsViewModelProvider.select((s) => s.success), ( - _, - success, - ) { - if (success == null) return; - final message = switch (success) { - WifiSettingsSuccess.networkSaved => - context.translate(I18n.wifiNetworkSaved), - WifiSettingsSuccess.networkDeleted => - context.translate(I18n.wifiNetworkDeleted), - WifiSettingsSuccess.connected => context.translate(I18n.wifiConnected), - WifiSettingsSuccess.networkSet => - context.translate(I18n.wifiNetworkSet), - }; - showTopSnackbar(context, message: message, type: MessageType.success); - vm.clearSuccess(); - }); + if (identificator == null) { + return Scaffold( + appBar: _appBar(context, primaryColor, onScan: null), + body: const SizedBox.shrink(), + ); + } + + final savedAsync = ref.watch(savedWifiNetworksProvider(identificator)); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.surface, - surfaceTintColor: Colors.transparent, - elevation: 0, - centerTitle: true, - automaticallyImplyLeading: false, - leading: IconButton( - onPressed: () => navigationContract.goBack(), - icon: Icon( - Icons.adaptive.arrow_back, - color: primaryColor, - size: SizeUtils.getByScreen(small: 32, big: 28), + appBar: _appBar( + context, + primaryColor, + onScan: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; + try { + await ref + .read(wifiScanProvider.notifier) + .scan(identificator); + } on WifiScanTimeoutException { + if (!context.mounted) return; + await showErrorDialog(context, I18n.wifiScanFailed); + } catch (_) { + if (!context.mounted) return; + await showErrorDialog(context, I18n.wifiScanFailed); + } + }, + ), + body: SafeArea( + top: false, + child: savedAsync.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => Center( + child: Text(context.translate(I18n.wifiLoadError)), + ), + data: (savedNetworks) => _Body( + deviceIdentificator: identificator, + savedNetworks: savedNetworks, ), ), - title: Text( - 'WiFi', - style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 20, big: 19), - fontWeight: FontWeight.w500, - letterSpacing: 0, - color: primaryColor, - ), + ), + ); + } + + PreferredSizeWidget _appBar( + BuildContext context, + Color primaryColor, { + required VoidCallback? onScan, + }) { + return AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + elevation: 0, + centerTitle: true, + automaticallyImplyLeading: false, + leading: IconButton( + onPressed: () => widget.navigationContract.goBack(), + icon: Icon( + Icons.adaptive.arrow_back, + color: primaryColor, + size: SizeUtils.getByScreen(small: 32, big: 28), ), - actions: [ + ), + title: Text( + 'WiFi', + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 20, big: 19), + fontWeight: FontWeight.w500, + letterSpacing: 0, + color: primaryColor, + ), + ), + actions: [ + if (onScan != null) Padding( padding: EdgeInsets.only( right: SizeUtils.getByScreen(small: 16, big: 14), @@ -98,10 +150,7 @@ class WifiSettingsScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () async { - if (!await guardDeviceCommand(context, ref)) return; - vm.scanNetworks(); - }, + onPressed: onScan, icon: Icon( Icons.wifi_find, color: Colors.white, @@ -110,31 +159,31 @@ class WifiSettingsScreen extends ConsumerWidget { ), ), ), - ], - ), - body: SafeArea( - top: false, - child: state.isLoading - ? const Center(child: CircularProgressIndicator()) - : _Body(), - ), + ], ); } } class _Body extends ConsumerWidget { - const _Body(); + final String deviceIdentificator; + final List savedNetworks; + + const _Body({ + required this.deviceIdentificator, + required this.savedNetworks, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(wifiSettingsViewModelProvider); final primaryColor = context.sfColors.legacyPrimary; + final current = ref.watch(wifiCurrentNetworkProvider); + final scan = ref.watch(wifiScanProvider); return SingleChildScrollView( child: Padding( padding: SizeUtils.getByScreen( - small: EdgeInsets.symmetric(horizontal: 22, vertical: 10), - big: EdgeInsets.symmetric(horizontal: 21, vertical: 8), + small: const EdgeInsets.symmetric(horizontal: 22, vertical: 10), + big: const EdgeInsets.symmetric(horizontal: 21, vertical: 8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -147,7 +196,7 @@ class _Body extends ConsumerWidget { context.translate(I18n.wifiCurrentNetwork), primaryColor, ), - if (state.isLoadingCurrentNetwork) + if (current.isLoading) SizedBox( width: SizeUtils.getByScreen(small: 16, big: 14), height: SizeUtils.getByScreen(small: 16, big: 14), @@ -159,12 +208,12 @@ class _Body extends ConsumerWidget { ], ), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)), - if (state.currentNetwork != null) + if (current.network != null) _CurrentNetworkCard( - ssid: state.currentNetwork!.ssid, - bssid: state.currentNetwork!.bssid, + ssid: current.network!.ssid, + bssid: current.network!.bssid, ) - else if (!state.isLoadingCurrentNetwork) + else if (!current.isLoading) Padding( padding: EdgeInsets.only( bottom: SizeUtils.getByScreen(small: 12, big: 10), @@ -173,12 +222,14 @@ class _Body extends ConsumerWidget { context.translate(I18n.wifiNoCurrentNetwork), style: TextStyle( fontSize: SizeUtils.getByScreen(small: 14, big: 15), - color: Theme.of(context).colorScheme.onSurface + color: Theme.of(context) + .colorScheme + .onSurface .withAlpha(178), ), ), ), - if (state.availableNetworks.isNotEmpty || state.isScanning) ...[ + if (scan.networks.isNotEmpty || scan.isScanning) ...[ SizedBox(height: SizeUtils.getByScreen(small: 16, big: 14)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -188,19 +239,21 @@ class _Body extends ConsumerWidget { context.translate(I18n.wifiAvailableNetworks), primaryColor, ), - if (state.isScanning) + if (scan.isScanning) Row( mainAxisSize: MainAxisSize.min, children: [ Text( - '${state.scanSecondsRemaining}s', + '${scan.secondsRemaining}s', style: TextStyle( - fontSize: SizeUtils.getByScreen(small: 13, big: 12), + fontSize: + SizeUtils.getByScreen(small: 13, big: 12), fontWeight: FontWeight.w600, color: primaryColor, ), ), - SizedBox(width: SizeUtils.getByScreen(small: 6, big: 5)), + SizedBox( + width: SizeUtils.getByScreen(small: 6, big: 5)), SizedBox( width: SizeUtils.getByScreen(small: 16, big: 14), height: SizeUtils.getByScreen(small: 16, big: 14), @@ -214,11 +267,12 @@ class _Body extends ConsumerWidget { ], ), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)), - ...state.availableNetworks.map( + ...scan.networks.map( (network) => AvailableWifiNetworkCard( network: network, onTap: () => showConnectWifiSheet( context, + deviceIdentificator: deviceIdentificator, ssid: network.ssid, bssid: network.bssid, ), @@ -230,12 +284,12 @@ class _Body extends ConsumerWidget { context, context.translate( I18n.wifiSavedNetworks, - args: {'count': state.savedNetworks.length.toString()}, + args: {'count': savedNetworks.length.toString()}, ), primaryColor, ), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 6)), - if (state.savedNetworks.isEmpty) + if (savedNetworks.isEmpty) Padding( padding: EdgeInsets.only( bottom: SizeUtils.getByScreen(small: 12, big: 10), @@ -244,20 +298,23 @@ class _Body extends ConsumerWidget { context.translate(I18n.noWifiNetworks), style: TextStyle( fontSize: SizeUtils.getByScreen(small: 14, big: 15), - color: Theme.of(context).colorScheme.onSurface + color: Theme.of(context) + .colorScheme + .onSurface .withAlpha(178), ), ), ) else - ...state.savedNetworks.map( + ...savedNetworks.map( (network) => WifiNetworkCard( network: network, onTap: () async { if (!await guardDeviceCommand(context, ref)) return; - ref - .read(wifiSettingsViewModelProvider.notifier) - .setNetwork(network); + ref.read(wifiControllerProvider.notifier).setNetwork( + deviceIdentificator: deviceIdentificator, + network: network, + ); }, onDelete: () => _confirmDelete(context, ref, network), ), @@ -283,15 +340,15 @@ class _Body extends ConsumerWidget { Future _confirmDelete( BuildContext context, WidgetRef ref, - dynamic network, + WifiNetworkEntity network, ) async { if (!await guardDeviceCommand(context, ref)) return; if (!context.mounted) return; final primaryColor = context.sfColors.legacyPrimary; - showDialog( + showDialog( context: context, - builder: (context) => AlertDialog( + builder: (dialogContext) => AlertDialog( title: Text(context.translate(I18n.removeWifiNetwork)), content: Text( context.translate( @@ -301,7 +358,7 @@ class _Body extends ConsumerWidget { ), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(dialogContext), child: Text( context.translate(I18n.cancel), style: TextStyle(color: primaryColor), @@ -309,10 +366,11 @@ class _Body extends ConsumerWidget { ), TextButton( onPressed: () { - ref - .read(wifiSettingsViewModelProvider.notifier) - .removeNetwork(network.id); - Navigator.pop(context); + ref.read(wifiControllerProvider.notifier).removeNetwork( + deviceIdentificator: deviceIdentificator, + networkId: network.id, + ); + Navigator.pop(dialogContext); }, child: Text( context.translate(I18n.delete), @@ -329,10 +387,7 @@ class _CurrentNetworkCard extends StatelessWidget { final String ssid; final String bssid; - const _CurrentNetworkCard({ - required this.ssid, - required this.bssid, - }); + const _CurrentNetworkCard({required this.ssid, required this.bssid}); @override Widget build(BuildContext context) { @@ -347,7 +402,7 @@ class _CurrentNetworkCard extends StatelessWidget { ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.all(Radius.circular(16)), + borderRadius: const BorderRadius.all(Radius.circular(16)), ), child: Row( children: [ @@ -369,11 +424,13 @@ class _CurrentNetworkCard extends StatelessWidget { fontSize: SizeUtils.getByScreen(small: 15, big: 16), ), ), - SizedBox(height: 2), + const SizedBox(height: 2), Text( bssid, style: TextStyle( - color: Theme.of(context).colorScheme.onSurface + color: Theme.of(context) + .colorScheme + .onSurface .withAlpha(178), fontSize: SizeUtils.getByScreen(small: 13, big: 14), ), diff --git a/modules/legacy/modules/settings/test/features/wifi_settings/wifi_controller_test.dart b/modules/legacy/modules/settings/test/features/wifi_settings/wifi_controller_test.dart new file mode 100644 index 00000000..0c26752d --- /dev/null +++ b/modules/legacy/modules/settings/test/features/wifi_settings/wifi_controller_test.dart @@ -0,0 +1,270 @@ +import 'dart:async'; + +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/core/domain/repositories/wifi_repository.dart'; +import 'package:settings/src/core/providers/wifi_repository_provider.dart'; +import 'package:settings/src/features/wifi_settings/domain/entities/wifi_network_entity.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/web_socket_service_provider.dart'; +import 'package:settings/src/features/wifi_settings/presentation/providers/wifi_controller.dart'; +import 'package:sf_infrastructure/sf_infrastructure.dart'; +import 'package:sf_shared/testing.dart'; +import 'package:sf_tracking/sf_tracking.dart'; + +class MockWifiRepository extends Mock implements WifiRepository {} + +class MockCommandsRepository extends Mock implements CommandsRepository {} + +class FakeWebSocketService implements WebSocketService { + final _controller = StreamController.broadcast(); + @override + Stream get events => _controller.stream; + + void close() => _controller.close(); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +const _network = WifiNetworkEntity( + id: 'net-1', + ssid: 'home', + bssid: '00:11:22:33:44:55', + password: 'secret', +); + +void main() { + setUpAll(() { + registerFallbackValue( + SendCommandRequestModel( + device: 'x', + command: DeviceCommand.wifiCurrent, + ), + ); + }); + + ProviderContainer buildContainer({ + required WifiRepository repo, + required CommandsRepository commands, + }) { + final ws = FakeWebSocketService(); + return makeContainer( + overrides: [ + wifiRepositoryProvider.overrideWithValue(repo), + commandsRepositoryProvider.overrideWithValue(commands), + webSocketServiceProvider.overrideWithValue(ws), + sfTrackingProvider.overrideWithValue( + SfTrackingRepository(clients: const []), + ), + ], + ); + } + + group('WifiController.connectAndSave', () { + test('sends SET_WIFI, persists, and refetches on success', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => commands.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + when( + () => repo.createWifiNetwork( + id: any(named: 'id'), + deviceIdentificator: any(named: 'deviceIdentificator'), + ssid: any(named: 'ssid'), + bssid: any(named: 'bssid'), + password: any(named: 'password'), + ), + ).thenAnswer((_) async {}); + when( + () => repo.getWifiNetworks( + deviceIdentificator: any(named: 'deviceIdentificator'), + ), + ).thenAnswer((_) async => const [_network]); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).connectAndSave( + deviceIdentificator: 'imei-1', + ssid: 'home', + bssid: '00:11:22:33:44:55', + password: 'secret', + ); + + expect(container.read(wifiControllerProvider), isA>()); + verify(() => commands.send(request: any(named: 'request'))).called(1); + verify( + () => repo.createWifiNetwork( + id: any(named: 'id'), + deviceIdentificator: 'imei-1', + ssid: 'home', + bssid: '00:11:22:33:44:55', + password: 'secret', + ), + ).called(1); + }); + + test('exposes AsyncError when command fails', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => commands.send(request: any(named: 'request'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).connectAndSave( + deviceIdentificator: 'imei-1', + ssid: 'home', + bssid: '00:11:22:33:44:55', + password: 'secret', + ); + + expect(container.read(wifiControllerProvider), isA>()); + verifyNever( + () => repo.createWifiNetwork( + id: any(named: 'id'), + deviceIdentificator: any(named: 'deviceIdentificator'), + ssid: any(named: 'ssid'), + bssid: any(named: 'bssid'), + password: any(named: 'password'), + ), + ); + }); + }); + + group('WifiController.removeNetwork', () { + test('deletes and transitions to AsyncData', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => repo.deleteWifiNetwork(networkId: any(named: 'networkId'))) + .thenAnswer((_) async {}); + when( + () => repo.getWifiNetworks( + deviceIdentificator: any(named: 'deviceIdentificator'), + ), + ).thenAnswer((_) async => const []); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).removeNetwork( + deviceIdentificator: 'imei-1', + networkId: 'net-1', + ); + + expect(container.read(wifiControllerProvider), isA>()); + verify(() => repo.deleteWifiNetwork(networkId: 'net-1')).called(1); + }); + + test('exposes AsyncError when repository fails', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => repo.deleteWifiNetwork(networkId: any(named: 'networkId'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).removeNetwork( + deviceIdentificator: 'imei-1', + networkId: 'net-1', + ); + + expect(container.read(wifiControllerProvider), isA>()); + }); + }); + + group('WifiController.setNetwork', () { + test('sends SET_WIFI on success', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => commands.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).setNetwork( + deviceIdentificator: 'imei-1', + network: _network, + ); + + expect(container.read(wifiControllerProvider), isA>()); + final sent = verify( + () => commands.send(request: captureAny(named: 'request')), + ).captured; + final setWifi = sent.firstWhere( + (r) => (r as SendCommandRequestModel).command == DeviceCommand.setWifi, + ) as SendCommandRequestModel; + expect(setWifi.data, { + 'ssid': 'home', + 'bssid': '00:11:22:33:44:55', + 'password': 'secret', + }); + }); + + test('exposes AsyncError when command fails', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + + when(() => commands.send(request: any(named: 'request'))) + .thenThrow(const ApiException(message: 'boom', isNetworkError: true)); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).setNetwork( + deviceIdentificator: 'imei-1', + network: _network, + ); + + expect(container.read(wifiControllerProvider), isA>()); + }); + }); + + group('lastAction tracking', () { + test('records connectAndSave', () async { + final repo = MockWifiRepository(); + final commands = MockCommandsRepository(); + when(() => commands.send(request: any(named: 'request'))) + .thenAnswer((_) async {}); + when( + () => repo.createWifiNetwork( + id: any(named: 'id'), + deviceIdentificator: any(named: 'deviceIdentificator'), + ssid: any(named: 'ssid'), + bssid: any(named: 'bssid'), + password: any(named: 'password'), + ), + ).thenAnswer((_) async {}); + when( + () => repo.getWifiNetworks( + deviceIdentificator: any(named: 'deviceIdentificator'), + ), + ).thenAnswer((_) async => const []); + + final container = buildContainer(repo: repo, commands: commands); + addTearDown(container.dispose); + + await container.read(wifiControllerProvider.notifier).connectAndSave( + deviceIdentificator: 'imei-1', + ssid: 'home', + bssid: '00:11:22:33:44:55', + password: 'secret', + ); + + expect( + container.read(wifiControllerProvider.notifier).lastAction, + WifiControllerAction.connectAndSave, + ); + }); + }); +}