From 99d6ad8a444242cb3da2689f28d35baa73726362 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 19 Jul 2025 15:59:05 -0500 Subject: [PATCH] submissions still not working, but oauth implemented and add camera sheet working. --- android/app/src/main/AndroidManifest.xml | 4 +- lib/app_state.dart | 18 ++++++-- lib/screens/home_screen.dart | 37 +++++++--------- lib/services/auth_service.dart | 54 ++++++++++++------------ lib/widgets/add_camera_sheet.dart | 25 +++++++---- lib/widgets/map_view.dart | 6 +-- 6 files changed, 78 insertions(+), 66 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ceb8589..65034ea 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,8 +7,8 @@ - + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> { + final GlobalKey _scaffoldKey = GlobalKey(); bool _followMe = true; - Future _startAddCamera(BuildContext context) async { + void _openAddCameraSheet() { final appState = context.read(); appState.startAddSession(); + final session = appState.session!; // guaranteed non‑null now - final submitted = await showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: false, - isDismissible: false, - builder: (_) => const AddCameraSheet(), + _scaffoldKey.currentState!.showBottomSheet( + (ctx) => AddCameraSheet(session: session), ); - - if (submitted == true) { - appState.commitSession(); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Camera queued'))); - } - } else { - appState.cancelSession(); - } } @override Widget build(BuildContext context) { + final appState = context.watch(); + return Scaffold( + key: _scaffoldKey, appBar: AppBar( title: const Text('Flock Map'), actions: [ @@ -61,11 +52,13 @@ class _HomeScreenState extends State { if (_followMe) setState(() => _followMe = false); }, ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => _startAddCamera(context), - icon: const Icon(Icons.add_location_alt), - label: const Text('Tag Camera'), - ), + floatingActionButton: appState.session == null + ? FloatingActionButton.extended( + onPressed: _openAddCameraSheet, + icon: const Icon(Icons.add_location_alt), + label: const Text('Tag Camera'), + ) + : null, ); } } diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 081d874..950340b 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,56 +1,54 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:oauth2_client/oauth2_client.dart'; import 'package:oauth2_client/oauth2_helper.dart'; import 'package:http/http.dart' as http; -/// Handles OAuth 2 PKCE login to OpenStreetMap and exposes -/// the stored access token & display name. -/// -/// ─ Requirements ─ -/// • Register an OAuth app at -/// https://www.openstreetmap.org/oauth2/applications -/// ‑ Redirect URI: flockmap://auth -/// • Put that client ID below (replace 'flockmap'). +/// Handles PKCE OAuth login with OpenStreetMap. class AuthService { - static const _clientId = 'lzEr2zjBGZ2TvJWr3QGxNcKxigp-mQ6pRWIUhI_Bqx8'; + /// Paste the **client ID** shown on the OSM OAuth2 application page + /// (it can be alphanumeric like ‘lzEr2zjBGZ2…’). + static const String _clientId = 'lzEr2zjBGZ2TvJWr3QGxNcKxigp-mQ6pRWIUhI_Bqx8'; static const _redirect = 'flockmap://auth'; late final OAuth2Helper _helper; - - String? _displayName; // cached after login - String? get displayName => _displayName; + String? _displayName; AuthService() { final client = OAuth2Client( authorizeUrl: 'https://www.openstreetmap.org/oauth2/authorize', tokenUrl: 'https://www.openstreetmap.org/oauth2/token', redirectUri: _redirect, - customUriScheme: 'flockmap', // matches redirect scheme + customUriScheme: 'flockmap', ); _helper = OAuth2Helper( client, clientId: _clientId, scopes: ['write_api'], - enablePKCE: true, // PKCE flow - // No custom token store needed: oauth2_client will - // auto‑use flutter_secure_storage when present. + enablePKCE: true, ); } - /* ───────── Public helpers ───────── */ - - /// Returns `true` if a non‑expired token is stored. Future isLoggedIn() async => (await _helper.getTokenFromStorage())?.isExpired() == false; - /// Launches browser login if necessary; caches display name. + String? get displayName => _displayName; + Future login() async { - final token = await _helper.getToken(); - if (token?.accessToken == null) return null; - _displayName = await _fetchUsername(token!.accessToken!); - return _displayName; + try { + final token = await _helper.getToken(); + if (token?.accessToken == null) { + log('OAuth error: token null or missing accessToken'); + return null; + } + _displayName = await _fetchUsername(token!.accessToken!); + return _displayName; + } catch (e) { + log('OAuth login failed: $e'); + rethrow; + } } Future logout() async { @@ -58,18 +56,20 @@ class AuthService { _displayName = null; } - /// Safely fetch current access token (or null). Future getAccessToken() async => (await _helper.getTokenFromStorage())?.accessToken; - /* ───────── Internal ───────── */ + /* ───────── 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) return null; + if (resp.statusCode != 200) { + log('fetchUsername response ${resp.statusCode}: ${resp.body}'); + return null; + } return jsonDecode(resp.body)['user']?['display_name']; } } diff --git a/lib/widgets/add_camera_sheet.dart b/lib/widgets/add_camera_sheet.dart index e441fcf..845df6d 100644 --- a/lib/widgets/add_camera_sheet.dart +++ b/lib/widgets/add_camera_sheet.dart @@ -5,19 +5,26 @@ import '../app_state.dart'; import '../models/camera_profile.dart'; class AddCameraSheet extends StatelessWidget { - const AddCameraSheet({super.key}); + const AddCameraSheet({super.key, required this.session}); + + final AddCameraSession session; @override Widget build(BuildContext context) { final appState = context.watch(); - final session = appState.session; - // If the session was cleared before this frame, bail safely. - if (session == null) { - return const SizedBox.shrink(); + void _commit() { + appState.commitSession(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Camera queued for upload')), + ); } - final profiles = appState.profiles; + void _cancel() { + appState.cancelSession(); + Navigator.pop(context); + } return Padding( padding: @@ -39,7 +46,7 @@ class AddCameraSheet extends StatelessWidget { title: const Text('Profile'), trailing: DropdownButton( value: session.profile, - items: profiles + items: appState.profiles .map((p) => DropdownMenuItem(value: p, child: Text(p.name))) .toList(), onChanged: (p) => @@ -64,14 +71,14 @@ class AddCameraSheet extends StatelessWidget { children: [ Expanded( child: OutlinedButton( - onPressed: () => Navigator.pop(context, false), + onPressed: _cancel, child: const Text('Cancel'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: _commit, child: const Text('Submit'), ), ), diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 17a8d11..fb7b569 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -173,13 +173,13 @@ class _MapViewState extends State { // Attribution overlay Positioned( - bottom: 8, - right: 8, + bottom: 20, + left: 10, child: Container( color: Colors.white70, padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: const Text( - '© OpenStreetMap contributors', + '© OpenStreetMap and contributors', style: TextStyle(fontSize: 11), ), ),