From 8aab359f22087b2be582353a60f84589f0f48a02 Mon Sep 17 00:00:00 2001 From: flockhopperdev Date: Mon, 1 Jun 2026 16:50:55 -0600 Subject: [PATCH 1/4] feat: add node deep link intent filter Co-Authored-By: Claude Opus 4.6 --- android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a127420..c1bb3bf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ + From 74d8214dda455ca57be85c1fc9c3c639a2ede59e Mon Sep 17 00:00:00 2001 From: flockhopperdev Date: Mon, 1 Jun 2026 16:52:40 -0600 Subject: [PATCH 2/4] feat: add node deep link handling to DeepLinkService Adds `deflockapp://node?id=` deep link support: parses the node ID, fetches the OSM node via the public API, and delivers it via an `onNodeDeepLink` callback for HomeScreen to register. Includes URL parsing unit tests. Co-Authored-By: Claude Opus 4.6 --- lib/services/deep_link_service.dart | 62 +++++++++++++++++++++++++++++ test/deep_link_service_test.dart | 26 ++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 test/deep_link_service_test.dart diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart index 0c178b0..30166f6 100644 --- a/lib/services/deep_link_service.dart +++ b/lib/services/deep_link_service.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'dart:convert'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart'; import '../models/node_profile.dart'; import '../models/operator_profile.dart'; +import '../models/osm_node.dart'; import 'profile_import_service.dart'; import 'operator_profile_import_service.dart'; import '../screens/profile_editor.dart'; @@ -16,6 +20,9 @@ class DeepLinkService { late AppLinks _appLinks; StreamSubscription? _linkSubscription; + + /// Callback for HomeScreen to receive node deep links + void Function(OsmNode node)? onNodeDeepLink; /// Initialize deep link handling (sets up stream listener only) Future init() async { @@ -45,6 +52,9 @@ class DeepLinkService { case 'profiles': _handleProfilesLink(uri); break; + case 'node': + _handleNodeLink(uri); + break; case 'auth': // OAuth links are handled by flutter_web_auth_2 debugPrint('[DeepLinkService] OAuth link handled by flutter_web_auth_2'); @@ -71,6 +81,58 @@ class DeepLinkService { } } + /// Handle node deep link: `deflockapp://node?id=` + Future _handleNodeLink(Uri uri) async { + final idStr = uri.queryParameters['id']; + final nodeId = int.tryParse(idStr ?? ''); + if (nodeId == null) { + _showError('Invalid node link: missing or invalid ID'); + return; + } + + final node = await _fetchNodeById(nodeId); + if (node == null) { + _showError('Node $nodeId not found'); + return; + } + + if (onNodeDeepLink != null) { + onNodeDeepLink!(node); + } else { + debugPrint('[DeepLinkService] No node deep link handler registered'); + } + } + + /// Fetch an OSM node by ID from the OpenStreetMap API + Future _fetchNodeById(int nodeId) async { + try { + final url = Uri.parse('https://api.openstreetmap.org/api/0.6/node/$nodeId.json'); + final response = await http.get(url); + if (response.statusCode != 200) return null; + + final json = jsonDecode(response.body); + final elements = json['elements'] as List?; + if (elements == null || elements.isEmpty) return null; + + final e = elements[0]; + final tags = {}; + if (e['tags'] != null) { + (e['tags'] as Map).forEach((k, v) { + tags[k] = v.toString(); + }); + } + + return OsmNode( + id: e['id'] as int, + coord: LatLng((e['lat'] as num).toDouble(), (e['lon'] as num).toDouble()), + tags: tags, + ); + } catch (e) { + debugPrint('[DeepLinkService] Failed to fetch node $nodeId: $e'); + return null; + } + } + /// Handle profile-related deep links void _handleProfilesLink(Uri uri) { final segments = uri.pathSegments; diff --git a/test/deep_link_service_test.dart b/test/deep_link_service_test.dart new file mode 100644 index 0000000..0aefd95 --- /dev/null +++ b/test/deep_link_service_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Node deep link URL parsing', () { + test('extracts node ID from valid link', () { + final uri = Uri.parse('deflockapp://node?id=1234567890'); + expect(uri.host, 'node'); + expect(uri.queryParameters['id'], '1234567890'); + }); + + test('returns null for missing id param', () { + final uri = Uri.parse('deflockapp://node'); + expect(uri.queryParameters['id'], isNull); + }); + + test('returns null for empty id param', () { + final uri = Uri.parse('deflockapp://node?id='); + expect(uri.queryParameters['id'], ''); + }); + + test('returns null for non-numeric id', () { + final uri = Uri.parse('deflockapp://node?id=abc'); + expect(int.tryParse(uri.queryParameters['id'] ?? ''), isNull); + }); + }); +} From 480f99b3608dc875697c432ef3684f329b4e92b3 Mon Sep 17 00:00:00 2001 From: flockhopperdev Date: Mon, 1 Jun 2026 16:54:00 -0600 Subject: [PATCH 3/4] feat: wire HomeScreen to handle node deep links Co-Authored-By: Claude Sonnet 4.6 --- lib/screens/home_screen.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index acc3e03..f97c0e2 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -19,6 +19,7 @@ import '../models/osm_node.dart'; import '../models/suspected_location.dart'; import '../models/search_result.dart'; import '../services/changelog_service.dart'; +import '../services/deep_link_service.dart'; import 'coordinators/sheet_coordinator.dart'; import 'coordinators/navigation_coordinator.dart'; import 'coordinators/map_interaction_handler.dart'; @@ -57,10 +58,12 @@ class _HomeScreenState extends State with TickerProviderStateMixin { _sheetCoordinator = SheetCoordinator(); _navigationCoordinator = NavigationCoordinator(); _mapInteractionHandler = MapInteractionHandler(); + DeepLinkService().onNodeDeepLink = _handleNodeDeepLink; } @override void dispose() { + DeepLinkService().onNodeDeepLink = null; _mapController.dispose(); super.dispose(); } @@ -286,6 +289,27 @@ class _HomeScreenState extends State with TickerProviderStateMixin { ); } + void _handleNodeDeepLink(OsmNode node) { + // Fly to node at zoom 16 + try { + _mapController.animateTo( + dest: node.coord, + zoom: 16.0, + duration: const Duration(milliseconds: 600), + curve: Curves.easeOut, + ); + } catch (e) { + debugPrint('[HomeScreen] Could not animate to deep link node: $e'); + } + + // Open the node details sheet after map animation settles + Future.delayed(const Duration(milliseconds: 700), () { + if (mounted) { + openNodeTagSheet(node); + } + }); + } + void openNodeTagSheet(OsmNode node) { // Handle the map interaction (centering and follow-me disable) _mapInteractionHandler.handleNodeTap( From 191b5ccae048c9dfaec2d183840120342cb1e89b Mon Sep 17 00:00:00 2001 From: flockhopperdev Date: Mon, 1 Jun 2026 17:17:08 -0600 Subject: [PATCH 4/4] refactor: use UserAgentClient for OSM API fetch, remove redundant comments Co-Authored-By: Claude Opus 4.6 --- lib/screens/home_screen.dart | 2 -- lib/services/deep_link_service.dart | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index f97c0e2..c84eeab 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -290,7 +290,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { } void _handleNodeDeepLink(OsmNode node) { - // Fly to node at zoom 16 try { _mapController.animateTo( dest: node.coord, @@ -302,7 +301,6 @@ class _HomeScreenState extends State with TickerProviderStateMixin { debugPrint('[HomeScreen] Could not animate to deep link node: $e'); } - // Open the node details sheet after map animation settles Future.delayed(const Duration(milliseconds: 700), () { if (mounted) { openNodeTagSheet(node); diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart index 30166f6..cdc7012 100644 --- a/lib/services/deep_link_service.dart +++ b/lib/services/deep_link_service.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../models/osm_node.dart'; +import 'http_client.dart'; import 'profile_import_service.dart'; import 'operator_profile_import_service.dart'; import '../screens/profile_editor.dart'; @@ -107,7 +107,8 @@ class DeepLinkService { Future _fetchNodeById(int nodeId) async { try { final url = Uri.parse('https://api.openstreetmap.org/api/0.6/node/$nodeId.json'); - final response = await http.get(url); + final client = UserAgentClient(); + final response = await client.get(url); if (response.statusCode != 200) return null; final json = jsonDecode(response.body);