feat: add node deep link handling to DeepLinkService

Adds `deflockapp://node?id=<nodeId>` 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 <noreply@anthropic.com>
This commit is contained in:
flockhopperdev
2026-06-01 16:52:40 -06:00
committed by Claude Code
parent 8aab359f22
commit 74d8214dda
2 changed files with 88 additions and 0 deletions
+62
View File
@@ -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<Uri>? _linkSubscription;
/// Callback for HomeScreen to receive node deep links
void Function(OsmNode node)? onNodeDeepLink;
/// Initialize deep link handling (sets up stream listener only)
Future<void> 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=<nodeId>`
Future<void> _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<OsmNode?> _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 = <String, String>{};
if (e['tags'] != null) {
(e['tags'] as Map<String, dynamic>).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;
+26
View File
@@ -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);
});
});
}