From 0b160758e22a88d6eefe9bac310781bd507aebae Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Fri, 17 Apr 2026 09:43:52 +0200 Subject: [PATCH] refactor(legacy): make guardDeviceCommand async with stale TTL refetch Convert the shared command guard to an async check that refetches /devices when the cached state is older than 30s, so the isDisconnect flag reflects reality before a command runs. A TopSnackBar explains the check only if the fetch takes longer than 400ms, avoiding noise on fast responses. Update all 44 call sites to await the guard. --- .../widgets/pedometer_toggle.dart | 2 +- .../presentation/background_image_screen.dart | 12 +++--- .../presentation/contacts_screen.dart | 5 ++- .../presentation/do_not_disturb_screen.dart | 4 +- .../health/presentation/health_screen.dart | 6 +-- .../widgets/locate_device_dialog.dart | 4 +- .../presentation/remote_camera_screen.dart | 4 +- .../remote_connection_screen.dart | 10 +++-- .../presentation/widgets/spy_call_dialog.dart | 8 ++-- .../scheduled_activities_screen.dart | 5 ++- .../widgets/activity_form_sheet.dart | 5 ++- .../presentation/widgets/day_timeline.dart | 10 +++-- .../presentation/volume_control_screen.dart | 4 +- .../widgets/create_frequent_place_sheet.dart | 2 +- .../widgets/create_geofence_sheet.dart | 2 +- .../presentation/widgets/location_map.dart | 39 +++++++++++-------- .../alarm/presentation/alarm_screen.dart | 9 +++-- .../alerts/presentation/alerts_screen.dart | 4 +- .../battery/presentation/battery_screen.dart | 4 +- .../presentation/block_phone_screen.dart | 12 +++--- .../disable_functions_screen.dart | 4 +- .../presentation/language_screen.dart | 4 +- .../remote_management_screen.dart | 15 ++++--- .../sound/presentation/sound_screen.dart | 4 +- .../presentation/timezone_screen.dart | 4 +- .../presentation/wifi_settings_screen.dart | 13 +++++-- .../lib/src/utils/device_command_guard.dart | 28 ++++++++++++- packages/sf_localizations/assets/l10n/de.json | 1 + packages/sf_localizations/assets/l10n/en.json | 1 + packages/sf_localizations/assets/l10n/es.json | 1 + packages/sf_localizations/assets/l10n/fr.json | 1 + packages/sf_localizations/assets/l10n/it.json | 1 + packages/sf_localizations/assets/l10n/pt.json | 1 + .../lib/src/generated/i18n.dart | 1 + .../providers/legacy_devices_provider.dart | 23 ++++++++++- 35 files changed, 163 insertions(+), 90 deletions(-) diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart index f1031d03..be092f0b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart @@ -39,7 +39,7 @@ class PedometerToggle extends ConsumerWidget { value: enabled, activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary), onChanged: (value) async { - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; final success = await ref .read(activityMeterViewModelProvider.notifier) .togglePedometer(enabled: value); diff --git a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart index e6992313..7500f801 100644 --- a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart @@ -97,8 +97,8 @@ class BackgroundImageScreen extends ConsumerWidget { child: IconButton( onPressed: state.isSaving ? null - : () { - if (!guardDeviceCommand(context, ref)) return; + : () async { + if (!await guardDeviceCommand(context, ref)) return; vm.uploadPhoto(); }, icon: Icon( @@ -119,16 +119,16 @@ class BackgroundImageScreen extends ConsumerWidget { ? const Center(child: CircularProgressIndicator()) : state.photos.isEmpty ? _EmptyState( - onUpload: () { - if (!guardDeviceCommand(context, ref)) return; + onUpload: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.uploadPhoto(); }, primaryColor: primaryColor, ) : _PhotoGrid( state: state, - onPhotoTap: (id) { - if (!guardDeviceCommand(context, ref)) return; + onPhotoTap: (id) async { + if (!await guardDeviceCommand(context, ref)) return; vm.setAsBackground(id); }, primaryColor: primaryColor, diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart index 38c1883e..4b4d9ecb 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart @@ -58,8 +58,9 @@ class ContactsScreen extends ConsumerWidget { shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), - onTap: () { - if (!guardDeviceCommand(context, ref)) return; + onTap: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; if (state.contacts.length >= state.maxContacts) { showTopSnackbar( context, diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart index 2d18f065..c7e1c8d7 100644 --- a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart @@ -155,8 +155,8 @@ class DoNotDisturbScreen extends ConsumerWidget { child: isSaving ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.save(); }, text: context.translate(I18n.doNotDisturbSave), diff --git a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart index e6b9fd10..6fa95f6b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart @@ -110,7 +110,7 @@ class _HealthScreenState extends ConsumerState options: device.capabilities!.heartbeats!.options, theme: theme, onChanged: (frequency) async { - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; final success = await vm.updateHeartRateFrequency( frequency: frequency, ); @@ -199,8 +199,8 @@ class _SaveSection extends ConsumerWidget { child: PrimaryButton( onPressed: isMeasuring ? null - : () { - if (!guardDeviceCommand(context, ref)) return; + : () async { + if (!await guardDeviceCommand(context, ref)) return; vm.measure(); }, text: isMeasuring ? '...' : context.translate(I18n.measure), diff --git a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart index 7c323183..cc860df8 100644 --- a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart @@ -90,8 +90,8 @@ class LocateDeviceDialog extends ConsumerWidget { SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)), Expanded( child: PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.locateDevice(); }, text: context.translate(I18n.accept), 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 6d4ff8a6..409fd16c 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 @@ -152,8 +152,8 @@ class _TakePictureSection extends ConsumerWidget { big: EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), child: PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.takePicture(); }, text: context.translate(I18n.takePicture), diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart index 7f99eef6..3c04cca9 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart @@ -33,8 +33,9 @@ class RemoteConnectionScreen extends ConsumerWidget { children: [ if (cameraEnabled) ...[ _SectionButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; Navigator.push( context, MaterialPageRoute( @@ -50,8 +51,9 @@ class RemoteConnectionScreen extends ConsumerWidget { SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)), ], _SectionButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showDialog( context: context, builder: (context) => Dialog(child: SpyCallDialog()), 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 021af859..5458fe3c 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 @@ -77,13 +77,13 @@ class SpyCallDialog extends ConsumerWidget { ), ), SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)), - _PhoneSection(onSubmit: () { - if (!guardDeviceCommand(context, ref)) return; + _PhoneSection(onSubmit: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.call(); }), SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)), - _CallSection(onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + _CallSection(onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.call(); }), ], diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart index 43f0cec6..1eb45a33 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart @@ -104,8 +104,9 @@ class _ScheduledActivitiesScreenState shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), - onTap: () { - if (!guardDeviceCommand(context, ref)) return; + onTap: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showActivityFormSheet(context, weekDay: _selectedWeekDay); }, child: SizedBox( diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart index d6304e53..45c5560b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart @@ -97,9 +97,10 @@ class _ActivityFormSheetState extends ConsumerState { }); } - void _submit() { + Future _submit() async { if (!_isFormValid) return; - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; final name = _nameController.text.trim(); final period = _buildPeriod(); diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart index 1a42737b..b2460047 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart @@ -188,8 +188,9 @@ class _ActivityTimelineCard extends ConsumerWidget { ), ), IconButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showActivityFormSheet(context, activity: activity); }, icon: Icon( @@ -201,8 +202,9 @@ class _ActivityTimelineCard extends ConsumerWidget { padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)), ), IconButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showDialog( context: context, builder: (_) => Dialog( diff --git a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart index 8cb821ce..d0d119ae 100644 --- a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart @@ -81,8 +81,8 @@ class VolumeControlScreen extends ConsumerWidget { child: state.isLoading ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.submit(); }, text: context.translate(I18n.volumeSend), diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart index 3775eeff..2b2d4eeb 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart @@ -75,7 +75,7 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> { Future _submit() async { if (!_canSave) return; - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; final vm = ref.read(locationViewModelProvider.notifier); final bool success; diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart index fad7c9c4..261cd218 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart @@ -91,7 +91,7 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> { Future _submit() async { if (!_canSave) return; - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; final vm = ref.read(locationViewModelProvider.notifier); final description = _descriptionController.text.trim(); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart index e3005488..a0247850 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart @@ -237,7 +237,7 @@ class _LocationMapState extends ConsumerState } Future _updateFrequency(int frequency) async { - if (!guardDeviceCommand(context, ref)) return; + if (!await guardDeviceCommand(context, ref)) return; final success = await ref .read(locationViewModelProvider.notifier) .updateLocationFrequency(frequency: frequency); @@ -264,8 +264,9 @@ class _LocationMapState extends ConsumerState } } - void _confirmPlacement() { - if (!guardDeviceCommand(context, ref)) return; + Future _confirmPlacement() async { + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; final center = _mapController.camera.center; final mapState = ref.read(locationMapViewModelProvider); @@ -289,8 +290,9 @@ class _LocationMapState extends ConsumerState } } - void _confirmRadius() { - if (!guardDeviceCommand(context, ref)) return; + Future _confirmRadius() async { + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; final mapState = ref.read(locationMapViewModelProvider); final point = mapState.previewPoint!; final radius = mapState.previewRadius; @@ -414,8 +416,9 @@ class _LocationMapState extends ConsumerState } } - void _onEditFrequentPlace(FrequentPlaceEntity fp) { - if (!guardDeviceCommand(context, ref)) return; + Future _onEditFrequentPlace(FrequentPlaceEntity fp) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!mounted) return; _vm.clearSelectedFrequentPlace(); showNameInputSheet( context, @@ -673,12 +676,13 @@ class _LocationMapState extends ConsumerState onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence), onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace), onShareTap: _shareLocation, - onRefreshTap: () { - if (!guardDeviceCommand(context, ref)) return; + onRefreshTap: () async { + if (!await guardDeviceCommand(context, ref)) return; unawaited( ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(), ); widget.onRefreshPosition(); + if (!mounted) return; showTopSnackbar( context, message: context.translate(I18n.locationMapRefreshRequested), @@ -686,9 +690,9 @@ class _LocationMapState extends ConsumerState ); }, onCenterTap: _centerOnDevice, - onToggleFollow: () { + onToggleFollow: () async { final willActivate = !mapState.isFollowing; - if (willActivate && !guardDeviceCommand(context, ref)) return; + if (willActivate && !await guardDeviceCommand(context, ref)) return; _vm.toggleFollowing(); unawaited( ref @@ -698,6 +702,7 @@ class _LocationMapState extends ConsumerState if (willActivate && widget.selectedPosition != null) { _centerOnDevice(); } + if (!mounted) return; showTopSnackbar( context, message: context.translate( @@ -721,12 +726,12 @@ class _LocationMapState extends ConsumerState child: GeofenceInfoCard( geofence: mapState.selectedGeofence!, onClose: _vm.clearSelectedGeofence, - onEdit: () { - if (!guardDeviceCommand(context, ref)) return; + onEdit: () async { + if (!await guardDeviceCommand(context, ref)) return; _vm.startEditingGeofence(mapState.selectedGeofence!); }, - onDelete: () { - if (!guardDeviceCommand(context, ref)) return; + onDelete: () async { + if (!await guardDeviceCommand(context, ref)) return; final id = mapState.selectedGeofence!.id; _vm.clearSelectedGeofence(); ref @@ -742,8 +747,8 @@ class _LocationMapState extends ConsumerState frequentPlace: mapState.selectedFrequentPlace!, onClose: _vm.clearSelectedFrequentPlace, onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!), - onDelete: () { - if (!guardDeviceCommand(context, ref)) return; + onDelete: () async { + if (!await guardDeviceCommand(context, ref)) return; final id = mapState.selectedFrequentPlace!.id; _vm.clearSelectedFrequentPlace(); ref diff --git a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart index ad308039..657a75b3 100644 --- a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart @@ -92,8 +92,9 @@ class AlarmScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; _openForm(context, vm); }, icon: Icon( @@ -126,8 +127,8 @@ class AlarmScreen extends ConsumerWidget { child: isSaving ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.save(); }, text: context.translate(I18n.alarmSave), diff --git a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart index c0152096..325dad5b 100644 --- a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart @@ -81,8 +81,8 @@ class AlertsScreen extends ConsumerWidget { child: PrimaryButton( onPressed: state.isSaving ? null - : () { - if (!guardDeviceCommand(context, ref)) return; + : () async { + if (!await guardDeviceCommand(context, ref)) return; vm.save(); }, text: state.isSaving ? '...' : context.translate(I18n.save), diff --git a/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart b/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart index 74350cb7..9b345ad3 100644 --- a/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart @@ -99,8 +99,8 @@ class BatteryScreen extends ConsumerWidget { activeTrackColor: primaryColor, onChanged: state.isSaving ? null - : (value) { - if (!guardDeviceCommand(context, ref)) return; + : (value) async { + if (!await guardDeviceCommand(context, ref)) return; vm.toggleNightMode(value); }, ), diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart index df7d33f3..0e554836 100644 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart @@ -84,8 +84,9 @@ class BlockPhoneScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showAddContactSheet(context); }, icon: Icon( @@ -216,13 +217,14 @@ class _ContactList extends ConsumerWidget { ); } - void _confirmDelete( + Future _confirmDelete( BuildContext context, WidgetRef ref, int index, String name, - ) { - if (!guardDeviceCommand(context, ref)) return; + ) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; final theme = ref.read(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); diff --git a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart index 85205550..08c42299 100644 --- a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart @@ -78,8 +78,8 @@ class DisableFunctionsScreen extends ConsumerWidget { child: PrimaryButton( onPressed: state.isSaving ? null - : () { - if (!guardDeviceCommand(context, ref)) return; + : () async { + if (!await guardDeviceCommand(context, ref)) return; vm.save(); }, text: state.isSaving ? '...' : context.translate(I18n.save), diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart index 0c2bcb44..ecb481d3 100644 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart @@ -110,8 +110,8 @@ class _SaveSection extends ConsumerWidget { child: isLoading ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.submit(); }, text: context.translate(I18n.save), diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart index cb403b26..9df5e9fe 100644 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart @@ -51,8 +51,9 @@ class _OptionsSection extends ConsumerWidget { title: context.translate(I18n.remoteTurnOff), subtitle: context.translate(I18n.remoteTurnOffMessage), icon: Icons.settings_power_outlined, - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showDialog( context: context, builder: (context) => Dialog( @@ -95,8 +96,9 @@ class _OptionsSection extends ConsumerWidget { title: context.translate(I18n.remoteRestart), subtitle: context.translate(I18n.remoteRestartMessage), icon: Icons.refresh_outlined, - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showDialog( context: context, builder: (context) => Dialog( @@ -117,8 +119,9 @@ class _OptionsSection extends ConsumerWidget { title: context.translate(I18n.remoteFactoryReset), subtitle: context.translate(I18n.remoteFactoryResetMessage), icon: Icons.restart_alt_outlined, - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; showDialog( context: context, builder: (context) => Dialog( diff --git a/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart b/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart index 7f642990..ac6c394a 100644 --- a/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart @@ -177,8 +177,8 @@ class _SaveSection extends ConsumerWidget { return Padding( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: PrimaryButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.submit(); }, text: context.translate(I18n.save), diff --git a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart index d55b7296..c751efb5 100644 --- a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart @@ -96,8 +96,8 @@ class TimezoneScreen extends ConsumerWidget { child: PrimaryButton( onPressed: state.isSaving ? null - : () { - if (!guardDeviceCommand(context, ref)) return; + : () async { + if (!await guardDeviceCommand(context, ref)) return; vm.save(); }, text: state.isSaving ? '...' : context.translate(I18n.save), 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 4c9821a2..15072683 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 @@ -94,8 +94,8 @@ class WifiSettingsScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () { - if (!guardDeviceCommand(context, ref)) return; + onPressed: () async { + if (!await guardDeviceCommand(context, ref)) return; vm.scanNetworks(); }, icon: Icon( @@ -275,8 +275,13 @@ class _Body extends ConsumerWidget { ); } - void _confirmDelete(BuildContext context, WidgetRef ref, dynamic network) { - if (!guardDeviceCommand(context, ref)) return; + Future _confirmDelete( + BuildContext context, + WidgetRef ref, + dynamic network, + ) async { + if (!await guardDeviceCommand(context, ref)) return; + if (!context.mounted) return; final theme = ref.read(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); diff --git a/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart b/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart index 6abc21c1..9c73fb4f 100644 --- a/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart +++ b/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart @@ -1,12 +1,36 @@ +import 'dart:async'; + import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:sf_shared/sf_shared.dart'; -bool guardDeviceCommand(BuildContext context, WidgetRef ref) { - final device = ref.read(selectedDeviceProvider).value; +const _deviceConnectionTtl = Duration(seconds: 30); +const _snackbarDelay = Duration(milliseconds: 400); + +Future guardDeviceCommand(BuildContext context, WidgetRef ref) async { + final notifier = ref.read(legacyDevicesProvider.notifier); + + if (notifier.isStale(_deviceConnectionTtl)) { + final fetchFuture = notifier.refreshIfStale(_deviceConnectionTtl); + final delayedSnackbar = Timer(_snackbarDelay, () { + if (!context.mounted) return; + showTopSnackbar( + context, + message: context.translate(I18n.checkingDeviceConnection), + type: MessageType.info, + ); + }); + await fetchFuture; + delayedSnackbar.cancel(); + } + + if (!context.mounted) return false; + + final device = await ref.read(selectedDeviceProvider.future); if (device == null || device.isDisconnected) { + if (!context.mounted) return false; showTopSnackbar( context, message: context.translate(I18n.errorDeviceDisconnected), diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 5428e63f..f7b1a5ab 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -691,6 +691,7 @@ "errorContactsMax": "Maximale Kontaktanzahl für dieses Gerät erreicht", "errorPositions": "Positionen konnten nicht geladen werden", "errorDeviceDisconnected": "Die Uhr ist getrennt und kann keine Befehle empfangen", + "checkingDeviceConnection": "Verbindung des Geräts wird überprüft...", "errorSosContactsMax": "Maximale SOS-Kontaktanzahl für dieses Gerät erreicht", "customBackground": "Benutzerdefiniertes Hintergrundbild", "backgroundImageDescription": "Legen Sie ein Foto als benutzerdefinierten Bildschirmschoner für das Gerät fest", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 0f48273d..993b8aec 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -859,6 +859,7 @@ "errorContactsMax": "Maximum contacts reached for this device", "errorPositions": "Could not load positions", "errorDeviceDisconnected": "The watch is disconnected and cannot receive commands", + "checkingDeviceConnection": "Checking if the device is connected...", "errorSosContactsMax": "Maximum SOS contacts reached for this device", "customBackground": "Custom background image", "backgroundImageDescription": "Set a photo as a custom screensaver for the device", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index e370255a..0a817071 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -860,6 +860,7 @@ "errorContactsMax": "Se ha alcanzado el máximo de contactos para este dispositivo", "errorPositions": "No se pudieron cargar las posiciones", "errorDeviceDisconnected": "El reloj está desconectado y no puede recibir comandos", + "checkingDeviceConnection": "Revisando si el dispositivo está conectado...", "errorSosContactsMax": "Se ha alcanzado el máximo de contactos SOS para este dispositivo", "customBackground": "Fondo de pantalla personalizado", "backgroundImageDescription": "Configura una foto como protector de pantalla exclusivo para el dispositivo", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index cfc2b5b7..fac75939 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -691,6 +691,7 @@ "errorContactsMax": "Nombre maximum de contacts atteint pour cet appareil", "errorPositions": "Impossible de charger les positions", "errorDeviceDisconnected": "La montre est déconnectée et ne peut pas recevoir de commandes", + "checkingDeviceConnection": "Vérification de la connexion de l'appareil...", "errorSosContactsMax": "Nombre maximum de contacts SOS atteint pour cet appareil", "customBackground": "Image de fond personnalisée", "backgroundImageDescription": "Définissez une photo comme écran de veille personnalisé pour l'appareil", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 40e488d0..8a54890b 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -691,6 +691,7 @@ "errorContactsMax": "Numero massimo di contatti raggiunto per questo dispositivo", "errorPositions": "Impossibile caricare le posizioni", "errorDeviceDisconnected": "L'orologio è disconnesso e non può ricevere comandi", + "checkingDeviceConnection": "Verifica della connessione del dispositivo...", "errorSosContactsMax": "Numero massimo di contatti SOS raggiunto per questo dispositivo", "customBackground": "Immagine di sfondo personalizzata", "backgroundImageDescription": "Imposta una foto come screensaver personalizzato per il dispositivo", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 34db8571..23785c64 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -691,6 +691,7 @@ "errorContactsMax": "Número máximo de contactos atingido para este dispositivo", "errorPositions": "Não foi possível carregar as posições", "errorDeviceDisconnected": "O relógio está desconectado e não pode receber comandos", + "checkingDeviceConnection": "A verificar se o dispositivo está conectado...", "errorSosContactsMax": "Número máximo de contactos SOS atingido para este dispositivo", "customBackground": "Imagem de fundo personalizada", "backgroundImageDescription": "Defina uma foto como protetor de ecrã personalizado para o dispositivo", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 66e93284..48dcae1e 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -395,6 +395,7 @@ class I18n { static const String errorContactsMax = 'errorContactsMax'; static const String errorPositions = 'errorPositions'; static const String errorDeviceDisconnected = 'errorDeviceDisconnected'; + static const String checkingDeviceConnection = 'checkingDeviceConnection'; static const String errorSosContactsMax = 'errorSosContactsMax'; static const String errorContactsMin = 'errorContactsMin'; static const String errorDisableFunctions = 'errorDisableFunctions'; diff --git a/packages/sf_shared/lib/src/providers/legacy_devices_provider.dart b/packages/sf_shared/lib/src/providers/legacy_devices_provider.dart index 454c903d..1584f438 100644 --- a/packages/sf_shared/lib/src/providers/legacy_devices_provider.dart +++ b/packages/sf_shared/lib/src/providers/legacy_devices_provider.dart @@ -2,9 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sf_shared/sf_shared.dart'; class LegacyDevices extends AsyncNotifier> { + DateTime? _lastFetchedAt; + @override - Future> build() { - return ref.read(sharedDevicesRepositoryProvider).getDevices(); + Future> build() async { + final devices = await ref.read(sharedDevicesRepositoryProvider).getDevices(); + _lastFetchedAt = DateTime.now(); + return devices; } void removeDevice(String deviceId) { @@ -30,6 +34,21 @@ class LegacyDevices extends AsyncNotifier> { state = await AsyncValue.guard( () => ref.read(sharedDevicesRepositoryProvider).getDevices(), ); + _lastFetchedAt = DateTime.now(); + } + + bool isStale(Duration ttl) { + final last = _lastFetchedAt; + return last == null || DateTime.now().difference(last) >= ttl; + } + + Future refreshIfStale(Duration ttl) async { + if (!isStale(ttl)) return; + final result = await AsyncValue.guard( + () => ref.read(sharedDevicesRepositoryProvider).getDevices(), + ); + state = result; + _lastFetchedAt = DateTime.now(); } }