diff --git a/lib/app_state.dart b/lib/app_state.dart index 5f2fe4f..b359ec0 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -42,16 +42,42 @@ class AppState extends ChangeNotifier { Future _init() async { _enabled.addAll(_profiles); await _loadQueue(); - if (await _auth.isLoggedIn()) { - _username = await _auth.login(); + + // Check if we're already logged in and get username + try { + if (await _auth.isLoggedIn()) { + print('AppState: User appears to be logged in, fetching username...'); + _username = await _auth.login(); + if (_username != null) { + print('AppState: Successfully retrieved username: $_username'); + } else { + print('AppState: Failed to retrieve username despite being logged in'); + } + } else { + print('AppState: User is not logged in'); + } + } catch (e) { + print('AppState: Error during auth initialization: $e'); } + _startUploader(); notifyListeners(); } // ---------- Auth ---------- Future login() async { - _username = await _auth.login(); + try { + print('AppState: Starting login process...'); + _username = await _auth.login(); + if (_username != null) { + print('AppState: Login successful for user: $_username'); + } else { + print('AppState: Login failed - no username returned'); + } + } catch (e) { + print('AppState: Login error: $e'); + _username = null; + } notifyListeners(); } @@ -61,6 +87,46 @@ class AppState extends ChangeNotifier { notifyListeners(); } + // Add method to refresh auth state + Future refreshAuthState() async { + try { + print('AppState: Refreshing auth state...'); + if (await _auth.isLoggedIn()) { + print('AppState: Token exists, fetching username...'); + _username = await _auth.login(); + if (_username != null) { + print('AppState: Auth refresh successful: $_username'); + } else { + print('AppState: Auth refresh failed - no username'); + } + } else { + print('AppState: No valid token found'); + _username = null; + } + } catch (e) { + print('AppState: Auth refresh error: $e'); + _username = null; + } + notifyListeners(); + } + + // Force a completely fresh login (clears stored tokens) + Future forceLogin() async { + try { + print('AppState: Starting forced fresh login...'); + _username = await _auth.forceLogin(); + if (_username != null) { + print('AppState: Forced login successful: $_username'); + } else { + print('AppState: Forced login failed - no username returned'); + } + } catch (e) { + print('AppState: Forced login error: $e'); + _username = null; + } + notifyListeners(); + } + // ---------- Profiles ---------- List get profiles => List.unmodifiable(_profiles); bool isEnabled(CameraProfile p) => _enabled.contains(p); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index bad1e36..5c7139e 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -21,8 +21,11 @@ class SettingsScreen extends StatelessWidget { color: appState.isLoggedIn ? Colors.green : null, ), title: Text(appState.isLoggedIn - ? 'Logged in as ${appState.username}' - : 'Log in to OpenStreetMap'), + ? 'Logged in as ${appState.username}' + : 'Log in to OpenStreetMap'), + subtitle: appState.isLoggedIn + ? const Text('Tap to logout') + : const Text('Tap to login'), onTap: () async { if (appState.isLoggedIn) { await appState.logout(); @@ -31,6 +34,41 @@ class SettingsScreen extends StatelessWidget { } }, ), + ListTile( + leading: const Icon(Icons.refresh), + title: const Text('Refresh Login Status'), + subtitle: const Text('Check if you\'re already logged in'), + onTap: () async { + await appState.refreshAuthState(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(appState.isLoggedIn + ? 'Logged in as ${appState.username}' + : 'Not logged in'), + ), + ); + } + }, + ), + ListTile( + leading: const Icon(Icons.login_outlined), + title: const Text('Force Fresh Login'), + subtitle: const Text('Clear stored tokens and login again'), + onTap: () async { + await appState.forceLogin(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(appState.isLoggedIn + ? 'Fresh login successful: ${appState.username}' + : 'Fresh login failed'), + backgroundColor: appState.isLoggedIn ? Colors.green : Colors.red, + ), + ); + } + }, + ), if (appState.isLoggedIn) ListTile( leading: const Icon(Icons.cloud_upload), diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 2949e14..b6d80d4 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'dart:math' as math; import 'package:oauth2_client/oauth2_client.dart'; import 'package:oauth2_client/oauth2_helper.dart'; @@ -7,7 +8,7 @@ import 'package:http/http.dart' as http; /// Handles PKCE OAuth login with OpenStreetMap. class AuthService { - static const String _clientId = 'HNbRD_Twxf0_lpkm-BmMB7-zb-v63VLdf_bVlNyU9qs'; + static const String _clientId = 'Js6Fn3NR3HEGaD0ZIiHBQlV9LrVcHmsOsDmApHtSyuY'; static const _redirect = 'flockmap://auth'; late final OAuth2Helper _helper; @@ -24,9 +25,13 @@ class AuthService { _helper = OAuth2Helper( client, clientId: _clientId, - scopes: ['write_api'], + scopes: ['read_prefs', 'write_api'], enablePKCE: true, ); + + print('AuthService: Initialized with scopes: [read_prefs, write_api]'); + print('AuthService: Client ID: $_clientId'); + print('AuthService: Redirect URI: $_redirect'); } Future isLoggedIn() async => @@ -36,14 +41,23 @@ class AuthService { Future login() async { try { + print('AuthService: Starting OAuth login...'); final token = await _helper.getToken(); if (token?.accessToken == null) { + print('AuthService: OAuth error - token null or missing accessToken'); log('OAuth error: token null or missing accessToken'); return null; } + print('AuthService: Got access token, fetching username...'); _displayName = await _fetchUsername(token!.accessToken!); + if (_displayName != null) { + print('AuthService: Successfully fetched username: $_displayName'); + } else { + print('AuthService: Failed to fetch username from OSM API'); + } return _displayName; } catch (e) { + print('AuthService: OAuth login failed: $e'); log('OAuth login failed: $e'); rethrow; } @@ -54,21 +68,58 @@ class AuthService { _displayName = null; } + // Force a fresh login by clearing stored tokens + Future forceLogin() async { + print('AuthService: Forcing fresh login by clearing stored tokens...'); + await _helper.removeAllTokens(); + _displayName = null; + return await login(); + } + Future getAccessToken() async => (await _helper.getTokenFromStorage())?.accessToken; /* ───────── helper ───────── */ Future _fetchUsername(String accessToken) async { - final resp = await http.get( - Uri.parse('https://api.openstreetmap.org/api/0.6/user/details.json'), - headers: {'Authorization': 'Bearer $accessToken'}, - ); - if (resp.statusCode != 200) { - log('fetchUsername response ${resp.statusCode}: ${resp.body}'); + try { + print('AuthService: Fetching username from OSM API...'); + print('AuthService: Access token (first 20 chars): ${accessToken.substring(0, math.min(20, accessToken.length))}...'); + + final resp = await http.get( + Uri.parse('https://api.openstreetmap.org/api/0.6/user/details.json'), + headers: {'Authorization': 'Bearer $accessToken'}, + ); + print('AuthService: OSM API response status: ${resp.statusCode}'); + print('AuthService: Response headers: ${resp.headers}'); + + if (resp.statusCode != 200) { + print('AuthService: fetchUsername failed with ${resp.statusCode}: ${resp.body}'); + log('fetchUsername response ${resp.statusCode}: ${resp.body}'); + + // Try to get more info about the token by checking permissions endpoint + try { + print('AuthService: Checking token permissions...'); + final permResp = await http.get( + Uri.parse('https://api.openstreetmap.org/api/0.6/permissions.json'), + headers: {'Authorization': 'Bearer $accessToken'}, + ); + print('AuthService: Permissions response ${permResp.statusCode}: ${permResp.body}'); + } catch (e) { + print('AuthService: Error checking permissions: $e'); + } + + return null; + } + final userData = jsonDecode(resp.body); + final displayName = userData['user']?['display_name']; + print('AuthService: Extracted display name: $displayName'); + return displayName; + } catch (e) { + print('AuthService: Error fetching username: $e'); + log('Error fetching username: $e'); return null; } - return jsonDecode(resp.body)['user']?['display_name']; } } diff --git a/lib/services/uploader.dart b/lib/services/uploader.dart index 624f865..e0d2182 100644 --- a/lib/services/uploader.dart +++ b/lib/services/uploader.dart @@ -11,6 +11,8 @@ class Uploader { Future upload(PendingUpload p) async { try { + print('Uploader: Starting upload for camera at ${p.coord.latitude}, ${p.coord.longitude}'); + // 1. open changeset final csXml = ''' @@ -19,9 +21,15 @@ class Uploader { '''; - final csResp = await _post('/api/0.6/changeset/create', csXml); - if (csResp.statusCode != 200) return false; - final csId = csResp.body; + print('Uploader: Creating changeset...'); + final csResp = await _put('/api/0.6/changeset/create', csXml); + print('Uploader: Changeset response: ${csResp.statusCode} - ${csResp.body}'); + if (csResp.statusCode != 200) { + print('Uploader: Failed to create changeset'); + return false; + } + final csId = csResp.body.trim(); + print('Uploader: Created changeset ID: $csId'); // 2. create node final nodeXml = ''' @@ -33,15 +41,26 @@ class Uploader { '''; + print('Uploader: Creating node...'); final nodeResp = await _put('/api/0.6/node/create', nodeXml); - if (nodeResp.statusCode != 200) return false; + print('Uploader: Node response: ${nodeResp.statusCode} - ${nodeResp.body}'); + if (nodeResp.statusCode != 200) { + print('Uploader: Failed to create node'); + return false; + } + final nodeId = nodeResp.body.trim(); + print('Uploader: Created node ID: $nodeId'); // 3. close changeset - await _put('/api/0.6/changeset/$csId/close', ''); + print('Uploader: Closing changeset...'); + final closeResp = await _put('/api/0.6/changeset/$csId/close', ''); + print('Uploader: Close response: ${closeResp.statusCode}'); + print('Uploader: Upload successful!'); onSuccess(); return true; - } catch (_) { + } catch (e) { + print('Uploader: Upload failed with error: $e'); return false; } }