diff --git a/example/lib/main.dart b/example/lib/main.dart index 0f31e32..31f78e8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -77,9 +77,14 @@ class MainScreenState extends State with TickerProviderStateMixin { // Legacy API Example (Backward Compatibility) class LegacyMapScreen extends StatefulWidget { - final String apiKey; + final String? apiKey; + final Uri? proxy; - const LegacyMapScreen({super.key, required this.apiKey}); + const LegacyMapScreen({ + super.key, + this.proxy, + this.apiKey, + }); @override LegacyMapScreenState createState() => LegacyMapScreenState(); @@ -99,7 +104,7 @@ class LegacyMapScreenState extends State { @override void initState() { super.initState(); - polylinePoints = PolylinePoints(apiKey: widget.apiKey); + polylinePoints = PolylinePoints(proxy: widget.proxy, apiKey: widget.apiKey); _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); @@ -246,9 +251,15 @@ class LegacyMapScreenState extends State { // Custom Headers Example (Android-restricted API keys) class CustomHeadersMapScreen extends StatefulWidget { - final String apiKey; + final String? apiKey; - const CustomHeadersMapScreen({super.key, required this.apiKey}); + final Uri? proxy; + + const CustomHeadersMapScreen({ + super.key, + this.proxy, + this.apiKey, + }); @override CustomHeadersMapScreenState createState() => CustomHeadersMapScreenState(); @@ -274,7 +285,8 @@ class CustomHeadersMapScreenState extends State { @override void initState() { super.initState(); - polylinePoints = PolylinePoints.enhanced(widget.apiKey); + polylinePoints = + PolylinePoints.enhanced(proxy: widget.proxy, apiKey: widget.apiKey); _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", @@ -497,9 +509,15 @@ class CustomHeadersMapScreenState extends State { // Routes API Example (Enhanced Features) class RoutesApiMapScreen extends StatefulWidget { - final String apiKey; + final String? apiKey; + + final Uri? proxy; - const RoutesApiMapScreen({super.key, required this.apiKey}); + const RoutesApiMapScreen({ + super.key, + this.proxy, + this.apiKey, + }); @override RoutesApiMapScreenState createState() => RoutesApiMapScreenState(); @@ -520,7 +538,8 @@ class RoutesApiMapScreenState extends State { @override void initState() { super.initState(); - polylinePoints = PolylinePoints.enhanced(widget.apiKey); + polylinePoints = + PolylinePoints.enhanced(proxy: widget.proxy, apiKey: widget.apiKey); _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", @@ -762,9 +781,11 @@ class RoutesApiMapScreenState extends State { // Two-Wheeler Example (Routes API Exclusive Feature) class TwoWheelerMapScreen extends StatefulWidget { - final String apiKey; + final String? apiKey; - const TwoWheelerMapScreen({super.key, required this.apiKey}); + final Uri? proxy; + + const TwoWheelerMapScreen({super.key, this.proxy, this.apiKey}); @override TwoWheelerMapScreenState createState() => TwoWheelerMapScreenState(); @@ -786,7 +807,10 @@ class TwoWheelerMapScreenState extends State { @override void initState() { super.initState(); - polylinePointsV2 = PolylinePoints.enhanced(widget.apiKey); + polylinePointsV2 = PolylinePoints.enhanced( + proxy: widget.proxy, + apiKey: widget.apiKey, + ); _addMarker(LatLng(_originLatitude, _originLongitude), "origin", BitmapDescriptor.defaultMarker); _addMarker(LatLng(_destLatitude, _destLongitude), "destination", diff --git a/lib/src/network/network_provider.dart b/lib/src/network/network_provider.dart index ef38d07..2a574e2 100644 --- a/lib/src/network/network_provider.dart +++ b/lib/src/network/network_provider.dart @@ -26,16 +26,27 @@ class NetworkProvider { /// Get route using legacy Directions API (for backward compatibility) /// Supports only basic features /// + /// @param [proxy] - Proxy the request to hide the google key application on your requests parameters to prevent bad actors from reaching and using the key exposed in the code. /// @param [apiKey] - Google Maps API key /// @param [PolylineRequest] - PolylineRequest object /// @param [timeout] - Optional timeout for the request /// @return [PolylineResult] object with decoded points static Future getRouteBetweenCoordinates( - String googleApiKey, + Uri? proxy, + String? googleApiKey, PolylineRequest request, { Duration? timeout, }) async { - final uri = _buildDirectionsUri(googleApiKey, request); + assert( + (proxy == null && googleApiKey != null && googleApiKey.isNotEmpty) || + (proxy != null && googleApiKey == null), + "Google API Key cannot be empty if proxy isn't provided"); + + final uri = _buildDirectionsUri( + proxy: proxy, + apiKey: googleApiKey, + request: request, + ); try { final response = await http @@ -62,19 +73,26 @@ class NetworkProvider { /// Get route using new Routes API with enhanced features /// Supports all Routes API features /// + /// @param [proxy] - Proxy the request to hide the google key application on your requests parameters to prevent bad actors from reaching and using the key exposed in the code. /// @param [apiKey] - Google Maps API key /// @param [RoutesApiRequest] - RoutesApiRequest object /// @param [timeout] - Optional timeout for the request /// @return [RoutesApiResponse] object with decoded points static Future getRouteBetweenCoordinatesV2( - String googleApiKey, + Uri? proxy, + String? googleApiKey, RoutesApiRequest request, { Duration? timeout, }) async { + assert( + (proxy == null && googleApiKey != null && googleApiKey.isNotEmpty) || + (proxy != null && googleApiKey == null), + "Google API Key cannot be empty if proxy isn't provided"); + try { final response = await http .post( - Uri.parse(_routesBaseUrl), + proxy ?? Uri.parse(_routesBaseUrl), headers: _getRoutesHeaders(googleApiKey, request), body: json.encode(request.toJson()), ) @@ -97,14 +115,22 @@ class NetworkProvider { } /// Build URI for legacy Directions API - static Uri _buildDirectionsUri(String apiKey, PolylineRequest request) { + static Uri _buildDirectionsUri({ + Uri? proxy, + String? apiKey, + required PolylineRequest request, + }) { final queryParams = { - 'key': apiKey, 'origin': _formatLocation(request.origin), 'destination': _formatLocation(request.destination), 'mode': request.mode.name, }; + // Adds Api key to parameters if given. + if (apiKey != null) { + queryParams.addAll({'key': apiKey}); + } + // Add waypoints if present if (request.wayPoints.isNotEmpty) { final waypoints = request.wayPoints @@ -133,7 +159,8 @@ class NetworkProvider { queryParams['optimize'] = 'true'; } - return Uri.parse(_directionsBaseUrl).replace(queryParameters: queryParams); + return (proxy ?? Uri.parse(_directionsBaseUrl)) + .replace(queryParameters: queryParams); } /// Format location for API request @@ -163,16 +190,21 @@ class NetworkProvider { /// Get headers for Routes API static Map _getRoutesHeaders( - String apiKey, + String? apiKey, RoutesApiRequest request, ) { final headers = { 'Content-Type': 'application/json', - 'X-Goog-Api-Key': apiKey, 'X-Goog-FieldMask': request.getFieldMask(), 'User-Agent': 'flutter_polyline_points/3.0.0', }; + if (apiKey != null) { + headers.addAll({ + 'X-Goog-Api-Key': apiKey, + }); + } + // Add optional headers if (request.languageCode != null) { headers['Accept-Language'] = request.languageCode!; @@ -238,7 +270,10 @@ class NetworkProvider { } /// Check API availability and capabilities - static Future> checkApiAvailability(String apiKey) async { + static Future> checkApiAvailability( + Uri? proxy, + String? apiKey, + ) async { final results = {}; // Test Directions API @@ -249,7 +284,7 @@ class NetworkProvider { mode: TravelMode.driving, ); - await getRouteBetweenCoordinates(apiKey, testRequest, + await getRouteBetweenCoordinates(proxy, apiKey, testRequest, timeout: Duration(seconds: 10)); results['directions_api'] = true; } catch (e) { @@ -263,7 +298,7 @@ class NetworkProvider { destination: PointLatLng(37.7849, -122.4094), ); - await getRouteBetweenCoordinatesV2(apiKey, testRequest, + await getRouteBetweenCoordinatesV2(proxy, apiKey, testRequest, timeout: Duration(seconds: 10)); results['routes_api'] = true; } catch (e) { diff --git a/lib/src/polyline_points.dart b/lib/src/polyline_points.dart index c73337c..a776098 100644 --- a/lib/src/polyline_points.dart +++ b/lib/src/polyline_points.dart @@ -14,8 +14,11 @@ import 'routes_api/routes_response.dart'; /// Provides backward compatibility while enabling access to advanced routing features // ignore_for_file: deprecated_member_use_from_same_package class PolylinePoints { + /// Proxy for circumventing exposing your API key in your code. + final Uri? proxy; + /// Google API key for accessing routing services - final String apiKey; + final String? apiKey; /// Default timeout for API requests final Duration defaultTimeout; @@ -25,22 +28,36 @@ class PolylinePoints { /// Create a new PolylinePointsV2 instance PolylinePoints({ - required this.apiKey, + this.proxy, + this.apiKey, this.defaultTimeout = const Duration(seconds: 30), this.preferRoutesApi = true, - }); + }) { + assert( + (proxy == null && apiKey != null && apiKey!.isNotEmpty) || + (proxy != null && apiKey == null), + "Google API Key cannot be empty if proxy isn't provided"); + } /// Create instance optimized for legacy API usage - factory PolylinePoints.legacy(String apiKey) { + factory PolylinePoints.legacy({ + Uri? proxy, + String? apiKey, + }) { return PolylinePoints( + proxy: proxy, apiKey: apiKey, preferRoutesApi: false, ); } /// Create instance optimized for Routes API usage - factory PolylinePoints.enhanced(String apiKey) { + factory PolylinePoints.enhanced({ + Uri? proxy, + String? apiKey, + }) { return PolylinePoints( + proxy: proxy, apiKey: apiKey, preferRoutesApi: true, ); @@ -48,11 +65,13 @@ class PolylinePoints { /// Create instance with custom configuration factory PolylinePoints.custom({ - required String apiKey, + Uri? proxy, + String? apiKey, Duration? timeout, bool? preferRoutesApi, }) { return PolylinePoints( + proxy: proxy, apiKey: apiKey, defaultTimeout: timeout ?? const Duration(seconds: 30), preferRoutesApi: preferRoutesApi ?? true, @@ -71,6 +90,7 @@ class PolylinePoints { Duration? timeout, }) async { return NetworkProvider.getRouteBetweenCoordinates( + proxy, apiKey, request, timeout: timeout ?? defaultTimeout, @@ -94,6 +114,7 @@ class PolylinePoints { Duration? timeout, }) async { return NetworkProvider.getRouteBetweenCoordinatesV2( + proxy, apiKey, request, timeout: timeout ?? defaultTimeout, @@ -127,7 +148,7 @@ class PolylinePoints { /// Check which APIs are available with the current API key Future> checkApiAvailability() async { - return NetworkProvider.checkApiAvailability(apiKey); + return NetworkProvider.checkApiAvailability(proxy, apiKey); } /// Get API usage recommendation for a given request diff --git a/lib/src/routes_api/routes_response.dart b/lib/src/routes_api/routes_response.dart index 56de674..1738daa 100644 --- a/lib/src/routes_api/routes_response.dart +++ b/lib/src/routes_api/routes_response.dart @@ -5,7 +5,7 @@ import '../commons/point_lat_lng.dart'; /// Focuses on polyline decoding with essential data and raw JSON access class RoutesApiResponse { /// List of simplified routes with decoded polylines - final List routes; + final List routes; /// Raw JSON response from the API for advanced usage final Map rawJson; @@ -29,7 +29,7 @@ class RoutesApiResponse { return RoutesApiResponse( routes: json['routes'] != null ? (json['routes'] as List) - .map((route) => Route.fromJson(route)) + .map((route) => PolylineRoute.fromJson(route)) .toList() : [], rawJson: json, @@ -49,10 +49,10 @@ class RoutesApiResponse { ); /// Get the primary (first) route - Route? get primaryRoute => routes.isNotEmpty ? routes.first : null; + PolylineRoute? get primaryRoute => routes.isNotEmpty ? routes.first : null; /// Get alternative routes (excluding the primary route) - List get alternativeRoutes => + List get alternativeRoutes => routes.length > 1 ? routes.skip(1).toList() : []; /// Check if the response contains any routes @@ -92,7 +92,7 @@ class RoutesApiResponse { } /// Simplified route information with essential data and decoded polyline -class Route { +class PolylineRoute { /// Total route duration in seconds final int? duration; @@ -108,7 +108,7 @@ class Route { /// Encoded polyline string (for reference) final String? polylineEncoded; - const Route({ + const PolylineRoute({ this.duration, this.staticDuration, this.distanceMeters, @@ -117,10 +117,10 @@ class Route { }); /// Create from JSON response - factory Route.fromJson(Map json) { + factory PolylineRoute.fromJson(Map json) { final polylineEncoded = json['polyline']?['encodedPolyline']; - return Route( + return PolylineRoute( duration: json['duration'] != null ? int.tryParse(json['duration'].toString().replaceAll('s', '')) : null, @@ -153,7 +153,7 @@ class Route { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is Route && + return other is PolylineRoute && other.duration == duration && other.staticDuration == staticDuration && other.distanceMeters == distanceMeters && diff --git a/test/routes_api/routes_response_test.dart b/test/routes_api/routes_response_test.dart index 6e18ff2..604ce98 100644 --- a/test/routes_api/routes_response_test.dart +++ b/test/routes_api/routes_response_test.dart @@ -124,13 +124,13 @@ void main() { test('should handle routes data correctly', () { final routes = [ - Route( + PolylineRoute( duration: 3600, staticDuration: 3300, distanceMeters: 150000, polylineEncoded: 'u{~vFvyys@fS]', ), - Route( + PolylineRoute( duration: 4200, distanceMeters: 180000, polylineEncoded: 'a{~vFxyys@gT^', @@ -180,7 +180,7 @@ void main() { group('Route', () { test('should create route with all fields', () { - final route = Route( + final route = PolylineRoute( duration: 3600, staticDuration: 3300, distanceMeters: 150000, @@ -194,7 +194,7 @@ void main() { }); test('should create route with minimal fields', () { - final route = Route(); + final route = PolylineRoute(); expect(route.duration, isNull); expect(route.staticDuration, isNull); @@ -210,7 +210,7 @@ void main() { 'polyline': {'encodedPolyline': 'u{~vFvyys@fS]'} }; - final route = Route.fromJson(json); + final route = PolylineRoute.fromJson(json); expect(route.duration, equals(3600)); expect(route.staticDuration, equals(3300)); @@ -219,7 +219,7 @@ void main() { }); test('should provide convenience getters', () { - final route = Route( + final route = PolylineRoute( duration: 3600, staticDuration: 3300, distanceMeters: 150000, @@ -233,7 +233,7 @@ void main() { }); test('should handle null values in convenience getters', () { - final route = Route( + final route = PolylineRoute( duration: null, distanceMeters: null, ); @@ -245,21 +245,21 @@ void main() { }); test('should implement equality correctly', () { - final route1 = Route( + final route1 = PolylineRoute( duration: 3600, staticDuration: 3300, distanceMeters: 150000, polylineEncoded: 'u{~vFvyys@fS]', ); - final route2 = Route( + final route2 = PolylineRoute( duration: 3600, staticDuration: 3300, distanceMeters: 150000, polylineEncoded: 'u{~vFvyys@fS]', ); - final route3 = Route( + final route3 = PolylineRoute( duration: 4200, staticDuration: 3300, distanceMeters: 150000,