diff --git a/analysis_options.yaml b/analysis_options.yaml index c4e3354..42dfc58 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,14 +10,14 @@ include: package:flutter_lints/flutter.yaml analyzer: + plugins: + - custom_lint exclude: - "**/*.g.dart" - "**/*.freezed.dart" - "build/**" - "test_programs/**" - "test/**/fixtures/**" - - "lib/core/services/mock_data_service.dart" - - "lib/features/devices/data/datasources/device_mock_data_source.dart" - "scripts/**" errors: invalid_annotation_target: ignore diff --git a/docs/plans/council-code-review-fixes.md b/docs/plans/council-code-review-fixes.md new file mode 100644 index 0000000..729987d --- /dev/null +++ b/docs/plans/council-code-review-fixes.md @@ -0,0 +1,144 @@ +# Council Implementation Plan: FDK Code Review Fixes + +**Branch**: `zew/AIConcerns` +**Workflow**: Each fix implemented by Claude, reviewed by Codex & Gemini +**Total Issues**: 18 (4 Critical, 6 High, 8 Medium) + +--- + +## Implementation Order + +Issues ordered by: **Severity → Dependency → Complexity** + +| Phase | Issue | Severity | Est. Changes | +|-------|-------|----------|--------------| +| 1 | Debug Route in Production | CRITICAL | 1 file, ~10 lines | +| 2 | State Leak on Sign-out | CRITICAL | 1 file, ~5 lines | +| 3 | Unbounded Family Providers | CRITICAL | 1 file, ~10 lines | +| 4 | Circular Provider Dependency | CRITICAL | 2-3 files, ~50 lines | +| 5 | Mock Data in Production | HIGH | 1 file, ~30 lines | +| 6 | Silent Error Handling | HIGH | 2 files, ~20 lines | +| 7 | SharedPreferences Silent Failure | HIGH | 1 file, ~30 lines | +| 8 | Async onDispose Not Awaited | HIGH | 1 file, ~5 lines | +| 9 | riverpod_lint Disabled | HIGH | 2 files, testing | +| 10 | Premature Stream Subscription | HIGH | 1 file, ~10 lines | +| 11-18 | Medium Priority Issues | MEDIUM | Backlog | + +--- + +## Phase 1: Debug Route in Production ✅ + +**File**: `lib/core/navigation/app_router.dart:57-61` + +**Fix**: Gate debug route behind `EnvironmentConfig.isProduction` check + +```dart +// Debug screen - only available in non-production builds +if (!EnvironmentConfig.isProduction) + GoRoute( + path: '/debug', + builder: (context, state) => const DebugScreen(), + ), +``` + +--- + +## Phase 2: State Leak on Sign-out ✅ + +**File**: `lib/features/auth/presentation/providers/auth_notifier.dart` + +**Fix**: Add rooms provider invalidation to sign-out cleanup + +```dart +ref.invalidate(rooms_providers.roomsNotifierProvider); +``` + +--- + +## Phase 3: Unbounded Family Providers ✅ + +**File**: `lib/features/devices/presentation/providers/devices_provider.dart` + +**Fix**: Remove `keepAlive: true` from `DeviceNotifier` and `DeviceSearchNotifier` + +Changed `@Riverpod(keepAlive: true)` to `@riverpod` for auto-dispose behavior. + +--- + +## Phase 4: Circular Provider Dependency ✅ + +**Files**: +- `lib/core/providers/websocket_providers.dart` +- `lib/core/providers/websocket_sync_providers.dart` (new) + +**Fix**: Extract sync-related providers to new file to break circular dependency between `repository_providers.dart` and `websocket_providers.dart`. + +--- + +## Phase 5: Mock Data in Production ✅ + +**File**: `lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart` + +**Fix**: Replace mock 80% online calculation with real device status from `devicesNotifierProvider`. + +--- + +## Phase 6: Silent Error Handling ✅ + +**Files**: +- `lib/features/initialization/presentation/providers/initialization_provider.dart` +- `lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart` + +**Fix**: Add proper error logging instead of swallowing errors silently. + +--- + +## Phase 7: SharedPreferences Silent Failure ✅ + +**File**: `lib/main.dart` + +**Fix**: Show error UI with retry button instead of silent exit when SharedPreferences fails to initialize. + +--- + +## Phase 8: Async onDispose Not Awaited ✅ + +**File**: `lib/core/providers/websocket_sync_providers.dart` + +**Fix**: Use `unawaited()` with error logging to document that Riverpod doesn't await async onDispose callbacks. + +--- + +## Phase 9: Enable riverpod_lint ✅ + +**Files**: +- `pubspec.yaml` +- `analysis_options.yaml` + +**Fix**: +- Enabled `custom_lint: ^0.6.3` and `riverpod_lint: ^2.3.10` in pubspec.yaml +- Added `analyzer.plugins: - custom_lint` to analysis_options.yaml +- Sorted dev_dependencies alphabetically to satisfy lint rules + +--- + +## Phase 10: Premature Stream Subscription ✅ + +**File**: `lib/features/devices/presentation/providers/devices_provider.dart` + +**Fix**: Move `_attachDevicesStream()` call to after authentication check. + +--- + +## Verification Checklist + +After all phases: +- [x] `flutter analyze` passes (only info-level issues, no errors) +- [x] `dart run build_runner build` succeeds +- [x] App builds successfully (`flutter build apk --debug`) +- [ ] App runs in dev mode - all features work +- [ ] App runs in prod mode - /debug inaccessible +- [ ] Sign out clears all user data +- [ ] Memory stable after browsing many devices/searches +- [ ] Dashboard shows real online counts +- [ ] Errors logged, not swallowed diff --git a/lib/core/navigation/app_router.dart b/lib/core/navigation/app_router.dart index 1651977..a9efc18 100644 --- a/lib/core/navigation/app_router.dart +++ b/lib/core/navigation/app_router.dart @@ -54,11 +54,12 @@ class AppRouter { }, ), - // Debug screen (outside of shell for direct access) - GoRoute( - path: '/debug', - builder: (context, state) => const DebugScreen(), - ), + // Debug screen - only available in non-production builds + if (!EnvironmentConfig.isProduction) + GoRoute( + path: '/debug', + builder: (context, state) => const DebugScreen(), + ), // Main app shell with bottom navigation ShellRoute( diff --git a/lib/core/providers/core_providers.dart b/lib/core/providers/core_providers.dart index 1e96577..a8f0ca0 100644 --- a/lib/core/providers/core_providers.dart +++ b/lib/core/providers/core_providers.dart @@ -5,6 +5,7 @@ import 'package:rgnets_fdk/core/services/device_update_event_bus.dart'; import 'package:rgnets_fdk/core/services/mock_data_service.dart'; import 'package:rgnets_fdk/core/services/notification_generation_service.dart'; import 'package:rgnets_fdk/core/services/performance_monitor_service.dart'; +import 'package:rgnets_fdk/core/services/secure_storage_service.dart'; import 'package:rgnets_fdk/core/services/storage_service.dart'; import 'package:rgnets_fdk/core/utils/image_url_normalizer.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -35,10 +36,16 @@ final sharedPreferencesProvider = Provider((ref) { ); }); +/// Secure storage service provider for sensitive credentials +final secureStorageServiceProvider = Provider((ref) { + return SecureStorageService(); +}); + /// Storage service provider final storageServiceProvider = Provider((ref) { final prefs = ref.watch(sharedPreferencesProvider); - return StorageService(prefs); + final secureStorage = ref.watch(secureStorageServiceProvider); + return StorageService(prefs, secureStorage); }); /// Performance monitor service provider (singleton) @@ -73,21 +80,24 @@ final deviceUpdateEventBusProvider = Provider((ref) { /// Provider for the current API key used for authenticated HTTP requests. /// This is the token stored during authentication, used to authenticate /// image requests to the RXG backend's ActiveStorage. -final apiKeyProvider = Provider((ref) { +/// Returns a Future since credentials are now stored in secure storage. +final apiKeyProvider = FutureProvider((ref) async { final storage = ref.watch(storageServiceProvider); - return storage.token; + return storage.getToken(); }); /// Provider for authenticating image URLs with the current API key. /// Returns a function that takes an image URL and returns an authenticated URL. final authenticatedImageUrlProvider = Provider((ref) { - final apiKey = ref.watch(apiKeyProvider); + final apiKeyAsync = ref.watch(apiKeyProvider); + final apiKey = apiKeyAsync.valueOrNull; return (String? imageUrl) => authenticateImageUrl(imageUrl, apiKey); }); /// Provider for authenticating a list of image URLs with the current API key. final authenticatedImageUrlsProvider = Provider Function(List)>((ref) { - final apiKey = ref.watch(apiKeyProvider); + final apiKeyAsync = ref.watch(apiKeyProvider); + final apiKey = apiKeyAsync.valueOrNull; return (List imageUrls) => authenticateImageUrls(imageUrls, apiKey); }); diff --git a/lib/core/providers/repository_providers.dart b/lib/core/providers/repository_providers.dart index b566219..22b5549 100644 --- a/lib/core/providers/repository_providers.dart +++ b/lib/core/providers/repository_providers.dart @@ -4,6 +4,7 @@ import 'package:rgnets_fdk/core/config/environment.dart'; import 'package:rgnets_fdk/core/config/logger_config.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/background_refresh_service.dart'; import 'package:rgnets_fdk/features/auth/data/datasources/auth_local_data_source.dart'; import 'package:rgnets_fdk/features/auth/data/repositories/auth_repository.dart' @@ -80,8 +81,8 @@ final deviceMockDataSourceProvider = Provider((ref) { /// Device data source provider (interface) final deviceDataSourceProvider = Provider((ref) { - if (EnvironmentConfig.isDevelopment) { - // Use mock data source in development + if (EnvironmentConfig.useSyntheticData) { + // Use mock data source only when synthetic data flag is enabled return ref.watch(deviceMockDataSourceProvider); } @@ -94,6 +95,7 @@ final deviceDataSourceProvider = Provider((ref) { webSocketCacheIntegration: webSocketCacheIntegration, imageBaseUrl: storageService.siteUrl, logger: logger, + storageService: storageService, ); }); diff --git a/lib/core/providers/websocket_providers.dart b/lib/core/providers/websocket_providers.dart index b3b39dc..cb866e9 100644 --- a/lib/core/providers/websocket_providers.dart +++ b/lib/core/providers/websocket_providers.dart @@ -1,22 +1,12 @@ -import 'dart:async'; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rgnets_fdk/core/config/environment.dart'; import 'package:rgnets_fdk/core/config/logger_config.dart'; -import 'package:rgnets_fdk/core/providers/core_providers.dart'; -import 'package:rgnets_fdk/core/providers/repository_providers.dart'; -import 'package:rgnets_fdk/core/services/cache_manager.dart'; -import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; -import 'package:rgnets_fdk/core/services/websocket_data_sync_service.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; -import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; -import 'package:rgnets_fdk/features/home/presentation/providers/dashboard_provider.dart'; -import 'package:rgnets_fdk/features/home/presentation/providers/home_screen_provider.dart'; -import 'package:rgnets_fdk/features/issues/presentation/providers/health_notices_provider.dart'; -import 'package:rgnets_fdk/features/notifications/presentation/providers/device_notification_provider.dart' - hide notificationGenerationServiceProvider; -import 'package:rgnets_fdk/features/notifications/presentation/providers/notifications_domain_provider.dart'; -import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart'; + +// NOTE: Sync providers are in websocket_sync_providers.dart +// Import that file directly if you need webSocketDataSyncServiceProvider, +// webSocketCacheIntegrationProvider, etc. We don't re-export here to avoid +// circular dependencies with repository_providers.dart. /// Provides the base WebSocket configuration derived from the environment. final webSocketConfigProvider = Provider((ref) { @@ -74,110 +64,3 @@ final webSocketAuthEventsProvider = StreamProvider((ref) { (message) => message.type.startsWith('auth.'), ); }); - -/// Handles WebSocket-driven cache hydration for devices/rooms. -final webSocketDataSyncServiceProvider = Provider(( - ref, -) { - final socketService = ref.watch(webSocketServiceProvider); - final roomLocalDataSource = ref.watch(roomLocalDataSourceProvider); - final cacheManager = ref.watch(cacheManagerProvider); - final storageService = ref.watch(storageServiceProvider); - final logger = LoggerConfig.getLogger(); - - // Typed device local data sources (new architecture) - final apLocalDataSource = ref.watch(apLocalDataSourceProvider); - final ontLocalDataSource = ref.watch(ontLocalDataSourceProvider); - final switchLocalDataSource = ref.watch(switchLocalDataSourceProvider); - final wlanLocalDataSource = ref.watch(wlanLocalDataSourceProvider); - - final service = WebSocketDataSyncService( - socketService: socketService, - apLocalDataSource: apLocalDataSource, - ontLocalDataSource: ontLocalDataSource, - switchLocalDataSource: switchLocalDataSource, - wlanLocalDataSource: wlanLocalDataSource, - storageService: storageService, - roomLocalDataSource: roomLocalDataSource, - cacheManager: cacheManager, - logger: logger, - ); - - ref.onDispose(() async { - await service.dispose(); - }); - return service; -}); - -/// Keeps WebSocket sync events wired to provider invalidation. -final webSocketDataSyncListenerProvider = Provider((ref) { - final service = ref.watch(webSocketDataSyncServiceProvider); - final logger = LoggerConfig.getLogger(); - final subscription = service.events.listen((event) { - switch (event.type) { - case WebSocketDataSyncEventType.devicesCached: - logger.i('WebSocketDataSync: devices cached -> refreshing providers'); - ref.invalidate(devicesNotifierProvider); - ref.invalidate(deviceNotificationsNotifierProvider); - ref.invalidate(notificationsDomainNotifierProvider); - ref.invalidate(homeScreenStatisticsProvider); - ref.invalidate(dashboardStatsProvider); - // Refresh health notices providers (they aggregate from device data) - ref.invalidate(aggregateHealthCountsNotifierProvider); - ref.invalidate(healthNoticesNotifierProvider); - break; - case WebSocketDataSyncEventType.roomsCached: - logger.i('WebSocketDataSync: rooms cached -> refreshing providers'); - ref.invalidate(roomsNotifierProvider); - break; - } - }); - - ref.onDispose(subscription.cancel); - return; -}); - -/// Provides the WebSocket cache integration for device data. -/// This keeps device caches in sync with WebSocket messages. -final webSocketCacheIntegrationProvider = Provider(( - ref, -) { - final webSocketService = ref.watch(webSocketServiceProvider); - final logger = LoggerConfig.getLogger(); - final storageService = ref.watch(storageServiceProvider); - final deviceUpdateEventBus = ref.watch(deviceUpdateEventBusProvider); - - final integration = WebSocketCacheIntegration( - webSocketService: webSocketService, - imageBaseUrl: storageService.siteUrl, - logger: logger, - deviceUpdateEventBus: deviceUpdateEventBus, - ); - - // Initialize the integration - integration.initialize(); - - ref.onDispose(integration.dispose); - - return integration; -}); - -/// Emits the last device-cache update time for WebSocket snapshots/updates. -final webSocketDeviceLastUpdateProvider = StreamProvider((ref) { - final integration = ref.watch(webSocketCacheIntegrationProvider); - final controller = StreamController(); - - void listener() { - controller.add(integration.lastDeviceUpdate.value); - } - - integration.lastDeviceUpdate.addListener(listener); - controller.add(integration.lastDeviceUpdate.value); - - ref.onDispose(() { - integration.lastDeviceUpdate.removeListener(listener); - controller.close(); - }); - - return controller.stream; -}); diff --git a/lib/core/providers/websocket_sync_providers.dart b/lib/core/providers/websocket_sync_providers.dart new file mode 100644 index 0000000..cdb1550 --- /dev/null +++ b/lib/core/providers/websocket_sync_providers.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rgnets_fdk/core/config/logger_config.dart'; +import 'package:rgnets_fdk/core/providers/core_providers.dart'; +import 'package:rgnets_fdk/core/providers/repository_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/services/cache_manager.dart'; +import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; +import 'package:rgnets_fdk/core/services/websocket_data_sync_service.dart'; +import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; +import 'package:rgnets_fdk/features/home/presentation/providers/dashboard_provider.dart'; +import 'package:rgnets_fdk/features/home/presentation/providers/home_screen_provider.dart'; +import 'package:rgnets_fdk/features/issues/presentation/providers/health_notices_provider.dart'; +import 'package:rgnets_fdk/features/notifications/presentation/providers/device_notification_provider.dart' + hide notificationGenerationServiceProvider; +import 'package:rgnets_fdk/features/notifications/presentation/providers/notifications_domain_provider.dart'; +import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart'; + +/// Handles WebSocket-driven cache hydration for devices/rooms. +/// +/// This provider is in a separate file to break the circular dependency +/// between repository_providers.dart and websocket_providers.dart. +final webSocketDataSyncServiceProvider = Provider(( + ref, +) { + final socketService = ref.watch(webSocketServiceProvider); + final roomLocalDataSource = ref.watch(roomLocalDataSourceProvider); + final cacheManager = ref.watch(cacheManagerProvider); + final storageService = ref.watch(storageServiceProvider); + final logger = LoggerConfig.getLogger(); + + // Typed device local data sources (new architecture) + final apLocalDataSource = ref.watch(apLocalDataSourceProvider); + final ontLocalDataSource = ref.watch(ontLocalDataSourceProvider); + final switchLocalDataSource = ref.watch(switchLocalDataSourceProvider); + final wlanLocalDataSource = ref.watch(wlanLocalDataSourceProvider); + + final service = WebSocketDataSyncService( + socketService: socketService, + apLocalDataSource: apLocalDataSource, + ontLocalDataSource: ontLocalDataSource, + switchLocalDataSource: switchLocalDataSource, + wlanLocalDataSource: wlanLocalDataSource, + storageService: storageService, + roomLocalDataSource: roomLocalDataSource, + cacheManager: cacheManager, + logger: logger, + ); + + ref.onDispose(() { + // Note: Riverpod doesn't await async onDispose callbacks. + // Using unawaited to make this explicit, with error logging. + unawaited( + service.dispose().catchError((Object e) { + LoggerConfig.getLogger().w('WebSocketDataSyncService dispose error: $e'); + }), + ); + }); + return service; +}); + +/// Keeps WebSocket sync events wired to provider invalidation. +final webSocketDataSyncListenerProvider = Provider((ref) { + final service = ref.watch(webSocketDataSyncServiceProvider); + final logger = LoggerConfig.getLogger(); + final subscription = service.events.listen((event) { + switch (event.type) { + case WebSocketDataSyncEventType.devicesCached: + logger.i('WebSocketDataSync: devices cached -> refreshing providers'); + ref.invalidate(devicesNotifierProvider); + ref.invalidate(deviceNotificationsNotifierProvider); + ref.invalidate(notificationsDomainNotifierProvider); + ref.invalidate(homeScreenStatisticsProvider); + ref.invalidate(dashboardStatsProvider); + // Refresh health notices providers (they aggregate from device data) + ref.invalidate(aggregateHealthCountsNotifierProvider); + ref.invalidate(healthNoticesNotifierProvider); + break; + case WebSocketDataSyncEventType.roomsCached: + logger.i('WebSocketDataSync: rooms cached -> refreshing providers'); + ref.invalidate(roomsNotifierProvider); + break; + } + }); + + ref.onDispose(subscription.cancel); + return; +}); + +/// Provides the WebSocket cache integration for device data. +/// This keeps device caches in sync with WebSocket messages. +final webSocketCacheIntegrationProvider = Provider(( + ref, +) { + final webSocketService = ref.watch(webSocketServiceProvider); + final logger = LoggerConfig.getLogger(); + final storageService = ref.watch(storageServiceProvider); + final deviceUpdateEventBus = ref.watch(deviceUpdateEventBusProvider); + + final integration = WebSocketCacheIntegration( + webSocketService: webSocketService, + imageBaseUrl: storageService.siteUrl, + logger: logger, + deviceUpdateEventBus: deviceUpdateEventBus, + ); + + // Initialize the integration + integration.initialize(); + + ref.onDispose(integration.dispose); + + return integration; +}); + +/// Emits the last device-cache update time for WebSocket snapshots/updates. +final webSocketDeviceLastUpdateProvider = StreamProvider((ref) { + final integration = ref.watch(webSocketCacheIntegrationProvider); + final controller = StreamController(); + + void listener() { + controller.add(integration.lastDeviceUpdate.value); + } + + integration.lastDeviceUpdate.addListener(listener); + controller.add(integration.lastDeviceUpdate.value); + + ref.onDispose(() { + integration.lastDeviceUpdate.removeListener(listener); + controller.close(); + }); + + return controller.stream; +}); diff --git a/lib/core/services/cache_manager.dart b/lib/core/services/cache_manager.dart index 1bf7f86..efa573f 100644 --- a/lib/core/services/cache_manager.dart +++ b/lib/core/services/cache_manager.dart @@ -20,8 +20,44 @@ class CacheEntry { /// Cache manager implementing stale-while-revalidate pattern class CacheManager { + CacheManager({this.maxEntries = 100}) { + // Run cleanup periodically to prevent unbounded growth + _cleanupTimer = Timer.periodic(const Duration(minutes: 5), (_) => _cleanup()); + } + + final int maxEntries; final Map> _cache = {}; final Map> _pendingRequests = {}; + Timer? _cleanupTimer; + + /// Remove expired entries and enforce max size + void _cleanup() { + // Remove expired entries + _cache.removeWhere((_, entry) => entry.isExpired); + + // If still over limit, remove oldest entries + if (_cache.length > maxEntries) { + final sortedKeys = _cache.entries.toList() + ..sort((a, b) => a.value.timestamp.compareTo(b.value.timestamp)); + + final keysToRemove = sortedKeys + .take(_cache.length - maxEntries) + .map((e) => e.key) + .toList(); + + for (final key in keysToRemove) { + _cache.remove(key); + } + } + } + + /// Dispose resources + void dispose() { + _cleanupTimer?.cancel(); + _cleanupTimer = null; + _cache.clear(); + _pendingRequests.clear(); + } /// Get cached data with stale-while-revalidate Future get({ @@ -152,5 +188,7 @@ class CacheManager { /// Provider for cache manager final cacheManagerProvider = Provider((ref) { - return CacheManager(); + final manager = CacheManager(); + ref.onDispose(manager.dispose); + return manager; }); \ No newline at end of file diff --git a/lib/core/services/deeplink_service.dart b/lib/core/services/deeplink_service.dart index 1b3b522..418edb4 100644 --- a/lib/core/services/deeplink_service.dart +++ b/lib/core/services/deeplink_service.dart @@ -5,6 +5,31 @@ import 'package:app_links/app_links.dart'; import 'package:flutter/widgets.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; +/// Sensitive query parameter names that should be redacted from logs. +const _sensitiveParams = {'api_key', 'apiKey', 'key', 'token', 'password', 'secret'}; + +/// Redacts sensitive parameters from a URI for safe logging. +String _redactUri(Uri? uri) { + if (uri == null) return 'null'; + + // If no query parameters, return as-is + if (uri.queryParameters.isEmpty) { + return uri.toString(); + } + + // Redact sensitive parameters + final redactedParams = {}; + for (final entry in uri.queryParameters.entries) { + if (_sensitiveParams.contains(entry.key)) { + redactedParams[entry.key] = '[REDACTED]'; + } else { + redactedParams[entry.key] = entry.value; + } + } + + return uri.replace(queryParameters: redactedParams).toString(); +} + /// Credentials extracted from a deeplink URL. class DeeplinkCredentials { const DeeplinkCredentials({ @@ -109,7 +134,7 @@ class DeeplinkService { // Check for initial deeplink (cold start) try { final initialUri = await _appLinks.getInitialLink(); - LoggerService.info('Initial URI: $initialUri', tag: _tag); + LoggerService.info('Initial URI: ${_redactUri(initialUri)}', tag: _tag); if (initialUri != null) { _hasPendingInitialLink = true; try { @@ -127,7 +152,7 @@ class DeeplinkService { LoggerService.info('Setting up deeplink stream listener', tag: _tag); _linkSubscription = _appLinks.uriLinkStream.listen( (uri) async { - LoggerService.info('Received URI from stream: $uri', tag: _tag); + LoggerService.info('Received URI from stream: ${_redactUri(uri)}', tag: _tag); await _handleUri(uri); }, onError: (Object err) => @@ -137,7 +162,7 @@ class DeeplinkService { /// Handle an incoming URI. Future _handleUri(Uri uri) async { - LoggerService.info('Received deeplink URI: $uri', tag: _tag); + LoggerService.info('Received deeplink URI: ${_redactUri(uri)}', tag: _tag); // Block if already processing if (_isProcessing) { diff --git a/lib/core/services/secure_storage_service.dart b/lib/core/services/secure_storage_service.dart new file mode 100644 index 0000000..09deb19 --- /dev/null +++ b/lib/core/services/secure_storage_service.dart @@ -0,0 +1,113 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// Service for securely storing sensitive credentials. +/// +/// Uses platform-native secure storage: +/// - iOS: Keychain +/// - Android: EncryptedSharedPreferences (backed by Keystore) +/// - macOS: Keychain +/// - Linux: libsecret +/// - Windows: Credential Manager +class SecureStorageService { + SecureStorageService() : _storage = _createStorage(); + + final FlutterSecureStorage _storage; + + // Keys for secure storage + static const String _keyToken = 'secure_token'; + static const String _keySessionToken = 'secure_session_token'; + static const String _keyAuthSignature = 'secure_auth_signature'; + static const String _keyApiKey = 'secure_api_key'; + + static FlutterSecureStorage _createStorage() { + // iOS: afterFirstUnlock allows access after device unlock, enabling auto-login + // Android: encryptedSharedPreferences provides better compatibility + const iOSOptions = IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + ); + const androidOptions = AndroidOptions( + encryptedSharedPreferences: true, + ); + return const FlutterSecureStorage( + iOptions: iOSOptions, + aOptions: androidOptions, + ); + } + + // Token (API token/auth token) + Future saveToken(String token) async { + await _storage.write(key: _keyToken, value: token); + } + + Future getToken() async { + return _storage.read(key: _keyToken); + } + + Future deleteToken() async { + await _storage.delete(key: _keyToken); + } + + // Session token + Future saveSessionToken(String token) async { + await _storage.write(key: _keySessionToken, value: token); + } + + Future getSessionToken() async { + return _storage.read(key: _keySessionToken); + } + + Future deleteSessionToken() async { + await _storage.delete(key: _keySessionToken); + } + + // Auth signature + Future saveAuthSignature(String signature) async { + await _storage.write(key: _keyAuthSignature, value: signature); + } + + Future getAuthSignature() async { + return _storage.read(key: _keyAuthSignature); + } + + Future deleteAuthSignature() async { + await _storage.delete(key: _keyAuthSignature); + } + + // API Key (if stored separately) + Future saveApiKey(String apiKey) async { + await _storage.write(key: _keyApiKey, value: apiKey); + } + + Future getApiKey() async { + return _storage.read(key: _keyApiKey); + } + + Future deleteApiKey() async { + await _storage.delete(key: _keyApiKey); + } + + /// Clears all secure credentials. + Future clearAll() async { + await Future.wait([ + deleteToken(), + deleteSessionToken(), + deleteAuthSignature(), + deleteApiKey(), + ]); + } + + /// Checks if secure storage is available and working. + /// Returns true if a test write/read/delete cycle succeeds. + Future isAvailable() async { + const testKey = 'secure_storage_test'; + const testValue = 'test_value'; + try { + await _storage.write(key: testKey, value: testValue); + final readBack = await _storage.read(key: testKey); + await _storage.delete(key: testKey); + return readBack == testValue; + } on Object { + return false; + } + } +} diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart index e7d81c8..087ed6e 100644 --- a/lib/core/services/storage_service.dart +++ b/lib/core/services/storage_service.dart @@ -1,22 +1,26 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; +import 'package:rgnets_fdk/core/services/secure_storage_service.dart'; import 'package:rgnets_fdk/features/auth/data/models/auth_attempt.dart'; import 'package:shared_preferences/shared_preferences.dart'; -/// Service for managing local storage +/// Service for managing local storage. +/// +/// Sensitive credentials are stored in secure storage (Keychain/Keystore). +/// Non-sensitive settings are stored in SharedPreferences. class StorageService { - StorageService(this._prefs); + StorageService(this._prefs, this._secureStorage); final SharedPreferences _prefs; + final SecureStorageService _secureStorage; - // Keys (WS-only semantics) + // Keys for SharedPreferences (non-sensitive data only) static const String _keySiteUrl = 'site_url'; - static const String _keyToken = 'token'; static const String _keyUsername = 'username'; static const String _keySiteName = 'site_name'; static const String _keyAuthIssuedAt = 'auth_issued_at'; - static const String _keyAuthSignature = 'auth_signature'; static const String _keyIsAuthenticated = 'is_authenticated'; - static const String _keySessionToken = 'ws_session_token'; static const String _keySessionExpiresAt = 'ws_session_expires_at'; static const String _keyAuthAttempts = 'auth_attempts'; static const String _keyThemeMode = 'theme_mode'; @@ -26,6 +30,14 @@ class StorageService { static const String _keyPhaseFilter = 'device_phase_filter'; static const String _keyStatusFilter = 'device_status_filter'; static const String _keyRoomFilter = 'device_room_filter'; + static const String _keySecureMigrationComplete = 'secure_migration_v1'; + + // Legacy keys (for migration cleanup) + static const String _legacyKeyToken = 'token'; + static const String _legacyKeySessionToken = 'ws_session_token'; + static const String _legacyKeyAuthSignature = 'auth_signature'; + static const String _legacyKeyApiUrl = 'api_url'; + static const String _legacyKeyApiToken = 'api_token'; /// Public key for phase filter (for tests and direct access) static const String keyPhaseFilter = _keyPhaseFilter; @@ -36,11 +48,167 @@ class StorageService { /// Public key for room filter (for tests and direct access) static const String keyRoomFilter = _keyRoomFilter; - // Legacy keys for migration - static const String _legacyKeyApiUrl = 'api_url'; - static const String _legacyKeyApiToken = 'api_token'; + /// Migrates credentials from plaintext SharedPreferences to secure storage. + /// + /// Uses atomic migration: read → write secure → verify → delete plaintext. + /// Only runs once; sets a flag on completion. + Future migrateToSecureStorageIfNeeded() async { + // Check if migration already completed + if (_prefs.getBool(_keySecureMigrationComplete) ?? false) { + return; + } + + LoggerService.info( + 'Starting secure storage migration', + tag: 'StorageService', + ); + + try { + // Check if secure storage is available + final isAvailable = await _secureStorage.isAvailable(); + if (!isAvailable) { + LoggerService.warning( + 'Secure storage unavailable - migration skipped', + tag: 'StorageService', + ); + // Don't set flag - retry on next launch + return; + } + + // Migrate token + final plaintextToken = _prefs.getString(_legacyKeyToken); + if (plaintextToken != null && plaintextToken.isNotEmpty) { + await _secureStorage.saveToken(plaintextToken); + // Verify write succeeded + final verifyToken = await _secureStorage.getToken(); + if (verifyToken == plaintextToken) { + await _prefs.remove(_legacyKeyToken); + } else { + throw Exception('Token migration verification failed'); + } + } - // Auth + // Migrate session token + final plaintextSessionToken = _prefs.getString(_legacyKeySessionToken); + if (plaintextSessionToken != null && plaintextSessionToken.isNotEmpty) { + await _secureStorage.saveSessionToken(plaintextSessionToken); + // Verify write succeeded + final verifySession = await _secureStorage.getSessionToken(); + if (verifySession == plaintextSessionToken) { + await _prefs.remove(_legacyKeySessionToken); + } else { + throw Exception('Session token migration verification failed'); + } + } + + // Migrate auth signature + final plaintextSignature = _prefs.getString(_legacyKeyAuthSignature); + if (plaintextSignature != null && plaintextSignature.isNotEmpty) { + await _secureStorage.saveAuthSignature(plaintextSignature); + // Verify write succeeded + final verifySignature = await _secureStorage.getAuthSignature(); + if (verifySignature == plaintextSignature) { + await _prefs.remove(_legacyKeyAuthSignature); + } else { + throw Exception('Auth signature migration verification failed'); + } + } + + // Migrate legacy api_url/api_token if present + await _migrateLegacyApiKeys(); + + // Also migrate ATT FE Tool legacy keys + await _migrateLegacyAttKeys(); + + // Mark migration complete + await _prefs.setBool(_keySecureMigrationComplete, true); + + LoggerService.info( + 'Secure storage migration completed successfully', + tag: 'StorageService', + ); + } on Exception catch (e) { + LoggerService.error( + 'Secure storage migration failed: $e', + tag: 'StorageService', + error: e, + ); + // Don't set flag - allow retry. User may need to re-authenticate. + } + } + + /// Migrates legacy api_url/api_token keys to secure storage. + Future _migrateLegacyApiKeys() async { + final hasLegacy = + _prefs.containsKey(_legacyKeyApiUrl) || + _prefs.containsKey(_legacyKeyApiToken); + + if (!hasLegacy) return; + + final apiUrl = _prefs.getString(_legacyKeyApiUrl); + final apiToken = _prefs.getString(_legacyKeyApiToken); + + // If we have a token, migrate it to secure storage + if (apiToken != null && apiToken.isNotEmpty) { + await _secureStorage.saveToken(apiToken); + // Verify write succeeded + final verifyToken = await _secureStorage.getToken(); + if (verifyToken != apiToken) { + throw Exception('Legacy api_token migration verification failed'); + } + } + + // If we have a URL and no siteUrl already set, migrate it + if (apiUrl != null && + apiUrl.isNotEmpty && + !_prefs.containsKey(_keySiteUrl)) { + await _prefs.setString(_keySiteUrl, apiUrl); + } + + // Remove legacy keys only after successful migration + await _prefs.remove(_legacyKeyApiUrl); + await _prefs.remove(_legacyKeyApiToken); + } + + Future _migrateLegacyAttKeys() async { + const legacyFqdnKey = 'att_fe_tool.fqdn'; + const legacyLoginKey = 'att_fe_tool.login'; + const legacyApiKey = 'att_fe_tool.api_key'; + + final hasAttLegacy = + _prefs.containsKey(legacyFqdnKey) && + _prefs.containsKey(legacyLoginKey) && + _prefs.containsKey(legacyApiKey); + + if (hasAttLegacy) { + final fqdn = _prefs.getString(legacyFqdnKey) ?? ''; + final login = _prefs.getString(legacyLoginKey) ?? ''; + final apiKey = _prefs.getString(legacyApiKey) ?? ''; + + if (fqdn.isNotEmpty && login.isNotEmpty && apiKey.isNotEmpty) { + // Save non-sensitive data to SharedPreferences + await _prefs.setString(_keySiteUrl, 'https://$fqdn'); + await _prefs.setString(_keyUsername, login); + // Save sensitive token to secure storage + await _secureStorage.saveToken(apiKey); + + // Verify write succeeded before deleting legacy keys + final verifyToken = await _secureStorage.getToken(); + if (verifyToken != apiKey) { + throw Exception('ATT legacy token migration verification failed'); + } + } + + // Remove legacy keys only after successful migration + await Future.wait([ + _prefs.remove(legacyFqdnKey), + _prefs.remove(legacyLoginKey), + _prefs.remove(legacyApiKey), + ]); + } + } + + // Auth - credentials now use secure storage Future saveCredentials({ required String siteUrl, required String token, @@ -50,8 +218,8 @@ class StorageService { String? signature, bool markAuthenticated = false, }) async { + // Non-sensitive data to SharedPreferences await _prefs.setString(_keySiteUrl, siteUrl); - await _prefs.setString(_keyToken, token); await _prefs.setString(_keyUsername, username); if (siteName != null) { await _prefs.setString(_keySiteName, siteName); @@ -59,17 +227,22 @@ class StorageService { if (issuedAtIso != null) { await _prefs.setString(_keyAuthIssuedAt, issuedAtIso); } + await _prefs.setBool(_keyIsAuthenticated, markAuthenticated); + + // Sensitive data to secure storage + await _secureStorage.saveToken(token); if (signature != null) { - await _prefs.setString(_keyAuthSignature, signature); + await _secureStorage.saveAuthSignature(signature); } - await _prefs.setBool(_keyIsAuthenticated, markAuthenticated); } Future saveSession({ required String token, required DateTime expiresAt, }) async { - await _prefs.setString(_keySessionToken, token); + // Session token to secure storage + await _secureStorage.saveSessionToken(token); + // Expiry time to SharedPreferences (not sensitive) await _prefs.setString( _keySessionExpiresAt, expiresAt.toUtc().toIso8601String(), @@ -77,15 +250,21 @@ class StorageService { } Future clearCredentials() async { + // Clear non-sensitive data from SharedPreferences await _prefs.remove(_keySiteUrl); - await _prefs.remove(_keyToken); await _prefs.remove(_keyUsername); await _prefs.remove(_keySiteName); await _prefs.remove(_keyAuthIssuedAt); - await _prefs.remove(_keyAuthSignature); - // Also clear any legacy keys + // Also clear any legacy keys that might exist + await _prefs.remove(_legacyKeyToken); await _prefs.remove(_legacyKeyApiUrl); await _prefs.remove(_legacyKeyApiToken); + await _prefs.remove(_legacyKeySessionToken); + await _prefs.remove(_legacyKeyAuthSignature); + + // Clear sensitive data from secure storage + await _secureStorage.clearAll(); + await clearSession(); await _prefs.setBool(_keyIsAuthenticated, false); } @@ -95,64 +274,10 @@ class StorageService { } Future clearSession() async { - await _prefs.remove(_keySessionToken); + await _secureStorage.deleteSessionToken(); await _prefs.remove(_keySessionExpiresAt); - } - - /// Migrates legacy credential storage formats to WS-only semantics. - /// Handles both att_fe_tool.* keys and api_url/api_token keys. - Future migrateLegacyCredentialsIfNeeded() async { - // Migration 1: ATT FE Tool legacy keys - const legacyFqdnKey = 'att_fe_tool.fqdn'; - const legacyLoginKey = 'att_fe_tool.login'; - const legacyApiKey = 'att_fe_tool.api_key'; - - final hasAttLegacy = - _prefs.containsKey(legacyFqdnKey) && - _prefs.containsKey(legacyLoginKey) && - _prefs.containsKey(legacyApiKey); - - if (hasAttLegacy) { - final fqdn = _prefs.getString(legacyFqdnKey) ?? ''; - final login = _prefs.getString(legacyLoginKey) ?? ''; - final apiKey = _prefs.getString(legacyApiKey) ?? ''; - - if (fqdn.isNotEmpty && login.isNotEmpty && apiKey.isNotEmpty) { - await saveCredentials( - siteUrl: 'https://$fqdn', - token: apiKey, - username: login, - markAuthenticated: false, - ); - } - - await Future.wait([ - _prefs.remove(legacyFqdnKey), - _prefs.remove(legacyLoginKey), - _prefs.remove(legacyApiKey), - ]); - } - - // Migration 2: api_url/api_token keys to site_url/token - final hasApiLegacy = - _prefs.containsKey(_legacyKeyApiUrl) || - _prefs.containsKey(_legacyKeyApiToken); - - if (hasApiLegacy && !_prefs.containsKey(_keySiteUrl)) { - final apiUrl = _prefs.getString(_legacyKeyApiUrl); - final apiToken = _prefs.getString(_legacyKeyApiToken); - - if (apiUrl != null && apiUrl.isNotEmpty) { - await _prefs.setString(_keySiteUrl, apiUrl); - } - if (apiToken != null && apiToken.isNotEmpty) { - await _prefs.setString(_keyToken, apiToken); - } - - // Remove legacy keys after migration - await _prefs.remove(_legacyKeyApiUrl); - await _prefs.remove(_legacyKeyApiToken); - } + // Also clear legacy key if present + await _prefs.remove(_legacyKeySessionToken); } Future logAuthAttempt(AuthAttempt attempt) async { @@ -188,16 +313,14 @@ class StorageService { } } + // Getters - synchronous for non-sensitive, async for sensitive String? get siteUrl => _prefs.getString(_keySiteUrl); - String? get token => _prefs.getString(_keyToken); String? get username => _prefs.getString(_keyUsername); String? get siteName => _prefs.getString(_keySiteName); String? get authIssuedAtIso => _prefs.getString(_keyAuthIssuedAt); DateTime? get authIssuedAt => authIssuedAtIso != null ? DateTime.tryParse(authIssuedAtIso!) : null; - String? get authSignature => _prefs.getString(_keyAuthSignature); bool get isAuthenticated => _prefs.getBool(_keyIsAuthenticated) ?? false; - String? get sessionToken => _prefs.getString(_keySessionToken); DateTime? get sessionExpiresAt { final iso = _prefs.getString(_keySessionExpiresAt); if (iso == null) { @@ -206,7 +329,51 @@ class StorageService { return DateTime.tryParse(iso); } - // Settings + // Async getters for sensitive data from secure storage + Future getToken() => _secureStorage.getToken(); + Future getSessionToken() => _secureStorage.getSessionToken(); + Future getAuthSignature() => _secureStorage.getAuthSignature(); + + // Synchronous token getter for backward compatibility during transition + // DEPRECATED: Use getToken() instead + @Deprecated('Use getToken() instead for secure access') + String? get token { + // This is only for backward compatibility during migration + // Returns null - callers must use async getToken() + if (kDebugMode) { + LoggerService.warning( + 'Deprecated synchronous token access - use getToken() instead', + tag: 'StorageService', + ); + } + return null; + } + + // DEPRECATED: Use getSessionToken() instead + @Deprecated('Use getSessionToken() instead for secure access') + String? get sessionToken { + if (kDebugMode) { + LoggerService.warning( + 'Deprecated synchronous sessionToken access - use getSessionToken()', + tag: 'StorageService', + ); + } + return null; + } + + // DEPRECATED: Use getAuthSignature() instead + @Deprecated('Use getAuthSignature() instead for secure access') + String? get authSignature { + if (kDebugMode) { + LoggerService.warning( + 'Deprecated synchronous authSignature access - use getAuthSignature()', + tag: 'StorageService', + ); + } + return null; + } + + // Settings (non-sensitive - remain in SharedPreferences) String get themeMode => _prefs.getString(_keyThemeMode) ?? 'dark'; Future setThemeMode(String mode) => _prefs.setString(_keyThemeMode, mode); @@ -242,7 +409,7 @@ class StorageService { _prefs.setString(_keyRoomFilter, room); Future clearRoomFilter() => _prefs.remove(_keyRoomFilter); - // Generic methods + // Generic methods (for non-sensitive data only) Future setBool(String key, {required bool value}) => _prefs.setBool(key, value); bool? getBool(String key) => _prefs.getBool(key); diff --git a/lib/core/services/websocket_cache_integration.dart b/lib/core/services/websocket_cache_integration.dart index d4848f1..0b00668 100644 --- a/lib/core/services/websocket_cache_integration.dart +++ b/lib/core/services/websocket_cache_integration.dart @@ -135,21 +135,41 @@ class WebSocketCacheIntegration { _deviceDataCallbacks.add(callback); } + /// Remove a callback for device data updates. + void removeDeviceDataCallback(DeviceDataCallback callback) { + _deviceDataCallbacks.remove(callback); + } + /// Register a callback for room data updates. void onRoomData(void Function(List>) callback) { _roomDataCallbacks.add(callback); } + /// Remove a callback for room data updates. + void removeRoomDataCallback(void Function(List>) callback) { + _roomDataCallbacks.remove(callback); + } + /// Register a callback for speed test config data updates. void onSpeedTestConfigData(void Function(List) callback) { _speedTestConfigCallbacks.add(callback); } + /// Remove a callback for speed test config data updates. + void removeSpeedTestConfigCallback(void Function(List) callback) { + _speedTestConfigCallbacks.remove(callback); + } + /// Register a callback for speed test result data updates. void onSpeedTestResultData(void Function(List) callback) { _speedTestResultCallbacks.add(callback); } + /// Remove a callback for speed test result data updates. + void removeSpeedTestResultCallback(void Function(List) callback) { + _speedTestResultCallbacks.remove(callback); + } + /// Get cached rooms. List> getCachedRooms() { return List.unmodifiable(_roomCache); @@ -773,10 +793,17 @@ class WebSocketCacheIntegration { } _logger.i('WebSocketCacheIntegration: Sending channel subscribe request'); _channelSubscribeSent = true; - _webSocketService.send({ - 'command': 'subscribe', - 'identifier': _channelIdentifier, - }); + try { + _webSocketService.send({ + 'command': 'subscribe', + 'identifier': _channelIdentifier, + }); + } on StateError catch (e) { + // Connection closed between isConnected check and send - this is expected + _logger.w('WebSocketCacheIntegration: Send failed (connection closed): $e'); + _channelSubscribeSent = false; + return false; + } return false; } @@ -786,12 +813,18 @@ class WebSocketCacheIntegration { _logger.w('WebSocketCacheIntegration: Skipping send, WebSocket not connected'); return false; } - _webSocketService.send({ - 'command': 'message', - 'identifier': _channelIdentifier, - 'data': jsonEncode(data), - }); - return true; + try { + _webSocketService.send({ + 'command': 'message', + 'identifier': _channelIdentifier, + 'data': jsonEncode(data), + }); + return true; + } on StateError catch (e) { + // Connection closed between isConnected check and send - this is expected + _logger.w('WebSocketCacheIntegration: Send failed (connection closed): $e'); + return false; + } } /// Request full snapshots for all resource types. diff --git a/lib/core/services/websocket_data_sync_service.dart b/lib/core/services/websocket_data_sync_service.dart index 4b45572..84649bd 100644 --- a/lib/core/services/websocket_data_sync_service.dart +++ b/lib/core/services/websocket_data_sync_service.dart @@ -187,11 +187,15 @@ class WebSocketDataSyncService { 'action': 'subscribe_to_resource', 'resource_type': resourceType, }); - _socketService.send({ - 'command': 'message', - 'identifier': _channelIdentifier(), - 'data': payload, - }); + try { + _socketService.send({ + 'command': 'message', + 'identifier': _channelIdentifier(), + 'data': payload, + }); + } on StateError catch (e) { + _logger.w('WebSocketDataSync: Subscribe send failed (connection closed): $e'); + } } void _sendSnapshotRequest(String resourceType) { @@ -204,11 +208,15 @@ class WebSocketDataSyncService { 'page_size': 10000, 'request_id': requestId, }); - _socketService.send({ - 'command': 'message', - 'identifier': _channelIdentifier(), - 'data': payload, - }); + try { + _socketService.send({ + 'command': 'message', + 'identifier': _channelIdentifier(), + 'data': payload, + }); + } on StateError catch (e) { + _logger.w('WebSocketDataSync: Snapshot request failed (connection closed): $e'); + } } String _channelIdentifier() => jsonEncode(const {'channel': _channelName}); @@ -629,7 +637,10 @@ class WebSocketDataSyncService { } else { return 'offline'; } - } on Exception catch (_) {} + } on Exception catch (e) { + // Date parsing failed - fallback to unknown status + _logger.d('WebSocketDataSync: Failed to parse device status date: $e'); + } } return 'unknown'; diff --git a/lib/features/auth/presentation/providers/auth_notifier.dart b/lib/features/auth/presentation/providers/auth_notifier.dart index 5a3e6b2..6c0b201 100644 --- a/lib/features/auth/presentation/providers/auth_notifier.dart +++ b/lib/features/auth/presentation/providers/auth_notifier.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; @@ -11,6 +10,7 @@ import 'package:rgnets_fdk/core/models/api_key_revocation_event.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/cache_manager.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; import 'package:rgnets_fdk/features/auth/data/models/auth_attempt.dart'; @@ -27,6 +27,8 @@ import 'package:rgnets_fdk/features/notifications/presentation/providers/device_ as device_notifications; import 'package:rgnets_fdk/features/notifications/presentation/providers/notifications_domain_provider.dart' as notifications_domain; +import 'package:rgnets_fdk/features/rooms/presentation/providers/rooms_riverpod_provider.dart' + as rooms_providers; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -51,7 +53,7 @@ class Auth extends _$Auth { Future build() async { _logger = ref.watch(loggerProvider); final storage = ref.read(storageServiceProvider); - await storage.migrateLegacyCredentialsIfNeeded(); + await storage.migrateToSecureStorageIfNeeded(); _logger ..i('🔐 AUTH_NOTIFIER: build() called - initializing auth state') @@ -116,7 +118,7 @@ class Auth extends _$Auth { final storage = ref.read(storageServiceProvider); // Check if we have the required credentials - final token = storage.token; + final token = await storage.getToken(); final siteUrl = storage.siteUrl; final username = storage.username; @@ -145,13 +147,15 @@ class Auth extends _$Auth { } try { + // Get auth signature from secure storage + final signature = await storage.getAuthSignature(); final resolvedUser = await _performWebSocketHandshake( fqdn: fqdn, login: username, token: token, siteName: storage.siteName ?? fqdn, issuedAt: storage.authIssuedAt, - signature: storage.authSignature, + signature: signature, ).timeout( const Duration(seconds: 15), onTimeout: () { @@ -178,12 +182,11 @@ class Auth extends _$Auth { String? signature, }) async { _authGeneration += 1; - final keyLength = math.min(4, token.length); _logger ..i('🔑 AUTH_NOTIFIER: authenticate() called') ..d('AUTH_NOTIFIER: FQDN: $fqdn') ..d('AUTH_NOTIFIER: Login: $login') - ..d('AUTH_NOTIFIER: API Key: ${token.substring(0, keyLength)}...') + ..d('AUTH_NOTIFIER: Token: [REDACTED, length=${token.length}]') ..d('AUTH_NOTIFIER: Auth generation: $_authGeneration') ..d('AUTH_NOTIFIER: Current state before auth: ${state.value}'); @@ -288,7 +291,7 @@ class Auth extends _$Auth { try { final storage = ref.read(storageServiceProvider); - final expectedToken = storage.token; + final expectedToken = await storage.getToken(); final expectedSiteUrl = storage.siteUrl; // Set state to unauthenticated FIRST to unblock UI @@ -364,7 +367,7 @@ class Auth extends _$Auth { }) async { try { final storage = ref.read(storageServiceProvider); - final currentToken = storage.token; + final currentToken = await storage.getToken(); final currentSiteUrl = storage.siteUrl; if (!_credentialsMatch( expectedToken: expectedToken, @@ -733,9 +736,10 @@ final authSignOutCleanupProvider = Provider((ref) { unawaited(ref.read(ontLocalDataSourceProvider).clearCache()); unawaited(ref.read(switchLocalDataSourceProvider).clearCache()); unawaited(ref.read(wlanLocalDataSourceProvider).clearCache()); - logger.d('AUTH_CLEANUP: Typed device caches cleared'); + unawaited(ref.read(roomLocalDataSourceProvider).clearCache()); + logger.d('AUTH_CLEANUP: Typed device and room caches cleared'); } on Exception catch (e) { - logger.w('AUTH_CLEANUP: Failed to clear typed device caches: $e'); + logger.w('AUTH_CLEANUP: Failed to clear typed caches: $e'); } // Clear WebSocket cache integration @@ -752,6 +756,7 @@ final authSignOutCleanupProvider = Provider((ref) { ref.invalidate(device_notifications.deviceNotificationsNotifierProvider); ref.invalidate(notifications_domain.notificationsDomainNotifierProvider); ref.invalidate(dashboard_providers.dashboardStatsProvider); + ref.invalidate(rooms_providers.roomsNotifierProvider); logger.d('AUTH_CLEANUP: Data providers invalidated'); } on Exception catch (e) { logger.w('AUTH_CLEANUP: Failed to invalidate data providers: $e'); diff --git a/lib/features/auth/presentation/providers/auth_notifier.g.dart b/lib/features/auth/presentation/providers/auth_notifier.g.dart index 4fc0613..810c197 100644 --- a/lib/features/auth/presentation/providers/auth_notifier.g.dart +++ b/lib/features/auth/presentation/providers/auth_notifier.g.dart @@ -49,7 +49,7 @@ final authStatusProvider = AutoDisposeProvider.internal( ); typedef AuthStatusRef = AutoDisposeProviderRef; -String _$authHash() => r'380d99e26cf5fe65c134f82d0feb376a7ee22ffd'; +String _$authHash() => r'61aed628ca39a3ff07d46be6f3999490f47f6724'; /// See also [Auth]. @ProviderFor(Auth) diff --git a/lib/features/auth/presentation/screens/auth_screen.dart b/lib/features/auth/presentation/screens/auth_screen.dart index 55e574c..2ac7269 100644 --- a/lib/features/auth/presentation/screens/auth_screen.dart +++ b/lib/features/auth/presentation/screens/auth_screen.dart @@ -205,7 +205,7 @@ class _AuthScreenState extends ConsumerState { ? siteNameRaw.trim() : null; - logger.d('AUTH_SCREEN: Parsed - fqdn=$fqdn, login=$login, token=${token != null ? "${token.substring(0, 4)}..." : "null"}'); + logger.d('AUTH_SCREEN: Parsed - fqdn=$fqdn, login=$login, token=${token != null ? "[REDACTED, length=${token.length}]" : "null"}'); if (fqdn == null || login == null || token == null) { logger.e('AUTH_SCREEN: Invalid credential payload - missing required fields'); diff --git a/lib/features/devices/data/datasources/device_websocket_data_source.dart b/lib/features/devices/data/datasources/device_websocket_data_source.dart index 5fad517..719145b 100644 --- a/lib/features/devices/data/datasources/device_websocket_data_source.dart +++ b/lib/features/devices/data/datasources/device_websocket_data_source.dart @@ -1,11 +1,11 @@ -import 'dart:convert'; - import 'package:logger/logger.dart'; +import 'package:rgnets_fdk/core/services/storage_service.dart'; import 'package:rgnets_fdk/core/services/websocket_cache_integration.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; import 'package:rgnets_fdk/core/utils/image_url_normalizer.dart'; import 'package:rgnets_fdk/features/devices/data/datasources/device_data_source.dart'; import 'package:rgnets_fdk/features/devices/data/models/device_model_sealed.dart'; +import 'package:rgnets_fdk/features/devices/data/services/rest_image_upload_service.dart'; /// WebSocket-based data source for fetching devices. /// Replaces DeviceRemoteDataSource with a WebSocket-backed implementation. @@ -14,12 +14,15 @@ class DeviceWebSocketDataSource implements DeviceDataSource { required WebSocketCacheIntegration webSocketCacheIntegration, String? imageBaseUrl, Logger? logger, + StorageService? storageService, }) : _cacheIntegration = webSocketCacheIntegration, _imageBaseUrl = imageBaseUrl, + _storageService = storageService, _logger = logger ?? Logger(); final WebSocketCacheIntegration _cacheIntegration; final String? _imageBaseUrl; + final StorageService? _storageService; final Logger _logger; static const Map _deviceEndpointByPrefix = { @@ -338,16 +341,23 @@ class DeviceWebSocketDataSource implements DeviceDataSource { throw Exception('Unknown device type for ID: $deviceId'); } - // Get current device to access imageSignedIds - final device = await getDevice(deviceId); - final currentSignedIds = device.map( - ap: (d) => d.imageSignedIds ?? [], - ont: (d) => d.imageSignedIds ?? [], - switchDevice: (d) => d.imageSignedIds ?? [], - wlan: (d) => d.imageSignedIds ?? [], + // Use REST API (HTTP PUT) for image deletion. + // The WebSocket update_resource action does not process the images + // parameter - only the REST API endpoint handles image updates. + // This matches how ATT-FE-Tool deletes images. + final restService = await _getRestImageUploadService(); + + // Fetch current signed IDs from the server via REST + final currentSignedIds = await restService.fetchCurrentSignedIds( + resourceType: resourceType, + deviceId: rawId, + ); + + _logger.i( + 'DeviceWebSocketDataSource: Current image count: ${currentSignedIds.length}', ); - // Filter out the signed ID to delete (like ATT-FE-Tool does) + // Filter out the signed ID to delete final updatedSignedIds = currentSignedIds .where((id) => id != signedIdToDelete) .toList(); @@ -359,52 +369,54 @@ class DeviceWebSocketDataSource implements DeviceDataSource { throw Exception('SignedId not found in device images.'); } - try { - // Send update request without waiting for response (fire-and-forget) - // The backend broadcasts resource_updated without request_id, causing timeouts - // Instead, we send and verify by fetching the updated device - final requestId = 'req-$resourceType-${DateTime.now().millisecondsSinceEpoch}'; - final data = { - 'action': 'update_resource', - 'resource_type': resourceType, - 'request_id': requestId, - 'id': rawId, - 'params': {'images': updatedSignedIds}, - }; - - _webSocketService.send({ - 'command': 'message', - 'identifier': '{"channel":"RxgChannel"}', - 'data': jsonEncode(data), - }); - - _logger.i('DeviceWebSocketDataSource: Image delete request sent'); - - // Wait briefly for the backend to process the update - await Future.delayed(const Duration(milliseconds: 1500)); - - // Verify the change by fetching the updated device - final updatedDevice = await getDevice(deviceId, forceRefresh: true); - - final newImageCount = updatedDevice.map( - ap: (d) => d.images?.length ?? 0, - ont: (d) => d.images?.length ?? 0, - switchDevice: (d) => d.images?.length ?? 0, - wlan: (d) => d.images?.length ?? 0, + // Send the remaining signed IDs via REST PUT + final result = await restService.uploadImages( + deviceId: rawId, + resourceType: resourceType, + images: updatedSignedIds, + ); + + if (!result.success) { + _logger.e( + 'DeviceWebSocketDataSource: REST delete failed: ${result.errorMessage}', ); + throw Exception('Failed to delete device image: ${result.errorMessage}'); + } - if (newImageCount < currentSignedIds.length) { - _logger.i('DeviceWebSocketDataSource: Image deletion verified successfully'); - return updatedDevice; - } else { - _logger.w('DeviceWebSocketDataSource: Image count unchanged after delete'); - // Still return the device - the cache may update later via WebSocket broadcast - return updatedDevice; - } - } on Exception catch (e) { - _logger.e('DeviceWebSocketDataSource: Failed to delete device image: $e'); - throw Exception('Failed to delete device image: $e'); + _logger.i('DeviceWebSocketDataSource: Image deleted via REST API successfully'); + + // Fetch the updated device data via REST to get the new state + final deviceData = await restService.fetchDeviceData( + resourceType: resourceType, + deviceId: rawId, + ); + + if (deviceData != null) { + return _mapToDeviceModel(resourceType, deviceData); + } + + // Fallback: return cached device if REST fetch fails + return getDevice(deviceId); + } + + /// Creates a [RestImageUploadService] using stored credentials. + Future _getRestImageUploadService() async { + final storage = _storageService; + if (storage == null) { + throw Exception('StorageService not available for REST API'); + } + + final siteUrl = storage.siteUrl; + if (siteUrl == null || siteUrl.isEmpty) { + throw Exception('Site URL not available for REST API'); } + + final apiKey = await storage.getToken(); + if (apiKey == null || apiKey.isEmpty) { + throw Exception('API key not available for REST API'); + } + + return RestImageUploadService(siteUrl: siteUrl, apiKey: apiKey); } String? _getResourceTypeFromId(String deviceId) { diff --git a/lib/features/devices/data/repositories/device_repository.dart b/lib/features/devices/data/repositories/device_repository.dart index d612e95..edc5d72 100644 --- a/lib/features/devices/data/repositories/device_repository.dart +++ b/lib/features/devices/data/repositories/device_repository.dart @@ -595,8 +595,6 @@ class DeviceRepositoryImpl implements DeviceRepository { String deviceId, String signedIdToDelete, ) async { - print('=== REPOSITORY DELETE IMAGE ==='); - print('DeviceRepositoryImpl: deleteDeviceImage called for $deviceId, signedId: $signedIdToDelete'); _logger.i('DeviceRepositoryImpl: deleteDeviceImage called for $deviceId, signedId: $signedIdToDelete'); try { if (!_isAuthenticated()) { diff --git a/lib/features/devices/data/services/rest_image_upload_service.dart b/lib/features/devices/data/services/rest_image_upload_service.dart index ab1faf8..da557d9 100644 --- a/lib/features/devices/data/services/rest_image_upload_service.dart +++ b/lib/features/devices/data/services/rest_image_upload_service.dart @@ -190,10 +190,7 @@ class RestImageUploadService { 'REST Upload: PUT $resourceType/$deviceId with ${images.length} images', tag: 'RestImageUploadService', ); - LoggerService.debug( - 'REST Upload URL: $url', - tag: 'RestImageUploadService', - ); + // Note: Not logging URL as it contains api_key try { final dio = _dio; diff --git a/lib/features/devices/presentation/providers/devices_provider.dart b/lib/features/devices/presentation/providers/devices_provider.dart index 88b7eb1..f01ff4a 100644 --- a/lib/features/devices/presentation/providers/devices_provider.dart +++ b/lib/features/devices/presentation/providers/devices_provider.dart @@ -39,7 +39,6 @@ class DevicesNotifier extends _$DevicesNotifier { Future> build() async { final authStatus = ref.watch(authStatusProvider); final isAuthenticated = authStatus?.isAuthenticated ?? false; - _attachDevicesStream(); if (isVerboseLoggingEnabled) { _logger.i('DevicesProvider: Loading devices'); @@ -53,6 +52,9 @@ class DevicesNotifier extends _$DevicesNotifier { return []; } + // Only attach stream subscription after confirming authentication + _attachDevicesStream(); + try { // Try to get from cache first with stale-while-revalidate // Use list fields for optimized loading @@ -273,7 +275,7 @@ class DevicesNotifier extends _$DevicesNotifier { } } -@Riverpod(keepAlive: true) +@riverpod class DeviceNotifier extends _$DeviceNotifier { Logger get _logger => ref.read(loggerProvider); StreamSubscription? _cacheInvalidationSub; @@ -469,8 +471,8 @@ class DeviceNotifier extends _$DeviceNotifier { } } -// Search provider -@Riverpod(keepAlive: true) +// Search provider - auto-dispose to prevent memory leaks with many searches +@riverpod class DeviceSearchNotifier extends _$DeviceSearchNotifier { SearchDevices get _searchDevices => SearchDevices(ref.read(deviceRepositoryProvider)); diff --git a/lib/features/devices/presentation/providers/devices_provider.g.dart b/lib/features/devices/presentation/providers/devices_provider.g.dart index c0a2eb5..35f30a3 100644 --- a/lib/features/devices/presentation/providers/devices_provider.g.dart +++ b/lib/features/devices/presentation/providers/devices_provider.g.dart @@ -6,7 +6,7 @@ part of 'devices_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$devicesNotifierHash() => r'b04f3d264b41ab17dfd141c9b1b21d9e9f294a64'; +String _$devicesNotifierHash() => r'766bf18de3493cae7dc0846a98e088328641f561'; /// See also [DevicesNotifier]. @ProviderFor(DevicesNotifier) @@ -22,7 +22,7 @@ final devicesNotifierProvider = ); typedef _$DevicesNotifier = AsyncNotifier>; -String _$deviceNotifierHash() => r'cf3ebe7c65173144858d1bc65dd476c168fe7a26'; +String _$deviceNotifierHash() => r'b9ac7feda3a075a2e1c6fafa009e9f81a662bd0b'; /// Copied from Dart SDK class _SystemHash { @@ -45,7 +45,8 @@ class _SystemHash { } } -abstract class _$DeviceNotifier extends BuildlessAsyncNotifier { +abstract class _$DeviceNotifier + extends BuildlessAutoDisposeAsyncNotifier { late final String deviceId; FutureOr build( @@ -97,7 +98,7 @@ class DeviceNotifierFamily extends Family> { /// See also [DeviceNotifier]. class DeviceNotifierProvider - extends AsyncNotifierProviderImpl { + extends AutoDisposeAsyncNotifierProviderImpl { /// See also [DeviceNotifier]. DeviceNotifierProvider( String deviceId, @@ -153,7 +154,8 @@ class DeviceNotifierProvider } @override - AsyncNotifierProviderElement createElement() { + AutoDisposeAsyncNotifierProviderElement + createElement() { return _DeviceNotifierProviderElement(this); } @@ -171,13 +173,13 @@ class DeviceNotifierProvider } } -mixin DeviceNotifierRef on AsyncNotifierProviderRef { +mixin DeviceNotifierRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `deviceId` of this provider. String get deviceId; } class _DeviceNotifierProviderElement - extends AsyncNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement with DeviceNotifierRef { _DeviceNotifierProviderElement(super.provider); @@ -186,10 +188,10 @@ class _DeviceNotifierProviderElement } String _$deviceSearchNotifierHash() => - r'c160a0e8a8ab0f1cb2282d84141ac5826b4ede3a'; + r'87a21b5273aeec608f0df6bb10f76c4554fbdbe8'; abstract class _$DeviceSearchNotifier - extends BuildlessAsyncNotifier> { + extends BuildlessAutoDisposeAsyncNotifier> { late final String query; FutureOr> build( @@ -240,8 +242,8 @@ class DeviceSearchNotifierFamily extends Family>> { } /// See also [DeviceSearchNotifier]. -class DeviceSearchNotifierProvider - extends AsyncNotifierProviderImpl> { +class DeviceSearchNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< + DeviceSearchNotifier, List> { /// See also [DeviceSearchNotifier]. DeviceSearchNotifierProvider( String query, @@ -297,7 +299,7 @@ class DeviceSearchNotifierProvider } @override - AsyncNotifierProviderElement> + AutoDisposeAsyncNotifierProviderElement> createElement() { return _DeviceSearchNotifierProviderElement(this); } @@ -316,14 +318,15 @@ class DeviceSearchNotifierProvider } } -mixin DeviceSearchNotifierRef on AsyncNotifierProviderRef> { +mixin DeviceSearchNotifierRef + on AutoDisposeAsyncNotifierProviderRef> { /// The parameter `query` of this provider. String get query; } class _DeviceSearchNotifierProviderElement - extends AsyncNotifierProviderElement> - with DeviceSearchNotifierRef { + extends AutoDisposeAsyncNotifierProviderElement> with DeviceSearchNotifierRef { _DeviceSearchNotifierProviderElement(super.provider); @override diff --git a/lib/features/devices/presentation/providers/image_upload_provider.dart b/lib/features/devices/presentation/providers/image_upload_provider.dart index 28f0dd4..9026cb7 100644 --- a/lib/features/devices/presentation/providers/image_upload_provider.dart +++ b/lib/features/devices/presentation/providers/image_upload_provider.dart @@ -13,7 +13,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/image_upload_event_bus.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/features/devices/data/services/rest_image_upload_service.dart'; @@ -53,11 +53,11 @@ ImageUploadVerifier imageUploadVerifier(ImageUploadVerifierRef ref) { /// Uses Dio internally with certificate validation for handling /// self-signed certificates. No external client needed. @riverpod -RestImageUploadService restImageUploadService(RestImageUploadServiceRef ref) { +Future restImageUploadService(RestImageUploadServiceRef ref) async { final storage = ref.watch(storageServiceProvider); final siteUrl = storage.siteUrl ?? ''; - final apiKey = storage.token ?? ''; + final apiKey = await storage.getToken() ?? ''; if (siteUrl.isEmpty || apiKey.isEmpty) { throw StateError('Not authenticated: missing siteUrl or apiKey'); @@ -76,8 +76,8 @@ RestImageUploadService restImageUploadService(RestImageUploadServiceRef ref) { /// After upload, fetches fresh device data via REST and updates the /// WebSocket cache to ensure the UI immediately reflects the new images. @riverpod -ImageUploadService imageUploadService(ImageUploadServiceRef ref) { - final restService = ref.watch(restImageUploadServiceProvider); +Future imageUploadService(ImageUploadServiceRef ref) async { + final restService = await ref.watch(restImageUploadServiceProvider.future); final verifier = ref.watch(imageUploadVerifierProvider); final eventBus = ref.watch(imageUploadEventBusProvider); final webSocketCacheIntegration = ref.watch(webSocketCacheIntegrationProvider); @@ -147,22 +147,6 @@ ImageUploadService imageUploadService(ImageUploadServiceRef ref) { ); } -/// Reconstruct full device ID from resource type and raw ID -String _reconstructDeviceId(String resourceType, String rawId) { - switch (resourceType) { - case 'access_points': - return 'ap_$rawId'; - case 'media_converters': - return 'ont_$rawId'; - case 'switch_devices': - return 'sw_$rawId'; - case 'wlan_devices': - return 'wlan_$rawId'; - default: - return rawId; - } -} - /// State for image upload operations class ImageUploadViewState { final bool isUploading; @@ -291,7 +275,7 @@ class ImageUploadNotifier extends _$ImageUploadNotifier { } // Upload images - final uploadService = ref.read(imageUploadServiceProvider); + final uploadService = await ref.read(imageUploadServiceProvider.future); final result = await uploadService.uploadImages( deviceType: deviceType, deviceId: deviceId, diff --git a/lib/features/devices/presentation/providers/image_upload_provider.g.dart b/lib/features/devices/presentation/providers/image_upload_provider.g.dart index 6d1af4e..ac39112 100644 --- a/lib/features/devices/presentation/providers/image_upload_provider.g.dart +++ b/lib/features/devices/presentation/providers/image_upload_provider.g.dart @@ -44,7 +44,7 @@ final imageUploadVerifierProvider = typedef ImageUploadVerifierRef = AutoDisposeProviderRef; String _$restImageUploadServiceHash() => - r'ae944432b957b5c5b28d0f3d1b3d2bfc83e8d8b7'; + r'fdb91a4bd0b69d14deccf8058ca32a02b27313ab'; /// Provider for RestImageUploadService /// @@ -54,7 +54,7 @@ String _$restImageUploadServiceHash() => /// Copied from [restImageUploadService]. @ProviderFor(restImageUploadService) final restImageUploadServiceProvider = - AutoDisposeProvider.internal( + AutoDisposeFutureProvider.internal( restImageUploadService, name: r'restImageUploadServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -65,9 +65,9 @@ final restImageUploadServiceProvider = ); typedef RestImageUploadServiceRef - = AutoDisposeProviderRef; + = AutoDisposeFutureProviderRef; String _$imageUploadServiceHash() => - r'aaa61109542998de8c8f6dbf99e6690d876a4ccd'; + r'779fb74815410e33add5ecba792de80462deae9b'; /// Provider for ImageUploadService /// @@ -79,7 +79,7 @@ String _$imageUploadServiceHash() => /// Copied from [imageUploadService]. @ProviderFor(imageUploadService) final imageUploadServiceProvider = - AutoDisposeProvider.internal( + AutoDisposeFutureProvider.internal( imageUploadService, name: r'imageUploadServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -89,7 +89,8 @@ final imageUploadServiceProvider = allTransitiveDependencies: null, ); -typedef ImageUploadServiceRef = AutoDisposeProviderRef; +typedef ImageUploadServiceRef + = AutoDisposeFutureProviderRef; String _$imageUploadEventsHash() => r'2b903f602f78530ec57b63f7b76fda4bfd3eebd2'; /// Stream provider for image upload events @@ -127,7 +128,7 @@ final cacheInvalidationEventsProvider = typedef CacheInvalidationEventsRef = StreamProviderRef; String _$imageUploadNotifierHash() => - r'ad3876fbcf972dddb303ed50cd83976df61b053e'; + r'916c0a163541be1de68776072e8ade6cbe32ea6c'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/devices/presentation/widgets/device_detail_sections.dart b/lib/features/devices/presentation/widgets/device_detail_sections.dart index 75b9b48..7092266 100644 --- a/lib/features/devices/presentation/widgets/device_detail_sections.dart +++ b/lib/features/devices/presentation/widgets/device_detail_sections.dart @@ -454,7 +454,7 @@ class DeviceDetailSections extends ConsumerWidget { ) { // Get api_key for passing to the dialog (images are already authenticated, // but we pass api_key for any additional operations the dialog may need) - final apiKey = ref.read(apiKeyProvider); + final apiKey = ref.read(apiKeyProvider).valueOrNull; final signedIds = _validImageSignedIds; showDialog( diff --git a/lib/features/devices/presentation/widgets/device_speed_test_section.dart b/lib/features/devices/presentation/widgets/device_speed_test_section.dart index 54cd748..8eb36a9 100644 --- a/lib/features/devices/presentation/widgets/device_speed_test_section.dart +++ b/lib/features/devices/presentation/widgets/device_speed_test_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/core/widgets/widgets.dart'; diff --git a/lib/features/home/presentation/widgets/network_overview_section.dart b/lib/features/home/presentation/widgets/network_overview_section.dart index df37c0c..59fc18c 100644 --- a/lib/features/home/presentation/widgets/network_overview_section.dart +++ b/lib/features/home/presentation/widgets/network_overview_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/navigation_service.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; import 'package:rgnets_fdk/features/home/domain/entities/home_statistics.dart'; diff --git a/lib/features/initialization/presentation/providers/initialization_provider.dart b/lib/features/initialization/presentation/providers/initialization_provider.dart index fb40922..0746938 100644 --- a/lib/features/initialization/presentation/providers/initialization_provider.dart +++ b/lib/features/initialization/presentation/providers/initialization_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/services/websocket_data_sync_service.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; @@ -175,8 +176,14 @@ class InitializationNotifier extends _$InitializationNotifier { unawaited( _dataSyncService .syncInitialData(timeout: const Duration(seconds: 45)) - .catchError((_) {}) - .whenComplete(() { + .catchError((Object e, StackTrace st) { + LoggerService.error( + 'Background sync failed: $e', + tag: 'InitProvider', + error: e, + stackTrace: st, + ); + }).whenComplete(() { _eventSubscription?.cancel(); _eventSubscription = null; }), diff --git a/lib/features/initialization/presentation/providers/initialization_provider.g.dart b/lib/features/initialization/presentation/providers/initialization_provider.g.dart index 0d96f68..afb98e5 100644 --- a/lib/features/initialization/presentation/providers/initialization_provider.g.dart +++ b/lib/features/initialization/presentation/providers/initialization_provider.g.dart @@ -25,7 +25,7 @@ final showInitializationOverlayProvider = AutoDisposeProvider.internal( typedef ShowInitializationOverlayRef = AutoDisposeProviderRef; String _$initializationNotifierHash() => - r'cd44edf371099d225f140bb9ccefba7280788fea'; + r'0948b5ad106e83ffe5548cbb547afbad16f5f3fd'; /// Manages the app initialization state and progress. /// diff --git a/lib/features/issues/data/datasources/health_notices_remote_data_source.dart b/lib/features/issues/data/datasources/health_notices_remote_data_source.dart index 66692ef..ab6d265 100644 --- a/lib/features/issues/data/datasources/health_notices_remote_data_source.dart +++ b/lib/features/issues/data/datasources/health_notices_remote_data_source.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/services/websocket_service.dart'; import 'package:rgnets_fdk/features/issues/data/models/health_notices_summary_model.dart'; @@ -12,25 +12,16 @@ class HealthNoticesRemoteDataSource { }) : _socketService = socketService; final WebSocketService _socketService; + static const _tag = 'HealthNoticesDataSource'; /// Fetches health notices summary (notices list + counts) from backend Future fetchSummary() async { - if (kDebugMode) { - print('HealthNoticesDataSource: fetchSummary called, isConnected=${_socketService.isConnected}'); - } - if (!_socketService.isConnected) { - if (kDebugMode) { - print('HealthNoticesDataSource: WebSocket not connected, returning empty'); - } + LoggerService.debug('WebSocket not connected, returning empty', tag: _tag); return const HealthNoticesSummaryModel(); } try { - if (kDebugMode) { - print('HealthNoticesDataSource: Sending request via requestActionCable...'); - } - // Use the WebSocket service's built-in request/response correlation final response = await _socketService.requestActionCable( action: 'resource_action', @@ -39,46 +30,32 @@ class HealthNoticesRemoteDataSource { timeout: const Duration(seconds: 10), ); - if (kDebugMode) { - print('HealthNoticesDataSource: Got response type=${response.type}'); - } - // Check for error response if (response.type == 'error') { - if (kDebugMode) { - print('HealthNoticesDataSource: Error response received'); - } + LoggerService.warning('Error response received', tag: _tag); return const HealthNoticesSummaryModel(); } // For resource_response, the data is in payload['data'] // which contains { notices: [...], counts: {...} } final responseData = response.payload['data']; - if (kDebugMode) { - print('HealthNoticesDataSource: responseData type=${responseData.runtimeType}'); - } if (responseData is! Map) { - if (kDebugMode) { - print('HealthNoticesDataSource: responseData is not a Map, returning empty'); - } + LoggerService.warning('Invalid response data type: ${responseData.runtimeType}', tag: _tag); return const HealthNoticesSummaryModel(); } final summary = HealthNoticesSummaryModel.fromJson(responseData); - if (kDebugMode) { - print('HealthNoticesDataSource: Parsed ${summary.notices.length} notices, counts=${summary.counts.total}'); - } + LoggerService.debug( + 'Parsed ${summary.notices.length} notices, counts=${summary.counts.total}', + tag: _tag, + ); return summary; } on TimeoutException { - if (kDebugMode) { - print('HealthNoticesDataSource: Request timed out'); - } + LoggerService.warning('Request timed out', tag: _tag); return const HealthNoticesSummaryModel(); } on Exception catch (e) { - if (kDebugMode) { - print('HealthNoticesDataSource: Request failed: $e'); - } + LoggerService.error('Request failed: $e', tag: _tag, error: e); return const HealthNoticesSummaryModel(); } } diff --git a/lib/features/issues/presentation/providers/health_notices_provider.dart b/lib/features/issues/presentation/providers/health_notices_provider.dart index e150b69..7f7596b 100644 --- a/lib/features/issues/presentation/providers/health_notices_provider.dart +++ b/lib/features/issues/presentation/providers/health_notices_provider.dart @@ -1,6 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/features/devices/data/models/device_model_sealed.dart'; import 'package:rgnets_fdk/features/issues/data/models/health_notice_model.dart'; @@ -48,10 +47,6 @@ class AggregateHealthCountsNotifier extends _$AggregateHealthCountsNotifier { // Get cached devices with health notice data from in-memory WebSocket cache final devices = cacheIntegration.getAllCachedDeviceModels(); - if (kDebugMode) { - print('AggregateHealthCountsNotifier: Found ${devices.length} cached devices'); - } - // Aggregate health counts from all devices var totalFatal = 0; var totalCritical = 0; @@ -67,22 +62,15 @@ class AggregateHealthCountsNotifier extends _$AggregateHealthCountsNotifier { totalCritical += counts.critical; totalWarning += counts.warning; totalNotice += counts.notice; - // Log first few devices with hn_counts to debug - if (kDebugMode && devicesWithHnCounts <= 5) { - print(' DEBUG Device ${device.deviceName} (${device.deviceId}): total=${counts.total}, fatal=${counts.fatal}, critical=${counts.critical}, warning=${counts.warning}, notice=${counts.notice}'); - } } } - if (kDebugMode) { - print('AggregateHealthCountsNotifier: $devicesWithHnCounts/${devices.length} devices have hn_counts'); - } - final total = totalFatal + totalCritical + totalWarning + totalNotice; - if (kDebugMode) { - print('AggregateHealthCountsNotifier: Aggregated counts - total=$total, fatal=$totalFatal, critical=$totalCritical, warning=$totalWarning, notice=$totalNotice'); - } + LoggerService.debug( + 'AggregateHealthCountsNotifier: $devicesWithHnCounts/${devices.length} devices, counts: total=$total, fatal=$totalFatal, critical=$totalCritical, warning=$totalWarning, notice=$totalNotice', + tag: 'HealthNotices', + ); return HealthCounts( total: total, @@ -117,11 +105,7 @@ HealthCounts aggregateHealthCounts(AggregateHealthCountsRef ref) { @Riverpod(keepAlive: true) int criticalIssueCount(CriticalIssueCountRef ref) { final notices = ref.watch(healthNoticesListProvider); - final count = notices.criticalCount; - if (kDebugMode) { - print('criticalIssueCountProvider: criticalCount=$count from ${notices.length} total notices'); - } - return count; + return notices.criticalCount; } /// Provider that extracts health notices from cached device data @@ -167,10 +151,6 @@ class HealthNoticesNotifier extends _$HealthNoticesNotifier { tag: 'HealthNotices', ); - if (kDebugMode) { - print('HealthNoticesNotifier: Found ${notices.length} total notices from ${devices.length} devices'); - } - // Sort by severity (highest first), then by creation time (newest first) notices.sort((a, b) { final severityCompare = b.severity.weight.compareTo(a.severity.weight); diff --git a/lib/features/onboarding/presentation/providers/device_onboarding_provider.dart b/lib/features/onboarding/presentation/providers/device_onboarding_provider.dart index 0dca0de..fb9e074 100644 --- a/lib/features/onboarding/presentation/providers/device_onboarding_provider.dart +++ b/lib/features/onboarding/presentation/providers/device_onboarding_provider.dart @@ -1,6 +1,5 @@ -import 'package:logger/logger.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/features/devices/data/models/device_model_sealed.dart'; import 'package:rgnets_fdk/features/onboarding/data/config/onboarding_config.dart'; import 'package:rgnets_fdk/features/onboarding/data/models/onboarding_state.dart'; @@ -8,7 +7,6 @@ import 'package:rgnets_fdk/features/onboarding/data/models/onboarding_status_pay import 'package:rgnets_fdk/features/onboarding/data/resolvers/message_resolver.dart'; import 'package:rgnets_fdk/features/onboarding/data/services/stage_timestamp_tracker.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; part 'device_onboarding_provider.g.dart'; @@ -30,7 +28,6 @@ MessageResolver messageResolver(MessageResolverRef ref) { /// Transforms DeviceModelSealed into OnboardingState with timing information. @Riverpod(keepAlive: true) class DeviceOnboardingNotifier extends _$DeviceOnboardingNotifier { - Logger get _logger => ref.read(loggerProvider); StageTimestampTracker get _tracker => ref.read(stageTimestampTrackerProvider); MessageResolver get _resolver => ref.read(messageResolverProvider); diff --git a/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart b/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart index fa29fab..67208a6 100644 --- a/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart +++ b/lib/features/room_readiness/data/datasources/room_readiness_data_source.dart @@ -288,7 +288,7 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { if (id is int) return id; if (id is String) return int.tryParse(id) ?? 0; } - // For DeviceModel objects + // For DeviceModel objects - try dynamic access try { final id = device.id; if (id is String) { @@ -296,7 +296,9 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { final match = RegExp(r'\d+').firstMatch(id); return int.tryParse(match?.group(0) ?? '') ?? 0; } - } catch (_) {} + } catch (_) { + // Expected: device may not have .id property - fallback to 0 + } return 0; } @@ -479,7 +481,9 @@ class RoomReadinessWebSocketDataSource implements RoomReadinessDataSource { } else if (deviceType == 'ONT') { return device.ontOnboardingStatus; } - } catch (_) {} + } catch (_) { + // Expected: device may not have onboarding status property + } return null; } diff --git a/lib/features/rooms/presentation/providers/room_device_view_model.dart b/lib/features/rooms/presentation/providers/room_device_view_model.dart index c05b945..3edf82e 100644 --- a/lib/features/rooms/presentation/providers/room_device_view_model.dart +++ b/lib/features/rooms/presentation/providers/room_device_view_model.dart @@ -40,7 +40,7 @@ class RoomDeviceStats with _$RoomDeviceStats { /// /// Provides device filtering and statistics for a specific room. /// Follows Clean Architecture and MVVM patterns. -@Riverpod(keepAlive: true) +@riverpod class RoomDeviceNotifier extends _$RoomDeviceNotifier { @override RoomDeviceState build(String roomId) { @@ -132,21 +132,11 @@ class RoomDeviceNotifier extends _$RoomDeviceNotifier { /// Filter devices for a specific room List _filterDevicesForRoom(List allDevices, int roomIdInt) { try { - // DEBUG: Log all devices with their pmsRoomId - print('DEBUG _filterDevicesForRoom: Looking for roomId=$roomIdInt in ${allDevices.length} devices'); - final filtered = allDevices.where((device) { // Use pmsRoomId for room association - // This is the established pattern from room_view_models.dart return device.pmsRoomId == roomIdInt; }).toList(); - // DEBUG: Log filtered devices - print('DEBUG _filterDevicesForRoom: Found ${filtered.length} devices for room $roomIdInt:'); - for (final d in filtered) { - print(' - id=${d.id}, name=${d.name}, type=${d.type}, pmsRoomId=${d.pmsRoomId}'); - } - return filtered; } on Exception catch (e) { // Defensive programming - if filtering fails, return empty list diff --git a/lib/features/rooms/presentation/providers/room_device_view_model.g.dart b/lib/features/rooms/presentation/providers/room_device_view_model.g.dart index d915b40..943139c 100644 --- a/lib/features/rooms/presentation/providers/room_device_view_model.g.dart +++ b/lib/features/rooms/presentation/providers/room_device_view_model.g.dart @@ -7,7 +7,7 @@ part of 'room_device_view_model.dart'; // ************************************************************************** String _$roomDeviceNotifierHash() => - r'162f6ee280103a8736f496c2943e0921738d1e2c'; + r'5ef3bc2a18b2721842fb1ef380e71261420254d4'; /// Copied from Dart SDK class _SystemHash { @@ -30,7 +30,8 @@ class _SystemHash { } } -abstract class _$RoomDeviceNotifier extends BuildlessNotifier { +abstract class _$RoomDeviceNotifier + extends BuildlessAutoDisposeNotifier { late final String roomId; RoomDeviceState build( @@ -106,8 +107,8 @@ class RoomDeviceNotifierFamily extends Family { /// Follows Clean Architecture and MVVM patterns. /// /// Copied from [RoomDeviceNotifier]. -class RoomDeviceNotifierProvider - extends NotifierProviderImpl { +class RoomDeviceNotifierProvider extends AutoDisposeNotifierProviderImpl< + RoomDeviceNotifier, RoomDeviceState> { /// Room device view model provider /// /// Provides device filtering and statistics for a specific room. @@ -168,7 +169,8 @@ class RoomDeviceNotifierProvider } @override - NotifierProviderElement createElement() { + AutoDisposeNotifierProviderElement + createElement() { return _RoomDeviceNotifierProviderElement(this); } @@ -186,14 +188,14 @@ class RoomDeviceNotifierProvider } } -mixin RoomDeviceNotifierRef on NotifierProviderRef { +mixin RoomDeviceNotifierRef on AutoDisposeNotifierProviderRef { /// The parameter `roomId` of this provider. String get roomId; } class _RoomDeviceNotifierProviderElement - extends NotifierProviderElement - with RoomDeviceNotifierRef { + extends AutoDisposeNotifierProviderElement with RoomDeviceNotifierRef { _RoomDeviceNotifierProviderElement(super.provider); @override diff --git a/lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart b/lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart index 826b787..08ab028 100644 --- a/lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart +++ b/lib/features/rooms/presentation/providers/rooms_riverpod_provider.dart @@ -1,8 +1,11 @@ import 'package:logger/logger.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; +import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/utils/logging_utils.dart'; +import 'package:rgnets_fdk/features/devices/domain/entities/device.dart'; import 'package:rgnets_fdk/features/devices/domain/entities/room.dart'; +import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; import 'package:rgnets_fdk/features/rooms/domain/usecases/get_rooms.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -55,29 +58,36 @@ class RoomsNotifier extends _$RoomsNotifier { @riverpod RoomStatistics roomStatistics(RoomStatisticsRef ref) { final rooms = ref.watch(roomsNotifierProvider); - + final devices = ref.watch(devicesNotifierProvider); + return rooms.when( data: (roomList) { final total = roomList.length; - // Assuming we can calculate device stats from room metadata or deviceIds var totalDevices = 0; var onlineDevices = 0; var roomsWithIssues = 0; - + + // Get the list of devices once (may be null/empty if still loading) + final deviceList = devices.valueOrNull ?? []; + for (final room in roomList) { - final deviceCount = room.deviceIds?.length ?? 0; + final roomDeviceIds = room.deviceIds ?? []; + final deviceCount = roomDeviceIds.length; totalDevices += deviceCount; - - // Mock calculation for demo - in real app this would come from device status - final mockOnlineDevices = (deviceCount * 0.8).round(); // Assume 80% online - onlineDevices += mockOnlineDevices; - - // Mock issues calculation - if (deviceCount > 0 && mockOnlineDevices < deviceCount) { + + // Use real device status from devicesNotifierProvider + final roomDevices = deviceList + .where((d) => roomDeviceIds.contains(d.id)) + .toList(); + final realOnlineDevices = roomDevices.where((d) => d.isOnline).length; + onlineDevices += realOnlineDevices; + + // A room has issues if it has devices but not all are online + if (deviceCount > 0 && realOnlineDevices < deviceCount) { roomsWithIssues++; } } - + return RoomStatistics( total: total, totalDevices: totalDevices, @@ -86,7 +96,15 @@ RoomStatistics roomStatistics(RoomStatisticsRef ref) { ); }, loading: () => const RoomStatistics(), - error: (_, __) => const RoomStatistics(), + error: (error, stack) { + LoggerService.error( + 'Failed to load room statistics', + tag: 'RoomsProvider', + error: error, + stackTrace: stack, + ); + return const RoomStatistics(); + }, ); } @@ -120,6 +138,14 @@ Room? roomById(RoomByIdRef ref, String roomId) { return matchingRooms.isNotEmpty ? matchingRooms.first : null; }, loading: () => null, - error: (_, __) => null, + error: (error, stack) { + LoggerService.error( + 'Failed to get room by ID: $roomId', + tag: 'RoomsProvider', + error: error, + stackTrace: stack, + ); + return null; + }, ); } diff --git a/lib/features/rooms/presentation/providers/rooms_riverpod_provider.g.dart b/lib/features/rooms/presentation/providers/rooms_riverpod_provider.g.dart index d233592..c897cf1 100644 --- a/lib/features/rooms/presentation/providers/rooms_riverpod_provider.g.dart +++ b/lib/features/rooms/presentation/providers/rooms_riverpod_provider.g.dart @@ -6,7 +6,7 @@ part of 'rooms_riverpod_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$roomStatisticsHash() => r'16009c7965b05d31a8c231f853860c87823e85b3'; +String _$roomStatisticsHash() => r'5835a02ebcc4d518451ac59dfb2a24af0242c0e1'; /// Provider for room statistics /// @@ -23,7 +23,7 @@ final roomStatisticsProvider = AutoDisposeProvider.internal( ); typedef RoomStatisticsRef = AutoDisposeProviderRef; -String _$roomByIdHash() => r'310270c2acbb8b5f589cbf41241b5fdc99e32db4'; +String _$roomByIdHash() => r'998a73691a6cfb86501a2f6e48287add8f011cbf'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/features/scanner/presentation/providers/device_registration_provider.g.dart b/lib/features/scanner/presentation/providers/device_registration_provider.g.dart index feea048..d9e4305 100644 --- a/lib/features/scanner/presentation/providers/device_registration_provider.g.dart +++ b/lib/features/scanner/presentation/providers/device_registration_provider.g.dart @@ -46,7 +46,7 @@ final deviceWebSocketEventsProvider = typedef DeviceWebSocketEventsRef = AutoDisposeStreamProviderRef; String _$deviceRegistrationNotifierHash() => - r'a099fb7c59527a970114438268f457e4d8c87e70'; + r'cd4ecef75b04489157cd8594d31fe47c6c60af40'; /// Provider for device registration with WebSocket integration. /// Handles checking existing devices and registering new ones. diff --git a/lib/features/scanner/presentation/providers/scanner_notifier_v2.g.dart b/lib/features/scanner/presentation/providers/scanner_notifier_v2.g.dart index 49b25ba..19a6dd3 100644 --- a/lib/features/scanner/presentation/providers/scanner_notifier_v2.g.dart +++ b/lib/features/scanner/presentation/providers/scanner_notifier_v2.g.dart @@ -6,7 +6,7 @@ part of 'scanner_notifier_v2.dart'; // RiverpodGenerator // ************************************************************************** -String _$scannerNotifierV2Hash() => r'8046e6396ee6a3f66139893ab4f6c053d3a9ac8c'; +String _$scannerNotifierV2Hash() => r'92c4b30d37c23353d925e36b46402942e4025eb0'; /// New scanner notifier using freezed ScannerState with auto-detection support. /// diff --git a/lib/features/settings/presentation/providers/settings_riverpod_provider.dart b/lib/features/settings/presentation/providers/settings_riverpod_provider.dart index 3134418..750b5fb 100644 --- a/lib/features/settings/presentation/providers/settings_riverpod_provider.dart +++ b/lib/features/settings/presentation/providers/settings_riverpod_provider.dart @@ -1,7 +1,7 @@ import 'package:rgnets_fdk/core/config/logger_config.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart' show notificationGenerationServiceProvider; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart' +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart' show webSocketCacheIntegrationProvider; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; import 'package:rgnets_fdk/features/devices/presentation/providers/devices_provider.dart'; diff --git a/lib/features/settings/presentation/screens/settings_screen.dart b/lib/features/settings/presentation/screens/settings_screen.dart index 0e6368e..88af8fd 100644 --- a/lib/features/settings/presentation/screens/settings_screen.dart +++ b/lib/features/settings/presentation/screens/settings_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:rgnets_fdk/core/config/environment.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/widgets/connection_details_dialog.dart'; import 'package:rgnets_fdk/core/widgets/hold_to_confirm_button.dart'; import 'package:rgnets_fdk/features/auth/domain/entities/auth_status.dart'; diff --git a/lib/features/speed_test/presentation/providers/speed_test_providers.dart b/lib/features/speed_test/presentation/providers/speed_test_providers.dart index a879ffd..0d3454e 100644 --- a/lib/features/speed_test/presentation/providers/speed_test_providers.dart +++ b/lib/features/speed_test/presentation/providers/speed_test_providers.dart @@ -5,6 +5,7 @@ import 'package:logger/logger.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_data_source.dart'; import 'package:rgnets_fdk/features/speed_test/data/datasources/speed_test_websocket_data_source.dart'; import 'package:rgnets_fdk/features/speed_test/data/repositories/speed_test_repository_impl.dart'; @@ -94,7 +95,7 @@ class SpeedTestConfigsNotifier extends _$SpeedTestConfigsNotifier { // Speed Test Results Provider // ============================================================================ -@Riverpod(keepAlive: true) +@riverpod class SpeedTestResultsNotifier extends _$SpeedTestResultsNotifier { Logger get _logger => ref.read(loggerProvider); diff --git a/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart b/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart index 9d9da22..d650d0f 100644 --- a/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart +++ b/lib/features/speed_test/presentation/providers/speed_test_providers.g.dart @@ -56,7 +56,7 @@ final speedTestConfigsNotifierProvider = AsyncNotifierProvider< typedef _$SpeedTestConfigsNotifier = AsyncNotifier>; String _$speedTestResultsNotifierHash() => - r'1e035a7a5d6105cc2577309ba1a1469642de508d'; + r'6950d1eff53c75eeb1ee3117ba2c30a98e0deee8'; /// Copied from Dart SDK class _SystemHash { @@ -80,7 +80,7 @@ class _SystemHash { } abstract class _$SpeedTestResultsNotifier - extends BuildlessAsyncNotifier> { + extends BuildlessAutoDisposeAsyncNotifier> { late final int? speedTestId; late final int? accessPointId; @@ -137,8 +137,9 @@ class SpeedTestResultsNotifierFamily } /// See also [SpeedTestResultsNotifier]. -class SpeedTestResultsNotifierProvider extends AsyncNotifierProviderImpl< - SpeedTestResultsNotifier, List> { +class SpeedTestResultsNotifierProvider + extends AutoDisposeAsyncNotifierProviderImpl> { /// See also [SpeedTestResultsNotifier]. SpeedTestResultsNotifierProvider({ int? speedTestId, @@ -204,8 +205,8 @@ class SpeedTestResultsNotifierProvider extends AsyncNotifierProviderImpl< } @override - AsyncNotifierProviderElement> - createElement() { + AutoDisposeAsyncNotifierProviderElement> createElement() { return _SpeedTestResultsNotifierProviderElement(this); } @@ -227,7 +228,7 @@ class SpeedTestResultsNotifierProvider extends AsyncNotifierProviderImpl< } mixin SpeedTestResultsNotifierRef - on AsyncNotifierProviderRef> { + on AutoDisposeAsyncNotifierProviderRef> { /// The parameter `speedTestId` of this provider. int? get speedTestId; @@ -236,7 +237,7 @@ mixin SpeedTestResultsNotifierRef } class _SpeedTestResultsNotifierProviderElement - extends AsyncNotifierProviderElement> with SpeedTestResultsNotifierRef { _SpeedTestResultsNotifierProviderElement(super.provider); diff --git a/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart b/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart index 0e06475..ec0598c 100644 --- a/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart +++ b/lib/features/speed_test/presentation/widgets/room_speed_test_selector.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_config.dart'; diff --git a/lib/features/speed_test/presentation/widgets/speed_test_card.dart b/lib/features/speed_test/presentation/widgets/speed_test_card.dart index 8c503e3..89020ed 100644 --- a/lib/features/speed_test/presentation/widgets/speed_test_card.dart +++ b/lib/features/speed_test/presentation/widgets/speed_test_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/theme/app_colors.dart'; import 'package:rgnets_fdk/features/speed_test/domain/entities/speed_test_result.dart'; diff --git a/lib/features/splash/presentation/screens/splash_screen.dart b/lib/features/splash/presentation/screens/splash_screen.dart index fc3b980..8d9638f 100644 --- a/lib/features/splash/presentation/screens/splash_screen.dart +++ b/lib/features/splash/presentation/screens/splash_screen.dart @@ -116,10 +116,8 @@ class _SplashScreenState extends ConsumerState { ..d('SPLASH_SCREEN: Decoded credentials from QR:') ..d('SPLASH_SCREEN: FQDN: $fqdn') ..d('SPLASH_SCREEN: Login: $login') - ..d('SPLASH_SCREEN: API Key: ${authToken.substring(0, 4)}...') - ..d('SPLASH_SCREEN: Site Name: $siteName') - ..d('SPLASH_SCREEN: Full FQDN type: ${fqdn.runtimeType}') - ..d('SPLASH_SCREEN: Full Login type: ${login.runtimeType}'); + ..d('SPLASH_SCREEN: Token: [REDACTED, length=${authToken.length}]') + ..d('SPLASH_SCREEN: Site Name: $siteName'); try { logger @@ -244,7 +242,7 @@ class _SplashScreenState extends ConsumerState { ..d('Using fallback credentials:') ..d('FQDN: $fqdn') ..d('Login: $login') - ..d('API Key: ${authToken.substring(0, 4)}...'); + ..d('Token: [REDACTED, length=${authToken.length}]'); try { await ref @@ -343,8 +341,9 @@ class _SplashScreenState extends ConsumerState { final storageService = ref.read(storageServiceProvider); // Check for actual stored credentials (not just the flag) - final hasStoredCredentials = storageService.token != null && - storageService.token!.isNotEmpty && + final storedToken = await storageService.getToken(); + final hasStoredCredentials = storedToken != null && + storedToken.isNotEmpty && storageService.siteUrl != null && storageService.siteUrl!.isNotEmpty && storageService.username != null && @@ -359,7 +358,7 @@ class _SplashScreenState extends ConsumerState { if (mounted) { if (hasCredentials) { final siteUrl = storageService.siteUrl ?? ''; - final authToken = storageService.token ?? ''; + final authToken = storedToken ?? ''; final login = storageService.username ?? ''; final siteName = storageService.siteName; diff --git a/lib/main.dart b/lib/main.dart index 8593ef6..4935b9d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,7 @@ import 'package:rgnets_fdk/core/navigation/app_router.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/providers/deeplink_provider.dart'; import 'package:rgnets_fdk/core/providers/repository_providers.dart'; -import 'package:rgnets_fdk/core/providers/websocket_providers.dart'; +import 'package:rgnets_fdk/core/providers/websocket_sync_providers.dart'; import 'package:rgnets_fdk/core/services/app_initializer.dart'; import 'package:rgnets_fdk/core/services/deeplink_service.dart'; import 'package:rgnets_fdk/core/services/error_reporter.dart'; @@ -91,8 +91,37 @@ void main() async { try { sharedPreferences = await SharedPreferences.getInstance(); } on Exception catch (e) { - // If SharedPreferences fails, provide a fallback or exit gracefully debugPrint('Failed to initialize SharedPreferences: $e'); + // Show error UI instead of silent exit + runApp( + MaterialApp( + home: Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 64, color: Colors.red), + const SizedBox(height: 16), + const Text( + 'Storage Initialization Failed', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text('Error: $e', textAlign: TextAlign.center), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => main(), + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ), + ); return; } @@ -137,17 +166,21 @@ class _FDKAppState extends ConsumerState { } /// Initialize background services (called once from initState callback) - void _initializeServices() { + Future _initializeServices() async { if (_servicesInitialized) return; _servicesInitialized = true; + // Migrate credentials to secure storage if needed (runs once) + final storageService = ref.read(storageServiceProvider); + await storageService.migrateToSecureStorageIfNeeded(); + ref.read(backgroundRefreshServiceProvider).startBackgroundRefresh(); // Initialize WebSocket data sync listener to refresh providers when data arrives ref.read(webSocketDataSyncListenerProvider); // Initialize auth sign-out cleanup listener to handle cache clearing and provider invalidation ref.read(authSignOutCleanupProvider); // Initialize deeplink service for handling fdk:// URLs - _initializeDeeplinkService(); + await _initializeDeeplinkService(); // Check if already authenticated on startup final isAuthenticated = ref.read(isAuthenticatedProvider); diff --git a/lib/main_development.dart b/lib/main_development.dart index f22871a..3d30d2d 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -6,7 +6,6 @@ import 'package:rgnets_fdk/core/navigation/app_router.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/theme/app_theme.dart'; -import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() async { diff --git a/lib/main_production.dart b/lib/main_production.dart index 30cc38f..b900a5c 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:rgnets_fdk/core/config/environment.dart'; -import 'package:rgnets_fdk/core/services/logger_service.dart'; import 'package:rgnets_fdk/core/navigation/app_router.dart'; import 'package:rgnets_fdk/core/providers/core_providers.dart'; import 'package:rgnets_fdk/core/services/logger_service.dart'; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 87fbd88..95d94c4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -16,6 +17,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 52d33ec..cd9dc93 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + flutter_secure_storage_linux gtk sentry_flutter sqlite3_flutter_libs diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 97701e4..3ceb030 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import app_links import battery_plus import connectivity_plus import file_selector_macos +import flutter_secure_storage_macos import mobile_scanner import network_info_plus import package_info_plus @@ -24,6 +25,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 88110de..a70f914 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" cli_util: dependency: transitive description: @@ -297,6 +305,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + url: "https://pub.dev" + source: hosted + version: "0.6.4" custom_lint_core: dependency: transitive description: @@ -483,6 +507,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -570,6 +642,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + url: "https://pub.dev" + source: hosted + version: "4.3.0" http: dependency: "direct main" description: @@ -691,10 +771,10 @@ packages: dependency: transitive description: name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -779,10 +859,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1095,14 +1175,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.10" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + url: "https://pub.dev" + source: hosted + version: "2.3.10" rxdart: dependency: transitive description: name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.27.7" sentry: dependency: transitive description: @@ -1368,10 +1456,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7275c1..f12298e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,10 +64,13 @@ dependencies: url_launcher: ^6.3.1 uuid: ^4.5.1 web_socket_channel: ^3.0.3 - flutter_cache_manager: any + flutter_cache_manager: ^3.4.1 + flutter_secure_storage: ^9.2.2 dev_dependencies: build_runner: ^2.4.8 + custom_lint: ^0.6.3 drift_dev: ^2.21.0 + flutter_launcher_icons: ^0.14.2 flutter_lints: ^5.0.0 flutter_test: sdk: flutter @@ -78,14 +81,7 @@ dev_dependencies: mocktail: ^1.0.4 retrofit_generator: ^8.2.1 riverpod_generator: 2.3.10 - flutter_launcher_icons: ^0.14.2 - # custom_lint: ^0.7.0 # Temporarily disabled due to analyzer conflicts - # riverpod_lint: ^2.6.3 # Temporarily disabled due to analyzer conflicts - -# Dependency overrides to resolve analyzer conflicts -# dependency_overrides: -# analyzer: ^6.7.0 -# analyzer_plugin: ^0.11.3 + riverpod_lint: ^2.3.10 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1effffb..0fe19bb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SentryFlutterPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 83f9e29..d82633a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST battery_plus connectivity_plus file_selector_windows + flutter_secure_storage_windows permission_handler_windows sentry_flutter sqlite3_flutter_libs