@woosmap/react-native-plugin-geofencing 1.0.0-beta.2 → 1.0.0-beta.3

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 (41) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +0 -9
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/AbstractPushHelper.java +1 -1
  5. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/AirshipPushHelper.java +1 -1
  6. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosLocationReadyListener.java +1 -1
  7. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosRegionReadyListener.java +1 -1
  8. package/android/src/main/java/com/woosmap/reactnativeplugingeofencing/WoosmapGeofencingTurboModule.java +1025 -0
  9. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosmapGeofencingTurboPackage.java +3 -7
  10. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosmapMessageAndKey.java +2 -3
  11. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosmapTask.java +34 -128
  12. package/android/src/main/java/com/{reactnativeplugingeofencing → woosmap/reactnativeplugingeofencing}/WoosmapUtil.java +1 -1
  13. package/ios/WoosmapGeofenceMessage.swift +1 -0
  14. package/ios/WoosmapGeofencingTurbo.mm +110 -10
  15. package/ios/WoosmapGeofencingTurbo.swift +873 -11
  16. package/lib/commonjs/NativeWoosmapGeofencingTurbo.js +6 -3
  17. package/lib/commonjs/NativeWoosmapGeofencingTurbo.js.map +1 -1
  18. package/lib/commonjs/index.js +37 -131
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/module/NativeWoosmapGeofencingTurbo.js +6 -3
  21. package/lib/module/NativeWoosmapGeofencingTurbo.js.map +1 -1
  22. package/lib/module/index.js +37 -131
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/typescript/src/NativeWoosmapGeofencingTurbo.d.ts +37 -3
  25. package/lib/typescript/src/NativeWoosmapGeofencingTurbo.d.ts.map +1 -1
  26. package/lib/typescript/src/index.d.ts +3 -3
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/NativeWoosmapGeofencingTurbo.ts +56 -3
  30. package/android/src/main/java/com/reactnativeplugingeofencing/PluginGeofencingModule.java +0 -1204
  31. package/android/src/main/java/com/reactnativeplugingeofencing/PluginGeofencingPackage.java +0 -28
  32. package/android/src/main/java/com/reactnativeplugingeofencing/WoosmapGeofencingTurboModule.java +0 -185
  33. package/ios/PluginGeofencing.mm +0 -123
  34. package/ios/PluginGeofencing.swift +0 -1243
  35. package/lib/commonjs/internal/nativeInterface.js +0 -13
  36. package/lib/commonjs/internal/nativeInterface.js.map +0 -1
  37. package/lib/module/internal/nativeInterface.js +0 -9
  38. package/lib/module/internal/nativeInterface.js.map +0 -1
  39. package/lib/typescript/src/internal/nativeInterface.d.ts +0 -3
  40. package/lib/typescript/src/internal/nativeInterface.d.ts.map +0 -1
  41. /package/ios/{PluginGeofencing-Bridging-Header.h → WoosmapGeofencing-Bridging-Header.h} +0 -0
@@ -1,22 +1,71 @@
1
1
  import CoreLocation
2
2
  import WoosmapGeofencing
3
3
 
4
- /// TurboModule implementation for the region-CRUD + key/radius slice migrated
5
- /// This is a thin adapter over the shared
6
- /// `WoosmapGeofenceService` — the same service the legacy `PluginGeofencing`
7
- /// module talks to — so the two modules stay independent without duplicating
8
- /// business logic.
4
+ /// TurboModule implementation Approach B: extends RCTEventEmitter so it can
5
+ /// both expose native methods and fire JS events (geolocationDidChange, etc.).
9
6
  ///
10
7
  /// The Objective-C++ bridge in `WoosmapGeofencingTurbo.mm` exposes these
11
8
  /// methods to the generated `RCTNativeWoosmapGeofencingTurboSpec` protocol and
12
9
  /// vends the JSI TurboModule.
13
10
  @objc(WoosmapGeofencingTurbo)
14
- class WoosmapGeofencingTurbo: NSObject {
11
+ class WoosmapGeofencingTurbo: RCTEventEmitter {
15
12
 
16
- @objc static func requiresMainQueueSetup() -> Bool {
13
+ /// Location manager used solely to request permissions.
14
+ private var templocationChecker: CLLocationManager!
15
+
16
+ /// Active location watch IDs keyed by watch-id string.
17
+ private var locationWatchStack: [String: String] = [:]
18
+
19
+ /// Active region watch IDs keyed by watch-id string.
20
+ private var regionWatchStack: [String: String] = [:]
21
+
22
+ /// Resolver for an in-flight `requestPermissions` call that is waiting for the
23
+ /// user to answer the system location dialog. Settled in
24
+ /// `locationManagerDidChangeAuthorization(_:)` once the OS reports the choice.
25
+ private var permissionResolve: RCTPromiseResolveBlock?
26
+
27
+ /// Forces the module to be initialised on the main queue.
28
+ /// - Returns: `true` so UIKit / CoreLocation calls are safe at setup.
29
+ @objc override static func requiresMainQueueSetup() -> Bool {
17
30
  return true
18
31
  }
19
32
 
33
+ // MARK: - RCTEventEmitter
34
+
35
+ /// Creates the module and configures the permission-checking location manager.
36
+ override init() {
37
+ super.init()
38
+ templocationChecker = CLLocationManager()
39
+ templocationChecker.delegate = self
40
+ templocationChecker.desiredAccuracy = kCLLocationAccuracyBest
41
+ locationWatchStack = [:]
42
+ regionWatchStack = [:]
43
+ }
44
+
45
+ /// The set of event names this emitter can dispatch to JS.
46
+ /// - Returns: The supported `geolocation` / `woosmapgeofenceRegion` event names.
47
+ override func supportedEvents() -> [String]! {
48
+ return ["geolocationDidChange", "geolocationError",
49
+ "woosmapgeofenceRegionDidChange", "woosmapgeofenceRegionError"]
50
+ }
51
+
52
+ /// Registers a JS listener for the given event. Required stub so the
53
+ /// TurboModule codegen emits the correct selector.
54
+ /// - Parameter eventName: The event the JS side is subscribing to.
55
+ @objc(addListener:)
56
+ override func addListener(_ eventName: String) { super.addListener(eventName) }
57
+
58
+ /// Removes JS listeners. Required stub so the TurboModule codegen emits the
59
+ /// correct selector.
60
+ /// - Parameter count: The number of listeners to remove.
61
+ @objc(removeListeners:)
62
+ override func removeListeners(_ count: Double) { super.removeListeners(count) }
63
+
64
+ // MARK: - Private helpers
65
+
66
+ /// Builds an `NSError` in the plugin's error domain.
67
+ /// - Parameter msg: The localized description for the error.
68
+ /// - Returns: An `NSError` carrying `msg` under `NSLocalizedDescriptionKey`.
20
69
  private func woosmapError(_ msg: String) -> NSError {
21
70
  return NSError(
22
71
  domain: WoosmapGeofenceMessage.plugin_errorDomain,
@@ -25,6 +74,11 @@ class WoosmapGeofencingTurbo: NSObject {
25
74
  )
26
75
  }
27
76
 
77
+ // MARK: - Formatters
78
+
79
+ /// Maps a native `Region` into the lower-cased dictionary shape the JS facade expects.
80
+ /// - Parameter woosdata: The region to serialize.
81
+ /// - Returns: A dictionary mirroring the `RegionRecord` spec type.
28
82
  private func formatRegionData(woosdata: Region) -> [AnyHashable: Any] {
29
83
  var result: [AnyHashable: Any] = [:]
30
84
  result["date"] = woosdata.date.timeIntervalSince1970 * 1000
@@ -39,6 +93,377 @@ class WoosmapGeofencingTurbo: NSObject {
39
93
  return result
40
94
  }
41
95
 
96
+ /// Maps a native `Location` into the dictionary shape the JS facade expects.
97
+ /// - Parameter woosdata: The location to serialize.
98
+ /// - Returns: A dictionary with the location's date/coordinates/identifier.
99
+ private func formatLocationData(woosdata: Location) -> [AnyHashable: Any] {
100
+ var result: [AnyHashable: Any] = [:]
101
+ if let date = woosdata.date {
102
+ result["date"] = date.timeIntervalSince1970 * 1000
103
+ } else {
104
+ result["date"] = 0
105
+ }
106
+ result["latitude"] = woosdata.latitude
107
+ result["locationdescription"] = woosdata.locationDescription
108
+ result["locationid"] = woosdata.locationId
109
+ result["longitude"] = woosdata.longitude
110
+ return result
111
+ }
112
+
113
+ /// Maps a native `POI` into the dictionary shape the JS facade expects.
114
+ /// - Parameter woosdata: The POI to serialize.
115
+ /// - Returns: A dictionary with the POI's metadata, coordinates and user properties.
116
+ private func formatPOIData(woosdata: POI) -> [AnyHashable: Any] {
117
+ var result: [AnyHashable: Any] = [:]
118
+ if let data = woosdata.jsonData {
119
+ do {
120
+ let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
121
+ result["jsondata"] = json
122
+ } catch { }
123
+ }
124
+ result["city"] = woosdata.city
125
+ result["idstore"] = woosdata.idstore
126
+ result["name"] = woosdata.name
127
+ if let date = woosdata.date {
128
+ result["date"] = date.timeIntervalSince1970 * 1000
129
+ } else {
130
+ result["date"] = 0
131
+ }
132
+ result["distance"] = woosdata.distance
133
+ result["duration"] = woosdata.duration ?? "-"
134
+ result["latitude"] = woosdata.latitude
135
+ result["locationid"] = woosdata.locationId
136
+ result["longitude"] = woosdata.longitude
137
+ result["zipcode"] = woosdata.zipCode
138
+ result["radius"] = woosdata.radius
139
+ result["address"] = woosdata.address
140
+ result["countrycode"] = woosdata.countryCode
141
+ result["tags"] = woosdata.tags
142
+ result["types"] = woosdata.types
143
+ result["contact"] = woosdata.contact
144
+ result["userProperties"] = woosdata.user_properties
145
+ result["openNow"] = woosdata.openNow
146
+ return result
147
+ }
148
+
149
+ /// Maps a native `IndoorBeacon` into the dictionary shape the JS facade expects.
150
+ /// - Parameter woosdata: The indoor beacon to serialize.
151
+ /// - Returns: A dictionary with the beacon's identifiers, coordinates and properties.
152
+ private func formatIndoorBeaconData(woosdata: IndoorBeacon) -> [AnyHashable: Any] {
153
+ var result: [AnyHashable: Any] = [:]
154
+ result["identifier"] = woosdata.identifier
155
+ result["properties"] = woosdata.properties
156
+ result["latitude"] = woosdata.latitude
157
+ result["longitude"] = woosdata.longitude
158
+ result["venueId"] = woosdata.venue_id
159
+ result["major"] = woosdata.Major
160
+ result["minor"] = woosdata.Minor
161
+ result["beaconId"] = woosdata.BeaconID
162
+ result["date"] = woosdata.date.timeIntervalSince1970 * 1000
163
+ return result
164
+ }
165
+
166
+ // MARK: - Initialize
167
+
168
+ /// Initializes the Woosmap geofencing service with the supplied options
169
+ /// (Woosmap key, tracking profile, protected-region slot, Airship flag).
170
+ /// - Parameters:
171
+ /// - command: A dictionary of initialization options.
172
+ /// - resolve: Promise resolver invoked on success.
173
+ /// - reject: Promise rejecter invoked on invalid options or failure.
174
+ @objc(initialize:resolve:reject:)
175
+ func initialize(command: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
176
+ guard let initParameters = command as? [String: Any] else {
177
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
178
+ WoosmapGeofenceMessage.plugin_parsingFailed,
179
+ woosmapError(WoosmapGeofenceMessage.plugin_parsingFailed))
180
+ return
181
+ }
182
+ var isCallUnsuccessful = false
183
+ var privateKeyWoosmapAPI = ""
184
+ var isAirshipCallbackEnable: Bool = false
185
+ var protectedRegionSlot: Int = -1
186
+
187
+ if let keyProtectedRegionSlot = initParameters["protectedRegionSlot"] as? Int {
188
+ if keyProtectedRegionSlot > 3 {
189
+ isCallUnsuccessful = true
190
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
191
+ WoosmapGeofenceMessage.invalidProtectedRegionSlot,
192
+ woosmapError(WoosmapGeofenceMessage.invalidProtectedRegionSlot))
193
+ } else {
194
+ protectedRegionSlot = keyProtectedRegionSlot
195
+ }
196
+ }
197
+
198
+ if let keyWoosmapAPI = initParameters["privateKeyWoosmapAPI"] as? String {
199
+ privateKeyWoosmapAPI = keyWoosmapAPI
200
+ }
201
+ #if canImport(AirshipKit)
202
+ if let keyAirshipPush = initParameters["enableAirshipConnector"] as? Bool {
203
+ isAirshipCallbackEnable = keyAirshipPush
204
+ }
205
+ #endif
206
+
207
+ if let trackingProfile = initParameters["trackingProfile"] as? String {
208
+ if ConfigurationProfile(rawValue: trackingProfile) != nil {
209
+ WoosmapGeofenceService.setup(woosmapKey: privateKeyWoosmapAPI,
210
+ configurationProfile: trackingProfile,
211
+ airshipTrackingEnable: isAirshipCallbackEnable,
212
+ protectedRegionSlot: protectedRegionSlot)
213
+ } else {
214
+ isCallUnsuccessful = true
215
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
216
+ WoosmapGeofenceMessage.invalidProfile,
217
+ woosmapError(WoosmapGeofenceMessage.invalidProfile))
218
+ }
219
+ } else {
220
+ WoosmapGeofenceService.setup(woosmapKey: privateKeyWoosmapAPI,
221
+ configurationProfile: "",
222
+ airshipTrackingEnable: isAirshipCallbackEnable,
223
+ protectedRegionSlot: protectedRegionSlot)
224
+ }
225
+ if isCallUnsuccessful == false {
226
+ resolve(WoosmapGeofenceMessage.initialize)
227
+ }
228
+ }
229
+
230
+ // MARK: - Permission requests
231
+
232
+ /// Requests location permission from the user, presenting a settings prompt
233
+ /// when the permission has already been decided.
234
+ /// - Parameters:
235
+ /// - background: Pass `true` for always-on access, `false` for when-in-use access.
236
+ /// - resolve: Promise resolver invoked with the permission-flow status.
237
+ /// - reject: Promise rejecter invoked on failure.
238
+ @objc(requestPermissions:resolve:reject:)
239
+ func requestPermissions(background: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
240
+ // CLLocationManager must be created/messaged on the main thread; bridge
241
+ // methods otherwise run on a background queue. Hop to main before touching it.
242
+ DispatchQueue.main.async {
243
+ self.performRequestPermissions(background: background, resolve: resolve, reject: reject)
244
+ }
245
+ }
246
+
247
+ /// Main-thread body of `requestPermissions`. See that method for parameter docs.
248
+ private func performRequestPermissions(background: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
249
+ // A request is already awaiting the OS dialog: reject this new call rather
250
+ // than overwriting (and leaking) the in-flight promise.
251
+ if permissionResolve != nil {
252
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
253
+ WoosmapGeofenceMessage.permissionRequestInProgress,
254
+ woosmapError(WoosmapGeofenceMessage.permissionRequestInProgress))
255
+ return
256
+ }
257
+ let status = CLLocationManager().authorizationStatus
258
+
259
+ if background {
260
+ if status == .notDetermined {
261
+ // Fresh install: defer the promise until the OS reports the user's
262
+ // choice via locationManagerDidChangeAuthorization(_:). Not settling
263
+ // here would hang the JS caller forever.
264
+ permissionResolve = resolve
265
+ self.templocationChecker.requestAlwaysAuthorization()
266
+ } else if status == .authorizedAlways {
267
+ resolve(WoosmapGeofenceMessage.samePermission)
268
+ } else {
269
+ resolve(WoosmapGeofenceMessage.showingPermissionBox)
270
+ Task { @MainActor in
271
+ let appname: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? ""
272
+ var alertInfo: String = ""
273
+ if status == .denied {
274
+ alertInfo = String(format: WoosmapGeofenceMessage.deniedPermission, appname)
275
+ } else {
276
+ alertInfo = String(format: WoosmapGeofenceMessage.replacePermission, appname)
277
+ }
278
+ let alert = UIAlertController(title: "", message: alertInfo, preferredStyle: UIAlertController.Style.alert)
279
+ alert.addAction(UIAlertAction(title: WoosmapGeofenceMessage.cancel, style: UIAlertAction.Style.default, handler: nil))
280
+ alert.addAction(UIAlertAction(title: WoosmapGeofenceMessage.setting, style: UIAlertAction.Style.default, handler: { _ in
281
+ if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
282
+ UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
283
+ }
284
+ }))
285
+ var rootViewController = UIApplication.shared.topViewController
286
+ if let navigationController = rootViewController as? UINavigationController {
287
+ rootViewController = navigationController.viewControllers.first
288
+ }
289
+ if let tabBarController = rootViewController as? UITabBarController {
290
+ rootViewController = tabBarController.selectedViewController
291
+ }
292
+ rootViewController?.present(alert, animated: true, completion: nil)
293
+ }
294
+ }
295
+ } else {
296
+ if status == .notDetermined {
297
+ // Fresh install: defer the promise until the OS reports the user's
298
+ // choice via locationManagerDidChangeAuthorization(_:). Not settling
299
+ // here would hang the JS caller forever.
300
+ permissionResolve = resolve
301
+ self.templocationChecker.requestWhenInUseAuthorization()
302
+ } else if status == .authorizedWhenInUse {
303
+ resolve(WoosmapGeofenceMessage.samePermission)
304
+ } else {
305
+ resolve(WoosmapGeofenceMessage.showingPermissionBox)
306
+ Task { @MainActor in
307
+ let appname: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? ""
308
+ var alertInfo: String = ""
309
+ if status == .denied {
310
+ alertInfo = String(format: WoosmapGeofenceMessage.deniedPermission, appname)
311
+ } else {
312
+ alertInfo = String(format: WoosmapGeofenceMessage.replacePermission, appname)
313
+ }
314
+ let alert = UIAlertController(title: "", message: alertInfo, preferredStyle: UIAlertController.Style.alert)
315
+ alert.addAction(UIAlertAction(title: WoosmapGeofenceMessage.cancel, style: UIAlertAction.Style.default, handler: nil))
316
+ alert.addAction(UIAlertAction(title: WoosmapGeofenceMessage.setting, style: UIAlertAction.Style.default, handler: { _ in
317
+ if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
318
+ UIApplication.shared.open(settingsUrl, options: [:], completionHandler: nil)
319
+ }
320
+ }))
321
+ var rootViewController = UIApplication.shared.topViewController
322
+ if let navigationController = rootViewController as? UINavigationController {
323
+ rootViewController = navigationController.viewControllers.first
324
+ }
325
+ if let tabBarController = rootViewController as? UITabBarController {
326
+ rootViewController = tabBarController.selectedViewController
327
+ }
328
+ rootViewController?.present(alert, animated: true, completion: nil)
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ /// Requests Bluetooth permission. iOS does not require an explicit BLE
335
+ /// permission for geofencing, so this always resolves `GRANTED`.
336
+ /// - Parameters:
337
+ /// - resolve: Promise resolver invoked with `"GRANTED"`.
338
+ /// - reject: Promise rejecter (unused on iOS).
339
+ @objc(requestBLEPermissions:reject:)
340
+ func requestBLEPermissions(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
341
+ resolve("GRANTED")
342
+ }
343
+
344
+ /// Requests notification permission. iOS does not require this for
345
+ /// geofencing, so this always resolves `GRANTED`.
346
+ /// - Parameters:
347
+ /// - resolve: Promise resolver invoked with `"GRANTED"`.
348
+ /// - reject: Promise rejecter (unused on iOS).
349
+ @objc(requestNotificationPermissions:reject:)
350
+ func requestNotificationPermissions(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
351
+ resolve("GRANTED")
352
+ }
353
+
354
+ // MARK: - Watch / clear location
355
+
356
+ /// Registers a location watch. The first watcher subscribes to `.newLocationSaved`.
357
+ /// - Parameters:
358
+ /// - watchid: The caller-provided identifier for this watch.
359
+ /// - resolve: Promise resolver invoked with `watchid` on success.
360
+ /// - reject: Promise rejecter invoked when the service is not initialized.
361
+ @objc(watchLocation:resolve:reject:)
362
+ func watchLocation(watchid: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
363
+ if WoosmapGeofenceService.shared != nil {
364
+ locationWatchStack[watchid] = watchid
365
+ if locationWatchStack.count == 1 {
366
+ NotificationCenter.default.addObserver(
367
+ self,
368
+ selector: #selector(newLocationAdded(_:)),
369
+ name: .newLocationSaved,
370
+ object: nil)
371
+ }
372
+ resolve(watchid)
373
+ } else {
374
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
375
+ WoosmapGeofenceMessage.woosmapNotInitialized,
376
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
377
+ }
378
+ }
379
+
380
+ /// Removes the location watch with the given ID, unsubscribing once empty.
381
+ /// - Parameters:
382
+ /// - watchid: The identifier of the watch to remove.
383
+ /// - resolve: Promise resolver invoked with `watchid`.
384
+ /// - reject: Promise rejecter (unused).
385
+ @objc(clearLocationWatch:resolve:reject:)
386
+ func clearLocationWatch(watchid: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
387
+ if let _ = locationWatchStack[watchid] {
388
+ locationWatchStack.removeValue(forKey: watchid)
389
+ }
390
+ if locationWatchStack.count == 0 {
391
+ NotificationCenter.default.removeObserver(self, name: .newLocationSaved, object: nil)
392
+ }
393
+ resolve(watchid)
394
+ }
395
+
396
+ /// Removes all active location watches and unsubscribes from updates.
397
+ /// - Parameters:
398
+ /// - resolve: Promise resolver invoked with a sentinel watch ID.
399
+ /// - reject: Promise rejecter (unused).
400
+ @objc(clearAllLocationWatch:reject:)
401
+ func clearAllLocationWatch(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
402
+ locationWatchStack.removeAll()
403
+ NotificationCenter.default.removeObserver(self, name: .newLocationSaved, object: nil)
404
+ resolve("00000-00000-0000")
405
+ }
406
+
407
+ // MARK: - Watch / clear regions
408
+
409
+ /// Registers a region watch. The first watcher subscribes to `.didEventPOIRegion`.
410
+ /// - Parameters:
411
+ /// - watchid: The caller-provided identifier for this watch.
412
+ /// - resolve: Promise resolver invoked with `watchid` on success.
413
+ /// - reject: Promise rejecter invoked when the service is not initialized.
414
+ @objc(watchRegions:resolve:reject:)
415
+ func watchRegions(watchid: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
416
+ if WoosmapGeofenceService.shared != nil {
417
+ regionWatchStack[watchid] = watchid
418
+ if regionWatchStack.count == 1 {
419
+ NotificationCenter.default.addObserver(
420
+ self,
421
+ selector: #selector(didEventPOIRegion(_:)),
422
+ name: .didEventPOIRegion,
423
+ object: nil)
424
+ }
425
+ resolve(watchid)
426
+ } else {
427
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
428
+ WoosmapGeofenceMessage.woosmapNotInitialized,
429
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
430
+ }
431
+ }
432
+
433
+ /// Removes the region watch with the given ID, unsubscribing once empty.
434
+ /// - Parameters:
435
+ /// - watchid: The identifier of the watch to remove.
436
+ /// - resolve: Promise resolver invoked with `watchid`.
437
+ /// - reject: Promise rejecter (unused).
438
+ @objc(clearRegionsWatch:resolve:reject:)
439
+ func clearRegionsWatch(watchid: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
440
+ if let _ = regionWatchStack[watchid] {
441
+ regionWatchStack.removeValue(forKey: watchid)
442
+ }
443
+ if regionWatchStack.count == 0 {
444
+ NotificationCenter.default.removeObserver(self, name: .didEventPOIRegion, object: nil)
445
+ }
446
+ resolve(watchid)
447
+ }
448
+
449
+ /// Removes all active region watches and unsubscribes from updates.
450
+ /// - Parameters:
451
+ /// - resolve: Promise resolver invoked with a sentinel watch ID.
452
+ /// - reject: Promise rejecter (unused).
453
+ @objc(clearAllRegionsWatch:reject:)
454
+ func clearAllRegionsWatch(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
455
+ regionWatchStack.removeAll()
456
+ NotificationCenter.default.removeObserver(self, name: .didEventPOIRegion, object: nil)
457
+ resolve("00000-00000-0000")
458
+ }
459
+
460
+ // MARK: - Region CRUD
461
+
462
+ /// Sets the Woosmap private API key on the running service.
463
+ /// - Parameters:
464
+ /// - apiKey: The new Woosmap private API key.
465
+ /// - resolve: Promise resolver invoked on success.
466
+ /// - reject: Promise rejecter invoked on an empty key or failure.
42
467
  @objc(setWoosmapApiKey:resolve:reject:)
43
468
  func setWoosmapApiKey(apiKey: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
44
469
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -63,6 +488,11 @@ class WoosmapGeofencingTurbo: NSObject {
63
488
  }
64
489
  }
65
490
 
491
+ /// Sets the POI geofence radius, accepting a number, double or property name.
492
+ /// - Parameters:
493
+ /// - radius: The radius value (e.g. `"100"`) or a user-property name.
494
+ /// - resolve: Promise resolver invoked on success.
495
+ /// - reject: Promise rejecter invoked on an empty/invalid value.
66
496
  @objc(setPoiRadius:resolve:reject:)
67
497
  func setPoiRadius(radius: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
68
498
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -89,6 +519,11 @@ class WoosmapGeofencingTurbo: NSObject {
89
519
  resolve(WoosmapGeofenceMessage.initialize)
90
520
  }
91
521
 
522
+ /// Adds a custom region (circle or isochrone) to monitor.
523
+ /// - Parameters:
524
+ /// - region: A dictionary with `regionId`, `lat`, `lng`, `radius`, `type`.
525
+ /// - resolve: Promise resolver invoked with the created region identifier.
526
+ /// - reject: Promise rejecter invoked on validation failure or a duplicate ID.
92
527
  @objc(addRegion:resolve:reject:)
93
528
  func addRegion(region: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
94
529
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -165,6 +600,11 @@ class WoosmapGeofencingTurbo: NSObject {
165
600
  }
166
601
  }
167
602
 
603
+ /// Retrieves saved regions. With an ID, returns the single match; otherwise all.
604
+ /// - Parameters:
605
+ /// - regionId: An optional region identifier to filter by.
606
+ /// - resolve: Promise resolver invoked with an array of region dictionaries.
607
+ /// - reject: Promise rejecter invoked when the ID is not found.
168
608
  @objc(getRegions:resolve:reject:)
169
609
  func getRegions(regionId: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
170
610
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -173,10 +613,6 @@ class WoosmapGeofencingTurbo: NSObject {
173
613
  woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
174
614
  return
175
615
  }
176
-
177
- // The spec collapses the legacy getAllRegions()/getRegions(id) pair into a
178
- // single optional-arg method that always resolves an array. A nil/empty id
179
- // returns the whole collection.
180
616
  if let regionId = regionId, !regionId.isEmpty {
181
617
  if let captured = woosService.getRegions(id: regionId) {
182
618
  resolve([formatRegionData(woosdata: captured)])
@@ -191,6 +627,10 @@ class WoosmapGeofencingTurbo: NSObject {
191
627
  }
192
628
  }
193
629
 
630
+ /// Removes all saved regions.
631
+ /// - Parameters:
632
+ /// - resolve: Promise resolver invoked once regions are deleted.
633
+ /// - reject: Promise rejecter invoked when the service is not initialized.
194
634
  @objc(removeAllRegions:reject:)
195
635
  func removeAllRegions(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
196
636
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -203,6 +643,11 @@ class WoosmapGeofencingTurbo: NSObject {
203
643
  resolve(WoosmapGeofenceMessage.regionDeleted)
204
644
  }
205
645
 
646
+ /// Removes the saved region with the given identifier.
647
+ /// - Parameters:
648
+ /// - regionId: The identifier of the region to remove.
649
+ /// - resolve: Promise resolver invoked once the region is deleted.
650
+ /// - reject: Promise rejecter invoked on a missing ID or failure.
206
651
  @objc(removeRegion:resolve:reject:)
207
652
  func removeRegion(regionId: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
208
653
  guard let woosService = WoosmapGeofenceService.shared else {
@@ -226,4 +671,421 @@ class WoosmapGeofencingTurbo: NSObject {
226
671
  woosmapError(error.localizedDescription))
227
672
  }
228
673
  }
674
+
675
+ // MARK: - Tracking
676
+
677
+ /// Starts tracking using the given configuration profile.
678
+ /// - Parameters:
679
+ /// - profile: The tracking profile name (e.g. `liveTracking`).
680
+ /// - resolve: Promise resolver invoked on success.
681
+ /// - reject: Promise rejecter invoked on an invalid profile or failure.
682
+ @objc(startTracking:resolve:reject:)
683
+ func startTracking(profile: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
684
+ guard let woosService = WoosmapGeofenceService.shared else {
685
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
686
+ WoosmapGeofenceMessage.woosmapNotInitialized,
687
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
688
+ return
689
+ }
690
+ do {
691
+ try woosService.startTracking(profile: profile)
692
+ resolve(WoosmapGeofenceMessage.initialize)
693
+ } catch {
694
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
695
+ error.localizedDescription,
696
+ woosmapError(error.localizedDescription))
697
+ }
698
+ }
699
+
700
+ /// Stops location tracking.
701
+ /// - Parameters:
702
+ /// - resolve: Promise resolver invoked on success.
703
+ /// - reject: Promise rejecter invoked when the service is not initialized.
704
+ @objc(stopTracking:reject:)
705
+ func stopTracking(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
706
+ guard let woosService = WoosmapGeofenceService.shared else {
707
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
708
+ WoosmapGeofenceMessage.woosmapNotInitialized,
709
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
710
+ return
711
+ }
712
+ woosService.stopTracking()
713
+ resolve(WoosmapGeofenceMessage.initialize)
714
+ }
715
+
716
+ /// Starts tracking from a custom profile hosted locally or externally.
717
+ /// - Parameters:
718
+ /// - mode: The profile source (`local` or `external`).
719
+ /// - source: The file name or URL of the profile.
720
+ /// - resolve: Promise resolver invoked once the profile is applied.
721
+ /// - reject: Promise rejecter invoked on failure.
722
+ @objc(startCustomTracking:source:resolve:reject:)
723
+ func startCustomTracking(mode: String,
724
+ source: String,
725
+ resolve: @escaping RCTPromiseResolveBlock,
726
+ reject: @escaping RCTPromiseRejectBlock) {
727
+ guard let woosService = WoosmapGeofenceService.shared else {
728
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
729
+ WoosmapGeofenceMessage.woosmapNotInitialized,
730
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
731
+ return
732
+ }
733
+ woosService.startCustomTracking(mode: mode, source: source) { value, error in
734
+ if let functionError = error {
735
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
736
+ functionError.localizedDescription,
737
+ self.woosmapError(functionError.localizedDescription))
738
+ } else if value {
739
+ resolve(WoosmapGeofenceMessage.initialize)
740
+ } else {
741
+ // value == false with no error: the profile could not be applied.
742
+ // Reject so the JS promise always settles instead of hanging.
743
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
744
+ WoosmapGeofenceMessage.invalid_profilefile,
745
+ self.woosmapError(WoosmapGeofenceMessage.invalid_profilefile))
746
+ }
747
+ }
748
+ }
749
+
750
+ // MARK: - Permissions status
751
+
752
+ /// Reports the current location-permission status.
753
+ /// - Parameters:
754
+ /// - resolve: Promise resolver invoked with `GRANTED_BACKGROUND`,
755
+ /// `GRANTED_FOREGROUND`, `DENIED` or `UNKNOWN`.
756
+ /// - reject: Promise rejecter (unused).
757
+ @objc(getPermissionsStatus:reject:)
758
+ func getPermissionsStatus(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
759
+ // CLLocationManager must be created/queried on the main thread; bridge
760
+ // methods otherwise run on a background queue.
761
+ DispatchQueue.main.async {
762
+ let authorizationStatus: CLAuthorizationStatus
763
+ if #available(iOS 14, *) {
764
+ authorizationStatus = CLLocationManager().authorizationStatus
765
+ } else {
766
+ authorizationStatus = CLLocationManager.authorizationStatus()
767
+ }
768
+ let str: String
769
+ switch authorizationStatus {
770
+ case .denied, .restricted:
771
+ str = "DENIED"
772
+ case .authorizedAlways:
773
+ str = "GRANTED_BACKGROUND"
774
+ case .authorizedWhenInUse:
775
+ str = "GRANTED_FOREGROUND"
776
+ default:
777
+ str = "UNKNOWN"
778
+ }
779
+ resolve(str)
780
+ }
781
+ }
782
+
783
+ /// Reports the Bluetooth-permission status. Not required on iOS for
784
+ /// geofencing, so this always resolves `GRANTED`.
785
+ /// - Parameters:
786
+ /// - resolve: Promise resolver invoked with `"GRANTED"`.
787
+ /// - reject: Promise rejecter (unused).
788
+ @objc(getBLEPermissionsStatus:reject:)
789
+ func getBLEPermissionsStatus(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
790
+ resolve("GRANTED")
791
+ }
792
+
793
+ /// Reports the notification-permission status. Not required on iOS for
794
+ /// geofencing tracking, so this always resolves `GRANTED`.
795
+ /// - Parameters:
796
+ /// - resolve: Promise resolver invoked with `"GRANTED"`.
797
+ /// - reject: Promise rejecter (unused).
798
+ @objc(getNotificationPermissionsStatus:reject:)
799
+ func getNotificationPermissionsStatus(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
800
+ resolve("GRANTED")
801
+ }
802
+
803
+ // MARK: - Configuration
804
+
805
+ /// Sets the Salesforce Marketing Cloud (SFMC) credentials.
806
+ /// - Parameters:
807
+ /// - credentials: A dictionary of SFMC credential key/value pairs.
808
+ /// - resolve: Promise resolver invoked on success.
809
+ /// - reject: Promise rejecter invoked on invalid credentials or failure.
810
+ @objc(setSFMCCredentials:resolve:reject:)
811
+ func setSFMCCredentials(credentials: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
812
+ guard let woosService = WoosmapGeofenceService.shared else {
813
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
814
+ WoosmapGeofenceMessage.woosmapNotInitialized,
815
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
816
+ return
817
+ }
818
+ guard let creds = credentials as? [String: String] else {
819
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
820
+ WoosmapGeofenceMessage.invalidSFMCCredentials,
821
+ woosmapError(WoosmapGeofenceMessage.invalidSFMCCredentials))
822
+ return
823
+ }
824
+ do {
825
+ try woosService.setSFMCCredentials(credentials: creds)
826
+ resolve(WoosmapGeofenceMessage.initialize)
827
+ } catch {
828
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
829
+ error.localizedDescription,
830
+ woosmapError(error.localizedDescription))
831
+ }
832
+ }
833
+
834
+ /// Reserves protected region slots for third-party SDKs (max 3).
835
+ /// - Parameters:
836
+ /// - slots: The number of slots to reserve.
837
+ /// - resolve: Promise resolver invoked on success.
838
+ /// - reject: Promise rejecter invoked on an invalid slot count.
839
+ @objc(setProtectedRegionSlot:resolve:reject:)
840
+ func setProtectedRegionSlot(slots: NSNumber, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
841
+ guard WoosmapGeofenceService.shared != nil else {
842
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
843
+ WoosmapGeofenceMessage.woosmapNotInitialized,
844
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
845
+ return
846
+ }
847
+ do {
848
+ try WoosmapGeofenceService.shared?.setProtectedRegionSlot(slots: slots.intValue)
849
+ resolve(WoosmapGeofenceMessage.initialize)
850
+ } catch {
851
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
852
+ WoosmapGeofenceMessage.invalidProtectedRegionSlot,
853
+ woosmapError(WoosmapGeofenceMessage.invalidProtectedRegionSlot))
854
+ }
855
+ }
856
+
857
+ // MARK: - Data queries
858
+
859
+ /// Retrieves saved locations. With an ID, returns the single match; otherwise all.
860
+ /// - Parameters:
861
+ /// - locationId: An optional location identifier to filter by.
862
+ /// - resolve: Promise resolver invoked with an array of location dictionaries.
863
+ /// - reject: Promise rejecter invoked when the ID is not found.
864
+ @objc(getLocations:resolve:reject:)
865
+ func getLocations(locationId: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
866
+ guard let woosService = WoosmapGeofenceService.shared else {
867
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
868
+ WoosmapGeofenceMessage.woosmapNotInitialized,
869
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
870
+ return
871
+ }
872
+ if let locationId = locationId, !locationId.isEmpty {
873
+ if let captured = woosService.getLocations(id: locationId) {
874
+ resolve([formatLocationData(woosdata: captured)])
875
+ } else {
876
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
877
+ WoosmapGeofenceMessage.notfound_locationid,
878
+ woosmapError(WoosmapGeofenceMessage.notfound_locationid))
879
+ }
880
+ } else {
881
+ resolve(woosService.getLocations().map { formatLocationData(woosdata: $0) })
882
+ }
883
+ }
884
+
885
+ /// Retrieves saved POIs. With an ID, returns the single match; otherwise all.
886
+ /// - Parameters:
887
+ /// - poiId: An optional POI identifier (location or store ID) to filter by.
888
+ /// - resolve: Promise resolver invoked with an array of POI dictionaries.
889
+ /// - reject: Promise rejecter invoked when the ID is not found.
890
+ @objc(getPois:resolve:reject:)
891
+ func getPois(poiId: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
892
+ guard let woosService = WoosmapGeofenceService.shared else {
893
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
894
+ WoosmapGeofenceMessage.woosmapNotInitialized,
895
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
896
+ return
897
+ }
898
+ if let poiId = poiId, !poiId.isEmpty {
899
+ if let captured = woosService.getPOIs(id: poiId) {
900
+ resolve([formatPOIData(woosdata: captured)])
901
+ } else {
902
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
903
+ WoosmapGeofenceMessage.notfound_poiid,
904
+ woosmapError(WoosmapGeofenceMessage.notfound_poiid))
905
+ }
906
+ } else {
907
+ resolve(woosService.getPOIs().map { formatPOIData(woosdata: $0) })
908
+ }
909
+ }
910
+
911
+ /// Retrieves saved indoor beacons, optionally filtered by venue.
912
+ /// - Parameters:
913
+ /// - venueId: An optional venue identifier to filter by.
914
+ /// - resolve: Promise resolver invoked with an array of beacon dictionaries.
915
+ /// - reject: Promise rejecter invoked when the service is not initialized.
916
+ @objc(getIndoorBeacons:resolve:reject:)
917
+ func getIndoorBeacons(venueId: String?, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
918
+ guard let woosService = WoosmapGeofenceService.shared else {
919
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
920
+ WoosmapGeofenceMessage.woosmapNotInitialized,
921
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
922
+ return
923
+ }
924
+ let beacons = woosService.getIndoorBeacons(id: venueId) ?? []
925
+ resolve(beacons.map { formatIndoorBeaconData(woosdata: $0) })
926
+ }
927
+
928
+ // MARK: - Data removal
929
+
930
+ /// Removes all saved locations.
931
+ /// - Parameters:
932
+ /// - resolve: Promise resolver invoked once locations are deleted.
933
+ /// - reject: Promise rejecter invoked when the service is not initialized.
934
+ @objc(removeLocations:reject:)
935
+ func removeLocations(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
936
+ guard let woosService = WoosmapGeofenceService.shared else {
937
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
938
+ WoosmapGeofenceMessage.woosmapNotInitialized,
939
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
940
+ return
941
+ }
942
+ woosService.deleteAllLocations()
943
+ resolve(WoosmapGeofenceMessage.locationDeleted)
944
+ }
945
+
946
+ /// Removes all saved POIs.
947
+ /// - Parameters:
948
+ /// - resolve: Promise resolver invoked once POIs are deleted.
949
+ /// - reject: Promise rejecter invoked when the service is not initialized.
950
+ @objc(removePois:reject:)
951
+ func removePois(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
952
+ guard let woosService = WoosmapGeofenceService.shared else {
953
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
954
+ WoosmapGeofenceMessage.woosmapNotInitialized,
955
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
956
+ return
957
+ }
958
+ woosService.deletePOI()
959
+ resolve(WoosmapGeofenceMessage.poiDeleted)
960
+ }
961
+
962
+ /// Removes all saved indoor beacons.
963
+ /// - Parameters:
964
+ /// - resolve: Promise resolver invoked once beacons are deleted.
965
+ /// - reject: Promise rejecter invoked when the service is not initialized.
966
+ @objc(removeIndoorBeacons:reject:)
967
+ func removeIndoorBeacons(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
968
+ guard let woosService = WoosmapGeofenceService.shared else {
969
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
970
+ WoosmapGeofenceMessage.woosmapNotInitialized,
971
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
972
+ return
973
+ }
974
+ woosService.deleteIndoorBeacons()
975
+ resolve(WoosmapGeofenceMessage.locationDeleted)
976
+ }
977
+
978
+ /// Refreshes POI data from the Woosmap server.
979
+ /// - Parameters:
980
+ /// - resolve: Promise resolver invoked with the refresh status.
981
+ /// - reject: Promise rejecter invoked when the service is not initialized.
982
+ @objc(refreshPois:reject:)
983
+ func refreshPois(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
984
+ guard let woosService = WoosmapGeofenceService.shared else {
985
+ reject(WoosmapGeofenceMessage.plugin_errorDomain,
986
+ WoosmapGeofenceMessage.woosmapNotInitialized,
987
+ woosmapError(WoosmapGeofenceMessage.woosmapNotInitialized))
988
+ return
989
+ }
990
+ woosService.refreshPOI()
991
+ resolve(WoosmapGeofenceMessage.refreshPOIStatus)
992
+ }
993
+
994
+ // MARK: - Notification handlers
995
+
996
+ /// Handles `.newLocationSaved` notifications and forwards them to JS as a
997
+ /// `geolocationDidChange` event when at least one watcher is active.
998
+ /// - Parameter notification: The notification carrying the new `Location`.
999
+ @objc func newLocationAdded(_ notification: Notification) {
1000
+ if let location = notification.userInfo?["Location"] as? Location {
1001
+ if locationWatchStack.count > 0 {
1002
+ sendEvent(withName: "geolocationDidChange", body: formatLocationData(woosdata: location))
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ /// Handles `.didEventPOIRegion` notifications and forwards them to JS as a
1008
+ /// `woosmapgeofenceRegionDidChange` event.
1009
+ /// - Parameter notification: The notification carrying the `Region`.
1010
+ @objc func didEventPOIRegion(_ notification: Notification) {
1011
+ if let region = notification.userInfo?["Region"] as? Region {
1012
+ sendEvent(withName: "woosmapgeofenceRegionDidChange", body: formatRegionData(woosdata: region))
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ // MARK: - CLLocationManagerDelegate
1018
+
1019
+ extension WoosmapGeofencingTurbo: CLLocationManagerDelegate {
1020
+ /// Receives location updates from the permission-checking manager.
1021
+ /// - Parameters:
1022
+ /// - manager: The location manager reporting the update.
1023
+ /// - locations: The array of new locations; only the last is used.
1024
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
1025
+ guard let location = locations.last else { return }
1026
+ print(location)
1027
+ }
1028
+
1029
+ /// Settles an in-flight `requestPermissions` promise once the user answers the
1030
+ /// system location dialog. Mirrors the Android `onRequestPermissionsResult`
1031
+ /// path so a fresh install no longer hangs. No-ops when there is no pending
1032
+ /// request (e.g. the initial callback fired on delegate assignment) or while
1033
+ /// the dialog is still showing (`.notDetermined`).
1034
+ /// - Parameter manager: The location manager reporting the change.
1035
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
1036
+ guard let resolve = permissionResolve else { return }
1037
+ let status = manager.authorizationStatus
1038
+ if status == .notDetermined { return }
1039
+ permissionResolve = nil
1040
+ switch status {
1041
+ case .authorizedAlways:
1042
+ resolve("GRANTED_BACKGROUND")
1043
+ case .authorizedWhenInUse:
1044
+ resolve("GRANTED_FOREGROUND")
1045
+ case .denied, .restricted:
1046
+ resolve("DENIED")
1047
+ default:
1048
+ resolve("UNKNOWN")
1049
+ }
1050
+ }
1051
+
1052
+ /// Receives location-manager failures.
1053
+ /// - Parameters:
1054
+ /// - manager: The location manager reporting the failure.
1055
+ /// - error: The error that occurred.
1056
+ private func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
1057
+ print(error)
1058
+ }
1059
+ }
1060
+
1061
+ // MARK: - UIApplication top view controller
1062
+
1063
+ extension UIApplication {
1064
+ /// The top-most view controller in the app, used to present permission alerts.
1065
+ /// - Returns: The visible view controller, or `nil` if no key window is found.
1066
+ var topViewController: UIViewController? {
1067
+ guard let windowScene = connectedScenes
1068
+ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
1069
+ let rootVC = windowScene.windows
1070
+ .first(where: { $0.isKeyWindow })?.rootViewController
1071
+ else {
1072
+ return nil
1073
+ }
1074
+ return topViewController(from: rootVC)
1075
+ }
1076
+
1077
+ /// Recursively walks presented / navigation / tab controllers to find the top one.
1078
+ /// - Parameter root: The view controller to start the walk from.
1079
+ /// - Returns: The top-most view controller reachable from `root`.
1080
+ private func topViewController(from root: UIViewController) -> UIViewController {
1081
+ if let presented = root.presentedViewController {
1082
+ return topViewController(from: presented)
1083
+ } else if let nav = root as? UINavigationController, let visible = nav.visibleViewController {
1084
+ return topViewController(from: visible)
1085
+ } else if let tab = root as? UITabBarController, let selected = tab.selectedViewController {
1086
+ return topViewController(from: selected)
1087
+ } else {
1088
+ return root
1089
+ }
1090
+ }
229
1091
  }