@doorstepai/dropoff-sdk 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/doorstepai/dropoffsdk/DoorstepAIModule.kt +147 -2
  3. package/ios/DoorstepAI.mm +15 -1
  4. package/ios/DoorstepAI.swift +127 -3
  5. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/DoorstepDropoffSDK +0 -0
  6. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Info.plist +0 -0
  7. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.abi.json +5289 -1482
  8. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +88 -4
  9. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  10. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.swiftinterface +88 -4
  11. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/_CodeSignature/CodeResources +11 -11
  12. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/DWARF/DoorstepDropoffSDK +0 -0
  13. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/DoorstepDropoffSDK.yml +2560 -2116
  14. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/DoorstepDropoffSDK +0 -0
  15. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Info.plist +0 -0
  16. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +5289 -1482
  17. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +88 -4
  18. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  19. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +88 -4
  20. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +5289 -1482
  21. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +88 -4
  22. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  23. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +88 -4
  24. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/_CodeSignature/CodeResources +21 -21
  25. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/DWARF/DoorstepDropoffSDK +0 -0
  26. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/DoorstepDropoffSDK.yml +2560 -2116
  27. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/DoorstepDropoffSDK.yml +2551 -2105
  28. package/lib/module/DoorstepAI.js +86 -3
  29. package/lib/module/DoorstepAI.js.map +1 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/typescript/src/DoorstepAI.d.ts +65 -2
  32. package/lib/typescript/src/DoorstepAI.d.ts.map +1 -1
  33. package/lib/typescript/src/RootDoorstepAI.d.ts +1 -1
  34. package/lib/typescript/src/RootDoorstepAI.d.ts.map +1 -1
  35. package/lib/typescript/src/index.d.ts +1 -1
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/DoorstepAI.tsx +143 -4
  39. package/src/index.tsx +4 -0
@@ -86,7 +86,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
86
86
  dependencies {
87
87
  implementation "com.facebook.react:react-android"
88
88
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
89
- implementation("ai.doorstep.com:doorstepai-dropoff-sdk:2.0.10")
89
+ implementation("ai.doorstep.com:doorstepai-dropoff-sdk:2.0.13")
90
90
  api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
91
91
  }
92
92
 
@@ -11,9 +11,19 @@ import com.doorstepai.sdks.tracking.AddressType
11
11
  import com.doorstepai.sdks.tracking.DropoffType
12
12
  import com.doorstepai.sdks.tracking.LatLngObject
13
13
  import com.facebook.react.bridge.ReadableMap
14
+ import com.facebook.react.bridge.ReadableArray
15
+ import com.facebook.react.bridge.WritableMap
16
+ import com.facebook.react.bridge.Arguments
17
+ import com.facebook.react.modules.core.DeviceEventManagerModule
18
+ import com.doorstepai.sdks.tracking.DeliveryStop
19
+ import com.doorstepai.sdks.tracking.RouteGeofenceOptions
14
20
  import kotlinx.coroutines.Dispatchers
15
21
  import kotlinx.coroutines.CoroutineScope
22
+ import kotlinx.coroutines.SupervisorJob
23
+ import kotlinx.coroutines.Job
16
24
  import kotlinx.coroutines.launch
25
+ import kotlinx.coroutines.flow.launchIn
26
+ import kotlinx.coroutines.flow.onEach
17
27
 
18
28
 
19
29
  class DoorstepAIModule(private val reactContext: ReactApplicationContext) :
@@ -203,9 +213,9 @@ class DoorstepAIModule(private val reactContext: ReactApplicationContext) :
203
213
  }
204
214
 
205
215
  @ReactMethod
206
- fun newEvent(eventName: String, deliveryId: String, promise: Promise) {
216
+ fun newEvent(eventName: String, deliveryId: String, timestamp: Double?, promise: Promise) {
207
217
  try {
208
- DoorstepAI.newEvent(eventName, deliveryId) { /* Callback might not be invoked or provide useful data */ }
218
+ DoorstepAI.newEvent(eventName, deliveryId, timestamp) { /* Callback not needed here */ }
209
219
  promise.resolve("Event $eventName triggered for $deliveryId")
210
220
  } catch (e: Exception) {
211
221
  promise.reject("EVENT_CREATION_ERROR", e.message ?: "Failed to save event")
@@ -289,7 +299,142 @@ class DoorstepAIModule(private val reactContext: ReactApplicationContext) :
289
299
  return LatLngObject(lat = lat, lng = lng)
290
300
  }
291
301
 
302
+ // ---- Route geofencing ----
303
+
304
+ private val moduleScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
305
+ private var geofenceJob: Job? = null
306
+
307
+ @ReactMethod
308
+ fun startRouteGeofencing(stops: ReadableArray, options: ReadableMap?, promise: Promise) {
309
+ try {
310
+ val parsed = parseStops(stops)
311
+ if (parsed.isEmpty()) {
312
+ promise.reject(
313
+ "ROUTE_GEOFENCING_ERROR",
314
+ "startRouteGeofencing requires at least one valid stop (deliveryId + latitude + longitude)"
315
+ )
316
+ return
317
+ }
318
+ DoorstepAI.startRouteGeofencing(parsed, parseRouteOptions(options)) { result ->
319
+ result.onSuccess { promise.resolve(true) }
320
+ .onFailure { promise.reject("ROUTE_GEOFENCING_ERROR", it.message ?: "Failed to start route geofencing") }
321
+ }
322
+ } catch (e: Exception) {
323
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to start route geofencing")
324
+ }
325
+ }
326
+
327
+ @ReactMethod
328
+ fun stopRouteGeofencing(promise: Promise) {
329
+ try {
330
+ DoorstepAI.stopRouteGeofencing()
331
+ promise.resolve(true)
332
+ } catch (e: Exception) {
333
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to stop route geofencing")
334
+ }
335
+ }
336
+
337
+ @ReactMethod
338
+ fun resumeRouteGeofencingIfNeeded(promise: Promise) {
339
+ try {
340
+ DoorstepAI.resumeRouteGeofencingIfNeeded()
341
+ promise.resolve(true)
342
+ } catch (e: Exception) {
343
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to resume route geofencing")
344
+ }
345
+ }
346
+
347
+ @ReactMethod
348
+ fun getMonitoredStops(promise: Promise) {
349
+ try {
350
+ val arr = Arguments.createArray()
351
+ DoorstepAI.monitoredStops.forEach { arr.pushMap(serializeStop(it)) }
352
+ promise.resolve(arr)
353
+ } catch (e: Exception) {
354
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to get monitored stops")
355
+ }
356
+ }
357
+
358
+ // ---- Geofence-session event stream (NativeEventEmitter) ----
359
+
360
+ @ReactMethod
361
+ fun addListener(eventName: String) {
362
+ // On the first JS subscription, bridge the SDK's geofenceSessionEvents Flow
363
+ // to JS. A single collector serves the module lifetime (cancelled in invalidate()).
364
+ if (geofenceJob == null) {
365
+ geofenceJob = DoorstepAI.geofenceSessionEvents
366
+ .onEach { ev ->
367
+ val map = Arguments.createMap().apply {
368
+ putString("deliveryId", ev.deliveryId)
369
+ putString("type", ev.type.name) // "STARTED" | "STOPPED"
370
+ putString("reason", ev.reason)
371
+ }
372
+ reactContext
373
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
374
+ .emit(GEOFENCE_EVENT, map)
375
+ }
376
+ .launchIn(moduleScope)
377
+ }
378
+ }
379
+
380
+ @ReactMethod
381
+ fun removeListeners(count: Int) {
382
+ // Required stub for NativeEventEmitter on Android. The single Flow collector
383
+ // is torn down in invalidate(); no per-listener teardown is needed.
384
+ }
385
+
386
+ override fun invalidate() {
387
+ geofenceJob?.cancel()
388
+ geofenceJob = null
389
+ super.invalidate()
390
+ }
391
+
392
+ private fun parseStops(arr: ReadableArray): List<DeliveryStop> {
393
+ val out = mutableListOf<DeliveryStop>()
394
+ for (i in 0 until arr.size()) {
395
+ val m = arr.getMap(i) ?: continue
396
+ val deliveryId = if (m.hasKey("deliveryId")) m.getString("deliveryId") else null
397
+ if (deliveryId.isNullOrEmpty()) continue
398
+ if (!m.hasKey("latitude") || !m.hasKey("longitude")) continue
399
+ out.add(
400
+ DeliveryStop(
401
+ deliveryId = deliveryId,
402
+ address = if (m.hasKey("address")) m.getString("address") ?: "" else "",
403
+ latitude = m.getDouble("latitude"),
404
+ longitude = m.getDouble("longitude"),
405
+ radiusMeters = if (m.hasKey("radiusMeters") && !m.isNull("radiusMeters")) m.getDouble("radiusMeters") else null,
406
+ customerId = if (m.hasKey("customerId")) m.getString("customerId") else null,
407
+ driverId = if (m.hasKey("driverId")) m.getString("driverId") else null
408
+ )
409
+ )
410
+ }
411
+ return out
412
+ }
413
+
414
+ private fun parseRouteOptions(m: ReadableMap?): RouteGeofenceOptions {
415
+ if (m == null) return RouteGeofenceOptions()
416
+ return RouteGeofenceOptions(
417
+ defaultRadiusMeters = if (m.hasKey("defaultRadiusMeters") && !m.isNull("defaultRadiusMeters")) m.getDouble("defaultRadiusMeters") else 250.0,
418
+ timeoutSeconds = if (m.hasKey("timeoutSeconds") && !m.isNull("timeoutSeconds")) m.getDouble("timeoutSeconds") else null,
419
+ autoStopAfterDropoffSeconds = if (m.hasKey("autoStopAfterDropoffSeconds") && !m.isNull("autoStopAfterDropoffSeconds")) m.getDouble("autoStopAfterDropoffSeconds") else null,
420
+ manualForeground = if (m.hasKey("manualForeground") && !m.isNull("manualForeground")) m.getBoolean("manualForeground") else false
421
+ )
422
+ }
423
+
424
+ private fun serializeStop(s: DeliveryStop): WritableMap {
425
+ val m = Arguments.createMap()
426
+ m.putString("deliveryId", s.deliveryId)
427
+ m.putString("address", s.address)
428
+ m.putDouble("latitude", s.latitude)
429
+ m.putDouble("longitude", s.longitude)
430
+ s.radiusMeters?.let { m.putDouble("radiusMeters", it) }
431
+ s.customerId?.let { m.putString("customerId", it) }
432
+ s.driverId?.let { m.putString("driverId", it) }
433
+ return m
434
+ }
435
+
292
436
  companion object {
293
437
  const val NAME = "DoorstepAI"
438
+ private const val GEOFENCE_EVENT = "DoorstepGeofenceSessionEvent"
294
439
  }
295
440
  }
package/ios/DoorstepAI.mm CHANGED
@@ -1,6 +1,7 @@
1
1
  #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
2
3
 
3
- @interface RCT_EXTERN_MODULE(DoorstepAI, NSObject)
4
+ @interface RCT_EXTERN_MODULE(DoorstepAI, RCTEventEmitter)
4
5
 
5
6
  RCT_EXTERN_METHOD(setApiKey:(nonnull NSString *)apiKey)
6
7
  RCT_EXTERN_METHOD(requestAllPermissions:(BOOL)requestAlwaysLocation)
@@ -58,6 +59,7 @@ RCT_EXTERN_METHOD(startDeliveryByLatLng:(nonnull NSNumber *)latitude
58
59
  rejecter:(RCTPromiseRejectBlock)reject)
59
60
  RCT_EXTERN_METHOD(newEvent:(nonnull NSString *)eventName
60
61
  deliveryId:(nonnull NSString *)deliveryId
62
+ timestamp:(id)timestamp
61
63
  resolver:(RCTPromiseResolveBlock)resolve
62
64
  rejecter:(RCTPromiseRejectBlock)reject)
63
65
  RCT_EXTERN_METHOD(markDropoff:(nonnull NSString *)deliveryId
@@ -68,4 +70,16 @@ RCT_EXTERN_METHOD(stopDelivery:(nonnull NSString *)deliveryId
68
70
  resolver:(RCTPromiseResolveBlock)resolve
69
71
  rejecter:(RCTPromiseRejectBlock)reject)
70
72
 
73
+ // ---- Route geofencing ----
74
+ RCT_EXTERN_METHOD(startRouteGeofencing:(nonnull NSArray *)stops
75
+ options:(nullable NSDictionary *)options
76
+ resolver:(RCTPromiseResolveBlock)resolve
77
+ rejecter:(RCTPromiseRejectBlock)reject)
78
+ RCT_EXTERN_METHOD(stopRouteGeofencing:(RCTPromiseResolveBlock)resolve
79
+ rejecter:(RCTPromiseRejectBlock)reject)
80
+ RCT_EXTERN_METHOD(resumeRouteGeofencingIfNeeded:(RCTPromiseResolveBlock)resolve
81
+ rejecter:(RCTPromiseRejectBlock)reject)
82
+ RCT_EXTERN_METHOD(getMonitoredStops:(RCTPromiseResolveBlock)resolve
83
+ rejecter:(RCTPromiseRejectBlock)reject)
84
+
71
85
  @end
@@ -3,12 +3,48 @@ import React
3
3
  import DoorstepDropoffSDK
4
4
 
5
5
  @objc(DoorstepAI)
6
- class DoorstepAIBridge: NSObject {
6
+ class DoorstepAIBridge: RCTEventEmitter {
7
7
  @objc
8
- static func requiresMainQueueSetup() -> Bool {
8
+ override static func requiresMainQueueSetup() -> Bool {
9
9
  return true
10
10
  }
11
11
 
12
+ // MARK: - Geofence session events (RCTEventEmitter)
13
+
14
+ private static let geofenceEventName = "DoorstepGeofenceSessionEvent"
15
+ private var geofenceObservers: [NSObjectProtocol] = []
16
+
17
+ override func supportedEvents() -> [String]! {
18
+ return [DoorstepAIBridge.geofenceEventName]
19
+ }
20
+
21
+ /// Called by RN when the first JS listener subscribes: bridge the SDK's
22
+ /// NotificationCenter geofence notifications to `sendEvent`.
23
+ override func startObserving() {
24
+ let center = NotificationCenter.default
25
+ let started = center.addObserver(
26
+ forName: DoorstepAI.geofenceSessionStartedNotification, object: nil, queue: .main
27
+ ) { [weak self] note in self?.emitGeofenceEvent(type: "STARTED", userInfo: note.userInfo) }
28
+ let stopped = center.addObserver(
29
+ forName: DoorstepAI.geofenceSessionStoppedNotification, object: nil, queue: .main
30
+ ) { [weak self] note in self?.emitGeofenceEvent(type: "STOPPED", userInfo: note.userInfo) }
31
+ geofenceObservers = [started, stopped]
32
+ }
33
+
34
+ /// Called by RN when the last JS listener unsubscribes.
35
+ override func stopObserving() {
36
+ let center = NotificationCenter.default
37
+ geofenceObservers.forEach { center.removeObserver($0) }
38
+ geofenceObservers.removeAll()
39
+ }
40
+
41
+ private func emitGeofenceEvent(type: String, userInfo: [AnyHashable: Any]?) {
42
+ let deliveryId = (userInfo?["deliveryId"] as? String) ?? ""
43
+ let reason = (userInfo?["reason"] as? String) ?? ""
44
+ sendEvent(withName: DoorstepAIBridge.geofenceEventName,
45
+ body: ["deliveryId": deliveryId, "type": type, "reason": reason])
46
+ }
47
+
12
48
  @objc
13
49
  func enableDevMode(_ apiKey: NSString,
14
50
  resolver resolve: @escaping RCTPromiseResolveBlock,
@@ -221,11 +257,13 @@ class DoorstepAIBridge: NSObject {
221
257
  @objc
222
258
  func newEvent(_ eventName: NSString,
223
259
  deliveryId: NSString,
260
+ timestamp: Any?,
224
261
  resolver resolve: @escaping RCTPromiseResolveBlock,
225
262
  rejecter reject: @escaping RCTPromiseRejectBlock) {
226
263
  Task {
227
264
  do {
228
- try await DoorstepAI.newEvent(eventName: eventName as String, deliveryId: deliveryId as String)
265
+ let ts = (timestamp as? NSNumber)?.doubleValue
266
+ try await DoorstepAI.newEvent(eventName: eventName as String, deliveryId: deliveryId as String, timestamp: ts)
229
267
  resolve(nil)
230
268
  } catch {
231
269
  reject("E_NEW_EVENT", "Failed to send event: \(error.localizedDescription)", error)
@@ -270,6 +308,92 @@ class DoorstepAIBridge: NSObject {
270
308
  }
271
309
  }
272
310
 
311
+ // MARK: - Route geofencing
312
+
313
+ @objc
314
+ func startRouteGeofencing(_ stops: NSArray,
315
+ options: NSDictionary?,
316
+ resolver resolve: @escaping RCTPromiseResolveBlock,
317
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
318
+ let parsed: [DeliveryStop] = (stops as? [[String: Any]] ?? []).compactMap { parseDeliveryStop($0) }
319
+ if parsed.isEmpty {
320
+ reject("E_ROUTE_GEOFENCING", "startRouteGeofencing requires at least one valid stop (deliveryId + latitude + longitude)", nil)
321
+ return
322
+ }
323
+ let opts = parseRouteOptions(options)
324
+ DispatchQueue.main.async {
325
+ DoorstepAI.startRouteGeofencing(stops: parsed, options: opts)
326
+ resolve(nil)
327
+ }
328
+ }
329
+
330
+ @objc
331
+ func stopRouteGeofencing(_ resolve: @escaping RCTPromiseResolveBlock,
332
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
333
+ DispatchQueue.main.async {
334
+ DoorstepAI.stopRouteGeofencing()
335
+ resolve(nil)
336
+ }
337
+ }
338
+
339
+ @objc
340
+ func resumeRouteGeofencingIfNeeded(_ resolve: @escaping RCTPromiseResolveBlock,
341
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
342
+ DispatchQueue.main.async {
343
+ DoorstepAI.resumeRouteGeofencingIfNeeded()
344
+ resolve(nil)
345
+ }
346
+ }
347
+
348
+ @objc
349
+ func getMonitoredStops(_ resolve: @escaping RCTPromiseResolveBlock,
350
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
351
+ DispatchQueue.main.async {
352
+ resolve(DoorstepAI.monitoredStops.map { self.serializeStop($0) })
353
+ }
354
+ }
355
+
356
+ private func parseDeliveryStop(_ raw: [String: Any]) -> DeliveryStop? {
357
+ guard let deliveryId = raw["deliveryId"] as? String,
358
+ let latitude = (raw["latitude"] as? NSNumber)?.doubleValue,
359
+ let longitude = (raw["longitude"] as? NSNumber)?.doubleValue else {
360
+ return nil
361
+ }
362
+ return DeliveryStop(
363
+ deliveryId: deliveryId,
364
+ address: (raw["address"] as? String) ?? "",
365
+ latitude: latitude,
366
+ longitude: longitude,
367
+ radiusMeters: (raw["radiusMeters"] as? NSNumber)?.doubleValue,
368
+ customerId: raw["customerId"] as? String,
369
+ driverId: raw["driverId"] as? String
370
+ )
371
+ }
372
+
373
+ private func parseRouteOptions(_ raw: NSDictionary?) -> RouteGeofenceOptions {
374
+ guard let o = raw as? [String: Any] else { return RouteGeofenceOptions() }
375
+ // iOS RouteGeofenceOptions has no `manualForeground` (OS owns background region
376
+ // monitoring); a JS-supplied manualForeground is intentionally ignored here.
377
+ return RouteGeofenceOptions(
378
+ defaultRadiusMeters: (o["defaultRadiusMeters"] as? NSNumber)?.doubleValue ?? 250,
379
+ timeoutSeconds: (o["timeoutSeconds"] as? NSNumber)?.doubleValue,
380
+ autoStopAfterDropoffSeconds: (o["autoStopAfterDropoffSeconds"] as? NSNumber)?.doubleValue
381
+ )
382
+ }
383
+
384
+ private func serializeStop(_ s: DeliveryStop) -> [String: Any] {
385
+ var m: [String: Any] = [
386
+ "deliveryId": s.deliveryId,
387
+ "address": s.address,
388
+ "latitude": s.latitude,
389
+ "longitude": s.longitude
390
+ ]
391
+ if let r = s.radiusMeters { m["radiusMeters"] = r }
392
+ if let c = s.customerId { m["customerId"] = c }
393
+ if let d = s.driverId { m["driverId"] = d }
394
+ return m
395
+ }
396
+
273
397
  private func parseCoordinates(_ raw: NSDictionary?) -> LatLngObject? {
274
398
  guard let coordinates = raw as? [String: Any],
275
399
  let lat = coordinates["lat"] as? Double,