diff --git a/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java index 8ac850a..5f6e731 100644 --- a/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java @@ -19,7 +19,7 @@ public interface IMapViewFragment extends IMapViewProperties { MapViewController getMapController(); - void setMapStyle(String url); + void setMapStyle(String mapStyle); GoogleMap getGoogleMap(); diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewController.java b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java index 8a10394..bb3a0c2 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewController.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java @@ -36,17 +36,10 @@ import com.google.android.gms.maps.model.PolygonOptions; import com.google.android.gms.maps.model.Polyline; import com.google.android.gms.maps.model.PolylineOptions; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; public class MapViewController implements INavigationViewControllerProperties { private GoogleMap mGoogleMap; @@ -67,8 +60,6 @@ public class MapViewController implements INavigationViewControllerProperties { private final Map groundOverlayNativeIdToEffectiveId = new HashMap<>(); private final Map circleNativeIdToEffectiveId = new HashMap<>(); - private String style = ""; - // Zoom level preferences (-1 means use map's current value) private Float minZoomLevelPreference = null; private Float maxZoomLevelPreference = null; @@ -770,25 +761,20 @@ public Map getGroundOverlayMap() { return groundOverlayMap; } - public void setMapStyle(String url) { - Executors.newSingleThreadExecutor() - .execute( - () -> { - try { - style = fetchJsonFromUrl(url); - } catch (IOException e) { - throw new RuntimeException(e); - } - - Activity activity = activitySupplier.get(); - if (activity != null) { - activity.runOnUiThread( - () -> { - MapStyleOptions options = new MapStyleOptions(style); - mGoogleMap.setMapStyle(options); - }); - } - }); + public void setMapStyle(String styleJson) { + Activity activity = activitySupplier.get(); + if (activity != null) { + activity.runOnUiThread( + () -> { + if (styleJson == null || styleJson.isEmpty()) { + // Reset to default map style + mGoogleMap.setMapStyle(null); + } else { + MapStyleOptions options = new MapStyleOptions(styleJson); + mGoogleMap.setMapStyle(options); + } + }); + } } /** Moves the position of the camera to the specified location. */ @@ -1037,29 +1023,6 @@ public void setPadding(int top, int left, int bottom, int right) { } } - private String fetchJsonFromUrl(String urlString) throws IOException { - URL url = new URL(urlString); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - int responseCode = connection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - InputStream inputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - StringBuilder stringBuilder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - reader.close(); - inputStream.close(); - return stringBuilder.toString(); - } else { - // Handle error response - throw new IOException("Error response: " + responseCode); - } - } - private LatLng createLatLng(Map map) { Double lat = null; Double lng = null; diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java index fc154e5..a0731ea 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java @@ -141,8 +141,8 @@ public MapViewController getMapController() { return mMapViewController; } - public void setMapStyle(String url) { - mMapViewController.setMapStyle(url); + public void setMapStyle(String mapStyle) { + mMapViewController.setMapStyle(mapStyle); } public GoogleMap getGoogleMap() { diff --git a/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java index 0af029b..8c8aae5 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java @@ -138,13 +138,12 @@ public void setMapType(double mapType) { @Override public void setMapStyle(String mapStyle) { - String url = mapStyle; UiThreadUtil.runOnUiThread( () -> { if (mMapViewController == null) { return; } - mMapViewController.setMapStyle(url); + mMapViewController.setMapStyle(mapStyle); }); } diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java index 16c734b..8e06b7d 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java @@ -122,8 +122,8 @@ public MapViewController getMapController() { return mMapViewController; } - public void setMapStyle(String url) { - mMapViewController.setMapStyle(url); + public void setMapStyle(String mapStyle) { + mMapViewController.setMapStyle(mapStyle); } public void applyStylingOptions() { diff --git a/example/.detoxrc.js b/example/.detoxrc.js index 102f826..66ae49d 100644 --- a/example/.detoxrc.js +++ b/example/.detoxrc.js @@ -60,8 +60,8 @@ module.exports = { simulator: { type: 'ios.simulator', device: { - type: 'iPhone 16 Pro', - os: 'iOS 18.6', + type: 'iPhone 17 Pro', + os: 'iOS 26.4', }, }, attached: { diff --git a/example/e2e/map.test.js b/example/e2e/map.test.js index 9f1ee93..dda09a7 100644 --- a/example/e2e/map.test.js +++ b/example/e2e/map.test.js @@ -82,4 +82,11 @@ describe('Map view tests', () => { await expectNoErrors(); await expectSuccess(); }); + + it('MT09 - test setting map style via JSON', async () => { + await selectTestByName('testMapStyle'); + await waitForTestToFinish(); + await expectNoErrors(); + await expectSuccess(); + }); }); diff --git a/example/ios/Podfile b/example/ios/Podfile index c2193d4..20b0857 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -59,4 +59,17 @@ post_install do |installer| :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + # Fix fmt compilation issue with Xcode 26.4+ (stricter consteval enforcement) + # fmt/base.h unconditionally redefines FMT_USE_CONSTEVAL based on __cplusplus, + # so preprocessor defines are overwritten. Compiling fmt in C++17 mode ensures + # FMT_CPLUSPLUS (201703L) < 201709L → FMT_USE_CONSTEVAL = 0. + # Fixed in React Native 0.85+, can be removed after upgrading. + installer.pods_project.targets.each do |target| + if target.name == 'fmt' + target.build_configurations.each do |config| + config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17' + end + end + end end diff --git a/example/src/controls/mapsControls.tsx b/example/src/controls/mapsControls.tsx index 9d80982..5680775 100644 --- a/example/src/controls/mapsControls.tsx +++ b/example/src/controls/mapsControls.tsx @@ -69,6 +69,9 @@ export interface MapControlsProps { readonly onZoomControlsEnabledChange?: (enabled: boolean) => void; readonly zoomGesturesEnabled?: boolean; readonly onZoomGesturesEnabledChange?: (enabled: boolean) => void; + // Map style + readonly mapStyleEnabled?: boolean; + readonly onMapStyleEnabledChange?: (enabled: boolean) => void; } export const defaultZoom: number = 15; @@ -103,6 +106,8 @@ const MapsControls: React.FC = ({ onZoomControlsEnabledChange, zoomGesturesEnabled = true, onZoomGesturesEnabledChange, + mapStyleEnabled = false, + onMapStyleEnabledChange, }) => { const mapTypeOptions = ['None', 'Normal', 'Satellite', 'Terrain', 'Hybrid']; const colorSchemeOptions = ['Follow System', 'Light', 'Dark']; @@ -517,6 +522,13 @@ const MapsControls: React.FC = ({ onPress={toggleCustomPadding} /> + + JSON Styling (Night mode) + onMapStyleEnabledChange?.(!mapStyleEnabled)} + /> + {/* Location & UI */} diff --git a/example/src/screens/IntegrationTestsScreen.tsx b/example/src/screens/IntegrationTestsScreen.tsx index 827cbb0..47d9019 100644 --- a/example/src/screens/IntegrationTestsScreen.tsx +++ b/example/src/screens/IntegrationTestsScreen.tsx @@ -54,6 +54,7 @@ import { testNavigationStateGuards, testStartGuidanceWithoutDestinations, testRouteTokenOptionsValidation, + testMapStyle, NO_ERRORS_DETECTED_LABEL, } from './integration_tests/integration_test'; @@ -124,6 +125,7 @@ const IntegrationTestsScreen = () => { const [mapToolbarEnabled, setMapToolbarEnabled] = useState< boolean | undefined >(undefined); + const [mapStyle, setMapStyle] = useState(undefined); const onMapReady = useCallback(async () => { try { @@ -231,6 +233,7 @@ const IntegrationTestsScreen = () => { setZoomGesturesEnabled, setZoomControlsEnabled, setMapToolbarEnabled, + setMapStyle, }; }; @@ -297,6 +300,9 @@ const IntegrationTestsScreen = () => { case 'testRouteTokenOptionsValidation': await testRouteTokenOptionsValidation(getTestTools()); break; + case 'testMapStyle': + await testMapStyle(getTestTools()); + break; default: resetTestState(); break; @@ -338,6 +344,7 @@ const IntegrationTestsScreen = () => { zoomGesturesEnabled={zoomGesturesEnabled} zoomControlsEnabled={zoomControlsEnabled} mapToolbarEnabled={mapToolbarEnabled} + mapStyle={mapStyle} /> @@ -502,6 +509,13 @@ const IntegrationTestsScreen = () => { }} testID="testRouteTokenOptionsValidation" /> + { + runTest('testMapStyle'); + }} + testID="testMapStyle" + /> ); diff --git a/example/src/screens/NavigationScreen.tsx b/example/src/screens/NavigationScreen.tsx index 1b81109..dfbd44f 100644 --- a/example/src/screens/NavigationScreen.tsx +++ b/example/src/screens/NavigationScreen.tsx @@ -49,6 +49,7 @@ import OverlayModal from '../helpers/overlayModal'; import { showSnackbar, Snackbar } from '../helpers/snackbar'; import { CommonStyles, MapStyles } from '../styles/components'; import { MapStylingOptions } from '../styles/mapStyling'; +import { NIGHT_MODE_STYLE } from '../styles/mapStyles'; import usePermissions from '../checkPermissions'; enum OverlayType { @@ -92,6 +93,13 @@ const NavigationScreen = () => { const [zoomControlsEnabled, setZoomControlsEnabled] = useState(true); const [zoomGesturesEnabled, setZoomGesturesEnabled] = useState(true); + // Custom map style (JSON string) + const [mapStyle, setMapStyle] = useState(undefined); + + const handleMapStyleEnabledChange = (enabled: boolean) => { + setMapStyle(enabled ? NIGHT_MODE_STYLE : undefined); + }; + // Navigation UI state const [tripProgressBarEnabled, setTripProgressBarEnabled] = useState(false); const [trafficPromptsEnabled, setTrafficPromptsEnabled] = useState(true); @@ -349,6 +357,7 @@ const NavigationScreen = () => { tiltGesturesEnabled={tiltGesturesEnabled} zoomControlsEnabled={zoomControlsEnabled} zoomGesturesEnabled={zoomGesturesEnabled} + mapStyle={mapStyle} navigationUIEnabledPreference={0} // 0 = AUTOMATIC tripProgressBarEnabled={tripProgressBarEnabled} trafficPromptsEnabled={trafficPromptsEnabled} @@ -443,6 +452,8 @@ const NavigationScreen = () => { onZoomControlsEnabledChange={setZoomControlsEnabled} zoomGesturesEnabled={zoomGesturesEnabled} onZoomGesturesEnabledChange={setZoomGesturesEnabled} + mapStyleEnabled={mapStyle !== undefined} + onMapStyleEnabledChange={handleMapStyleEnabledChange} /> )} diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index 806d242..5a82ec2 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -26,6 +26,7 @@ import { } from '@googlemaps/react-native-navigation-sdk'; import { Platform } from 'react-native'; import { delay, roundDown } from './utils'; +import { NIGHT_MODE_STYLE } from '../../styles/mapStyles'; interface TestTools { navigationController: NavigationController; @@ -55,6 +56,7 @@ interface TestTools { setZoomGesturesEnabled: (enabled: boolean | undefined) => void; setZoomControlsEnabled: (enabled: boolean | undefined) => void; setMapToolbarEnabled: (enabled: boolean | undefined) => void; + setMapStyle: (style: string | undefined) => void; } const NAVIGATOR_NOT_READY_ERROR_CODE = 'NO_NAVIGATOR_ERROR_CODE'; @@ -1480,3 +1482,33 @@ export const testRouteTokenOptionsValidation = async (testTools: TestTools) => { }); await initializeNavigation(navigationController, failTest); }; + +/** + * Test that mapStyle prop can be set with valid JSON without errors. + * This verifies the fix for issue #548 where mapStyle was incorrectly + * treated as a URL on Android instead of a JSON string. + */ +export const testMapStyle = async (testTools: TestTools) => { + const { mapViewController, passTest, failTest, setMapStyle } = testTools; + + if (!mapViewController) { + return failTest('mapViewController was expected to exist'); + } + + try { + // Set a valid JSON map style (night mode) + setMapStyle(NIGHT_MODE_STYLE); + + // Give time for the style to be applied + await delay(500); + + // Reset to default style + setMapStyle(undefined); + + await delay(200); + + passTest(); + } catch (error) { + failTest(`Failed to set mapStyle: ${error}`); + } +}; diff --git a/example/src/styles/mapStyles.ts b/example/src/styles/mapStyles.ts new file mode 100644 index 0000000..c87a165 --- /dev/null +++ b/example/src/styles/mapStyles.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Complete night mode JSON style based on official Google Maps styling. + * Can be used with the mapStyle prop on NavigationView or MapView. + */ +export const NIGHT_MODE_STYLE = JSON.stringify([ + { + featureType: 'all', + elementType: 'geometry', + stylers: [{ color: '#242f3e' }], + }, + { + featureType: 'all', + elementType: 'labels.text.stroke', + stylers: [{ lightness: -80 }], + }, + { + featureType: 'landscape.man_made', + elementType: 'geometry.fill', + stylers: [{ color: '#382a51' }], + }, + { + featureType: 'administrative', + elementType: 'labels.text.fill', + stylers: [{ color: '#746855' }], + }, + { + featureType: 'administrative.locality', + elementType: 'labels.text.fill', + stylers: [{ color: '#d59563' }], + }, + { + featureType: 'poi', + elementType: 'labels.text.fill', + stylers: [{ color: '#d59563' }], + }, + { + featureType: 'poi.park', + elementType: 'geometry', + stylers: [{ color: '#263c3f' }], + }, + { + featureType: 'poi.park', + elementType: 'labels.text.fill', + stylers: [{ color: '#6b9a76' }], + }, + { + featureType: 'road', + elementType: 'geometry.fill', + stylers: [{ color: '#2b3544' }], + }, + { + featureType: 'road', + elementType: 'labels.text.fill', + stylers: [{ color: '#9ca5b3' }], + }, + { + featureType: 'road.arterial', + elementType: 'geometry.fill', + stylers: [{ color: '#38414e' }], + }, + { + featureType: 'road.arterial', + elementType: 'geometry.stroke', + stylers: [{ color: '#212a37' }], + }, + { + featureType: 'road.highway', + elementType: 'geometry.fill', + stylers: [{ color: '#746855' }], + }, + { + featureType: 'road.highway', + elementType: 'geometry.stroke', + stylers: [{ color: '#1f2835' }], + }, + { + featureType: 'road.highway', + elementType: 'labels.text.fill', + stylers: [{ color: '#f3d19c' }], + }, + { + featureType: 'road.local', + elementType: 'geometry.fill', + stylers: [{ color: '#38414e' }], + }, + { + featureType: 'road.local', + elementType: 'geometry.stroke', + stylers: [{ color: '#212a37' }], + }, + { + featureType: 'transit', + elementType: 'geometry', + stylers: [{ color: '#2f3948' }], + }, + { + featureType: 'transit.station', + elementType: 'labels.text.fill', + stylers: [{ color: '#d59563' }], + }, + { + featureType: 'water', + elementType: 'geometry', + stylers: [{ color: '#17263c' }], + }, + { + featureType: 'water', + elementType: 'labels.text.fill', + stylers: [{ color: '#515c6d' }], + }, + { + featureType: 'water', + elementType: 'labels.text.stroke', + stylers: [{ lightness: -20 }], + }, +]); diff --git a/ios/react-native-navigation-sdk/NavView.mm b/ios/react-native-navigation-sdk/NavView.mm index cac635d..a3667b6 100644 --- a/ios/react-native-navigation-sdk/NavView.mm +++ b/ios/react-native-navigation-sdk/NavView.mm @@ -221,7 +221,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & // Update map style if (previousViewProps.mapStyle != newViewProps.mapStyle) { NSString *jsonString = [NSString stringWithUTF8String:newViewProps.mapStyle.c_str()]; - GMSMapStyle *style = [GMSMapStyle styleWithJSONString:jsonString error:nil]; + GMSMapStyle *style = nil; + if (jsonString.length > 0) { + style = [GMSMapStyle styleWithJSONString:jsonString error:nil]; + } + // Setting nil resets to default map style [_viewController setMapStyle:style]; }