Skip to content

Commit bcf3679

Browse files
authored
feat!: support route tokens (#528)
Added support for route tokens BREAKING CHANGE: Updates setDestinations call signature. DisplayOptions, RoutingOptions and RouteTokenOptions are now optional named parameters.
1 parent c3f755e commit bcf3679

File tree

19 files changed

+1042
-86
lines changed

19 files changed

+1042
-86
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ try {
227227
showTrafficLights: true,
228228
};
229229

230-
await navigationController.setDestinations([waypoint], routingOptions, displayOptions);
230+
await navigationController.setDestinations([waypoint], { routingOptions, displayOptions });
231231
await navigationController.startGuidance();
232232
} catch (error) {
233233
console.error('Error starting navigation', error);
@@ -240,6 +240,33 @@ try {
240240
>
241241
> To avoid this, ensure that the SDK has provided a valid user location before calling the setDestinations function. You can do this by subscribing to the onLocationChanged navigation callback and waiting for the first valid location update.
242242
243+
#### Using Route Tokens
244+
245+
You can use a pre-computed route from the [Routes API](https://developers.google.com/maps/documentation/routes) by providing a route token. This is useful when you want to ensure the navigation follows a specific route that was calculated server-side.
246+
247+
To use a route token:
248+
249+
1. Pass the token using `routeTokenOptions` instead of `routingOptions`
250+
2. **Important:** The waypoints passed to `setDestinations` must match the waypoints used when generating the route token
251+
252+
```tsx
253+
const waypoint = {
254+
title: 'Destination',
255+
position: { lat: 37.7749, lng: -122.4194 },
256+
};
257+
258+
const routeTokenOptions = {
259+
routeToken: 'your-route-token-from-routes-api',
260+
travelMode: TravelMode.DRIVING, // Must match the travel mode used to generate the token
261+
};
262+
263+
await navigationController.setDestinations([waypoint], { routeTokenOptions });
264+
await navigationController.startGuidance();
265+
```
266+
267+
> [!IMPORTANT]
268+
> `routingOptions` and `routeTokenOptions` are mutually exclusive. Providing both will throw an error.
269+
243270

244271
#### Adding navigation listeners
245272

android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.android.libraries.navigation.AlternateRoutesStrategy;
2020
import com.google.android.libraries.navigation.ForceNightMode;
2121
import com.google.android.libraries.navigation.Navigator;
22+
import com.google.android.libraries.navigation.RoutingOptions;
2223

2324
public class EnumTranslationUtil {
2425
public static AlternateRoutesStrategy getAlternateRoutesStrategyFromJsValue(int jsValue) {
@@ -108,4 +109,19 @@ public static CustomTypes.MapViewType getMapViewTypeFromJsValue(int jsValue) {
108109
return MapColorScheme.FOLLOW_SYSTEM;
109110
}
110111
}
112+
113+
public static @RoutingOptions.TravelMode int getTravelModeFromJsValue(int jsValue) {
114+
switch (jsValue) {
115+
case 1:
116+
return RoutingOptions.TravelMode.CYCLING;
117+
case 2:
118+
return RoutingOptions.TravelMode.WALKING;
119+
case 3:
120+
return RoutingOptions.TravelMode.TWO_WHEELER;
121+
case 4:
122+
return RoutingOptions.TravelMode.TAXI;
123+
default:
124+
return RoutingOptions.TravelMode.DRIVING;
125+
}
126+
}
111127
}

android/src/main/java/com/google/android/react/navsdk/NavModule.java

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,16 @@
3636
import com.google.android.libraries.mapsplatform.turnbyturn.model.NavInfo;
3737
import com.google.android.libraries.mapsplatform.turnbyturn.model.StepInfo;
3838
import com.google.android.libraries.navigation.ArrivalEvent;
39+
import com.google.android.libraries.navigation.CustomRoutesOptions;
40+
import com.google.android.libraries.navigation.DisplayOptions;
3941
import com.google.android.libraries.navigation.ListenableResultFuture;
4042
import com.google.android.libraries.navigation.NavigationApi;
4143
import com.google.android.libraries.navigation.NavigationApi.OnTermsResponseListener;
4244
import com.google.android.libraries.navigation.Navigator;
4345
import com.google.android.libraries.navigation.RoadSnappedLocationProvider;
4446
import com.google.android.libraries.navigation.RoadSnappedLocationProvider.LocationListener;
4547
import com.google.android.libraries.navigation.RouteSegment;
48+
import com.google.android.libraries.navigation.RoutingOptions;
4649
import com.google.android.libraries.navigation.SimulationOptions;
4750
import com.google.android.libraries.navigation.SpeedAlertOptions;
4851
import com.google.android.libraries.navigation.SpeedAlertSeverity;
@@ -429,22 +432,12 @@ private void createWaypoint(Map map) {
429432
}
430433
}
431434

432-
@ReactMethod
433-
public void setDestination(
434-
ReadableMap waypoint,
435-
@Nullable ReadableMap routingOptions,
436-
@Nullable ReadableMap displayOptions,
437-
final Promise promise) {
438-
WritableArray array = new WritableNativeArray();
439-
array.pushMap(waypoint);
440-
setDestinations(array, routingOptions, displayOptions, promise);
441-
}
442-
443435
@ReactMethod
444436
public void setDestinations(
445437
ReadableArray waypoints,
446438
@Nullable ReadableMap routingOptions,
447439
@Nullable ReadableMap displayOptions,
440+
@Nullable ReadableMap routeTokenOptions,
448441
final Promise promise) {
449442
if (!ensureNavigatorAvailable(promise)) {
450443
return;
@@ -459,19 +452,44 @@ public void setDestinations(
459452
createWaypoint(map);
460453
}
461454

462-
if (routingOptions != null) {
463-
if (displayOptions != null) {
455+
// Get display options if provided
456+
DisplayOptions parsedDisplayOptions =
457+
displayOptions != null
458+
? ObjectTranslationUtil.getDisplayOptionsFromMap(displayOptions.toHashMap())
459+
: null;
460+
461+
// If route token options are provided, use CustomRoutesOptions
462+
if (routeTokenOptions != null) {
463+
CustomRoutesOptions customRoutesOptions;
464+
try {
465+
customRoutesOptions =
466+
ObjectTranslationUtil.getCustomRoutesOptionsFromMap(routeTokenOptions.toHashMap());
467+
} catch (IllegalStateException e) {
468+
promise.reject("routeTokenMalformed", "The route token passed is malformed", e);
469+
return;
470+
}
471+
472+
if (parsedDisplayOptions != null) {
464473
pendingRoute =
465-
mNavigator.setDestinations(
466-
mWaypoints,
467-
ObjectTranslationUtil.getRoutingOptionsFromMap(routingOptions.toHashMap()),
468-
ObjectTranslationUtil.getDisplayOptionsFromMap(displayOptions.toHashMap()));
474+
mNavigator.setDestinations(mWaypoints, customRoutesOptions, parsedDisplayOptions);
469475
} else {
476+
pendingRoute = mNavigator.setDestinations(mWaypoints, customRoutesOptions);
477+
}
478+
} else if (routingOptions != null) {
479+
RoutingOptions parsedRoutingOptions =
480+
ObjectTranslationUtil.getRoutingOptionsFromMap(routingOptions.toHashMap());
481+
482+
if (parsedDisplayOptions != null) {
470483
pendingRoute =
471-
mNavigator.setDestinations(
472-
mWaypoints,
473-
ObjectTranslationUtil.getRoutingOptionsFromMap(routingOptions.toHashMap()));
484+
mNavigator.setDestinations(mWaypoints, parsedRoutingOptions, parsedDisplayOptions);
485+
} else {
486+
pendingRoute = mNavigator.setDestinations(mWaypoints, parsedRoutingOptions);
474487
}
488+
} else if (parsedDisplayOptions != null) {
489+
// No routing options provided: use defaults, but still honor display options if
490+
// supplied.
491+
pendingRoute =
492+
mNavigator.setDestinations(mWaypoints, new RoutingOptions(), parsedDisplayOptions);
475493
} else {
476494
pendingRoute = mNavigator.setDestinations(mWaypoints);
477495
}

android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.android.gms.maps.model.Polyline;
2727
import com.google.android.libraries.mapsplatform.turnbyturn.model.StepInfo;
2828
import com.google.android.libraries.navigation.AlternateRoutesStrategy;
29+
import com.google.android.libraries.navigation.CustomRoutesOptions;
2930
import com.google.android.libraries.navigation.DisplayOptions;
3031
import com.google.android.libraries.navigation.NavigationRoadStretchRenderingData;
3132
import com.google.android.libraries.navigation.RouteSegment;
@@ -146,8 +147,9 @@ public static RoutingOptions getRoutingOptionsFromMap(Map map) {
146147
}
147148

148149
if (map.containsKey("travelMode")) {
149-
options.travelMode(
150-
CollectionUtil.getInt("travelMode", map, RoutingOptions.TravelMode.DRIVING));
150+
int travelModeJsValue =
151+
CollectionUtil.getInt("travelMode", map, RoutingOptions.TravelMode.DRIVING);
152+
options.travelMode(EnumTranslationUtil.getTravelModeFromJsValue(travelModeJsValue));
151153
}
152154

153155
if (map.containsKey("routingStrategy")) {
@@ -168,6 +170,21 @@ public static RoutingOptions getRoutingOptionsFromMap(Map map) {
168170
return options;
169171
}
170172

173+
public static CustomRoutesOptions getCustomRoutesOptionsFromMap(Map map)
174+
throws IllegalStateException {
175+
String routeToken = CollectionUtil.getString("routeToken", map);
176+
177+
CustomRoutesOptions.Builder builder = CustomRoutesOptions.builder().setRouteToken(routeToken);
178+
179+
if (map.containsKey("travelMode")) {
180+
int travelModeJsValue =
181+
CollectionUtil.getInt("travelMode", map, RoutingOptions.TravelMode.DRIVING);
182+
builder.setTravelMode(EnumTranslationUtil.getTravelModeFromJsValue(travelModeJsValue));
183+
}
184+
185+
return builder.build();
186+
}
187+
171188
public static LatLng getLatLngFromMap(Map map) {
172189
if (map.get(Constants.LAT_FIELD_KEY) == null || map.get(Constants.LNG_FIELD_KEY) == null) {
173190
return null;

example/e2e/navigation.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,12 @@ describe('Navigation tests', () => {
8282
await expectNoErrors();
8383
await expectSuccess();
8484
});
85+
86+
it('T08 - setDestinations with both routingOptions and routeTokenOptions should throw error', async () => {
87+
await selectTestByName('testRouteTokenOptionsValidation');
88+
await agreeToTermsAndConditions();
89+
await waitForTestToFinish();
90+
await expectNoErrors();
91+
await expectSuccess();
92+
});
8593
});

example/e2e/shared.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ export const initializeIntegrationTestsPage = async () => {
7171
};
7272

7373
export const selectTestByName = async name => {
74+
await waitFor(element(by.id('tests_menu_button')))
75+
.toBeVisible()
76+
.withTimeout(10000);
7477
await element(by.id('tests_menu_button')).tap();
78+
// Scroll to make the test button visible before tapping
79+
await waitFor(element(by.id(name)))
80+
.toBeVisible()
81+
.whileElement(by.id('overlay_scroll_view'))
82+
.scroll(100, 'down');
7583
await element(by.id(name)).tap();
7684
};

example/ios/SampleApp.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -641,12 +641,12 @@
641641
CODE_SIGN_ENTITLEMENTS = SampleApp/SampleApp.entitlements;
642642
CURRENT_PROJECT_VERSION = 1;
643643
ENABLE_BITCODE = NO;
644-
INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist";
645-
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
646644
GCC_PREPROCESSOR_DEFINITIONS = (
647645
"$(inherited)",
648646
"CARPLAY=1",
649647
);
648+
INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist";
649+
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
650650
LD_RUNPATH_SEARCH_PATHS = (
651651
"$(inherited)",
652652
"@executable_path/Frameworks",
@@ -674,12 +674,12 @@
674674
CLANG_ENABLE_MODULES = YES;
675675
CODE_SIGN_ENTITLEMENTS = SampleApp/SampleApp.entitlements;
676676
CURRENT_PROJECT_VERSION = 1;
677-
INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist";
678-
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
679677
GCC_PREPROCESSOR_DEFINITIONS = (
680678
"$(inherited)",
681679
"CARPLAY=1",
682680
);
681+
INFOPLIST_FILE = "SampleApp/Info-CarPlay.plist";
682+
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
683683
LD_RUNPATH_SEARCH_PATHS = (
684684
"$(inherited)",
685685
"@executable_path/Frameworks",

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@react-native/typescript-config": "0.81.1",
4848
"@types/jest": "^29.5.14",
4949
"@types/node": "^22.9.0",
50-
"detox": "^20.27.6",
50+
"detox": "^20.46.3",
5151
"jest": "^29.7.0",
5252
"react-native-builder-bob": "^0.40.13",
5353
"react-native-monorepo-config": "^0.1.9",

example/src/App.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ExampleAppButton } from './controls/ExampleAppButton';
2929
import NavigationScreen from './screens/NavigationScreen';
3030
import MultipleMapsScreen from './screens/MultipleMapsScreen';
3131
import MapIdScreen from './screens/MapIdScreen';
32+
import RouteTokenScreen from './screens/RouteTokenScreen';
3233
import {
3334
NavigationProvider,
3435
TaskRemovedBehavior,
@@ -41,6 +42,7 @@ export type ScreenNames = [
4142
'Navigation',
4243
'Multiple maps',
4344
'Map ID',
45+
'Route Token',
4446
'Integration tests',
4547
];
4648

@@ -70,7 +72,9 @@ const HomeScreen = () => {
7072
}, [navigationController]);
7173

7274
return (
73-
<View style={[CommonStyles.centered, { paddingBottom: insets.bottom }]}>
75+
<View
76+
style={[CommonStyles.centered, { paddingBottom: insets.bottom + 100 }]}
77+
>
7478
{/* SDK Version Display */}
7579
<View style={{ padding: 16, alignItems: 'center' }}>
7680
<Text style={{ fontSize: 16, fontWeight: 'bold', color: '#333' }}>
@@ -95,6 +99,12 @@ const HomeScreen = () => {
9599
onPress={() => isFocused && navigate('Map ID')}
96100
/>
97101
</View>
102+
<View style={CommonStyles.buttonContainer}>
103+
<ExampleAppButton
104+
title="Route Token"
105+
onPress={() => isFocused && navigate('Route Token')}
106+
/>
107+
</View>
98108
<View style={CommonStyles.container} />
99109
<View style={CommonStyles.buttonContainer}>
100110
<ExampleAppButton
@@ -131,6 +141,7 @@ export default function App() {
131141
<Stack.Screen name="Navigation" component={NavigationScreen} />
132142
<Stack.Screen name="Multiple maps" component={MultipleMapsScreen} />
133143
<Stack.Screen name="Map ID" component={MapIdScreen} />
144+
<Stack.Screen name="Route Token" component={RouteTokenScreen} />
134145
<Stack.Screen
135146
name="Integration tests"
136147
component={IntegrationTestsScreen}

example/src/controls/navigationControls.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,10 @@ const NavigationControls = ({
127127
showTrafficLights: true,
128128
};
129129

130-
navigationController.setDestination(
131-
waypoint,
130+
navigationController.setDestination(waypoint, {
132131
routingOptions,
133-
displayOptions
134-
);
132+
displayOptions,
133+
});
135134
};
136135

137136
const setLocationFromCameraLocation = async () => {
@@ -166,11 +165,10 @@ const NavigationControls = ({
166165
showTrafficLights: true,
167166
};
168167

169-
navigationController.setDestinations(
170-
waypoints,
168+
navigationController.setDestinations(waypoints, {
171169
routingOptions,
172-
displayOptions
173-
);
170+
displayOptions,
171+
});
174172
};
175173

176174
const setFollowingPerspective = (index: CameraPerspective) => {

0 commit comments

Comments
 (0)