@doorstepai/dropoff-sdk 2.0.8 → 2.1.0

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 (43) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/com/doorstepai/dropoffsdk/DoorstepAIModule.kt +159 -2
  3. package/ios/DoorstepAI.mm +20 -2
  4. package/ios/DoorstepAI.swift +140 -5
  5. package/ios/DoorstepDropoffSDK.xcframework/Info.plist +5 -5
  6. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/DoorstepDropoffSDK +0 -0
  7. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Info.plist +0 -0
  8. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.abi.json +5341 -1609
  9. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +77 -4
  10. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  11. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios.swiftinterface +77 -4
  12. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/DoorstepDropoffSDK.framework/_CodeSignature/CodeResources +11 -11
  13. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/DWARF/DoorstepDropoffSDK +0 -0
  14. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/DoorstepDropoffSDK.yml +2630 -2116
  15. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/DoorstepDropoffSDK +0 -0
  16. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Info.plist +0 -0
  17. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +5341 -1609
  18. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +77 -4
  19. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  20. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +77 -4
  21. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +5341 -1609
  22. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +77 -4
  23. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  24. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/Modules/DoorstepDropoffSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +77 -4
  25. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/DoorstepDropoffSDK.framework/_CodeSignature/CodeResources +21 -21
  26. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/DWARF/DoorstepDropoffSDK +0 -0
  27. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/DoorstepDropoffSDK.yml +2630 -2116
  28. package/ios/DoorstepDropoffSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/DoorstepDropoffSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/DoorstepDropoffSDK.yml +2616 -2105
  29. package/lib/module/DoorstepAI.js +102 -5
  30. package/lib/module/DoorstepAI.js.map +1 -1
  31. package/lib/module/RootDoorstepAI.js +10 -1
  32. package/lib/module/RootDoorstepAI.js.map +1 -1
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/typescript/src/DoorstepAI.d.ts +72 -3
  35. package/lib/typescript/src/DoorstepAI.d.ts.map +1 -1
  36. package/lib/typescript/src/RootDoorstepAI.d.ts +4 -1
  37. package/lib/typescript/src/RootDoorstepAI.d.ts.map +1 -1
  38. package/lib/typescript/src/index.d.ts +1 -1
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/DoorstepAI.tsx +162 -6
  42. package/src/RootDoorstepAI.tsx +18 -1
  43. 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.1.0")
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,154 @@ 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 updateRouteStops(stops: ReadableArray, promise: Promise) {
329
+ try {
330
+ DoorstepAI.updateRouteStops(parseStops(stops)) { result ->
331
+ result.onSuccess { promise.resolve(true) }
332
+ .onFailure { promise.reject("ROUTE_GEOFENCING_ERROR", it.message ?: "Failed to update route stops") }
333
+ }
334
+ } catch (e: Exception) {
335
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to update route stops")
336
+ }
337
+ }
338
+
339
+ @ReactMethod
340
+ fun stopRouteGeofencing(promise: Promise) {
341
+ try {
342
+ DoorstepAI.stopRouteGeofencing()
343
+ promise.resolve(true)
344
+ } catch (e: Exception) {
345
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to stop route geofencing")
346
+ }
347
+ }
348
+
349
+ @ReactMethod
350
+ fun resumeRouteGeofencingIfNeeded(promise: Promise) {
351
+ try {
352
+ DoorstepAI.resumeRouteGeofencingIfNeeded()
353
+ promise.resolve(true)
354
+ } catch (e: Exception) {
355
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to resume route geofencing")
356
+ }
357
+ }
358
+
359
+ @ReactMethod
360
+ fun getMonitoredStops(promise: Promise) {
361
+ try {
362
+ val arr = Arguments.createArray()
363
+ DoorstepAI.monitoredStops.forEach { arr.pushMap(serializeStop(it)) }
364
+ promise.resolve(arr)
365
+ } catch (e: Exception) {
366
+ promise.reject("ROUTE_GEOFENCING_ERROR", e.message ?: "Failed to get monitored stops")
367
+ }
368
+ }
369
+
370
+ // ---- Geofence-session event stream (NativeEventEmitter) ----
371
+
372
+ @ReactMethod
373
+ fun addListener(eventName: String) {
374
+ // On the first JS subscription, bridge the SDK's geofenceSessionEvents Flow
375
+ // to JS. A single collector serves the module lifetime (cancelled in invalidate()).
376
+ if (geofenceJob == null) {
377
+ geofenceJob = DoorstepAI.geofenceSessionEvents
378
+ .onEach { ev ->
379
+ val map = Arguments.createMap().apply {
380
+ putString("deliveryId", ev.deliveryId)
381
+ putString("type", ev.type.name) // "STARTED" | "STOPPED"
382
+ putString("reason", ev.reason)
383
+ }
384
+ reactContext
385
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
386
+ .emit(GEOFENCE_EVENT, map)
387
+ }
388
+ .launchIn(moduleScope)
389
+ }
390
+ }
391
+
392
+ @ReactMethod
393
+ fun removeListeners(count: Int) {
394
+ // Required stub for NativeEventEmitter on Android. The single Flow collector
395
+ // is torn down in invalidate(); no per-listener teardown is needed.
396
+ }
397
+
398
+ override fun invalidate() {
399
+ geofenceJob?.cancel()
400
+ geofenceJob = null
401
+ super.invalidate()
402
+ }
403
+
404
+ private fun parseStops(arr: ReadableArray): List<DeliveryStop> {
405
+ val out = mutableListOf<DeliveryStop>()
406
+ for (i in 0 until arr.size()) {
407
+ val m = arr.getMap(i) ?: continue
408
+ val deliveryId = if (m.hasKey("deliveryId")) m.getString("deliveryId") else null
409
+ if (deliveryId.isNullOrEmpty()) continue
410
+ if (!m.hasKey("latitude") || !m.hasKey("longitude")) continue
411
+ out.add(
412
+ DeliveryStop(
413
+ deliveryId = deliveryId,
414
+ address = if (m.hasKey("address")) m.getString("address") ?: "" else "",
415
+ latitude = m.getDouble("latitude"),
416
+ longitude = m.getDouble("longitude"),
417
+ radiusMeters = if (m.hasKey("radiusMeters") && !m.isNull("radiusMeters")) m.getDouble("radiusMeters") else null,
418
+ customerId = if (m.hasKey("customerId")) m.getString("customerId") else null,
419
+ driverId = if (m.hasKey("driverId")) m.getString("driverId") else null
420
+ )
421
+ )
422
+ }
423
+ return out
424
+ }
425
+
426
+ private fun parseRouteOptions(m: ReadableMap?): RouteGeofenceOptions {
427
+ if (m == null) return RouteGeofenceOptions()
428
+ return RouteGeofenceOptions(
429
+ defaultRadiusMeters = if (m.hasKey("defaultRadiusMeters") && !m.isNull("defaultRadiusMeters")) m.getDouble("defaultRadiusMeters") else 250.0,
430
+ timeoutSeconds = if (m.hasKey("timeoutSeconds") && !m.isNull("timeoutSeconds")) m.getDouble("timeoutSeconds") else null,
431
+ autoStopAfterDropoffSeconds = if (m.hasKey("autoStopAfterDropoffSeconds") && !m.isNull("autoStopAfterDropoffSeconds")) m.getDouble("autoStopAfterDropoffSeconds") else null,
432
+ manualForeground = if (m.hasKey("manualForeground") && !m.isNull("manualForeground")) m.getBoolean("manualForeground") else false
433
+ )
434
+ }
435
+
436
+ private fun serializeStop(s: DeliveryStop): WritableMap {
437
+ val m = Arguments.createMap()
438
+ m.putString("deliveryId", s.deliveryId)
439
+ m.putString("address", s.address)
440
+ m.putDouble("latitude", s.latitude)
441
+ m.putDouble("longitude", s.longitude)
442
+ s.radiusMeters?.let { m.putDouble("radiusMeters", it) }
443
+ s.customerId?.let { m.putString("customerId", it) }
444
+ s.driverId?.let { m.putString("driverId", it) }
445
+ return m
446
+ }
447
+
292
448
  companion object {
293
449
  const val NAME = "DoorstepAI"
450
+ private const val GEOFENCE_EVENT = "DoorstepGeofenceSessionEvent"
294
451
  }
295
452
  }
package/ios/DoorstepAI.mm CHANGED
@@ -1,9 +1,11 @@
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
- RCT_EXTERN_METHOD(requestAllPermissions:(BOOL)requestAlwaysLocation)
7
+ RCT_EXTERN_METHOD(requestAllPermissions:(BOOL)requestAlwaysLocation
8
+ requestBluetooth:(BOOL)requestBluetooth)
7
9
  RCT_EXTERN_METHOD(configureRemoteLogging:(BOOL)enabled
8
10
  minLevel:(id)minLevel
9
11
  flushIntervalSeconds:(id)flushIntervalSeconds
@@ -58,6 +60,7 @@ RCT_EXTERN_METHOD(startDeliveryByLatLng:(nonnull NSNumber *)latitude
58
60
  rejecter:(RCTPromiseRejectBlock)reject)
59
61
  RCT_EXTERN_METHOD(newEvent:(nonnull NSString *)eventName
60
62
  deliveryId:(nonnull NSString *)deliveryId
63
+ timestamp:(id)timestamp
61
64
  resolver:(RCTPromiseResolveBlock)resolve
62
65
  rejecter:(RCTPromiseRejectBlock)reject)
63
66
  RCT_EXTERN_METHOD(markDropoff:(nonnull NSString *)deliveryId
@@ -68,4 +71,19 @@ RCT_EXTERN_METHOD(stopDelivery:(nonnull NSString *)deliveryId
68
71
  resolver:(RCTPromiseResolveBlock)resolve
69
72
  rejecter:(RCTPromiseRejectBlock)reject)
70
73
 
74
+ // ---- Route geofencing ----
75
+ RCT_EXTERN_METHOD(startRouteGeofencing:(nonnull NSArray *)stops
76
+ options:(nullable NSDictionary *)options
77
+ resolver:(RCTPromiseResolveBlock)resolve
78
+ rejecter:(RCTPromiseRejectBlock)reject)
79
+ RCT_EXTERN_METHOD(updateRouteStops:(nonnull NSArray *)stops
80
+ resolver:(RCTPromiseResolveBlock)resolve
81
+ rejecter:(RCTPromiseRejectBlock)reject)
82
+ RCT_EXTERN_METHOD(stopRouteGeofencing:(RCTPromiseResolveBlock)resolve
83
+ rejecter:(RCTPromiseRejectBlock)reject)
84
+ RCT_EXTERN_METHOD(resumeRouteGeofencingIfNeeded:(RCTPromiseResolveBlock)resolve
85
+ rejecter:(RCTPromiseRejectBlock)reject)
86
+ RCT_EXTERN_METHOD(getMonitoredStops:(RCTPromiseResolveBlock)resolve
87
+ rejecter:(RCTPromiseRejectBlock)reject)
88
+
71
89
  @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,
@@ -25,8 +61,8 @@ class DoorstepAIBridge: NSObject {
25
61
  }
26
62
 
27
63
  @objc
28
- func requestAllPermissions(_ requestAlwaysLocation: Bool) {
29
- DoorstepAI.requestAllPermissions(requestAlwaysLocation: requestAlwaysLocation)
64
+ func requestAllPermissions(_ requestAlwaysLocation: Bool, requestBluetooth: Bool) {
65
+ DoorstepAI.requestAllPermissions(requestAlwaysLocation: requestAlwaysLocation, requestBluetooth: requestBluetooth)
30
66
  }
31
67
 
32
68
  @objc
@@ -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,103 @@ 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 updateRouteStops(_ stops: NSArray,
332
+ resolver resolve: @escaping RCTPromiseResolveBlock,
333
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
334
+ let parsed: [DeliveryStop] = (stops as? [[String: Any]] ?? []).compactMap { parseDeliveryStop($0) }
335
+ DispatchQueue.main.async {
336
+ DoorstepAI.updateRouteStops(parsed)
337
+ resolve(nil)
338
+ }
339
+ }
340
+
341
+ @objc
342
+ func stopRouteGeofencing(_ resolve: @escaping RCTPromiseResolveBlock,
343
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
344
+ DispatchQueue.main.async {
345
+ DoorstepAI.stopRouteGeofencing()
346
+ resolve(nil)
347
+ }
348
+ }
349
+
350
+ @objc
351
+ func resumeRouteGeofencingIfNeeded(_ resolve: @escaping RCTPromiseResolveBlock,
352
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
353
+ DispatchQueue.main.async {
354
+ DoorstepAI.resumeRouteGeofencingIfNeeded()
355
+ resolve(nil)
356
+ }
357
+ }
358
+
359
+ @objc
360
+ func getMonitoredStops(_ resolve: @escaping RCTPromiseResolveBlock,
361
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
362
+ DispatchQueue.main.async {
363
+ resolve(DoorstepAI.monitoredStops.map { self.serializeStop($0) })
364
+ }
365
+ }
366
+
367
+ private func parseDeliveryStop(_ raw: [String: Any]) -> DeliveryStop? {
368
+ guard let deliveryId = raw["deliveryId"] as? String,
369
+ let latitude = (raw["latitude"] as? NSNumber)?.doubleValue,
370
+ let longitude = (raw["longitude"] as? NSNumber)?.doubleValue else {
371
+ return nil
372
+ }
373
+ return DeliveryStop(
374
+ deliveryId: deliveryId,
375
+ address: (raw["address"] as? String) ?? "",
376
+ latitude: latitude,
377
+ longitude: longitude,
378
+ radiusMeters: (raw["radiusMeters"] as? NSNumber)?.doubleValue,
379
+ customerId: raw["customerId"] as? String,
380
+ driverId: raw["driverId"] as? String
381
+ )
382
+ }
383
+
384
+ private func parseRouteOptions(_ raw: NSDictionary?) -> RouteGeofenceOptions {
385
+ guard let o = raw as? [String: Any] else { return RouteGeofenceOptions() }
386
+ // iOS RouteGeofenceOptions has no `manualForeground` (OS owns background region
387
+ // monitoring); a JS-supplied manualForeground is intentionally ignored here.
388
+ return RouteGeofenceOptions(
389
+ defaultRadiusMeters: (o["defaultRadiusMeters"] as? NSNumber)?.doubleValue ?? 250,
390
+ timeoutSeconds: (o["timeoutSeconds"] as? NSNumber)?.doubleValue,
391
+ autoStopAfterDropoffSeconds: (o["autoStopAfterDropoffSeconds"] as? NSNumber)?.doubleValue
392
+ )
393
+ }
394
+
395
+ private func serializeStop(_ s: DeliveryStop) -> [String: Any] {
396
+ var m: [String: Any] = [
397
+ "deliveryId": s.deliveryId,
398
+ "address": s.address,
399
+ "latitude": s.latitude,
400
+ "longitude": s.longitude
401
+ ]
402
+ if let r = s.radiusMeters { m["radiusMeters"] = r }
403
+ if let c = s.customerId { m["customerId"] = c }
404
+ if let d = s.driverId { m["driverId"] = d }
405
+ return m
406
+ }
407
+
273
408
  private func parseCoordinates(_ raw: NSDictionary?) -> LatLngObject? {
274
409
  guard let coordinates = raw as? [String: Any],
275
410
  let lat = coordinates["lat"] as? Double,
@@ -10,15 +10,18 @@
10
10
  <key>DebugSymbolsPath</key>
11
11
  <string>dSYMs</string>
12
12
  <key>LibraryIdentifier</key>
13
- <string>ios-arm64</string>
13
+ <string>ios-arm64_x86_64-simulator</string>
14
14
  <key>LibraryPath</key>
15
15
  <string>DoorstepDropoffSDK.framework</string>
16
16
  <key>SupportedArchitectures</key>
17
17
  <array>
18
18
  <string>arm64</string>
19
+ <string>x86_64</string>
19
20
  </array>
20
21
  <key>SupportedPlatform</key>
21
22
  <string>ios</string>
23
+ <key>SupportedPlatformVariant</key>
24
+ <string>simulator</string>
22
25
  </dict>
23
26
  <dict>
24
27
  <key>BinaryPath</key>
@@ -26,18 +29,15 @@
26
29
  <key>DebugSymbolsPath</key>
27
30
  <string>dSYMs</string>
28
31
  <key>LibraryIdentifier</key>
29
- <string>ios-arm64_x86_64-simulator</string>
32
+ <string>ios-arm64</string>
30
33
  <key>LibraryPath</key>
31
34
  <string>DoorstepDropoffSDK.framework</string>
32
35
  <key>SupportedArchitectures</key>
33
36
  <array>
34
37
  <string>arm64</string>
35
- <string>x86_64</string>
36
38
  </array>
37
39
  <key>SupportedPlatform</key>
38
40
  <string>ios</string>
39
- <key>SupportedPlatformVariant</key>
40
- <string>simulator</string>
41
41
  </dict>
42
42
  </array>
43
43
  <key>CFBundlePackageType</key>