From 14b65e0d358519d5cf69d06a2b804ceeef04a806 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Wed, 29 Apr 2026 15:12:36 +0300 Subject: [PATCH 1/2] fix!: token handling and internal event handling --- .eslintrc.js | 9 +- MIGRATING.md | 119 ++++++++- README.md | 62 ++--- .../driversdk/lmfs/DeliveryDriverModule.java | 90 +++---- .../driversdk/lmfs/ObjectTranslationUtil.java | 29 ++- .../driversdk/odrd/RidesharingModule.java | 97 +++---- .../shared/DriverAuthTokenFactory.java | 66 ++++- example/LMFS/src/App.tsx | 77 +++--- example/ODRD/src/App.tsx | 90 ++++--- ios/AuthTokenFactory.h | 27 +- ios/AuthTokenFactory.m | 108 ++++++-- ios/DeliveryDriverController.h | 17 +- ios/DeliveryDriverController.m | 47 ++-- ios/RCTDeliveryDriverModule.mm | 81 +++--- ios/RCTRideSharingModule.mm | 81 +++--- ios/RidesharingDriverController.h | 16 +- ios/RidesharingDriverController.m | 50 ++-- jest.setup.js | 69 +++++ package.json | 5 +- .../__tests__/deliveryDriverModule.test.tsx | 33 ++- src/delivery/deliveryDriverApi.ts | 56 ++++- src/delivery/types.ts | 5 +- src/native/NativeDeliveryDriverModule.ts | 117 ++++++--- src/native/NativeRidesharingModule.ts | 65 ++++- .../__tests__/ridesharingModule.test.tsx | 37 +-- src/ridesharing/ridesharingDriverApi.ts | 15 +- src/shared/driverApi.ts | 236 ++++++++---------- src/shared/types.ts | 69 +++-- 28 files changed, 1132 insertions(+), 641 deletions(-) create mode 100644 jest.setup.js diff --git a/.eslintrc.js b/.eslintrc.js index e4e0626..2f6b589 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -79,9 +79,14 @@ module.exports = { '@typescript-eslint/no-require-imports': 'off', }, }, - // Test files + // Test files and Jest setup { - files: ['**/*.test.js', '**/*.test.ts', '**/e2e/**/*.js'], + files: [ + '**/*.test.js', + '**/*.test.ts', + '**/e2e/**/*.js', + 'jest.setup.js', + ], env: { jest: true, }, diff --git a/MIGRATING.md b/MIGRATING.md index f02bb1b..085dcf8 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -14,12 +14,17 @@ Version 0.5.0 introduces React Native's **New Architecture** (TurboModules) as a ### Summary of Breaking Changes -| Category | Change | -| -------------- | -------------------------------------------------------------------------------------- | -| Architecture | New Architecture required (React Native 0.79+) | -| Navigation SDK | Upgraded to `@googlemaps/react-native-navigation-sdk` 0.15.x (New Architecture) | -| Native Modules | Migrated from `NativeModules` bridge to TurboModules (JSI) | -| React Native | Support for React Native versions below 0.79.x has been dropped | +| Category | Change | +| -------------- | ----------------------------------------------------------------------------------------------- | +| Architecture | New Architecture required (React Native 0.79+) | +| Navigation SDK | Upgraded to `@googlemaps/react-native-navigation-sdk` 0.15.x (New Architecture) | +| Native Modules | Migrated from `NativeModules` bridge to TurboModules (JSI) | +| React Native | Support for React Native versions below 0.79.x has been dropped | +| Auth Tokens | `onGetToken` callback is now called on-demand by the native SDK for each token request | +| Events | Vehicle reporter events and status updates use TurboModule EventEmitter pattern | +| Types | `VehicleStop.waypoint` is now optional | +| Types | `OnStatusUpdateCallback` now uses `DriverStatusLevel` and `DriverStatusCode` enums | +| Types | `VehicleReporterListener` removed; use `setOnVehicleUpdateSucceed` / `setOnVehicleUpdateFailed` | ### Prerequisites @@ -106,7 +111,41 @@ import { > [!NOTE] > The platform-specific module name difference for Ridesharing (`RideSharingModule` on iOS vs `RidesharingModule` on Android) has been unified under the TurboModule system. -### Step 4: Update Build Configuration +### Step 4: Update Auth Token Handling (Breaking Change) + +The auth token mechanism has been completely redesigned. Previously, the `onGetToken` callback was only called once during initialization and the token would go stale. Now, the native Driver SDK calls `onGetToken` **on demand** whenever it needs a fresh token (e.g., on each location update). This matches the [recommended pattern from Google Maps documentation](https://developers.google.com/maps/documentation/transportation-platform/driver-sdk) and the Flutter Driver SDK implementation. + +**Your `onGetToken` callback must now fetch a fresh token on every call.** + +#### Before (0.4.x) + +```tsx +// ❌ Token was fetched once and stored in state — went stale +const [authToken, setAuthToken] = useState(null); + +useEffect(() => { + fetchTokenFromBackend().then(setAuthToken); +}, []); + +await driverApi.initialize(providerId, vehicleId, () => { + return Promise.resolve(authToken || ''); +}, onStatusUpdate); +``` + +#### After (0.5.x) + +```tsx +// ✅ Fresh token fetched on every native SDK request +await driverApi.initialize(providerId, vehicleId, async (tokenContext) => { + const response = await fetch(`${BASE_URL}/token/driver/${tokenContext.vehicleId}`); + const { token } = await response.json(); + return token; +}, onStatusUpdate); +``` + +The `tokenContext` parameter provides `vehicleId` and `taskId` from the native SDK's authorization context, which you can use when requesting tokens from your backend. + +### Step 5: Update Build Configuration #### Android @@ -126,6 +165,72 @@ Ensure your `Podfile` specifies iOS 16.0+ as the deployment target: platform :ios, '16.0' ``` +### Step 6: VehicleStop.waypoint Is Now Optional + +The `waypoint` field on `VehicleStop` is now optional (`waypoint?: Waypoint`) to match cases where the native SDK returns a stop without waypoint data. If your code accesses `stop.waypoint`, add a null check: + +```diff +- const position = stop.waypoint.position; ++ const position = stop.waypoint?.position; +``` + +### Step 7: Update OnStatusUpdateCallback Usage + +The `onStatusUpdate` callback passed to `initialize` now uses typed enums instead of raw strings: + +```diff +- (statusLevel: string, statusCode: string, statusMsg: string) => { ++ (statusLevel: DriverStatusLevel, statusCode: DriverStatusCode, statusMsg: string) => { + console.log(statusLevel, statusCode, statusMsg); + } +``` + +Import the enums if you reference them directly: + +```typescript +import { DriverStatusLevel, DriverStatusCode } from '@googlemaps/react-native-driver-sdk'; +``` + +> [!NOTE] +> **Platform availability:** `onStatusUpdate` fires on **Android only**. On iOS, use the vehicle reporter's `setOnVehicleUpdateSucceed` and `setOnVehicleUpdateFailed` methods to receive vehicle update callbacks. + +### Step 8: Replace VehicleReporterListener with Individual Setters + +The `VehicleReporterListener` interface and `setListener()` method on the vehicle reporter have been removed. Use the individual setter methods instead: + +#### Before (0.4.x) + +```tsx +reporter.setListener({ + onVehicleUpdateSucceed(vehicleUpdate) { + console.log('onVehicleUpdateSucceed: ', vehicleUpdate); + }, + onVehicleUpdateFailed(_vehicleUpdate, error) { + console.log('onVehicleUpdateFailed: ', error); + }, +}); +``` + +#### After (0.5.x) + +```tsx +reporter.setOnVehicleUpdateSucceed(vehicleUpdate => { + console.log('onVehicleUpdateSucceed: ', vehicleUpdate); +}); +reporter.setOnVehicleUpdateFailed((vehicleUpdate, error) => { + console.log( + 'onVehicleUpdateFailed: ', + error.code, + error.message, + 'vehicleState:', + vehicleUpdate.vehicleState + ); +}); +``` + +> [!NOTE] +> **Platform availability:** Vehicle reporter update callbacks fire on **iOS only**. On Android, use `onStatusUpdate` (passed to `initialize`) instead. + ### Need Help? If you encounter issues during migration: diff --git a/README.md b/README.md index d7fdc15..64dad5d 100644 --- a/README.md +++ b/README.md @@ -175,25 +175,25 @@ To set up, specify your API key in the application delegate `ios/Runner/AppDeleg 3. Second step is to initialize the Api. Navigation must be initialized before the Driver SDK is initialized. ```typescript - await ridesharingDriverApi - .initialize( - PROVIDER_ID, - VEHICLE_ID, - (tokenContext) => { - // Check if the token is expired, in such case request a new one. - return Promise.resolve(authToken || ""); - }, - (statusLevel, statusCode, message) => { - console.log("onStatusUpdate: " + statusLevel + " " + statusCode + " " + message); - } - ); + await ridesharingDriverApi.initialize( + PROVIDER_ID, + VEHICLE_ID, + async (tokenContext) => { + // Fetch a fresh token from your backend. + // tokenContext.vehicleId contains the vehicle ID that needs the token. + const token = await fetchTokenFromBackend(tokenContext.vehicleId); + return token; + }, + // Android only — on iOS, use vehicleReporter.setOnVehicleUpdateSucceed/setOnVehicleUpdateFailed instead. + (statusLevel, statusCode, message) => { + console.log("onStatusUpdate: " + statusLevel + " " + statusCode + " " + message); + } + ); ``` -Note: The `initialize` method takes a `onGetTokenCallback` field as parameter. This will be called periodically to ensure the token stays refresh while there's requests to Fleet Engine. Please make sure to check that the token is valid (e.g. checking expiration time) before setting it. +Note: The `initialize` method takes an `onGetToken` callback as parameter. This callback is invoked by the native SDK whenever a fresh auth token is needed (e.g. on each location update cycle). The callback receives an `AuthTokenContext` with the `vehicleId` and should return a `Promise` that resolves to a valid token. Always fetch a fresh token in this callback rather than caching one. -#### Getting a `RidesharingVehicleReporter` - The vehicle reporter allows developers to enable/disable location reporting to Fleet Engine, as well as to report changes in the vehicle state (E.g. Online or Offline). ```typescript @@ -218,21 +218,24 @@ The vehicle reporter allows developers to enable/disable location reporting to F 2. Second step is to initialize the Api. ```typescript - await deliveryApi - .initialize( - PROVIDER_ID, - DELIVERY_VEHICLE_ID, - (tokenContext) => { - // Check if the token is expired, in such case request a new one. - return Promise.resolve(authToken || ""); - }, - (statusLevel, statusCode, message) => { - console.log("onStatusUpdate: " + statusLevel + " " + statusCode + " " + message); - } - ); + await deliveryApi.initialize( + PROVIDER_ID, + DELIVERY_VEHICLE_ID, + async (tokenContext) => { + // Fetch a fresh token from your backend. + // tokenContext.vehicleId contains the vehicle ID that needs the token. + // tokenContext.taskId may contain the task ID for delivery-specific tokens. + const token = await fetchTokenFromBackend(tokenContext.vehicleId); + return token; + }, + // Android only — on iOS, use vehicleReporter.setOnVehicleUpdateSucceed/setOnVehicleUpdateFailed instead. + (statusLevel, statusCode, message) => { + console.log("onStatusUpdate: " + statusLevel + " " + statusCode + " " + message); + } + ); ``` -Note: The `initialize` method takes a `onGetTokenCallback` field as parameter. This will be called periodically to ensure the token stays refresh while there's requests to Fleet Engine. Please make sure to check that the token is valid (e.g. checking expiration time) before setting it. +Note: The `initialize` method takes an `onGetToken` callback as parameter. This callback is invoked by the native SDK whenever a fresh auth token is needed (e.g. on each location update cycle). The callback receives an `AuthTokenContext` with the `vehicleId` (and optionally `taskId` for delivery) and should return a `Promise` that resolves to a valid token. Always fetch a fresh token in this callback rather than caching one. #### Getting a `DeliveryVehicleReporter` @@ -246,11 +249,12 @@ The vehicle reporter allows developers to enable/disable location reporting to F #### Getting a `DeliveryVehicleManager` -The vehicle managers allows developers to fetch the `DeliveryVehicle` linked to the Driver Api from Fleet Engine. +The vehicle manager allows developers to fetch the `DeliveryVehicle` linked to the Driver Api from Fleet Engine. ```typescript const vehicleManager = deliveryApi.getDeliveryVehicleManager() const deliveryVehicle = await vehicleManager.getDeliveryVehicle(); + console.log(deliveryVehicle.vehicleName, deliveryVehicle.vehicleId, deliveryVehicle.vehicleStops); ``` ### Other APIs diff --git a/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java b/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java index 8662595..a94b71b 100644 --- a/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java +++ b/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java @@ -16,14 +16,12 @@ import static java.util.Objects.requireNonNull; import android.app.Application; -import android.util.Log; import androidx.annotation.NonNull; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext.DriverStatusListener.StatusCode; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext.DriverStatusListener.StatusLevel; @@ -47,14 +45,26 @@ public class DeliveryDriverModule extends NativeDeliveryDriverModuleSpec { public static final String REACT_CLASS = NAME; private DeliveryVehicleReporter vehicleReporter = null; - private DriverAuthTokenFactory tokenFactory = new DriverAuthTokenFactory(); + private final DriverAuthTokenFactory tokenFactory = new DriverAuthTokenFactory(); ReactApplicationContext reactContext; - private int listenerCount = 0; public DeliveryDriverModule(ReactApplicationContext context) { super(context); this.reactContext = context; + + // Wire up the token factory to emit events to JS when a token is needed. + tokenFactory.setTokenRequestCallback( + (requestId, vehicleId, taskId) -> { + UiThreadUtil.runOnUiThread( + () -> { + WritableMap map = Arguments.createMap(); + map.putString("requestId", requestId); + map.putString("vehicleId", vehicleId); + map.putString("taskId", taskId); + emitOnGetToken(map); + }); + }); } @Override @@ -91,7 +101,7 @@ public void createDeliveryDriverInstance(String providerId, String vehicleId, Pr NavigationApi.getRoadSnappedLocationProvider(application)) .setDriverStatusListener( (statusLevel, statusCode, statusMsg, error) -> { - updateStatus(statusLevel, statusCode, statusMsg); + emitStatusUpdate(statusLevel, statusCode, statusMsg); }) .build(); @@ -157,14 +167,17 @@ public void getDriverSdkVersion(Promise promise) { /** Clears the instance of the DeliveryDriverApi */ @Override public void clearInstance(Promise promise) { - try { - vehicleReporter = null; - DeliveryDriverApi.clearInstance(); + UiThreadUtil.runOnUiThread( + () -> { + try { + vehicleReporter = null; + DeliveryDriverApi.clearInstance(); - promise.resolve(true); - } catch (Exception e) { - promise.reject(e.toString(), e.getMessage(), e); - } + promise.resolve(true); + } catch (Exception e) { + promise.reject(e.toString(), e.getMessage(), e); + } + }); } /** @@ -209,54 +222,25 @@ public void setAbnormalTerminationReporting(boolean isEnabled) { DeliveryDriverApi.setAbnormalTerminationReportingEnabled(isEnabled); } - private void showToast(String errorMessage) { - Log.d(TAG, "showToast: " + errorMessage); + /** Called from JS to resolve a pending auth token request. */ + @Override + public void resolveAuthToken(String requestId, String token) { + tokenFactory.resolveToken(requestId, token); } - /** - * The function that accepts update codes from the driverContext listener - * - * @param statusLevel - * @param statusCode - * @param statusMsg - */ - public void updateStatus(StatusLevel statusLevel, StatusCode statusCode, String statusMsg) { + /** Called from JS to reject a pending auth token request. */ + @Override + public void rejectAuthToken(String requestId, String error) { + tokenFactory.rejectToken(requestId, error); + } + + private void emitStatusUpdate(StatusLevel statusLevel, StatusCode statusCode, String statusMsg) { if (reactContext != null) { WritableMap map = Arguments.createMap(); map.putString("statusLevel", statusLevel.toString()); map.putString("statusCode", statusCode.toString()); map.putString("statusMsg", statusMsg); - - this.reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("updateStatus", map); - } - } - - /** - * Create an AuthTokenFactory to be used by DriverContext when creating a DeliveryDriverInstance. - * - * @param token jwt token from an authentication service - * @param vehicleId the vehicle ID - * @param promise - */ - @Override - public void setAuthToken(String token, String vehicleId, Promise promise) { - try { - tokenFactory.setToken(token); - promise.resolve(null); - } catch (Exception e) { - promise.reject(e.getMessage()); + emitOnStatusUpdate(map); } } - - @Override - public void addListener(String eventName) { - listenerCount++; - } - - @Override - public void removeListeners(double count) { - listenerCount -= (int) count; - } } diff --git a/android/src/main/java/com/google/android/react/driversdk/lmfs/ObjectTranslationUtil.java b/android/src/main/java/com/google/android/react/driversdk/lmfs/ObjectTranslationUtil.java index 49e3d15..edb0f61 100644 --- a/android/src/main/java/com/google/android/react/driversdk/lmfs/ObjectTranslationUtil.java +++ b/android/src/main/java/com/google/android/react/driversdk/lmfs/ObjectTranslationUtil.java @@ -16,6 +16,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.maps.model.LatLng; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.TaskInfo; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.VehicleStop; import com.google.android.libraries.mapsplatform.transportation.driver.api.delivery.data.DeliveryVehicle; @@ -36,19 +37,23 @@ public static WritableMap getMapFromDeliveryVehicle(DeliveryVehicle vehicle) { WritableMap vehicleStopMap = Arguments.createMap(); // getWaypoint: - WritableMap waypointMap = Arguments.createMap(); Waypoint waypoint = vehicleStop.getWaypoint(); - - waypointMap.putString("title", waypoint.getTitle()); - waypointMap.putString("placeId", waypoint.getPlaceId()); - WritableMap mapDestWaypointLatLng = Arguments.createMap(); - mapDestWaypointLatLng.putDouble("lat", waypoint.getPosition().latitude); - mapDestWaypointLatLng.putDouble("lng", waypoint.getPosition().longitude); - waypointMap.putMap("position", mapDestWaypointLatLng); - waypointMap.putInt("preferredHeading", waypoint.getPreferredHeading()); - waypointMap.putBoolean("vehicleStopover", waypoint.getVehicleStopover()); - waypointMap.putBoolean("preferSameSideOfRoad", waypoint.getPreferSameSideOfRoad()); - vehicleStopMap.putMap("waypoint", waypointMap); + if (waypoint != null) { + WritableMap waypointMap = Arguments.createMap(); + waypointMap.putString("title", waypoint.getTitle()); + waypointMap.putString("placeId", waypoint.getPlaceId()); + LatLng position = waypoint.getPosition(); + if (position != null) { + WritableMap positionMap = Arguments.createMap(); + positionMap.putDouble("lat", position.latitude); + positionMap.putDouble("lng", position.longitude); + waypointMap.putMap("position", positionMap); + } + waypointMap.putInt("preferredHeading", waypoint.getPreferredHeading()); + waypointMap.putBoolean("vehicleStopover", waypoint.getVehicleStopover()); + waypointMap.putBoolean("preferSameSideOfRoad", waypoint.getPreferSameSideOfRoad()); + vehicleStopMap.putMap("waypoint", waypointMap); + } // getTaskInfoList(): WritableArray taskInfoList = Arguments.createArray(); diff --git a/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java b/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java index ad7dd10..13912b1 100644 --- a/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java +++ b/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java @@ -16,13 +16,11 @@ import static java.util.Objects.requireNonNull; import android.app.Application; -import android.util.Log; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.DeviceEventManagerModule; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext.DriverStatusListener.StatusCode; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.DriverContext.DriverStatusListener.StatusLevel; @@ -41,20 +39,31 @@ public class RidesharingModule extends NativeRidesharingModuleSpec { public static final String TAG = "RidesharingAPI"; public static final String REACT_CLASS = NAME; - private static final String UPDATE_STATUS_EVENT_NAME = "updateStatus"; private Navigator mNavigator = null; private RidesharingVehicleReporter vehicleReporter = null; private DriverContext driverContext = null; - private DriverAuthTokenFactory tokenFactory = new DriverAuthTokenFactory(); + private final DriverAuthTokenFactory tokenFactory = new DriverAuthTokenFactory(); ReactApplicationContext reactContext; - private int listenerCount = 0; public RidesharingModule(ReactApplicationContext context) { super(context); this.reactContext = context; + + // Wire up the token factory to emit events to JS when a token is needed. + tokenFactory.setTokenRequestCallback( + (requestId, vehicleId, taskId) -> { + UiThreadUtil.runOnUiThread( + () -> { + WritableMap map = Arguments.createMap(); + map.putString("requestId", requestId); + map.putString("vehicleId", vehicleId); + map.putString("taskId", taskId); + emitOnGetToken(map); + }); + }); } @Override @@ -92,7 +101,7 @@ public void createRidesharingInstance(String providerId, String vehicleId, Promi NavigationApi.getRoadSnappedLocationProvider(application)) .setDriverStatusListener( (statusLevel, statusCode, statusMsg, error) -> { - updateStatus(statusLevel, statusCode, statusMsg); + emitStatusUpdate(statusLevel, statusCode, statusMsg); }) .build(); @@ -181,14 +190,17 @@ public void getDriverSdkVersion(Promise promise) { /** Clears the instance of the RideSharingAPI */ @Override public void clearInstance(Promise promise) { - try { - RidesharingDriverApi.clearInstance(); - vehicleReporter = null; + UiThreadUtil.runOnUiThread( + () -> { + try { + RidesharingDriverApi.clearInstance(); + vehicleReporter = null; - promise.resolve(true); - } catch (Exception e) { - promise.reject(e.toString(), e.getMessage(), e); - } + promise.resolve(true); + } catch (Exception e) { + promise.reject(e.toString(), e.getMessage(), e); + } + }); } /** Enables/disables abnormal termination reporting */ @@ -197,60 +209,25 @@ public void setAbnormalTerminationReporting(boolean isEnabled) { RidesharingDriverApi.setAbnormalTerminationReportingEnabled(isEnabled); } - private void showToast(String errorMessage) { - Log.d(TAG, "showToast: " + errorMessage); + /** Called from JS to resolve a pending auth token request. */ + @Override + public void resolveAuthToken(String requestId, String token) { + tokenFactory.resolveToken(requestId, token); } - /** - * The function that accepts update codes from the driverContext listener - * - * @param statusLevel - * @param statusCode - * @param statusMsg - */ - public void updateStatus(StatusLevel statusLevel, StatusCode statusCode, String statusMsg) { + /** Called from JS to reject a pending auth token request. */ + @Override + public void rejectAuthToken(String requestId, String error) { + tokenFactory.rejectToken(requestId, error); + } + + private void emitStatusUpdate(StatusLevel statusLevel, StatusCode statusCode, String statusMsg) { if (reactContext != null) { WritableMap map = Arguments.createMap(); map.putString("statusLevel", statusLevel.toString()); map.putString("statusCode", statusCode.toString()); map.putString("statusMsg", statusMsg); - - this.reactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(UPDATE_STATUS_EVENT_NAME, map); - } - } - - /** - * Adds listeners for the status updates given by fleet engine - * - * @param eventName - */ - @Override - public void addListener(String eventName) { - listenerCount++; - } - - /** Removes all status update listeners */ - @Override - public void removeListeners(double count) { - listenerCount -= (int) count; - } - - /** - * Create an AuthTokenFactory to be used by DriverContext when creating a RideSharingInstance. - * - * @param token jwt token from an authentication service - * @param vehicleId the vehicle ID - * @param promise - */ - @Override - public void setAuthToken(String token, String vehicleId, Promise promise) { - try { - tokenFactory.setToken(token); - promise.resolve(null); - } catch (Exception e) { - promise.reject(e.getMessage()); + emitOnStatusUpdate(map); } } } diff --git a/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java b/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java index 2636e35..d735ca6 100644 --- a/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java +++ b/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java @@ -15,16 +15,74 @@ import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.AuthTokenContext; import com.google.android.libraries.mapsplatform.transportation.driver.api.base.data.AuthTokenContext.AuthTokenFactory; +import com.google.common.util.concurrent.SettableFuture; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +/** + * Auth token factory that requests tokens from JS via the React Native bridge. + * + *

When the native Driver SDK needs a token (on each location update), this factory: 1. Generates + * a unique requestId. 2. Emits an event to JS via the provided callback. 3. Blocks until JS + * resolves or rejects the request. + * + *

This mirrors the pattern used in the Flutter Driver SDK's AccessTokenProvider. + */ public class DriverAuthTokenFactory implements AuthTokenFactory { - private String token = ""; + + /** Callback interface for requesting tokens from JS. */ + public interface TokenRequestCallback { + void onTokenRequested(String requestId, String vehicleId, String taskId); + } + + private static final long TOKEN_TIMEOUT_SECONDS = 30; + + private final ConcurrentHashMap> pendingRequests = + new ConcurrentHashMap<>(); + + private TokenRequestCallback tokenRequestCallback; + + public void setTokenRequestCallback(TokenRequestCallback callback) { + this.tokenRequestCallback = callback; + } @Override public String getToken(AuthTokenContext context) { - return token; + if (tokenRequestCallback == null) { + throw new RuntimeException( + "Token request callback not set. Ensure the module is initialized."); + } + + String requestId = UUID.randomUUID().toString(); + SettableFuture future = SettableFuture.create(); + pendingRequests.put(requestId, future); + + try { + String vehicleId = context.getVehicleId() != null ? context.getVehicleId() : ""; + String taskId = context.getTaskId() != null ? context.getTaskId() : ""; + tokenRequestCallback.onTokenRequested(requestId, vehicleId, taskId); + return future.get(TOKEN_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException("Failed to get auth token from JS", e); + } finally { + pendingRequests.remove(requestId); + } + } + + /** Called from JS when a token request is resolved successfully. */ + public void resolveToken(String requestId, String token) { + SettableFuture future = pendingRequests.get(requestId); + if (future != null) { + future.set(token); + } } - public void setToken(String token) { - this.token = token; + /** Called from JS when a token request fails. */ + public void rejectToken(String requestId, String error) { + SettableFuture future = pendingRequests.get(requestId); + if (future != null) { + future.setException(new RuntimeException(error)); + } } } diff --git a/example/LMFS/src/App.tsx b/example/LMFS/src/App.tsx index 6757119..6aa0f53 100644 --- a/example/LMFS/src/App.tsx +++ b/example/LMFS/src/App.tsx @@ -85,14 +85,18 @@ function LMFSSampleApp() { useState(true); const [isLocationTrackingEnabled, setLocationTrackingEnabled] = useState(false); - const [authToken, setAuthToken] = useState(null); const [driverSdkVersion, setDriverSdkVersion] = useState(''); const [vehicleId, setVehicleId] = useState(VEHICLE_ID_DEFAULT); const [tempVehicleId, setTempVehicleId] = useState(VEHICLE_ID_DEFAULT); const clearInstance = useCallback(async () => { - await deliveryDriverApi.clearInstance(); + deliveryDriverApi + .clearInstance() + .then(() => { + setLocationTrackingEnabled(false); + }) + .catch(e => console.warn('clearInstance failed', e)); }, []); const onNavigationReady = useCallback(() => { @@ -115,26 +119,21 @@ function LMFSSampleApp() { [navigationController] ); - const fetchAuthToken = useCallback(async () => { - if (!vehicleId) { - console.log('Vehicle ID not set, skipping auth token fetch'); - return; - } - try { + const fetchAuthToken = useCallback( + async (tokenVehicleId?: string): Promise => { + const vid = tokenVehicleId || vehicleId; + if (!vid) { + throw new Error('Vehicle ID not set'); + } console.log('Fetching auth token...'); - const tokenUrl = BASE_URL + '/token/delivery_driver/' + vehicleId; + const tokenUrl = BASE_URL + '/token/delivery_driver/' + vid; const response = await fetch(tokenUrl); const { token } = await response.json(); console.log('Got token:', token); - - setAuthToken(token); - } catch (error) { - console.log( - 'There has been a problem connecting to the provider, please make sure it is running. ', - error - ); - } - }, [vehicleId]); + return token; + }, + [vehicleId] + ); useEffect(() => { if (!vehicleId) { @@ -145,7 +144,6 @@ function LMFSSampleApp() { console.log('Init LMFS Example app'); setOnArrival(onArrival); setOnNavigationReady(onNavigationReady); - fetchAuthToken(); deliveryDriverApi .getDriverSdkVersion() .then(version => setDriverSdkVersion(version)) @@ -163,7 +161,6 @@ function LMFSSampleApp() { setOnNavigationReady, removeAllListeners, vehicleId, - fetchAuthToken, ]); const createInstance = async () => { @@ -172,10 +169,10 @@ function LMFSSampleApp() { await deliveryDriverApi.initialize( PROVIDER_ID, vehicleId, - _tokenContext => { - console.log('onGetToken call, return token: ', authToken); - // Check if the token is expired, in such case request a new one. - return Promise.resolve(authToken || ''); + async tokenContext => { + console.log('onGetToken call for vehicle: ', tokenContext.vehicleId); + const token = await fetchAuthToken(tokenContext.vehicleId); + return token; }, (statusLevel, statusCode, message) => { console.log( @@ -184,13 +181,18 @@ function LMFSSampleApp() { } ); - deliveryDriverApi.getDeliveryVehicleReporter().setListener({ - onVehicleUpdateSucceed(vehicleUpdate) { - console.log('onVehicleUpdateSucceed: ', vehicleUpdate); - }, - onVehicleUpdateFailed(_vehicleUpdate, error) { - console.log('onVehicleUpdateFailed: ', error); - }, + const reporter = deliveryDriverApi.getDeliveryVehicleReporter(); + reporter.setOnVehicleUpdateSucceed(vehicleUpdate => { + console.log('onVehicleUpdateSucceed: ', vehicleUpdate); + }); + reporter.setOnVehicleUpdateFailed((vehicleUpdate, error) => { + console.log( + 'onVehicleUpdateFailed: ', + error.code, + error.message, + 'vehicleState:', + vehicleUpdate.vehicleState + ); }); } catch (error) { console.log('createInstance ', error); @@ -294,7 +296,7 @@ function LMFSSampleApp() { console.log('Error: Starting Guidance Error'); }; - const toggleLocationTrackingEnabled = (value: boolean) => { + const toggleLocationTrackingEnabled = async (value: boolean) => { setLocationTrackingEnabled(value); if (value) { @@ -303,9 +305,14 @@ function LMFSSampleApp() { navigationController?.stopUpdatingLocation(); } - deliveryDriverApi - .getDeliveryVehicleReporter() - .setLocationTrackingEnabled(value); + try { + await deliveryDriverApi + .getDeliveryVehicleReporter() + .setLocationTrackingEnabled(value); + } catch (e) { + console.warn('setLocationTrackingEnabled failed:', e); + setLocationTrackingEnabled(!value); + } }; const toggleAbnormalTerminationReporting = (value: boolean) => { diff --git a/example/ODRD/src/App.tsx b/example/ODRD/src/App.tsx index 70f24a5..5fc9a35 100644 --- a/example/ODRD/src/App.tsx +++ b/example/ODRD/src/App.tsx @@ -88,14 +88,18 @@ function ODRDSampleApp() { const [isVehicleStateOnline, setIsVehicleStateOnline] = useState(false); const [isLocationTrackingEnabled, setLocationTrackingEnabled] = useState(false); - const [authToken, setAuthToken] = useState(null); const [driverSdkVersion, setDriverSdkVersion] = useState(''); const [vehicleId, setVehicleId] = useState(VEHICLE_ID_DEFAULT); const [tempVehicleId, setTempVehicleId] = useState(VEHICLE_ID_DEFAULT); const clearInstance = useCallback(async () => { - await ridesharingDriverApi.clearInstance(); + ridesharingDriverApi + .clearInstance() + .then(() => { + setLocationTrackingEnabled(false); + }) + .catch(e => console.warn('clearInstance failed', e)); }, []); const onNavigationReady = useCallback(() => { @@ -118,26 +122,21 @@ function ODRDSampleApp() { [navigationController] ); - const fetchAuthToken = useCallback(async () => { - if (!vehicleId) { - console.log('Vehicle ID not set, skipping auth token fetch'); - return; - } - try { + const fetchAuthToken = useCallback( + async (tokenVehicleId?: string): Promise => { + const vid = tokenVehicleId || vehicleId; + if (!vid) { + throw new Error('Vehicle ID not set'); + } console.log('Fetching auth token...'); - const tokenUrl = BASE_URL + '/token/driver/' + vehicleId; + const tokenUrl = BASE_URL + '/token/driver/' + vid; const response = await fetch(tokenUrl); const token = await response.json(); console.log('Got token:', token); - - setAuthToken(token.jwt); - } catch (error) { - console.log( - 'There has been a problem connecting to the provider, please make sure it is running. ', - error - ); - } - }, [vehicleId]); + return token.jwt; + }, + [vehicleId] + ); useEffect(() => { if (!vehicleId) { @@ -148,7 +147,6 @@ function ODRDSampleApp() { console.log('Init ODRD Example app'); setOnArrival(onArrival); setOnNavigationReady(onNavigationReady); - fetchAuthToken(); ridesharingDriverApi .getDriverSdkVersion() .then(version => setDriverSdkVersion(version)) @@ -164,7 +162,6 @@ function ODRDSampleApp() { setOnNavigationReady, removeAllListeners, vehicleId, - fetchAuthToken, onArrival, onNavigationReady, ]); @@ -175,10 +172,10 @@ function ODRDSampleApp() { await ridesharingDriverApi.initialize( PROVIDER_ID, vehicleId, - _tokenContext => { - console.log('onGetToken call, return token: ', authToken); - // Check if the token is expired, in such case request a new one. - return Promise.resolve(authToken || ''); + async tokenContext => { + console.log('onGetToken call for vehicle: ', tokenContext.vehicleId); + const token = await fetchAuthToken(tokenContext.vehicleId); + return token; }, (statusLevel, statusCode, message) => { console.log( @@ -187,13 +184,18 @@ function ODRDSampleApp() { } ); - ridesharingDriverApi.getRidesharingVehicleReporter().setListener({ - onVehicleUpdateSucceed(vehicleUpdate) { - console.log('onVehicleUpdateSucceed: ', vehicleUpdate); - }, - onVehicleUpdateFailed(_vehicleUpdate, error) { - console.log('onVehicleUpdateFailed: ', error); - }, + const reporter = ridesharingDriverApi.getRidesharingVehicleReporter(); + reporter.setOnVehicleUpdateSucceed(vehicleUpdate => { + console.log('onVehicleUpdateSucceed: ', vehicleUpdate); + }); + reporter.setOnVehicleUpdateFailed((vehicleUpdate, error) => { + console.log( + 'onVehicleUpdateFailed: ', + error.code, + error.message, + 'vehicleState:', + vehicleUpdate.vehicleState + ); }); } catch (error) { console.log('createInstance ', error); @@ -295,7 +297,7 @@ function ODRDSampleApp() { console.log('Error: Starting Guidance Error'); }; - const toggleLocationTrackingEnabled = (value: boolean) => { + const toggleLocationTrackingEnabled = async (value: boolean) => { setLocationTrackingEnabled(value); if (value) { @@ -304,17 +306,27 @@ function ODRDSampleApp() { navigationController?.stopUpdatingLocation(); } - ridesharingDriverApi - .getRidesharingVehicleReporter() - .setLocationTrackingEnabled(value); + try { + await ridesharingDriverApi + .getRidesharingVehicleReporter() + .setLocationTrackingEnabled(value); + } catch (e) { + console.warn('setLocationTrackingEnabled failed:', e); + setLocationTrackingEnabled(!value); + } }; - const toggleVehicleState = (value: boolean) => { + const toggleVehicleState = async (value: boolean) => { setIsVehicleStateOnline(value); - ridesharingDriverApi - .getRidesharingVehicleReporter() - .setVehicleState(value ? VehicleState.ONLINE : VehicleState.OFFLINE); + try { + await ridesharingDriverApi + .getRidesharingVehicleReporter() + .setVehicleState(value ? VehicleState.ONLINE : VehicleState.OFFLINE); + } catch (e) { + console.warn('setVehicleState failed:', e); + setIsVehicleStateOnline(!value); + } }; const toggleAbnormalTerminationReporting = (value: boolean) => { diff --git a/ios/AuthTokenFactory.h b/ios/AuthTokenFactory.h index 4fd0dbb..f6d722c 100644 --- a/ios/AuthTokenFactory.h +++ b/ios/AuthTokenFactory.h @@ -19,10 +19,33 @@ NS_ASSUME_NONNULL_BEGIN -@class GRSDVehicleModel; +/** + * Callback block invoked when the native Driver SDK needs an auth token. + * The implementation should emit an event to JS to request the token. + * + * @param requestId Unique identifier for this token request. + * @param vehicleId The vehicle ID from the authorization context. + * @param taskId The task ID from the authorization context (may be empty). + */ +typedef void (^TokenRequestCallback)(NSString *requestId, NSString *vehicleId, NSString *taskId); +/** + * Auth token factory that requests tokens from JS via the React Native bridge. + * + * When the native Driver SDK needs a token (on each location update), this factory: + * 1. Generates a unique requestId + * 2. Invokes the callback to emit an event to JS + * 3. Blocks until JS resolves or rejects the request via resolveToken:/rejectToken: + * + * This mirrors the pattern used in the Flutter Driver SDK's AccessTokenProvider. + */ @interface AuthTokenFactory : NSObject -- (void)setAuthToken:(nonnull NSString *)authToken; + +@property(nonatomic, copy, nullable) TokenRequestCallback tokenRequestCallback; + +- (void)resolveToken:(NSString *)requestId token:(NSString *)token; +- (void)rejectToken:(NSString *)requestId error:(NSString *)error; + @end NS_ASSUME_NONNULL_END diff --git a/ios/AuthTokenFactory.m b/ios/AuthTokenFactory.m index 40a786a..6c64637 100644 --- a/ios/AuthTokenFactory.m +++ b/ios/AuthTokenFactory.m @@ -18,41 +18,109 @@ #import #import -// Used by GMTSAuthorization. static NSString *const kGRSDErrorDomain = @"GRSDErrorDomain"; - static const int kProviderErrorCode = 1000; - -static NSString *const kFailedToRetrieveTokenMessage = - @"There was an error retrieving the auth token."; +static const NSTimeInterval kTokenTimeoutSeconds = 30.0; @implementation AuthTokenFactory { - NSString *_vehicleID; - NSString *_userToken; -} - -static NSError *GRSDError(NSInteger errorCode, NSString *description) { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey : description, - }; - return [NSError errorWithDomain:kGRSDErrorDomain code:errorCode userInfo:userInfo]; + NSMutableDictionary *_pendingSemaphores; + NSMutableDictionary *_pendingTokens; + NSMutableDictionary *_pendingErrors; } -- (void)setAuthToken:(nonnull NSString *)authToken { - _userToken = authToken; +- (instancetype)init { + self = [super init]; + if (self) { + _pendingSemaphores = [NSMutableDictionary new]; + _pendingTokens = [NSMutableDictionary new]; + _pendingErrors = [NSMutableDictionary new]; + } + return self; } #pragma mark - GMTDAuthorization -// Function implementation for GMTDAuthorization to fetch token - (void)fetchTokenWithContext:(nullable GMTDAuthorizationContext *)authorizationContext completion:(nonnull GMTDAuthTokenFetchCompletionHandler)completion { - if (!_userToken) { - completion(nil, GRSDError(kProviderErrorCode, kFailedToRetrieveTokenMessage)); + if (!self.tokenRequestCallback) { + NSError *error = + [NSError errorWithDomain:kGRSDErrorDomain + code:kProviderErrorCode + userInfo:@{NSLocalizedDescriptionKey : @"Token request callback not set."}]; + completion(nil, error); return; } - completion(_userToken, nil); + NSString *requestId = [[NSUUID UUID] UUIDString]; + NSString *vehicleId = authorizationContext.vehicleID ?: @""; + NSString *taskId = authorizationContext.taskID ?: @""; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + @synchronized(self) { + _pendingSemaphores[requestId] = semaphore; + } + + // Request token from JS + self.tokenRequestCallback(requestId, vehicleId, taskId); + + // Block until JS responds (on a background queue — this is called by the Driver SDK + // on a dedicated thread, so blocking is expected and safe). + dispatch_time_t timeout = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kTokenTimeoutSeconds * NSEC_PER_SEC)); + long result = dispatch_semaphore_wait(semaphore, timeout); + + NSString *token = nil; + NSString *errorMessage = nil; + + @synchronized(self) { + token = _pendingTokens[requestId]; + errorMessage = _pendingErrors[requestId]; + [_pendingSemaphores removeObjectForKey:requestId]; + [_pendingTokens removeObjectForKey:requestId]; + [_pendingErrors removeObjectForKey:requestId]; + } + + if (result != 0) { + NSError *error = + [NSError errorWithDomain:kGRSDErrorDomain + code:kProviderErrorCode + userInfo:@{NSLocalizedDescriptionKey : @"Auth token request timed out."}]; + completion(nil, error); + return; + } + + if (errorMessage) { + NSError *error = [NSError errorWithDomain:kGRSDErrorDomain + code:kProviderErrorCode + userInfo:@{NSLocalizedDescriptionKey : errorMessage}]; + completion(nil, error); + return; + } + + completion(token, nil); +} + +#pragma mark - Token Resolution + +- (void)resolveToken:(NSString *)requestId token:(NSString *)token { + @synchronized(self) { + dispatch_semaphore_t semaphore = _pendingSemaphores[requestId]; + if (semaphore) { + _pendingTokens[requestId] = token; + dispatch_semaphore_signal(semaphore); + } + } +} + +- (void)rejectToken:(NSString *)requestId error:(NSString *)error { + @synchronized(self) { + dispatch_semaphore_t semaphore = _pendingSemaphores[requestId]; + if (semaphore) { + _pendingErrors[requestId] = error; + dispatch_semaphore_signal(semaphore); + } + } } @end diff --git a/ios/DeliveryDriverController.h b/ios/DeliveryDriverController.h index a010a60..0a8b523 100644 --- a/ios/DeliveryDriverController.h +++ b/ios/DeliveryDriverController.h @@ -16,16 +16,24 @@ #import #import +#import #import -#import "DriverEventDispatcher.h" +#import "AuthTokenFactory.h" NS_ASSUME_NONNULL_BEGIN +typedef void (^VehicleUpdateSuccessBlock)(GMTDVehicleUpdate *vehicleUpdate); +typedef void (^VehicleUpdateFailureBlock)(GMTDVehicleUpdate *vehicleUpdate, NSError *error); + @interface DeliveryDriverController : UIViewController @property GMTDVehicleReporter *vehicleReporter; +@property(nonatomic, copy, nullable) VehicleUpdateSuccessBlock onVehicleUpdateSucceed; +@property(nonatomic, copy, nullable) VehicleUpdateFailureBlock onVehicleUpdateFailed; - (void)initializeWithSession:(GMSNavigationSession *)session; -- (void)createDeliveryDriverInstance:(NSString *)providerId vehicleId:(NSString *)vehicleId; +- (void)createDeliveryDriverInstance:(NSString *)providerId + vehicleId:(NSString *)vehicleId + tokenRequestCallback:(TokenRequestCallback)callback; - (void)setLocationTrackingEnabled:(BOOL)isEnabled; - (void)setLocationReportingInterval:(double)interval; + (NSString *)getDriverSdkVersion; @@ -33,9 +41,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearInstance; + (void)setAbnormalTerminationReporting:(BOOL)isEnabled; - (void)getDeliveryVehicle:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; -- (void)setAuthToken:(NSString *)authToken; -- (void)addListener:(NSString *)eventName; -- (void)removeListeners:(NSString *)eventName; +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token; +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error; - (bool)isNavigatorInitialized; - (bool)isDriverApiInitialized; + (NSDictionary *)transformCLLocationToDictionary:(CLLocation *)location; diff --git a/ios/DeliveryDriverController.m b/ios/DeliveryDriverController.m index 5878257..4917553 100644 --- a/ios/DeliveryDriverController.m +++ b/ios/DeliveryDriverController.m @@ -25,7 +25,6 @@ @implementation DeliveryDriverController AuthTokenFactory *_lmfsTokenFactory; GMTDDriverContext *_driverContext; GMTDDeliveryDriverAPI *_driverAPI; -DriverEventDispatcher *lmfsEventDispatch; - (void)viewDidLoad { [super viewDidLoad]; @@ -36,8 +35,11 @@ - (void)initializeWithSession:(GMSNavigationSession *)session { _deliverySession = session; } -- (void)createDeliveryDriverInstance:(NSString *)providerId vehicleId:(NSString *)vehicleId { +- (void)createDeliveryDriverInstance:(NSString *)providerId + vehicleId:(NSString *)vehicleId + tokenRequestCallback:(TokenRequestCallback)callback { _lmfsTokenFactory = [[AuthTokenFactory alloc] init]; + _lmfsTokenFactory.tokenRequestCallback = callback; GMTDDriverContext *driverContext = [[GMTDDriverContext alloc] initWithAccessTokenProvider:_lmfsTokenFactory @@ -49,9 +51,6 @@ - (void)createDeliveryDriverInstance:(NSString *)providerId vehicleId:(NSString _vehicleReporter = _driverAPI.vehicleReporter; [_vehicleReporter addListener:self]; [_deliverySession.roadSnappedLocationProvider addListener:_vehicleReporter]; - - lmfsEventDispatch = [DriverEventDispatcher allocWithZone:nil]; - [lmfsEventDispatch startObserving]; // Enable event emission } - (void)setLocationTrackingEnabled:(BOOL)isEnabled { @@ -141,8 +140,12 @@ + (void)setAbnormalTerminationReporting:(BOOL)isEnabled { [GMTDDeliveryDriverAPI setAbnormalTerminationReportingEnabled:isEnabled]; } -- (void)setAuthToken:(NSString *)authToken { - [_lmfsTokenFactory setAuthToken:authToken]; +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token { + [_lmfsTokenFactory resolveToken:requestId token:token]; +} + +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error { + [_lmfsTokenFactory rejectToken:requestId error:error]; } #pragma mark - GMTDVehicleReporterListener @@ -150,38 +153,18 @@ - (void)setAuthToken:(NSString *)authToken { // Vehicle Reporter Listener for when vehicle updates are successful - (void)vehicleReporter:(GMTDVehicleReporter *)vehicleReporter didSucceedVehicleUpdate:(GMTDVehicleUpdate *)vehicleUpdate { - NSMutableDictionary *eventBody = [[NSMutableDictionary alloc] init]; - eventBody[@"vehicleUpdate"] = - [DeliveryDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; - - [lmfsEventDispatch sendEventName:@"didSucceedVehicleUpdate" body:eventBody]; + if (self.onVehicleUpdateSucceed) { + self.onVehicleUpdateSucceed(vehicleUpdate); + } } // Vehicle Reporter Listener for when vehicle updates fail - (void)vehicleReporter:(GMTDVehicleReporter *)vehicleReporter didFailVehicleUpdate:(GMTDVehicleUpdate *)vehicleUpdate withError:(NSError *)error { - NSMutableDictionary *eventBody = [[NSMutableDictionary alloc] init]; - eventBody[@"vehicleUpdate"] = - [DeliveryDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; - - if (error != nil) { - eventBody[@"error"] = @{ - @"code" : @(error.code), - @"domain" : error.domain, - @"message" : error.description, - }; + if (self.onVehicleUpdateFailed) { + self.onVehicleUpdateFailed(vehicleUpdate, error); } - - [lmfsEventDispatch sendEventName:@"didFailVehicleUpdate" body:eventBody]; -} - -- (void)addListener:(NSString *)eventName { - [lmfsEventDispatch startObserving]; -} - -- (void)removeListeners:(NSString *)eventName { - [lmfsEventDispatch stopObserving]; } - (bool)isNavigatorInitialized { diff --git a/ios/RCTDeliveryDriverModule.mm b/ios/RCTDeliveryDriverModule.mm index bdf3d73..e281d97 100644 --- a/ios/RCTDeliveryDriverModule.mm +++ b/ios/RCTDeliveryDriverModule.mm @@ -55,7 +55,50 @@ - (void)createDeliveryDriverInstance:(NSString *)providerId self->_driverController = [[DeliveryDriverController alloc] init]; [self->_driverController initializeWithSession:session]; - [self->_driverController createDeliveryDriverInstance:providerId vehicleId:vehicleId]; + + // Wire up vehicle reporter event callbacks to TurboModule EventEmitter. + __weak RCTDeliveryDriverModule *weakSelf = self; + self->_driverController.onVehicleUpdateSucceed = ^(GMTDVehicleUpdate *vehicleUpdate) { + RCTDeliveryDriverModule *strongSelf = weakSelf; + if (strongSelf) { + NSDictionary *updateDict = + [DeliveryDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"vehicleUpdate"] = updateDict; + [strongSelf emitOnVehicleUpdateSucceed:map]; + } + }; + self->_driverController.onVehicleUpdateFailed = + ^(GMTDVehicleUpdate *vehicleUpdate, NSError *error) { + RCTDeliveryDriverModule *strongSelf = weakSelf; + if (strongSelf) { + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"vehicleUpdate"] = + [DeliveryDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; + if (error != nil) { + map[@"error"] = @{ + @"code" : @(error.code), + @"domain" : error.domain, + @"message" : error.description, + }; + } + [strongSelf emitOnVehicleUpdateFailed:map]; + } + }; + + [self->_driverController + createDeliveryDriverInstance:providerId + vehicleId:vehicleId + tokenRequestCallback:^(NSString *requestId, NSString *vehicleId, NSString *taskId) { + RCTDeliveryDriverModule *strongSelf = weakSelf; + if (strongSelf) { + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"requestId"] = requestId; + map[@"vehicleId"] = vehicleId; + map[@"taskId"] = taskId; + [strongSelf emitOnGetToken:map]; + } + }]; resolve(@(YES)); }); } @@ -76,9 +119,7 @@ - (void)setLocationTrackingEnabled:(BOOL)isEnabled } - (void)setAbnormalTerminationReporting:(BOOL)isEnabled { - dispatch_async(dispatch_get_main_queue(), ^{ - [DeliveryDriverController setAbnormalTerminationReporting:isEnabled]; - }); + [DeliveryDriverController setAbnormalTerminationReporting:isEnabled]; } - (void)setLocationReportingInterval:(double)intervalSeconds @@ -97,9 +138,7 @@ - (void)setLocationReportingInterval:(double)intervalSeconds } - (void)getDriverSdkVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - dispatch_async(dispatch_get_main_queue(), ^{ - resolve([DeliveryDriverController getDriverSdkVersion]); - }); + resolve([DeliveryDriverController getDriverSdkVersion]); } - (void)getDeliveryVehicle:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { @@ -121,32 +160,12 @@ - (void)clearInstance:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBl }); } -- (void)setAuthToken:(NSString *)authToken - vehicleId:(NSString *)vehicleId - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject { - dispatch_async(dispatch_get_main_queue(), ^{ - if (self->_driverController == nil || - [self->_driverController isDriverApiInitialized] == false) { - reject(kDriverApiNotInitializedErrorCode, kDriverApiNotInitializedErrorMessage, nil); - return; - } - - [self->_driverController setAuthToken:authToken]; - resolve(nil); - }); -} - -- (void)addListener:(NSString *)eventName { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_driverController addListener:eventName]; - }); +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token { + [self->_driverController resolveAuthToken:requestId token:token]; } -- (void)removeListeners:(double)count { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_driverController removeListeners:[NSString stringWithFormat:@"%d", (int)count]]; - }); +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error { + [self->_driverController rejectAuthToken:requestId error:error]; } @end diff --git a/ios/RCTRideSharingModule.mm b/ios/RCTRideSharingModule.mm index 1d61b3b..525393b 100644 --- a/ios/RCTRideSharingModule.mm +++ b/ios/RCTRideSharingModule.mm @@ -55,7 +55,50 @@ - (void)createRidesharingInstance:(NSString *)providerId self->_driverController = [[RidesharingDriverController alloc] init]; [self->_driverController initializeWithSession:session]; - [self->_driverController createRidesharingInstance:providerId vehicleId:vehicleId]; + + // Wire up vehicle reporter event callbacks to TurboModule EventEmitter. + __weak RCTRideSharingModule *weakSelf = self; + self->_driverController.onVehicleUpdateSucceed = ^(GMTDVehicleUpdate *vehicleUpdate) { + RCTRideSharingModule *strongSelf = weakSelf; + if (strongSelf) { + NSDictionary *updateDict = + [RidesharingDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"vehicleUpdate"] = updateDict; + [strongSelf emitOnVehicleUpdateSucceed:map]; + } + }; + self->_driverController.onVehicleUpdateFailed = + ^(GMTDVehicleUpdate *vehicleUpdate, NSError *error) { + RCTRideSharingModule *strongSelf = weakSelf; + if (strongSelf) { + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"vehicleUpdate"] = + [RidesharingDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; + if (error != nil) { + map[@"error"] = @{ + @"code" : @(error.code), + @"domain" : error.domain, + @"message" : error.description, + }; + } + [strongSelf emitOnVehicleUpdateFailed:map]; + } + }; + + [self->_driverController + createRidesharingInstance:providerId + vehicleId:vehicleId + tokenRequestCallback:^(NSString *requestId, NSString *vehicleId, NSString *taskId) { + RCTRideSharingModule *strongSelf = weakSelf; + if (strongSelf) { + NSMutableDictionary *map = [NSMutableDictionary dictionary]; + map[@"requestId"] = requestId; + map[@"vehicleId"] = vehicleId; + map[@"taskId"] = taskId; + [strongSelf emitOnGetToken:map]; + } + }]; resolve(@(YES)); }); } @@ -91,9 +134,7 @@ - (void)setVehicleState:(BOOL)isOnline } - (void)setAbnormalTerminationReporting:(BOOL)isEnabled { - dispatch_async(dispatch_get_main_queue(), ^{ - [RidesharingDriverController setAbnormalTerminationReporting:isEnabled]; - }); + [RidesharingDriverController setAbnormalTerminationReporting:isEnabled]; } - (void)setLocationReportingInterval:(double)intervalSeconds @@ -112,9 +153,7 @@ - (void)setLocationReportingInterval:(double)intervalSeconds } - (void)getDriverSdkVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - dispatch_async(dispatch_get_main_queue(), ^{ - resolve([RidesharingDriverController getDriverSdkVersion]); - }); + resolve([RidesharingDriverController getDriverSdkVersion]); } - (void)clearInstance:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { @@ -124,32 +163,12 @@ - (void)clearInstance:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBl }); } -- (void)setAuthToken:(NSString *)authToken - vehicleId:(NSString *)vehicleId - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject { - dispatch_async(dispatch_get_main_queue(), ^{ - if (self->_driverController == nil || - [self->_driverController isDriverApiInitialized] == false) { - reject(kDriverApiNotInitializedErrorCode, kDriverApiNotInitializedErrorMessage, nil); - return; - } - - [self->_driverController setAuthToken:authToken]; - resolve(nil); - }); -} - -- (void)addListener:(NSString *)eventName { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_driverController addListener:eventName]; - }); +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token { + [self->_driverController resolveAuthToken:requestId token:token]; } -- (void)removeListeners:(double)count { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_driverController removeListeners:[NSString stringWithFormat:@"%d", (int)count]]; - }); +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error { + [self->_driverController rejectAuthToken:requestId error:error]; } @end diff --git a/ios/RidesharingDriverController.h b/ios/RidesharingDriverController.h index 99e2258..330930d 100644 --- a/ios/RidesharingDriverController.h +++ b/ios/RidesharingDriverController.h @@ -17,17 +17,24 @@ #import #import #import -#import "DriverEventDispatcher.h" +#import "AuthTokenFactory.h" NS_ASSUME_NONNULL_BEGIN +typedef void (^VehicleUpdateSuccessBlock)(GMTDVehicleUpdate *vehicleUpdate); +typedef void (^VehicleUpdateFailureBlock)(GMTDVehicleUpdate *vehicleUpdate, NSError *error); + @interface RidesharingDriverController : UIViewController @property GMTDVehicleReporter *vReporter; +@property(nonatomic, copy, nullable) VehicleUpdateSuccessBlock onVehicleUpdateSucceed; +@property(nonatomic, copy, nullable) VehicleUpdateFailureBlock onVehicleUpdateFailed; // Retrieve the NavigationSDK navigation session - (void)initializeWithSession:(GMSNavigationSession *)session; -- (void)createRidesharingInstance:(NSString *)providerId vehicleId:(NSString *)vehicleId; +- (void)createRidesharingInstance:(NSString *)providerId + vehicleId:(NSString *)vehicleId + tokenRequestCallback:(TokenRequestCallback)callback; - (void)setLocationTrackingEnabled:(BOOL)isEnabled; - (void)setVehicleState:(BOOL)isOnline; - (void)setLocationReportingInterval:(double)interval; @@ -35,9 +42,8 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)getRidesharingDriverSDKLongVersion; - (void)clearInstance; + (void)setAbnormalTerminationReporting:(BOOL)isEnabled; -- (void)setAuthToken:(NSString *)authToken; -- (void)addListener:(NSString *)eventName; -- (void)removeListeners:(NSString *)eventName; +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token; +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error; - (bool)isNavigatorInitialized; - (bool)isDriverApiInitialized; + (NSDictionary *)transformCLLocationToDictionary:(CLLocation *)location; diff --git a/ios/RidesharingDriverController.m b/ios/RidesharingDriverController.m index 66909ce..8f83c32 100644 --- a/ios/RidesharingDriverController.m +++ b/ios/RidesharingDriverController.m @@ -26,14 +26,16 @@ @implementation RidesharingDriverController AuthTokenFactory *_tokenFactory; GMTDRidesharingDriverAPI *_rideSharingDriverAPI; GMTDDriverContext *_rideSharingDriverContext; -DriverEventDispatcher *driverEventDispatch; - (void)initializeWithSession:(GMSNavigationSession *)session { _ridesharingSession = session; } -- (void)createRidesharingInstance:(NSString *)providerId vehicleId:(NSString *)vehicleId { +- (void)createRidesharingInstance:(NSString *)providerId + vehicleId:(NSString *)vehicleId + tokenRequestCallback:(TokenRequestCallback)callback { _tokenFactory = [[AuthTokenFactory alloc] init]; + _tokenFactory.tokenRequestCallback = callback; _rideSharingDriverContext = [[GMTDDriverContext alloc] initWithAccessTokenProvider:_tokenFactory @@ -43,8 +45,6 @@ - (void)createRidesharingInstance:(NSString *)providerId vehicleId:(NSString *)v _rideSharingDriverAPI = [[GMTDRidesharingDriverAPI alloc] initWithDriverContext:_rideSharingDriverContext]; - driverEventDispatch = [DriverEventDispatcher allocWithZone:nil]; - [driverEventDispatch startObserving]; // Enable event emission _ridesharingVehicleReporter = _rideSharingDriverAPI.vehicleReporter; [_ridesharingVehicleReporter addListener:self]; @@ -87,8 +87,12 @@ + (void)setAbnormalTerminationReporting:(BOOL)isEnabled { [GMTDRidesharingDriverAPI setAbnormalTerminationReportingEnabled:isEnabled]; } -- (void)setAuthToken:(NSString *)authToken { - [_tokenFactory setAuthToken:authToken]; +- (void)resolveAuthToken:(NSString *)requestId token:(NSString *)token { + [_tokenFactory resolveToken:requestId token:token]; +} + +- (void)rejectAuthToken:(NSString *)requestId error:(NSString *)error { + [_tokenFactory rejectToken:requestId error:error]; } #pragma mark - GMTDVehicleReporterListener @@ -96,44 +100,18 @@ - (void)setAuthToken:(NSString *)authToken { // Vehicle Reporter Listener for when vehicle updates are successful - (void)vehicleReporter:(GMTDVehicleReporter *)vehicleReporter didSucceedVehicleUpdate:(GMTDVehicleUpdate *)vehicleUpdate { - NSMutableDictionary *eventBody = [[NSMutableDictionary alloc] init]; - if (vehicleUpdate != nil) { - NSDictionary *dictionary = - [RidesharingDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; - eventBody[@"vehicleUpdate"] = dictionary; + if (self.onVehicleUpdateSucceed) { + self.onVehicleUpdateSucceed(vehicleUpdate); } - - [driverEventDispatch sendEventName:@"didSucceedVehicleUpdate" body:eventBody]; } // Vehicle Reporter Listener for when vehicle updates fail - (void)vehicleReporter:(GMTDVehicleReporter *)vehicleReporter didFailVehicleUpdate:(GMTDVehicleUpdate *)vehicleUpdate withError:(NSError *)error { - NSMutableDictionary *eventBody = [[NSMutableDictionary alloc] init]; - - if (vehicleUpdate != nil) { - eventBody[@"vehicleUpdate"] = - [RidesharingDriverController transformVehicleUpdateToDictionary:vehicleUpdate]; - } - - if (error != nil) { - eventBody[@"error"] = @{ - @"code" : @(error.code), - @"domain" : error.domain, - @"message" : error.description, - }; + if (self.onVehicleUpdateFailed) { + self.onVehicleUpdateFailed(vehicleUpdate, error); } - - [driverEventDispatch sendEventName:@"didFailVehicleUpdate" body:eventBody]; -} - -- (void)addListener:(NSString *)eventName { - [driverEventDispatch startObserving]; -} - -- (void)removeListeners:(NSString *)eventName { - [driverEventDispatch stopObserving]; } - (bool)isNavigatorInitialized { diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..060a846 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,69 @@ +/** + * 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. + */ + +jest.mock('react-native', () => { + const RN = jest.requireActual('react-native'); + + const mockDeliveryDriverModule = { + createDeliveryDriverInstance: jest.fn().mockResolvedValue(true), + clearInstance: jest.fn().mockResolvedValue(true), + setLocationTrackingEnabled: jest.fn().mockResolvedValue(true), + setLocationReportingInterval: jest.fn().mockResolvedValue(undefined), + getDeliveryVehicle: jest.fn().mockResolvedValue({ + name: 'test-vehicle', + navigationStatus: 'NO_GUIDANCE', + remainingDistanceMeters: 0, + remainingDuration: '0s', + remainingVehicleJourneySegments: [], + }), + getDriverSdkVersion: jest.fn().mockResolvedValue('1.0.0'), + resolveAuthToken: jest.fn(), + rejectAuthToken: jest.fn(), + setAbnormalTerminationReporting: jest.fn(), + onGetToken: jest.fn(() => ({ remove: jest.fn() })), + onStatusUpdate: jest.fn(() => ({ remove: jest.fn() })), + onVehicleUpdateSucceed: jest.fn(() => ({ remove: jest.fn() })), + onVehicleUpdateFailed: jest.fn(() => ({ remove: jest.fn() })), + addListener: jest.fn(), + removeListeners: jest.fn(), + }; + + const mockRidesharingModule = { + createRidesharingInstance: jest.fn().mockResolvedValue(true), + clearInstance: jest.fn().mockResolvedValue(true), + setLocationTrackingEnabled: jest.fn().mockResolvedValue(true), + setLocationReportingInterval: jest.fn().mockResolvedValue(undefined), + setVehicleState: jest.fn().mockResolvedValue(true), + getDriverSdkVersion: jest.fn().mockResolvedValue('1.0.0'), + resolveAuthToken: jest.fn(), + rejectAuthToken: jest.fn(), + setAbnormalTerminationReporting: jest.fn(), + onGetToken: jest.fn(() => ({ remove: jest.fn() })), + onStatusUpdate: jest.fn(() => ({ remove: jest.fn() })), + onVehicleUpdateSucceed: jest.fn(() => ({ remove: jest.fn() })), + onVehicleUpdateFailed: jest.fn(() => ({ remove: jest.fn() })), + addListener: jest.fn(), + removeListeners: jest.fn(), + }; + + RN.TurboModuleRegistry.getEnforcing = jest.fn(name => { + if (name === 'DeliveryDriverModule') return mockDeliveryDriverModule; + if (name === 'RidesharingModule') return mockRidesharingModule; + return null; + }); + + return RN; +}); diff --git a/package.json b/package.json index dea5f14..daa41b3 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,10 @@ "moduleNameMapper": { "^react-native$": "/node_modules/react-native", "^@googlemaps/react-native-navigation-sdk$": "/node_modules/@googlemaps/react-native-navigation-sdk" - } + }, + "setupFiles": [ + "./jest.setup.js" + ] }, "commitlint": { "extends": [ diff --git a/src/delivery/__tests__/deliveryDriverModule.test.tsx b/src/delivery/__tests__/deliveryDriverModule.test.tsx index f49f97c..3abc976 100644 --- a/src/delivery/__tests__/deliveryDriverModule.test.tsx +++ b/src/delivery/__tests__/deliveryDriverModule.test.tsx @@ -19,47 +19,46 @@ import { DeliveryDriverApi } from '../deliveryDriverApi'; const deliveryDriver = new DeliveryDriverApi(); describe('LMFS', () => { - test('initialize', () => { - expect( + test('initialize', async () => { + await expect( deliveryDriver.initialize( 'mobility-partner-lmfs', 'vehicle_3_1689729828602', () => Promise.resolve(''), () => {} ) - ).toHaveBeenCalled(); + ).resolves.toBeUndefined(); }); - test('setLocationTrackingEnabled', () => { - expect( + test('setLocationTrackingEnabled', async () => { + await expect( deliveryDriver .getDeliveryVehicleReporter() .setLocationTrackingEnabled(true) - ).toBe(true); + ).resolves.toBeUndefined(); }); - test('setLocationReportingInterval', () => { - expect( + test('setLocationReportingInterval', async () => { + await expect( deliveryDriver .getDeliveryVehicleReporter() .setLocationReportingInterval(20) - ).toHaveBeenCalled(); + ).resolves.toBeUndefined(); }); - test('clearInstance', () => { - expect(deliveryDriver.clearInstance()).toHaveBeenCalled(); + test('clearInstance', async () => { + await expect(deliveryDriver.clearInstance()).resolves.toBeUndefined(); }); test('setAbnormalTerminationReporting', () => { - expect(deliveryDriver.setAbnormalTerminationReportingEnabled(true)).toBe( - true - ); + expect(() => + deliveryDriver.setAbnormalTerminationReportingEnabled(true) + ).not.toThrow(); }); it('async/await', async () => { expect.assertions(1); - const datagetRidesharingDriverSDKVersion = - await deliveryDriver.getDriverSdkVersion(); - expect(datagetRidesharingDriverSDKVersion).toBe('3.1.1'); + const version = await deliveryDriver.getDriverSdkVersion(); + expect(version).toBe('1.0.0'); }); }); diff --git a/src/delivery/deliveryDriverApi.ts b/src/delivery/deliveryDriverApi.ts index 3c92a9c..27c0aa3 100644 --- a/src/delivery/deliveryDriverApi.ts +++ b/src/delivery/deliveryDriverApi.ts @@ -16,14 +16,14 @@ * limitations under the License. */ -import { DeliveryDriverModule } from '../native'; +import { DeliveryDriverModule, type DeliveryDriverModuleSpec } from '../native'; import { DriverApi, type OnGetTokenCallback, type OnStatusUpdateCallback, type VehicleReporter, } from '../shared'; -import type { DeliveryVehicle } from './types'; +import { VehicleStopState, type DeliveryVehicle } from './types'; type DeliveryVehicleReporter = VehicleReporter; @@ -38,7 +38,7 @@ interface DeliveryVehicleManager { } /** Entry point into the DriverApi for the delivery vertical. */ -export class DeliveryDriverApi extends DriverApi { +export class DeliveryDriverApi extends DriverApi { constructor() { super(DeliveryDriverModule); } @@ -52,11 +52,10 @@ export class DeliveryDriverApi extends DriverApi { this.onGetTokenCallback = onGetToken; this.vehicleId = vehicleId; - await this.nativeModule.createDeliveryDriverInstance(providerId, vehicleId); - - await this.fetchAndSetToken(); + // Set up event listeners first so the native token request can be handled + this.initializeEventListeners(onGetToken, onStatusUpdate); - this.initializeEventEmitter(onGetToken, onStatusUpdate); + await this.nativeModule.createDeliveryDriverInstance(providerId, vehicleId); } /** @@ -68,7 +67,8 @@ export class DeliveryDriverApi extends DriverApi { setLocationTrackingEnabled: this.setLocationTrackingEnabled, setLocationReportingInterval: intervalSeconds => this.nativeModule.setLocationReportingInterval(intervalSeconds), - setListener: this.setVehicleReporterListener, + setOnVehicleUpdateSucceed: this.setOnVehicleUpdateSucceed, + setOnVehicleUpdateFailed: this.setOnVehicleUpdateFailed, }; } @@ -78,8 +78,46 @@ export class DeliveryDriverApi extends DriverApi { */ getDeliveryVehicleManager(): DeliveryVehicleManager { return { - getDeliveryVehicle: () => this.nativeModule.getDeliveryVehicle(), + getDeliveryVehicle: async (): Promise => { + const spec = await this.nativeModule.getDeliveryVehicle(); + return { + providerId: spec.providerId, + vehicleName: spec.vehicleName, + vehicleId: spec.vehicleId, + vehicleStops: spec.vehicleStops.map(stop => ({ + vehicleStopState: toVehicleStopState(stop.vehicleStopState), + waypoint: stop.waypoint + ? { + position: stop.waypoint.position, + title: stop.waypoint.title, + placeId: stop.waypoint.placeId, + preferredHeading: stop.waypoint.preferredHeading, + vehicleStopover: stop.waypoint.vehicleStopover, + preferSameSideOfRoad: stop.waypoint.preferSameSideOfRoad, + } + : undefined, + taskInfoList: stop.taskInfoList.map(task => ({ + taskId: task.taskId, + taskDurationSeconds: task.taskDurationSeconds, + })), + })), + }; + }, }; } } + +function toVehicleStopState(value: number): VehicleStopState { + switch (value) { + case 1: + return VehicleStopState.NEW; + case 2: + return VehicleStopState.ENROUTE; + case 3: + return VehicleStopState.ARRIVED; + default: + return VehicleStopState.UNSPECIFIED; + } +} + export default DeliveryDriverApi; diff --git a/src/delivery/types.ts b/src/delivery/types.ts index ba8e0cb..e5b6fd7 100644 --- a/src/delivery/types.ts +++ b/src/delivery/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { type Waypoint } from '@googlemaps/react-native-navigation-sdk'; +import type { Waypoint } from '@googlemaps/react-native-navigation-sdk'; + export enum VehicleStopState { UNSPECIFIED = 0, NEW = 1, @@ -28,8 +29,8 @@ export interface TaskInfo { } export interface VehicleStop { - waypoint: Waypoint; vehicleStopState: VehicleStopState; + waypoint?: Waypoint; taskInfoList: TaskInfo[]; } diff --git a/src/native/NativeDeliveryDriverModule.ts b/src/native/NativeDeliveryDriverModule.ts index a74ba98..84a1886 100644 --- a/src/native/NativeDeliveryDriverModule.ts +++ b/src/native/NativeDeliveryDriverModule.ts @@ -16,49 +16,70 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypesNamespace'; // Note: Using Double instead of Int32 as codegen for TurboModules currently // fails to unbox values to Integer on iOS. +type TaskInfoSpec = Readonly<{ + taskId: string; + taskDurationSeconds: number; +}>; + +type VehicleStopSpec = Readonly<{ + vehicleStopState: number; + waypoint?: Readonly<{ + position?: Readonly<{ lat: number; lng: number }>; + title?: string; + placeId?: string; + preferredHeading?: number; + vehicleStopover?: boolean; + preferSameSideOfRoad?: boolean; + }>; + taskInfoList: ReadonlyArray; +}>; + type DeliveryVehicleSpec = Readonly<{ - name: string; - lastLocation?: Readonly<{ + providerId: string; + vehicleName: string; + vehicleId: string; + vehicleStops: ReadonlyArray; +}>; + +type VehicleUpdateSpec = Readonly<{ + location: Readonly<{ lat: number; lng: number; + time: number; + accuracy?: number; + altitude?: number; + bearing?: number; + speed: number; + verticalAccuracy?: number; }>; - navigationStatus: string; - remainingDistanceMeters: number; - remainingDuration: string; - remainingVehicleJourneySegments: ReadonlyArray< - Readonly<{ - stop?: Readonly<{ - plannedLocation?: Readonly<{ - lat: number; - lng: number; - }>; - state: string; - tasks: ReadonlyArray< - Readonly<{ - taskId: string; - taskType: string; - taskOutcome: string; - }> - >; - }>; - drivingDistanceMeters: number; - drivingDuration: string; - path: ReadonlyArray< - Readonly<{ - lat: number; - lng: number; - }> - >; - }> - >; - currentRouteSegmentEndPoint?: Readonly<{ - lat: number; - lng: number; + vehicleState: number; + destinationWaypoint?: Readonly<{ + position: Readonly<{ lat: number; lng: number }>; + title?: string; + placeId?: string; + preferredHeading?: number; + vehicleStopover?: boolean; + preferSameSideOfRoad?: boolean; }>; + remainingTimeInSeconds?: number; + remainingDistanceInMeters?: number; +}>; + +type VehicleUpdateErrorSpec = Readonly<{ + code: number; + domain: string; + message: string; +}>; + +type AuthTokenRequestSpec = Readonly<{ + requestId: string; + vehicleId: string; + taskId: string; }>; export interface Spec extends TurboModule { @@ -79,15 +100,33 @@ export interface Spec extends TurboModule { // SDK info getDriverSdkVersion(): Promise; - // Auth token - setAuthToken(authToken: string, vehicleId: string): Promise; + // Auth token - JS resolves a pending native token request + resolveAuthToken(requestId: string, token: string): void; + rejectAuthToken(requestId: string, error: string): void; // Abnormal termination setAbnormalTerminationReporting(isEnabled: boolean): void; - // Event emitter support - addListener(eventName: string): void; - removeListeners(count: number): void; + // Events emitted by native when auth token is needed + onGetToken: EventEmitter; + + // Status & vehicle reporter events + onStatusUpdate: EventEmitter< + Readonly<{ + statusLevel: string; + statusCode: string; + statusMsg: string; + }> + >; + onVehicleUpdateSucceed: EventEmitter< + Readonly<{ vehicleUpdate: VehicleUpdateSpec }> + >; + onVehicleUpdateFailed: EventEmitter< + Readonly<{ + vehicleUpdate: VehicleUpdateSpec; + error: VehicleUpdateErrorSpec; + }> + >; } export default TurboModuleRegistry.getEnforcing('DeliveryDriverModule'); diff --git a/src/native/NativeRidesharingModule.ts b/src/native/NativeRidesharingModule.ts index c85f5c1..fd554e3 100644 --- a/src/native/NativeRidesharingModule.ts +++ b/src/native/NativeRidesharingModule.ts @@ -16,6 +16,43 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; +import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypesNamespace'; + +type VehicleUpdateSpec = Readonly<{ + location: Readonly<{ + lat: number; + lng: number; + time: number; + accuracy?: number; + altitude?: number; + bearing?: number; + speed: number; + verticalAccuracy?: number; + }>; + vehicleState: number; + destinationWaypoint?: Readonly<{ + position: Readonly<{ lat: number; lng: number }>; + title?: string; + placeId?: string; + preferredHeading?: number; + vehicleStopover?: boolean; + preferSameSideOfRoad?: boolean; + }>; + remainingTimeInSeconds?: number; + remainingDistanceInMeters?: number; +}>; + +type VehicleUpdateErrorSpec = Readonly<{ + code: number; + domain: string; + message: string; +}>; + +type AuthTokenRequestSpec = Readonly<{ + requestId: string; + vehicleId: string; + taskId: string; +}>; export interface Spec extends TurboModule { // Instance management @@ -35,15 +72,33 @@ export interface Spec extends TurboModule { // SDK info getDriverSdkVersion(): Promise; - // Auth token - setAuthToken(authToken: string, vehicleId: string): Promise; + // Auth token - JS resolves a pending native token request + resolveAuthToken(requestId: string, token: string): void; + rejectAuthToken(requestId: string, error: string): void; // Abnormal termination setAbnormalTerminationReporting(isEnabled: boolean): void; - // Event emitter support - addListener(eventName: string): void; - removeListeners(count: number): void; + // Events emitted by native when auth token is needed + onGetToken: EventEmitter; + + // Status & vehicle reporter events + onStatusUpdate: EventEmitter< + Readonly<{ + statusLevel: string; + statusCode: string; + statusMsg: string; + }> + >; + onVehicleUpdateSucceed: EventEmitter< + Readonly<{ vehicleUpdate: VehicleUpdateSpec }> + >; + onVehicleUpdateFailed: EventEmitter< + Readonly<{ + vehicleUpdate: VehicleUpdateSpec; + error: VehicleUpdateErrorSpec; + }> + >; } export default TurboModuleRegistry.getEnforcing('RidesharingModule'); diff --git a/src/ridesharing/__tests__/ridesharingModule.test.tsx b/src/ridesharing/__tests__/ridesharingModule.test.tsx index 8f7fdfb..63a19c4 100644 --- a/src/ridesharing/__tests__/ridesharingModule.test.tsx +++ b/src/ridesharing/__tests__/ridesharingModule.test.tsx @@ -20,53 +20,54 @@ import { RidesharingDriverApi } from '../ridesharingDriverApi'; const ridesharing = new RidesharingDriverApi(); describe('ODRD', () => { - test('createRidesharingInstance', () => { - expect( + test('createRidesharingInstance', async () => { + await expect( ridesharing.initialize( 'mobility-partner-lmfs', 'vehicleId', () => Promise.resolve(''), () => {} ) - ).toHaveBeenCalled(); + ).resolves.toBeUndefined(); }); - test('setLocationTrackingEnabled', () => { - expect( + test('setLocationTrackingEnabled', async () => { + await expect( ridesharing .getRidesharingVehicleReporter() .setLocationTrackingEnabled(true) - ).toBe(true); + ).resolves.toBeUndefined(); }); - test('setVehicleState', () => { - expect( + test('setVehicleState', async () => { + await expect( ridesharing .getRidesharingVehicleReporter() .setVehicleState(VehicleState.ONLINE) - ).toBe(true); + ).resolves.toBeUndefined(); }); - test('setLocationReportingInterval', () => { - expect( + test('setLocationReportingInterval', async () => { + await expect( ridesharing .getRidesharingVehicleReporter() .setLocationReportingInterval(20) - ).toHaveBeenCalled(); + ).resolves.toBeUndefined(); }); - test('clearInstance', () => { - expect(ridesharing.clearInstance()).toHaveBeenCalled(); + test('clearInstance', async () => { + await expect(ridesharing.clearInstance()).resolves.toBeUndefined(); }); test('setAbnormalTerminationReporting', () => { - expect(ridesharing.setAbnormalTerminationReportingEnabled(true)).toBe(true); + expect(() => + ridesharing.setAbnormalTerminationReportingEnabled(true) + ).not.toThrow(); }); it('async/await', async () => { expect.assertions(1); - const datagetRidesharingDriverSDKVersion = - await ridesharing.getDriverSdkVersion(); - expect(datagetRidesharingDriverSDKVersion).toBe('3.1.1'); + const version = await ridesharing.getDriverSdkVersion(); + expect(version).toBe('1.0.0'); }); }); diff --git a/src/ridesharing/ridesharingDriverApi.ts b/src/ridesharing/ridesharingDriverApi.ts index 5b7702a..03caaf9 100644 --- a/src/ridesharing/ridesharingDriverApi.ts +++ b/src/ridesharing/ridesharingDriverApi.ts @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RidesharingModule } from '../native'; +import { RidesharingModule, type RidesharingModuleSpec } from '../native'; import { type VehicleReporter, VehicleState, @@ -33,7 +33,7 @@ interface RidesharingVehicleReporter extends VehicleReporter { } /** Entry point into the DriverApi for the ridesharing vertical. */ -export class RidesharingDriverApi extends DriverApi { +export class RidesharingDriverApi extends DriverApi { constructor() { super(RidesharingModule); } @@ -47,11 +47,10 @@ export class RidesharingDriverApi extends DriverApi { this.onGetTokenCallback = onGetToken; this.vehicleId = vehicleId; - await this.nativeModule.createRidesharingInstance(providerId, vehicleId); - - await this.fetchAndSetToken(); + // Set up event listeners first so the native token request can be handled + this.initializeEventListeners(onGetToken, onStatusUpdate); - this.initializeEventEmitter(onGetToken, onStatusUpdate); + await this.nativeModule.createRidesharingInstance(providerId, vehicleId); } /** @@ -61,13 +60,13 @@ export class RidesharingDriverApi extends DriverApi { getRidesharingVehicleReporter = (): RidesharingVehicleReporter => { return { setVehicleState: async state => { - await this.fetchAndSetToken(); await this.nativeModule.setVehicleState(state === VehicleState.ONLINE); }, setLocationTrackingEnabled: this.setLocationTrackingEnabled, setLocationReportingInterval: (intervalSeconds: number) => this.nativeModule.setLocationReportingInterval(intervalSeconds), - setListener: this.setVehicleReporterListener, + setOnVehicleUpdateSucceed: this.setOnVehicleUpdateSucceed, + setOnVehicleUpdateFailed: this.setOnVehicleUpdateFailed, }; }; } diff --git a/src/shared/driverApi.ts b/src/shared/driverApi.ts index b80f333..a587c37 100644 --- a/src/shared/driverApi.ts +++ b/src/shared/driverApi.ts @@ -13,57 +13,58 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { NativeEventEmitter, NativeModules } from 'react-native'; -import type { - OnGetTokenCallback, - OnStatusUpdateCallback, - VehicleReporterListener, - VehicleUpdate, - VehicleUpdateError, +import type { EventSubscription } from 'react-native'; +import { + DriverStatusCode, + DriverStatusLevel, + type AuthTokenContext, + type OnGetTokenCallback, + type OnStatusUpdateCallback, + type VehicleUpdate, + type VehicleUpdateError, } from './types'; -export const STATUS_UPDATE_EVENT_TYPE = 'updateStatus'; -export const VEHICLE_REPORTER_SUCCEED_UPDATE_TYPE = 'didSucceedVehicleUpdate'; -export const VEHICLE_REPORTER_FAILED_UPDATE_TYPE = 'didFailVehicleUpdate'; +/** A callable that subscribes to an event and returns a removable subscription. */ +type EventEmitterFn = (handler: (event: T) => void) => EventSubscription; -type StatusUpdateEvent = { - statusLevel: string; - statusCode: string; - statusMsg: string; -}; - -type VehicleReporterSuccessEvent = { - vehicleUpdate: VehicleUpdate; -}; - -type VehicleReporterFailureEvent = { - vehicleUpdate: VehicleUpdate; - error: VehicleUpdateError; -}; - -const TOKEN_UPDATE_INTERVAL_SECONDS = 10; - -type DriverEventDispatcherModule = { - addListener: (eventType: string) => void; - removeListeners: (count: number) => void; -}; +/** + * Interface for the native TurboModule used by DriverApi. + * Both DeliveryDriverModule and RidesharingModule extend this shape. + * This keeps codegen spec types internal — event payloads are converted to + * public types (VehicleUpdate, VehicleUpdateError, AuthTokenContext) at the boundary. + */ +export interface DriverNativeModule { + clearInstance(): Promise; + setAbnormalTerminationReporting(isEnabled: boolean): void; + getDriverSdkVersion(): Promise; + setLocationTrackingEnabled(isEnabled: boolean): Promise; + setLocationReportingInterval(intervalSeconds: number): Promise; + resolveAuthToken(requestId: string, token: string): void; + rejectAuthToken(requestId: string, error: string): void; + onGetToken: EventEmitterFn< + Readonly<{ requestId: string; vehicleId: string; taskId: string }> + >; + onStatusUpdate: EventEmitterFn< + Readonly<{ statusLevel: string; statusCode: string; statusMsg: string }> + >; + onVehicleUpdateSucceed: EventEmitterFn< + Readonly<{ vehicleUpdate: VehicleUpdate }> + >; + onVehicleUpdateFailed: EventEmitterFn< + Readonly<{ vehicleUpdate: VehicleUpdate; error: VehicleUpdateError }> + >; +} -export abstract class DriverApi { - nativeModule: typeof NativeModules; +export abstract class DriverApi< + T extends DriverNativeModule = DriverNativeModule, +> { + nativeModule: T; onGetTokenCallback?: OnGetTokenCallback; vehicleId?: string; - isLocationTrackingEnabled: boolean = false; - fetchTokenTimeoutId?: ReturnType; - private createEventEmitter = (): NativeEventEmitter => { - const driverEventDispatcher = ( - NativeModules as Record - ).DriverEventDispatcher; - - return new NativeEventEmitter(driverEventDispatcher ?? null); - }; + private subscriptions: EventSubscription[] = []; - constructor(nativeModule: typeof NativeModules) { + constructor(nativeModule: T) { this.nativeModule = nativeModule; } @@ -71,7 +72,8 @@ export abstract class DriverApi { * Creates an Api instance. This can be used to retrieve a vehicle reporter that allows enabling location reporting to FleetEngine. * @param providerId - Unique identifier for the server provider. * @param vehicleId - Unique identifier for this vehicle for this provider. - * @param viewId - React ViewId given to the NavigationView component set up for the application. Can be retrieved using the {@link findNodeHandle()} API. + * @param onGetToken - Callback invoked by the native SDK whenever a fresh auth token is needed. This is called on every location update cycle. + * @param onStatusUpdate - Optional callback for status updates from Fleet Engine. **Android only** — on iOS, use `setOnVehicleUpdateSucceed` / `setOnVehicleUpdateFailed` on the vehicle reporter instead. * @returns Promise that resolves once the Api instance has been created. * @throws This rejects the promise in case there's an Api instance already created. */ @@ -82,40 +84,65 @@ export abstract class DriverApi { onStatusUpdate?: OnStatusUpdateCallback ): Promise; - protected initializeEventEmitter = ( - _onGetToken: OnGetTokenCallback, // TODO(jokerttu): consider removing or implementing this parameter + protected initializeEventListeners = ( + onGetToken: OnGetTokenCallback, onStatusUpdate?: OnStatusUpdateCallback ): void => { - const eventEmitter = this.createEventEmitter(); - - // Allow a single active listener. - eventEmitter.removeAllListeners(STATUS_UPDATE_EVENT_TYPE); + // Clear any previous subscriptions to avoid duplicates on re-initialize + this.removeAllSubscriptions(); + + // Subscribe to native token requests — this is the core auth token bridge. + // When the native Driver SDK needs a token (e.g. on each location update), + // it emits onGetToken. We call the user's callback and resolve/reject back to native. + this.addSubscription( + this.nativeModule.onGetToken(event => { + onGetToken({ + vehicleId: event.vehicleId ?? undefined, + taskId: event.taskId ?? undefined, + } satisfies AuthTokenContext) + .then(token => { + this.nativeModule.resolveAuthToken(event.requestId, token); + }) + .catch(error => { + this.nativeModule.rejectAuthToken( + event.requestId, + error?.message ?? String(error) + ); + }); + }) + ); if (onStatusUpdate) { - eventEmitter.addListener(STATUS_UPDATE_EVENT_TYPE, event => { - const payload = event as StatusUpdateEvent | undefined; - if (!payload) return; - const { statusLevel, statusCode, statusMsg } = payload; - onStatusUpdate(statusLevel, statusCode, statusMsg); - }); + this.addSubscription( + this.nativeModule.onStatusUpdate(event => { + onStatusUpdate( + (event.statusLevel as DriverStatusLevel) ?? DriverStatusLevel.INFO, + (event.statusCode as DriverStatusCode) ?? DriverStatusCode.DEFAULT, + event.statusMsg + ); + }) + ); } }; + private addSubscription(subscription: EventSubscription): void { + this.subscriptions.push(subscription); + } + + private removeAllSubscriptions(): void { + for (const sub of this.subscriptions) { + sub.remove(); + } + this.subscriptions = []; + } + /** * Clears up the Api instance. This should be done once the Api is no longer required to prevent memory leaks. * @returns Promise that resolves once the Api has been cleared. */ - clearInstance = (): Promise => { - clearTimeout(this.fetchTokenTimeoutId); - - const eventEmitter = this.createEventEmitter(); - - // Allow a single active listener. - eventEmitter.removeAllListeners(STATUS_UPDATE_EVENT_TYPE); - eventEmitter.removeAllListeners(VEHICLE_REPORTER_SUCCEED_UPDATE_TYPE); - eventEmitter.removeAllListeners(VEHICLE_REPORTER_FAILED_UPDATE_TYPE); - - return this.nativeModule.clearInstance(); + clearInstance = async (): Promise => { + await this.nativeModule.clearInstance(); + this.removeAllSubscriptions(); }; /** @@ -123,12 +150,8 @@ export abstract class DriverApi { * * @param enabled - whether abnormal SDK terminations should be reported. */ - setAbnormalTerminationReportingEnabled = ( - isEnabled: boolean - ): Promise => { - return this.nativeModule.setAbnormalTerminationReporting( - isEnabled - ) as Promise; + setAbnormalTerminationReportingEnabled = (isEnabled: boolean): void => { + this.nativeModule.setAbnormalTerminationReporting(isEnabled); }; /** @@ -141,63 +164,26 @@ export abstract class DriverApi { protected setLocationTrackingEnabled = async ( isEnabled: boolean ): Promise => { - this.isLocationTrackingEnabled = isEnabled; - - if (isEnabled) { - await this.fetchAndSetToken(); - } else { - clearTimeout(this.fetchTokenTimeoutId); - } - await this.nativeModule.setLocationTrackingEnabled(isEnabled); }; - protected pollAuthToken = (): void => { - this.fetchTokenTimeoutId = setTimeout(async () => { - await this.fetchAndSetToken(); - - if (this.isLocationTrackingEnabled) { - this.pollAuthToken(); - } - }, TOKEN_UPDATE_INTERVAL_SECONDS); - }; - - protected setVehicleReporterListener = ( - listener: VehicleReporterListener + protected setOnVehicleUpdateSucceed = ( + callback: (vehicleUpdate: VehicleUpdate) => void ): void => { - const eventEmitter = this.createEventEmitter(); - - eventEmitter.removeAllListeners(VEHICLE_REPORTER_SUCCEED_UPDATE_TYPE); - eventEmitter.removeAllListeners(VEHICLE_REPORTER_FAILED_UPDATE_TYPE); - - eventEmitter.addListener(VEHICLE_REPORTER_SUCCEED_UPDATE_TYPE, event => { - const payload = event as VehicleReporterSuccessEvent | undefined; - if (payload && listener.onVehicleUpdateSucceed) { - listener.onVehicleUpdateSucceed(payload.vehicleUpdate); - } - }); - - eventEmitter.addListener(VEHICLE_REPORTER_FAILED_UPDATE_TYPE, event => { - const payload = event as VehicleReporterFailureEvent | undefined; - if (payload && listener.onVehicleUpdateFailed) { - listener.onVehicleUpdateFailed(payload.vehicleUpdate, payload.error); - } - }); + this.addSubscription( + this.nativeModule.onVehicleUpdateSucceed(event => { + callback(event.vehicleUpdate); + }) + ); }; - protected fetchAndSetToken = async (): Promise => { - if (this.onGetTokenCallback == null) { - return; - } - - if (!this.vehicleId) { - return; - } - - const token = await this.onGetTokenCallback({ - vehicleId: this.vehicleId, - }); - - await this.nativeModule.setAuthToken(token, this.vehicleId); + protected setOnVehicleUpdateFailed = ( + callback: (vehicleUpdate: VehicleUpdate, error: VehicleUpdateError) => void + ): void => { + this.addSubscription( + this.nativeModule.onVehicleUpdateFailed(event => { + callback(event.vehicleUpdate, event.error); + }) + ); }; } diff --git a/src/shared/types.ts b/src/shared/types.ts index f2a8158..57b6f1a 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -19,9 +19,45 @@ import type { Waypoint, } from '@googlemaps/react-native-navigation-sdk'; +/** + * Severity level of a driver status update from Fleet Engine. + * + * **Android only.** No status updates are delivered on iOS; + * use the vehicle reporter's `setOnVehicleUpdateSucceed` / `setOnVehicleUpdateFailed` instead. + */ +export enum DriverStatusLevel { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARNING = 'WARNING', + ERROR = 'ERROR', +} + +/** + * Status code describing the type of driver status update. + * + * **Android only.** No status updates are delivered on iOS; + * use the vehicle reporter's `setOnVehicleUpdateSucceed` / `setOnVehicleUpdateFailed` instead. + */ +export enum DriverStatusCode { + DEFAULT = 'DEFAULT', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', + VEHICLE_NOT_FOUND = 'VEHICLE_NOT_FOUND', + BACKEND_CONNECTIVITY_ERROR = 'BACKEND_CONNECTIVITY_ERROR', + PERMISSION_DENIED = 'PERMISSION_DENIED', + SERVICE_ERROR = 'SERVICE_ERROR', + FILE_ACCESS_ERROR = 'FILE_ACCESS_ERROR', + TRAVELED_ROUTE_ERROR = 'TRAVELED_ROUTE_ERROR', +} + +/** + * Callback for driver status updates from Fleet Engine. + * + * **Android only.** No status updates are delivered on iOS; + * use the vehicle reporter's `setOnVehicleUpdateSucceed` / `setOnVehicleUpdateFailed` for iOS vehicle update callbacks. + */ export type OnStatusUpdateCallback = ( - statusLevel: string, - statusCode: string, + statusLevel: DriverStatusLevel, + statusCode: DriverStatusCode, statusMsg: string ) => void; @@ -52,11 +88,24 @@ export interface VehicleReporter { setLocationReportingInterval(intervalSeconds: number): Promise; /** - * Allows setting a listener for reporting updates. This is only - * available for iOS. For Android, please use {@link OnStatusUpdateCallback}. - * @param listener + * Sets a callback for successful vehicle updates. + * + * **iOS only.** No vehicle reporter updates are delivered on Android; + * use {@link OnStatusUpdateCallback} (passed to `initialize`) instead. + */ + setOnVehicleUpdateSucceed( + callback: (vehicleUpdate: VehicleUpdate) => void + ): void; + + /** + * Sets a callback for failed vehicle updates. + * + * **iOS only.** No vehicle reporter updates are delivered on Android; + * use {@link OnStatusUpdateCallback} (passed to `initialize`) instead. */ - setListener(listener: VehicleReporterListener): void; + setOnVehicleUpdateFailed( + callback: (vehicleUpdate: VehicleUpdate, error: VehicleUpdateError) => void + ): void; } export interface VehicleUpdate { @@ -73,11 +122,3 @@ export interface VehicleUpdateError { domain: string; message: string; } - -export interface VehicleReporterListener { - onVehicleUpdateSucceed(vehicleUpdate: VehicleUpdate): void; - onVehicleUpdateFailed( - vehicleUpdate: VehicleUpdate, - error: VehicleUpdateError - ): void; -} From 6972dbbf204f74205016a0f743448a7db4871ea6 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Thu, 30 Apr 2026 10:10:30 +0300 Subject: [PATCH 2/2] fix: thread blocking issues on clear instance --- .../driversdk/lmfs/DeliveryDriverModule.java | 1 + .../driversdk/odrd/RidesharingModule.java | 1 + .../shared/DriverAuthTokenFactory.java | 8 ++ ios/AuthTokenFactory.h | 4 +- ios/AuthTokenFactory.m | 81 +++++++------------ ios/DeliveryDriverController.m | 1 + ios/RidesharingDriverController.m | 1 + 7 files changed, 44 insertions(+), 53 deletions(-) diff --git a/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java b/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java index a94b71b..087c38f 100644 --- a/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java +++ b/android/src/main/java/com/google/android/react/driversdk/lmfs/DeliveryDriverModule.java @@ -170,6 +170,7 @@ public void clearInstance(Promise promise) { UiThreadUtil.runOnUiThread( () -> { try { + tokenFactory.cancelAllPendingRequests(); vehicleReporter = null; DeliveryDriverApi.clearInstance(); diff --git a/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java b/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java index 13912b1..93dc870 100644 --- a/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java +++ b/android/src/main/java/com/google/android/react/driversdk/odrd/RidesharingModule.java @@ -193,6 +193,7 @@ public void clearInstance(Promise promise) { UiThreadUtil.runOnUiThread( () -> { try { + tokenFactory.cancelAllPendingRequests(); RidesharingDriverApi.clearInstance(); vehicleReporter = null; diff --git a/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java b/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java index d735ca6..359b67a 100644 --- a/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java +++ b/android/src/main/java/com/google/android/react/driversdk/shared/DriverAuthTokenFactory.java @@ -85,4 +85,12 @@ public void rejectToken(String requestId, String error) { future.setException(new RuntimeException(error)); } } + + /** Cancels all pending token requests. Called when the driver instance is cleared. */ + public void cancelAllPendingRequests() { + for (SettableFuture future : pendingRequests.values()) { + future.setException(new RuntimeException("Driver instance cleared")); + } + pendingRequests.clear(); + } } diff --git a/ios/AuthTokenFactory.h b/ios/AuthTokenFactory.h index f6d722c..9cc97ef 100644 --- a/ios/AuthTokenFactory.h +++ b/ios/AuthTokenFactory.h @@ -35,7 +35,8 @@ typedef void (^TokenRequestCallback)(NSString *requestId, NSString *vehicleId, N * When the native Driver SDK needs a token (on each location update), this factory: * 1. Generates a unique requestId * 2. Invokes the callback to emit an event to JS - * 3. Blocks until JS resolves or rejects the request via resolveToken:/rejectToken: + * 3. Stores the completion handler and invokes it when JS resolves or rejects via + * resolveToken:/rejectToken: * * This mirrors the pattern used in the Flutter Driver SDK's AccessTokenProvider. */ @@ -45,6 +46,7 @@ typedef void (^TokenRequestCallback)(NSString *requestId, NSString *vehicleId, N - (void)resolveToken:(NSString *)requestId token:(NSString *)token; - (void)rejectToken:(NSString *)requestId error:(NSString *)error; +- (void)cancelAllPendingRequests; @end diff --git a/ios/AuthTokenFactory.m b/ios/AuthTokenFactory.m index 6c64637..1e13d8c 100644 --- a/ios/AuthTokenFactory.m +++ b/ios/AuthTokenFactory.m @@ -20,20 +20,15 @@ static NSString *const kGRSDErrorDomain = @"GRSDErrorDomain"; static const int kProviderErrorCode = 1000; -static const NSTimeInterval kTokenTimeoutSeconds = 30.0; @implementation AuthTokenFactory { - NSMutableDictionary *_pendingSemaphores; - NSMutableDictionary *_pendingTokens; - NSMutableDictionary *_pendingErrors; + NSMutableDictionary *_pendingCompletions; } - (instancetype)init { self = [super init]; if (self) { - _pendingSemaphores = [NSMutableDictionary new]; - _pendingTokens = [NSMutableDictionary new]; - _pendingErrors = [NSMutableDictionary new]; + _pendingCompletions = [NSMutableDictionary new]; } return self; } @@ -55,71 +50,53 @@ - (void)fetchTokenWithContext:(nullable GMTDAuthorizationContext *)authorization NSString *vehicleId = authorizationContext.vehicleID ?: @""; NSString *taskId = authorizationContext.taskID ?: @""; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - @synchronized(self) { - _pendingSemaphores[requestId] = semaphore; + _pendingCompletions[requestId] = [completion copy]; } // Request token from JS self.tokenRequestCallback(requestId, vehicleId, taskId); +} - // Block until JS responds (on a background queue — this is called by the Driver SDK - // on a dedicated thread, so blocking is expected and safe). - dispatch_time_t timeout = - dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kTokenTimeoutSeconds * NSEC_PER_SEC)); - long result = dispatch_semaphore_wait(semaphore, timeout); - - NSString *token = nil; - NSString *errorMessage = nil; +#pragma mark - Token Resolution +- (void)resolveToken:(NSString *)requestId token:(NSString *)token { + GMTDAuthTokenFetchCompletionHandler completion; @synchronized(self) { - token = _pendingTokens[requestId]; - errorMessage = _pendingErrors[requestId]; - [_pendingSemaphores removeObjectForKey:requestId]; - [_pendingTokens removeObjectForKey:requestId]; - [_pendingErrors removeObjectForKey:requestId]; + completion = _pendingCompletions[requestId]; + [_pendingCompletions removeObjectForKey:requestId]; } - - if (result != 0) { - NSError *error = - [NSError errorWithDomain:kGRSDErrorDomain - code:kProviderErrorCode - userInfo:@{NSLocalizedDescriptionKey : @"Auth token request timed out."}]; - completion(nil, error); - return; + if (completion) { + completion(token, nil); } +} - if (errorMessage) { +- (void)rejectToken:(NSString *)requestId error:(NSString *)errorMessage { + GMTDAuthTokenFetchCompletionHandler completion; + @synchronized(self) { + completion = _pendingCompletions[requestId]; + [_pendingCompletions removeObjectForKey:requestId]; + } + if (completion) { NSError *error = [NSError errorWithDomain:kGRSDErrorDomain code:kProviderErrorCode userInfo:@{NSLocalizedDescriptionKey : errorMessage}]; completion(nil, error); - return; } - - completion(token, nil); } -#pragma mark - Token Resolution - -- (void)resolveToken:(NSString *)requestId token:(NSString *)token { +- (void)cancelAllPendingRequests { + NSDictionary *completions; @synchronized(self) { - dispatch_semaphore_t semaphore = _pendingSemaphores[requestId]; - if (semaphore) { - _pendingTokens[requestId] = token; - dispatch_semaphore_signal(semaphore); - } + completions = [_pendingCompletions copy]; + [_pendingCompletions removeAllObjects]; } -} - -- (void)rejectToken:(NSString *)requestId error:(NSString *)error { - @synchronized(self) { - dispatch_semaphore_t semaphore = _pendingSemaphores[requestId]; - if (semaphore) { - _pendingErrors[requestId] = error; - dispatch_semaphore_signal(semaphore); - } + NSError *error = + [NSError errorWithDomain:kGRSDErrorDomain + code:kProviderErrorCode + userInfo:@{NSLocalizedDescriptionKey : @"Driver instance cleared"}]; + for (GMTDAuthTokenFetchCompletionHandler completion in completions.allValues) { + completion(nil, error); } } diff --git a/ios/DeliveryDriverController.m b/ios/DeliveryDriverController.m index 4917553..bf34795 100644 --- a/ios/DeliveryDriverController.m +++ b/ios/DeliveryDriverController.m @@ -70,6 +70,7 @@ + (nonnull NSString *)getDriverSdkVersion { } - (void)clearInstance { + [_lmfsTokenFactory cancelAllPendingRequests]; [_vehicleReporter setLocationTrackingEnabled:NO]; [_vehicleReporter removeListener:self]; [_deliverySession.roadSnappedLocationProvider removeListener:_vehicleReporter]; diff --git a/ios/RidesharingDriverController.m b/ios/RidesharingDriverController.m index 8f83c32..fcf6353 100644 --- a/ios/RidesharingDriverController.m +++ b/ios/RidesharingDriverController.m @@ -76,6 +76,7 @@ + (NSString *)getRidesharingDriverSDKLongVersion { } - (void)clearInstance { + [_tokenFactory cancelAllPendingRequests]; [_ridesharingVehicleReporter setLocationTrackingEnabled:NO]; [_ridesharingVehicleReporter removeListener:self]; [_ridesharingSession.roadSnappedLocationProvider removeListener:_ridesharingVehicleReporter];