diff --git a/lib/app_state.dart b/lib/app_state.dart index bf26143..8c08314 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,6 +20,7 @@ import 'services/changelog_service.dart'; import 'services/operator_profile_service.dart'; import 'services/profile_service.dart'; import 'widgets/proximity_warning_dialog.dart'; +import 'widgets/reauth_messages_dialog.dart'; import 'dev_config.dart'; import 'state/auth_state.dart'; import 'state/messages_state.dart'; @@ -208,6 +211,13 @@ class AppState extends ChangeNotifier { await _uploadQueueState.init(); await _authState.init(_settingsState.uploadMode); + // Check for messages on app launch if user is already logged in + if (isLoggedIn) { + checkMessages(); + } + + // Note: Re-auth check will be triggered from home screen after init + // Initialize OfflineAreaService to ensure offline areas are loaded await OfflineAreaService().ensureInitialized(); @@ -284,6 +294,76 @@ class AppState extends ChangeNotifier { void clearMessages() { _messagesState.clearMessages(); } + + /// Check if the current OAuth token has required scopes for message notifications + /// Returns true if re-authentication is needed + Future needsReauthForMessages() async { + // Only check if logged in and not in simulate mode + if (!isLoggedIn || uploadMode == UploadMode.simulate) { + return false; + } + + final accessToken = await _authState.getAccessToken(); + if (accessToken == null) return false; + + try { + // Try to fetch user details - this should include message data if scope is correct + final response = await http.get( + Uri.parse('${_getApiHost()}/api/0.6/user/details.json'), + headers: {'Authorization': 'Bearer $accessToken'}, + ); + + if (response.statusCode == 403) { + // Forbidden - likely missing scope + return true; + } + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + final messages = data['user']?['messages']; + // If messages field is missing, we might not have the right scope + return messages == null; + } + + return false; + } catch (e) { + // On error, assume no re-auth needed to avoid annoying users + return false; + } + } + + /// Show re-authentication dialog if needed + Future checkAndPromptReauthForMessages(BuildContext context) async { + if (await needsReauthForMessages()) { + _showReauthDialog(context); + } + } + + void _showReauthDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => ReauthMessagesDialog( + onReauth: () { + // Navigate to OSM account page where user can re-authenticate + Navigator.of(context).pushNamed('/settings/osm-account'); + }, + onDismiss: () { + // Just dismiss - will show again on next app start or mode change + }, + ), + ); + } + + String _getApiHost() { + switch (uploadMode) { + case UploadMode.production: + return 'https://api.openstreetmap.org'; + case UploadMode.sandbox: + return 'https://api06.dev.openstreetmap.org'; + case UploadMode.simulate: + return 'https://api.openstreetmap.org'; + } + } // ---------- Profile Methods ---------- void toggleProfile(NodeProfile p, bool e) { @@ -503,6 +583,8 @@ class AppState extends ChangeNotifier { if (isLoggedIn) { // Don't await - let it run in background checkMessages(); + + // Note: Re-auth check will be triggered from the settings screen after mode change } _startUploader(); // Restart uploader with new mode diff --git a/lib/localizations/de.json b/lib/localizations/de.json index ee07e4a..5752533 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -183,7 +183,12 @@ "currentDestinationSimulate": "Derzeit im: Simulationsmodus (kein echtes Konto)", "viewMessages": "Nachrichten auf OSM anzeigen", "unreadMessagesCount": "Sie haben {} ungelesene Nachrichten", - "noUnreadMessages": "Keine ungelesenen Nachrichten" + "noUnreadMessages": "Keine ungelesenen Nachrichten", + "reauthRequired": "Authentifizierung aktualisieren", + "reauthExplanation": "Sie müssen Ihre Authentifizierung aktualisieren, um OSM-Nachrichtenbenachrichtigungen über die App zu erhalten.", + "reauthBenefit": "Dies ermöglicht Benachrichtigungspunkte, wenn Sie ungelesene Nachrichten auf OpenStreetMap haben.", + "reauthNow": "Jetzt machen", + "reauthLater": "Später" }, "queue": { "title": "Upload-Warteschlange", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 05953a5..3fdbc75 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "Currently in: Simulate mode (no real account)", "viewMessages": "View Messages on OSM", "unreadMessagesCount": "You have {} unread messages", - "noUnreadMessages": "No unread messages" + "noUnreadMessages": "No unread messages", + "reauthRequired": "Refresh Authentication", + "reauthExplanation": "You must refresh your authentication to receive OSM message notifications through the app.", + "reauthBenefit": "This will enable notification dots when you have unread messages on OpenStreetMap.", + "reauthNow": "Do That Now", + "reauthLater": "Later" }, "queue": { "title": "Upload Queue", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index abd53b4..410665a 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "Actualmente en: Modo de simulación (sin cuenta real)", "viewMessages": "Ver Mensajes en OSM", "unreadMessagesCount": "Tienes {} mensajes sin leer", - "noUnreadMessages": "No hay mensajes sin leer" + "noUnreadMessages": "No hay mensajes sin leer", + "reauthRequired": "Actualizar Autenticación", + "reauthExplanation": "Debes actualizar tu autenticación para recibir notificaciones de mensajes OSM a través de la aplicación.", + "reauthBenefit": "Esto habilitará puntos de notificación cuando tengas mensajes sin leer en OpenStreetMap.", + "reauthNow": "Hazlo Ahora", + "reauthLater": "Más Tarde" }, "queue": { "title": "Cola de Subida", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index d521e95..9d74089 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "Actuellement en : Mode simulation (pas de compte réel)", "viewMessages": "Voir les Messages sur OSM", "unreadMessagesCount": "Vous avez {} messages non lus", - "noUnreadMessages": "Aucun message non lu" + "noUnreadMessages": "Aucun message non lu", + "reauthRequired": "Actualiser l'Authentification", + "reauthExplanation": "Vous devez actualiser votre authentification pour recevoir des notifications de messages OSM via l'application.", + "reauthBenefit": "Cela activera les points de notification lorsque vous avez des messages non lus sur OpenStreetMap.", + "reauthNow": "Le Faire Maintenant", + "reauthLater": "Plus Tard" }, "queue": { "title": "File de Téléchargement", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index fdbef0e..8ca98cd 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "Attualmente in: Modalità simulazione (nessun account reale)", "viewMessages": "Visualizza Messaggi su OSM", "unreadMessagesCount": "Hai {} messaggi non letti", - "noUnreadMessages": "Nessun messaggio non letto" + "noUnreadMessages": "Nessun messaggio non letto", + "reauthRequired": "Aggiorna Autenticazione", + "reauthExplanation": "Devi aggiornare la tua autenticazione per ricevere notifiche di messaggi OSM tramite l'app.", + "reauthBenefit": "Questo abiliterà i punti di notifica quando hai messaggi non letti su OpenStreetMap.", + "reauthNow": "Fallo Ora", + "reauthLater": "Più Tardi" }, "queue": { "title": "Coda di Upload", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 766feb6..e7bd20d 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "Atualmente em: Modo de simulação (sem conta real)", "viewMessages": "Ver Mensagens no OSM", "unreadMessagesCount": "Você tem {} mensagens não lidas", - "noUnreadMessages": "Nenhuma mensagem não lida" + "noUnreadMessages": "Nenhuma mensagem não lida", + "reauthRequired": "Atualizar Autenticação", + "reauthExplanation": "Você deve atualizar sua autenticação para receber notificações de mensagens OSM através do aplicativo.", + "reauthBenefit": "Isso habilitará pontos de notificação quando você tiver mensagens não lidas no OpenStreetMap.", + "reauthNow": "Fazer Agora", + "reauthLater": "Mais Tarde" }, "queue": { "title": "Fila de Upload", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 3d77592..778c689 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -215,7 +215,12 @@ "currentDestinationSimulate": "当前处于:模拟模式(无真实账户)", "viewMessages": "在 OSM 上查看消息", "unreadMessagesCount": "您有 {} 条未读消息", - "noUnreadMessages": "没有未读消息" + "noUnreadMessages": "没有未读消息", + "reauthRequired": "刷新身份验证", + "reauthExplanation": "您必须刷新身份验证才能通过应用接收 OSM 消息通知。", + "reauthBenefit": "这将在您在 OpenStreetMap 上有未读消息时启用通知点。", + "reauthNow": "现在执行", + "reauthLater": "稍后" }, "queue": { "title": "上传队列", diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 007011b..f8fef26 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -673,7 +673,11 @@ class _HomeScreenState extends State with TickerProviderStateMixin { // Check for welcome/changelog popup after app is fully initialized if (appState.isInitialized && !_hasCheckedForPopup) { _hasCheckedForPopup = true; - WidgetsBinding.instance.addPostFrameCallback((_) => _checkForPopup()); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkForPopup(); + // Check if re-authentication is needed for message notifications + appState.checkAndPromptReauthForMessages(context); + }); } // Pass the active sheet height directly to the map @@ -720,7 +724,7 @@ class _HomeScreenState extends State with TickerProviderStateMixin { icon: Stack( children: [ const Icon(Icons.settings), - if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate) + if (appState.hasUnreadMessages) Positioned( right: 0, top: 0, diff --git a/lib/screens/osm_account_screen.dart b/lib/screens/osm_account_screen.dart index 285c46e..11135ca 100644 --- a/lib/screens/osm_account_screen.dart +++ b/lib/screens/osm_account_screen.dart @@ -239,8 +239,8 @@ class _OSMAccountScreenState extends State { ), ), - // Account deletion section - only show when logged in - if (appState.isLoggedIn) ...[ + // Account deletion section - only show when logged in and not in simulate mode + if (appState.isLoggedIn && appState.uploadMode != UploadMode.simulate) ...[ const SizedBox(height: 16), _buildAccountDeletionSection(context, appState), ], diff --git a/lib/screens/settings/sections/upload_mode_section.dart b/lib/screens/settings/sections/upload_mode_section.dart index 6aefeba..ae4c192 100644 --- a/lib/screens/settings/sections/upload_mode_section.dart +++ b/lib/screens/settings/sections/upload_mode_section.dart @@ -38,7 +38,13 @@ class UploadModeSection extends StatelessWidget { ), ], onChanged: (mode) { - if (mode != null) appState.setUploadMode(mode); + if (mode != null) { + appState.setUploadMode(mode); + // Check if re-authentication is needed after mode change + WidgetsBinding.instance.addPostFrameCallback((_) { + appState.checkAndPromptReauthForMessages(context); + }); + } }, ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f63e48a..ea88105 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -120,7 +120,7 @@ class SettingsScreen extends StatelessWidget { leading: Stack( children: [ const Icon(Icons.account_circle), - if (appState.hasUnreadMessages && appState.uploadMode != UploadMode.simulate) + if (appState.hasUnreadMessages) Positioned( right: 0, top: 0, diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 53809eb..bdc1b9c 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -51,7 +51,7 @@ class AuthService { _helper = OAuth2Helper( client, clientId: clientId, - scopes: ['read_prefs', 'write_api'], + scopes: ['read_prefs', 'write_api', 'consume_messages'], enablePKCE: true, // tokenStorageKey: _tokenKey, // not supported by this package version ); diff --git a/lib/services/osm_messages_service.dart b/lib/services/osm_messages_service.dart index 3627d12..5bbde87 100644 --- a/lib/services/osm_messages_service.dart +++ b/lib/services/osm_messages_service.dart @@ -55,8 +55,11 @@ class OSMMessagesService { final messages = user['messages']; if (messages == null) return null; - // Get unread count - final unreadCount = messages['unread']?['count'] ?? 0; + // Get unread count from correct path: messages.received.unread + final received = messages['received']; + if (received == null) return null; + + final unreadCount = received['unread'] ?? 0; // Update cache _lastCheck = DateTime.now(); diff --git a/lib/widgets/reauth_messages_dialog.dart b/lib/widgets/reauth_messages_dialog.dart new file mode 100644 index 0000000..891f7c4 --- /dev/null +++ b/lib/widgets/reauth_messages_dialog.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import '../services/localization_service.dart'; + +/// Dialog to prompt user to re-authenticate for message notifications +class ReauthMessagesDialog extends StatelessWidget { + final VoidCallback onReauth; + final VoidCallback onDismiss; + + const ReauthMessagesDialog({ + super.key, + required this.onReauth, + required this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + final locService = LocalizationService.instance; + + return AlertDialog( + title: Row( + children: [ + Icon( + Icons.message_outlined, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text(locService.t('auth.reauthRequired')), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(locService.t('auth.reauthExplanation')), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + size: 16, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + locService.t('auth.reauthBenefit'), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + onDismiss(); + }, + child: Text(locService.t('auth.reauthLater')), + ), + FilledButton.icon( + onPressed: () { + Navigator.of(context).pop(); + onReauth(); + }, + icon: const Icon(Icons.refresh, size: 18), + label: Text(locService.t('auth.reauthNow')), + ), + ], + ); + } +} \ No newline at end of file