From 2824aa72d9540fd0f54c053cf3ef7123217546eb Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 6 Aug 2025 14:15:47 -0500 Subject: [PATCH 1/2] fix for user-per-mode --- lib/app_state.dart | 25 ++++++++++++ lib/services/auth_service.dart | 75 +++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/lib/app_state.dart b/lib/app_state.dart index 6e52227..252248b 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -43,6 +43,31 @@ class AppState extends ChangeNotifier { _uploadMode = mode; // Update AuthService to match new mode _auth.setUploadMode(mode); + // Refresh user display for active mode, validating token + try { + if (await _auth.isLoggedIn()) { + print('AppState: Switching mode, token exists; validating...'); + final isValid = await validateToken(); + if (isValid) { + print('AppState: Switching mode; fetching username for $mode...'); + _username = await _auth.login(); + if (_username != null) { + print('AppState: Switched mode, now logged in as $_username'); + } else { + print('AppState: Switched mode but failed to retrieve username'); + } + } else { + print('AppState: Switching mode, token invalid—auto-logout.'); + await logout(); // This clears _username also. + } + } else { + _username = null; + print('AppState: Mode change: not logged in in $mode'); + } + } catch (e) { + _username = null; + print('AppState: Mode change user restoration error: $e'); + } final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_uploadModePrefsKey, mode.index); print('AppState: Upload mode set to $mode'); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index e681c60..30f5898 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:oauth2_client/oauth2_client.dart'; import 'package:oauth2_client/oauth2_helper.dart'; import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; /// Handles PKCE OAuth login with OpenStreetMap. import '../app_state.dart'; @@ -23,11 +24,23 @@ class AuthService { setUploadMode(mode); } + String get _tokenKey { + switch (_mode) { + case UploadMode.production: + return 'osm_token_prod'; + case UploadMode.sandbox: + return 'osm_token_sandbox'; + case UploadMode.simulate: + default: + return 'osm_token_simulate'; + } + } + void setUploadMode(UploadMode mode) { _mode = mode; final isSandbox = (mode == UploadMode.sandbox); final authBase = isSandbox - ? 'https://master.apis.dev.openstreetmap.org' // sandbox auth + ? 'https://master.apis.dev.openstreetmap.org' : 'https://www.openstreetmap.org'; final clientId = isSandbox ? kOsmSandboxClientId : kOsmProdClientId; final client = OAuth2Client( @@ -41,16 +54,39 @@ class AuthService { clientId: clientId, scopes: ['read_prefs', 'write_api'], enablePKCE: true, + // tokenStorageKey: _tokenKey, // not supported by this package version ); - print('AuthService: Initialized for $mode with $authBase and clientId $clientId'); + print('AuthService: Initialized for $mode with $authBase, clientId $clientId [manual token storage as needed]'); } - Future isLoggedIn() async => - (await _helper.getTokenFromStorage())?.isExpired() == false; + Future isLoggedIn() async { + if (_mode == UploadMode.simulate) { + // In simulate, a login is faked by writing to shared prefs + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool('sim_user_logged_in') ?? false; + } + // Manually check for mode-specific token + final prefs = await SharedPreferences.getInstance(); + final tokenJson = prefs.getString(_tokenKey); + if (tokenJson == null) return false; + try { + final data = jsonDecode(tokenJson); + return data['accessToken'] != null && data['accessToken'].toString().isNotEmpty; + } catch (_) { + return false; + } + } String? get displayName => _displayName; Future login() async { + if (_mode == UploadMode.simulate) { + print('AuthService: Simulate login (no OAuth)'); + final prefs = await SharedPreferences.getInstance(); + _displayName = 'Demo User'; + await prefs.setBool('sim_user_logged_in', true); + return _displayName; + } try { print('AuthService: Starting OAuth login...'); final token = await _helper.getToken(); @@ -59,6 +95,13 @@ class AuthService { log('OAuth error: token null or missing accessToken'); return null; } + final tokenMap = { + 'accessToken': token!.accessToken, + 'refreshToken': token.refreshToken, + }; + final tokenJson = jsonEncode(tokenMap); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, tokenJson); // Save token for current mode print('AuthService: Got access token, fetching username...'); _displayName = await _fetchUsername(token!.accessToken!); if (_displayName != null) { @@ -75,6 +118,14 @@ class AuthService { } Future logout() async { + if (_mode == UploadMode.simulate) { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('sim_user_logged_in'); + _displayName = null; + return; + } + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); await _helper.removeAllTokens(); _displayName = null; } @@ -87,8 +138,20 @@ class AuthService { return await login(); } - Future getAccessToken() async => - (await _helper.getTokenFromStorage())?.accessToken; + Future getAccessToken() async { + if (_mode == UploadMode.simulate) { + return 'sim-user-token'; + } + final prefs = await SharedPreferences.getInstance(); + final tokenJson = prefs.getString(_tokenKey); + if (tokenJson == null) return null; + try { + final data = jsonDecode(tokenJson); + return data['accessToken']; + } catch (_) { + return null; + } + } /* ───────── helper ───────── */ From 61e5a2d9b6211ee254a30f4e95aab0664c830884 Mon Sep 17 00:00:00 2001 From: stopflock Date: Wed, 6 Aug 2025 14:27:44 -0500 Subject: [PATCH 2/2] stop hammering OSM, fetch points from overpass only when necessary --- lib/widgets/map_view.dart | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 44c6f6f..b335321 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -38,6 +38,22 @@ class _MapViewState extends State { LatLng? _currentLatLng; List _cameras = []; + List _lastProfileIds = []; + UploadMode? _lastUploadMode; + + void _maybeRefreshCameras(AppState appState) { + final currProfileIds = appState.enabledProfiles.map((p) => p.id).toList(); + final currMode = appState.uploadMode; + if (_lastProfileIds.isEmpty || + currProfileIds.length != _lastProfileIds.length || + !_lastProfileIds.asMap().entries.every((entry) => currProfileIds[entry.key] == entry.value) || + _lastUploadMode != currMode) { + // If this is first load, or list/ids/mode changed, refetch + _debounce(() => _refreshCameras(appState)); + _lastProfileIds = List.from(currProfileIds); + _lastUploadMode = currMode; + } + } @override void initState() { @@ -103,11 +119,10 @@ class _MapViewState extends State { final appState = context.watch(); final session = appState.session; - // Always watch for changes on uploadMode/profiles and refresh if needed - // (debounced, to avoid flooding when quickly toggling) - WidgetsBinding.instance.addPostFrameCallback((_) { - _debounce(() => _refreshCameras(appState)); - }); + // Refetch only if profiles or mode changed + // This avoids repeated fetches on every build + // We track last seen values (local to the State class) + _maybeRefreshCameras(appState); // Seed add‑mode target once, after first controller center is available. if (session != null && session.target == null) {