diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f698..83d4e12 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,5 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
diff --git a/android/app/src/debug/res/xml/network_security_config.xml b/android/app/src/debug/res/xml/network_security_config.xml
new file mode 100644
index 0000000..3f1b761
--- /dev/null
+++ b/android/app/src/debug/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+ 10.0.2.2
+ 127.0.0.1
+ localhost
+
+
diff --git a/lib/features/routes/widgets/address_autocomplete_field.dart b/lib/features/routes/widgets/address_autocomplete_field.dart
index 1d3cb25..c88f0f1 100644
--- a/lib/features/routes/widgets/address_autocomplete_field.dart
+++ b/lib/features/routes/widgets/address_autocomplete_field.dart
@@ -1,4 +1,4 @@
-import 'dart:async';
+import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/theme_extensions.dart';
@@ -72,6 +72,8 @@ class _AddressAutocompleteFieldState
bool _showDropdown = false;
bool _hasSearched = false;
+ String _lastText = '';
+
@override
void initState() {
super.initState();
@@ -92,8 +94,17 @@ class _AddressAutocompleteFieldState
}
_resolvedPlace = widget.initialPlace;
+ _lastText = _controller.text;
_focusNode.addListener(_handleFocusChange);
+ _controller.addListener(_handleControllerChange);
+ }
+
+ void _handleControllerChange() {
+ if (_controller.text != _lastText) {
+ _lastText = _controller.text;
+ _onTextChanged(_controller.text);
+ }
}
@override
@@ -105,6 +116,7 @@ class _AddressAutocompleteFieldState
if (_controller.text != newText) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _controller.text != newText) {
+ _lastText = newText;
_controller.text = newText;
}
});
@@ -115,6 +127,7 @@ class _AddressAutocompleteFieldState
@override
void dispose() {
_debounceTimer?.cancel();
+ _controller.removeListener(_handleControllerChange);
_focusNode.removeListener(_handleFocusChange);
if (_internalFocusNode) {
_focusNode.dispose();
@@ -232,6 +245,7 @@ class _AddressAutocompleteFieldState
void _applyResolvedPlace(ResolvedPlace place) {
_resolvedPlace = place;
+ _lastText = place.displayName;
_controller.text = place.displayName;
setState(() {
_isResolving = false;
@@ -244,6 +258,7 @@ class _AddressAutocompleteFieldState
}
void _clear() {
+ _lastText = '';
_controller.clear();
_resolvedPlace = null;
_suggestions = const [];
diff --git a/lib/services/auth/auth_service.dart b/lib/services/auth/auth_service.dart
index dd2f5d9..4f9ffa7 100644
--- a/lib/services/auth/auth_service.dart
+++ b/lib/services/auth/auth_service.dart
@@ -20,6 +20,7 @@ abstract class AuthService {
Future createUserWithEmailAndPassword(String email, String password, {String? displayName});
Future signInWithGoogle();
Future signInAnonymously();
+ Future ensureBackendIdentity();
Future sendPasswordResetEmail(String email);
Future sendEmailVerification() async => throw UnsupportedError('Email verification is not supported by this auth service.');
Future reloadCurrentUser() async => throw UnsupportedError('User reload is not supported by this auth service.');
@@ -351,6 +352,38 @@ class FirebaseAuthService implements AuthService {
}
}
+ Future? _identityBootstrapFuture;
+
+ @override
+ Future ensureBackendIdentity() {
+ final auth = _firebaseAuth;
+ if (auth != null && auth.currentUser != null) {
+ return Future.value();
+ }
+
+ _identityBootstrapFuture ??= _performIdentityBootstrap().whenComplete(() {
+ _identityBootstrapFuture = null;
+ });
+ return _identityBootstrapFuture!;
+ }
+
+ Future _performIdentityBootstrap() async {
+ final auth = _firebaseAuth;
+ if (auth == null || !isConfigured) {
+ throw StateError('Firebase is not configured. Backend identity cannot be established.');
+ }
+ if (auth.currentUser != null) return;
+ try {
+ final credential = await auth.signInAnonymously();
+ if (credential.user != null) {
+ final user = _mapFirebaseUser(credential.user!);
+ _updateState(AuthState.anonymous(user, isFirebaseConfigured: true));
+ }
+ } catch (e) {
+ throw Exception('Backend identity bootstrap failed: $e');
+ }
+ }
+
@override
Future sendPasswordResetEmail(String email) async {
final auth = _firebaseAuth;
diff --git a/lib/services/map/open_street_map_provider.dart b/lib/services/map/open_street_map_provider.dart
index 44583e2..7129b04 100644
--- a/lib/services/map/open_street_map_provider.dart
+++ b/lib/services/map/open_street_map_provider.dart
@@ -5,10 +5,6 @@ import 'package:latlong2/latlong.dart' as ll;
import 'map_provider_interface.dart';
class OpenStreetMapProvider implements AppMapProvider {
- final MapController _mapController = MapController();
- ll.LatLng? _lastFollowPos;
- DateTime? _lastFollowTime;
-
@override
AppMapType get type => AppMapType.openStreetMap;
@@ -27,9 +23,60 @@ class OpenStreetMapProvider implements AppMapProvider {
required ValueChanged? onPositionChanged,
required VoidCallback? onUserInteraction,
}) {
- final centerLl = ll.LatLng(center.latitude, center.longitude);
+ return _OsmMapView(
+ center: center,
+ zoom: zoom,
+ markers: markers,
+ polylines: polylines,
+ isFollowMode: isFollowMode,
+ config: config,
+ onPositionChanged: onPositionChanged,
+ onUserInteraction: onUserInteraction,
+ );
+ }
+}
- if (isFollowMode) {
+class _OsmMapView extends StatefulWidget {
+ final AppLatLng center;
+ final double zoom;
+ final List markers;
+ final List polylines;
+ final bool isFollowMode;
+ final MapConfig config;
+ final ValueChanged? onPositionChanged;
+ final VoidCallback? onUserInteraction;
+
+ const _OsmMapView({
+ required this.center,
+ required this.zoom,
+ required this.markers,
+ required this.polylines,
+ required this.isFollowMode,
+ required this.config,
+ required this.onPositionChanged,
+ required this.onUserInteraction,
+ });
+
+ @override
+ State<_OsmMapView> createState() => _OsmMapViewState();
+}
+
+class _OsmMapViewState extends State<_OsmMapView> {
+ late final MapController _mapController;
+ ll.LatLng? _lastFollowPos;
+ DateTime? _lastFollowTime;
+
+ @override
+ void initState() {
+ super.initState();
+ _mapController = MapController();
+ }
+
+ @override
+ void didUpdateWidget(covariant _OsmMapView oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.isFollowMode) {
+ final centerLl = ll.LatLng(widget.center.latitude, widget.center.longitude);
final now = DateTime.now();
final posDelta = _lastFollowPos == null
? 1.0
@@ -43,13 +90,25 @@ class OpenStreetMapProvider implements AppMapProvider {
_lastFollowPos = centerLl;
_lastFollowTime = now;
WidgetsBinding.instance.addPostFrameCallback((_) {
- try {
- _mapController.move(centerLl, zoom);
- } catch (_) {}
+ if (mounted) {
+ try {
+ _mapController.move(centerLl, widget.zoom);
+ } catch (_) {}
+ }
});
}
}
+ }
+ @override
+ void dispose() {
+ _mapController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final centerLl = ll.LatLng(widget.center.latitude, widget.center.longitude);
final media = MediaQuery.of(context);
final sizeKey = '${media.orientation}_${media.size.width.toInt()}x${media.size.height.toInt()}';
@@ -58,30 +117,30 @@ class OpenStreetMapProvider implements AppMapProvider {
mapController: _mapController,
options: MapOptions(
initialCenter: centerLl,
- initialZoom: zoom,
- minZoom: config.minZoom,
- maxZoom: config.maxZoom,
+ initialZoom: widget.zoom,
+ minZoom: widget.config.minZoom,
+ maxZoom: widget.config.maxZoom,
backgroundColor: const Color(0xFF0F0F0F),
onPositionChanged: (position, hasGesture) {
if (hasGesture) {
- onUserInteraction?.call();
+ widget.onUserInteraction?.call();
}
- if (onPositionChanged != null) {
+ if (widget.onPositionChanged != null) {
final c = position.center;
- onPositionChanged(AppLatLng(c.latitude, c.longitude));
+ widget.onPositionChanged!(AppLatLng(c.latitude, c.longitude));
}
},
),
children: [
TileLayer(
- urlTemplate: config.tileUrlTemplate,
- subdomains: config.subdomains,
+ urlTemplate: widget.config.tileUrlTemplate,
+ subdomains: widget.config.subdomains,
userAgentPackageName: 'app.motorsignal.motorcycle',
maxZoom: 20,
maxNativeZoom: 19,
),
PolylineLayer(
- polylines: polylines.map((p) {
+ polylines: widget.polylines.map((p) {
return Polyline(
points: p.points.map((pt) => ll.LatLng(pt.latitude, pt.longitude)).toList(),
strokeWidth: p.width,
@@ -90,7 +149,7 @@ class OpenStreetMapProvider implements AppMapProvider {
}).toList(),
),
MarkerLayer(
- markers: markers.map((m) {
+ markers: widget.markers.map((m) {
return Marker(
point: ll.LatLng(m.position.latitude, m.position.longitude),
width: 50,
@@ -113,7 +172,7 @@ class OpenStreetMapProvider implements AppMapProvider {
RichAttributionWidget(
attributions: [
TextSourceAttribution(
- config.attributionText,
+ widget.config.attributionText,
textStyle: const TextStyle(color: Colors.white70, fontSize: 10),
),
],
diff --git a/lib/services/routes/route_providers.dart b/lib/services/routes/route_providers.dart
index ef7db2c..12a4325 100644
--- a/lib/services/routes/route_providers.dart
+++ b/lib/services/routes/route_providers.dart
@@ -5,15 +5,21 @@ import 'navigation_handoff.dart';
import 'route_models.dart';
import 'route_provider.dart';
import 'route_service.dart';
-
+import 'valhalla_route_provider.dart';
/// Provider for RouteService.
final routeServiceProvider = Provider((ref) {
return RouteService();
});
-/// Active RouteProvider implementation (defaults to honest straight-line preview).
+/// Active RouteProvider implementation.
final activeRouteCalculationProvider = Provider((ref) {
- return const StraightLineRouteProvider();
+ final authService = ref.watch(authServiceProvider);
+ return ValhallaRouteProvider(
+ tokenProvider: () async {
+ await authService.ensureBackendIdentity();
+ return await authService.getIdToken(true);
+ },
+ );
});
/// Provider for NavigationHandoffService.
diff --git a/lib/services/routes/valhalla_route_provider.dart b/lib/services/routes/valhalla_route_provider.dart
index eb1f679..244a2ba 100644
--- a/lib/services/routes/valhalla_route_provider.dart
+++ b/lib/services/routes/valhalla_route_provider.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/foundation.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
@@ -11,20 +12,32 @@ import 'valhalla_polyline_decoder.dart';
class ValhallaRouteProvider implements RouteProvider {
final String backendUrl;
final http.Client _client;
- final Future Function()? _tokenProvider;
+ final Future Function()? tokenProvider;
ValhallaRouteProvider({
- this.backendUrl = 'http://10.0.2.2:3000',
+ String? backendUrl,
http.Client? client,
- this._tokenProvider,
- }) : _client = client ?? http.Client();
+ this.tokenProvider,
+ }) : _client = client ?? http.Client(),
+ backendUrl = backendUrl ?? _getEffectiveBackendUrl();
+
+ static String _getEffectiveBackendUrl() {
+ const definedUrl = String.fromEnvironment('MOTORSIGNAL_API_BASE_URL');
+ if (definedUrl.isNotEmpty) {
+ return definedUrl;
+ }
+ if (kReleaseMode) {
+ throw StateError('MOTORSIGNAL_API_BASE_URL is not configured for production build.');
+ }
+ return 'http://10.0.2.2:3000';
+ }
@override
String get name => 'Stadia Maps (Valhalla)';
Future