@capgo/background-geolocation 8.0.31 → 8.0.33

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.
@@ -38,7 +38,7 @@ func formatLocation(_ location: CLLocation) -> PluginCallResultData {
38
38
  @objc(BackgroundGeolocation)
39
39
  // swiftlint:disable:next type_body_length
40
40
  public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBridgedPlugin {
41
- private let pluginVersion: String = "8.0.31"
41
+ private let pluginVersion: String = "8.0.33"
42
42
  public let identifier = "BackgroundGeolocationPlugin"
43
43
  public let jsName = "BackgroundGeolocation"
44
44
  public let pluginMethods: [CAPPluginMethod] = [
@@ -46,9 +46,15 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
46
46
  CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
47
47
  CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise),
48
48
  CAPPluginMethod(name: "setPlannedRoute", returnType: CAPPluginReturnPromise),
49
+ CAPPluginMethod(name: "setupGeofencing", returnType: CAPPluginReturnPromise),
50
+ CAPPluginMethod(name: "addGeofence", returnType: CAPPluginReturnPromise),
51
+ CAPPluginMethod(name: "removeGeofence", returnType: CAPPluginReturnPromise),
52
+ CAPPluginMethod(name: "removeAllGeofences", returnType: CAPPluginReturnPromise),
53
+ CAPPluginMethod(name: "getMonitoredGeofences", returnType: CAPPluginReturnPromise),
49
54
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
50
55
  ]
51
56
  private var locationManager: CLLocationManager?
57
+ private var geofenceLocationManager: CLLocationManager?
52
58
  private var created: Date?
53
59
  private var allowStale: Bool = false
54
60
  private var isUpdatingLocation: Bool = false
@@ -57,12 +63,31 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
57
63
  private var plannedRoute: [[Double]] = []
58
64
  private var isOffRoute: Bool = true
59
65
  private var distanceThreshold: Double = 50.0 // Default distance threshold in meters
66
+ private var geofenceBackendUrl: URL?
67
+ private var geofenceNotifyOnEntry: Bool = true
68
+ private var geofenceNotifyOnExit: Bool = true
69
+ private var geofencePayload: [String: Any] = [:]
70
+ private var pendingGeofenceSetupCall: CAPPluginCall?
71
+ private var pendingGeofenceSetupTimeout: DispatchWorkItem?
72
+ private var pendingGeofenceAddCalls: [String: CAPPluginCall] = [:]
73
+ private var pendingGeofenceRegions: [String: (region: CLCircularRegion, payload: [String: Any])] = [:]
74
+ private var lastGeofenceTransition: [String: String] = [:]
75
+
76
+ private let geofenceUrlKey = "CapgoBackgroundGeolocation.geofence.url"
77
+ private let geofenceNotifyOnEntryKey = "CapgoBackgroundGeolocation.geofence.notifyOnEntry"
78
+ private let geofenceNotifyOnExitKey = "CapgoBackgroundGeolocation.geofence.notifyOnExit"
79
+ private let geofencePayloadKey = "CapgoBackgroundGeolocation.geofence.payload"
80
+ private let geofenceRegionPrefix = "CapgoBackgroundGeolocation.geofence.region."
60
81
 
61
82
  // Earth radius in meters for distance calculations
62
83
  private static let earthRadiusMeters: Double = 6371000.0
63
84
 
64
85
  @objc override public func load() {
65
86
  UIDevice.current.isBatteryMonitoringEnabled = true
87
+ restoreGeofenceConfiguration()
88
+ DispatchQueue.main.async {
89
+ _ = self.ensureGeofenceLocationManager()
90
+ }
66
91
  }
67
92
 
68
93
  @objc func start(_ call: CAPPluginCall) {
@@ -225,6 +250,338 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
225
250
  }
226
251
  }
227
252
 
253
+ private func requestGeofenceAlwaysAuthorization(_ call: CAPPluginCall, manager: CLLocationManager, status: CLAuthorizationStatus) {
254
+ pendingGeofenceSetupCall = call
255
+ pendingGeofenceSetupTimeout?.cancel()
256
+ let timeout = DispatchWorkItem { [weak self] in
257
+ guard let self = self, let pendingCall = self.pendingGeofenceSetupCall else { return }
258
+ self.pendingGeofenceSetupCall = nil
259
+ self.pendingGeofenceSetupTimeout = nil
260
+ if manager.authorizationStatus == .authorizedAlways {
261
+ pendingCall.resolve()
262
+ } else {
263
+ pendingCall.reject(
264
+ "Always location permission is required for geofencing",
265
+ "NOT_AUTHORIZED"
266
+ )
267
+ }
268
+ }
269
+ pendingGeofenceSetupTimeout = timeout
270
+ manager.requestAlwaysAuthorization()
271
+ if status == .authorizedWhenInUse {
272
+ DispatchQueue.main.asyncAfter(deadline: .now() + 30, execute: timeout)
273
+ }
274
+ }
275
+
276
+ @objc func setupGeofencing(_ call: CAPPluginCall) {
277
+ DispatchQueue.main.async {
278
+ if self.pendingGeofenceSetupCall != nil {
279
+ return call.reject("A geofence permission request is already in progress", "PERMISSION_REQUEST_IN_PROGRESS")
280
+ }
281
+ guard CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) else {
282
+ return call.reject("Geofencing is not available on this device", "NOT_AVAILABLE")
283
+ }
284
+ var backendUrl: URL?
285
+ if let urlString = call.getString("url"), !urlString.isEmpty {
286
+ guard let url = URL(string: urlString),
287
+ let scheme = url.scheme?.lowercased(),
288
+ ["http", "https"].contains(scheme) else {
289
+ return call.reject("Given url is not valid")
290
+ }
291
+ backendUrl = url
292
+ }
293
+ let payload = call.getObject("payload") ?? [:]
294
+ guard JSONSerialization.isValidJSONObject(payload) else {
295
+ return call.reject("Payload must be valid JSON")
296
+ }
297
+
298
+ self.geofenceBackendUrl = backendUrl
299
+ self.geofenceNotifyOnEntry = call.getBool("notifyOnEntry") ?? true
300
+ self.geofenceNotifyOnExit = call.getBool("notifyOnExit") ?? true
301
+ self.geofencePayload = payload
302
+ self.persistGeofenceConfiguration()
303
+
304
+ let manager = self.ensureGeofenceLocationManager()
305
+ let status = manager.authorizationStatus
306
+ if status == .authorizedAlways {
307
+ return call.resolve()
308
+ }
309
+ if call.getBool("requestPermissions") == false {
310
+ return call.reject("Always location permission is required for geofencing", "NOT_AUTHORIZED")
311
+ }
312
+ if [.denied, .restricted].contains(status) {
313
+ return call.reject("Always location permission is required for geofencing", "NOT_AUTHORIZED")
314
+ }
315
+ self.requestGeofenceAlwaysAuthorization(call, manager: manager, status: status)
316
+ }
317
+ }
318
+
319
+ @objc func addGeofence(_ call: CAPPluginCall) {
320
+ DispatchQueue.main.async {
321
+ let manager = self.ensureGeofenceLocationManager()
322
+ guard self.geofenceAvailable(manager) else {
323
+ return call.reject("Always location permission is required for geofencing", "NOT_AUTHORIZED")
324
+ }
325
+ guard let latitude = call.getDouble("latitude") else {
326
+ return call.reject("Latitude is required")
327
+ }
328
+ guard let longitude = call.getDouble("longitude") else {
329
+ return call.reject("Longitude is required")
330
+ }
331
+ guard let identifier = call.getString("identifier"), !identifier.isEmpty else {
332
+ return call.reject("Identifier is required")
333
+ }
334
+ let radius = call.getDouble("radius") ?? 50.0
335
+ guard CLLocationCoordinate2DIsValid(CLLocationCoordinate2D(latitude: latitude, longitude: longitude)) else {
336
+ return call.reject("Invalid latitude or longitude")
337
+ }
338
+ guard radius > 0 else {
339
+ return call.reject("Radius must be greater than 0")
340
+ }
341
+ let maximumDistance = manager.maximumRegionMonitoringDistance
342
+ guard maximumDistance <= 0 || radius <= maximumDistance else {
343
+ return call.reject("Radius exceeds the maximum supported region monitoring distance")
344
+ }
345
+ let notifyOnEntry = call.getBool("notifyOnEntry") ?? self.geofenceNotifyOnEntry
346
+ let notifyOnExit = call.getBool("notifyOnExit") ?? self.geofenceNotifyOnExit
347
+ guard notifyOnEntry || notifyOnExit else {
348
+ return call.reject("At least one transition must be enabled")
349
+ }
350
+ let payload = call.getObject("payload") ?? [:]
351
+ guard JSONSerialization.isValidJSONObject(payload) else {
352
+ return call.reject("Payload must be valid JSON")
353
+ }
354
+
355
+ guard self.pendingGeofenceAddCalls[identifier] == nil else {
356
+ return call.reject("A geofence with that identifier is already being added", "PENDING")
357
+ }
358
+
359
+ let center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
360
+ let region = CLCircularRegion(center: center, radius: radius, identifier: identifier)
361
+ region.notifyOnEntry = notifyOnEntry
362
+ region.notifyOnExit = notifyOnExit
363
+ self.pendingGeofenceAddCalls[identifier] = call
364
+ self.pendingGeofenceRegions[identifier] = (region, payload)
365
+ manager.startMonitoring(for: region)
366
+ }
367
+ }
368
+
369
+ @objc func removeGeofence(_ call: CAPPluginCall) {
370
+ DispatchQueue.main.async {
371
+ guard let identifier = call.getString("identifier"), !identifier.isEmpty else {
372
+ return call.reject("Identifier is required")
373
+ }
374
+ let manager = self.ensureGeofenceLocationManager()
375
+ if let pending = self.pendingGeofenceRegions.removeValue(forKey: identifier) {
376
+ manager.stopMonitoring(for: pending.region)
377
+ self.pendingGeofenceAddCalls.removeValue(forKey: identifier)?.reject(
378
+ "Geofence was removed before monitoring started",
379
+ "CANCELLED"
380
+ )
381
+ self.removePersistedGeofenceRegion(identifier)
382
+ self.lastGeofenceTransition.removeValue(forKey: identifier)
383
+ return call.resolve()
384
+ }
385
+ guard self.persistedGeofenceRegionIds().contains(identifier) else {
386
+ return call.reject("Could not find a region with that identifier", "NOT_FOUND")
387
+ }
388
+ guard let region = manager.monitoredRegions.first(where: { $0.identifier == identifier && $0 is CLCircularRegion }) else {
389
+ self.pendingGeofenceAddCalls.removeValue(forKey: identifier)?.reject("Geofence was removed before monitoring started", "CANCELLED")
390
+ self.pendingGeofenceRegions.removeValue(forKey: identifier)
391
+ self.removePersistedGeofenceRegion(identifier)
392
+ self.lastGeofenceTransition.removeValue(forKey: identifier)
393
+ return call.resolve()
394
+ }
395
+ manager.stopMonitoring(for: region)
396
+ self.pendingGeofenceAddCalls.removeValue(forKey: identifier)?.reject("Geofence was removed before monitoring started", "CANCELLED")
397
+ self.pendingGeofenceRegions.removeValue(forKey: identifier)
398
+ self.removePersistedGeofenceRegion(identifier)
399
+ self.lastGeofenceTransition.removeValue(forKey: identifier)
400
+ call.resolve()
401
+ }
402
+ }
403
+
404
+ @objc func removeAllGeofences(_ call: CAPPluginCall) {
405
+ DispatchQueue.main.async {
406
+ let manager = self.ensureGeofenceLocationManager()
407
+ let identifiers = self.persistedGeofenceRegionIds()
408
+ for region in manager.monitoredRegions where region is CLCircularRegion && identifiers.contains(region.identifier) {
409
+ manager.stopMonitoring(for: region)
410
+ }
411
+ for identifier in identifiers {
412
+ self.removePersistedGeofenceRegion(identifier)
413
+ self.lastGeofenceTransition.removeValue(forKey: identifier)
414
+ }
415
+ for (_, pendingCall) in self.pendingGeofenceAddCalls {
416
+ pendingCall.reject("Geofences were removed before monitoring started", "CANCELLED")
417
+ }
418
+ self.pendingGeofenceAddCalls.removeAll()
419
+ self.pendingGeofenceRegions.removeAll()
420
+ call.resolve()
421
+ }
422
+ }
423
+
424
+ @objc func getMonitoredGeofences(_ call: CAPPluginCall) {
425
+ DispatchQueue.main.async {
426
+ let manager = self.ensureGeofenceLocationManager()
427
+ let identifiers = self.persistedGeofenceRegionIds()
428
+ let regions = manager.monitoredRegions.compactMap { region -> String? in
429
+ region is CLCircularRegion && identifiers.contains(region.identifier) ? region.identifier : nil
430
+ }.sorted()
431
+ call.resolve(["regions": regions])
432
+ }
433
+ }
434
+
435
+ private func ensureGeofenceLocationManager() -> CLLocationManager {
436
+ if let manager = geofenceLocationManager {
437
+ return manager
438
+ }
439
+ let manager = CLLocationManager()
440
+ manager.delegate = self
441
+ manager.pausesLocationUpdatesAutomatically = false
442
+ geofenceLocationManager = manager
443
+ return manager
444
+ }
445
+
446
+ private func geofenceAvailable(_ manager: CLLocationManager) -> Bool {
447
+ CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) && manager.authorizationStatus == .authorizedAlways
448
+ }
449
+
450
+ private func persistGeofenceConfiguration() {
451
+ let defaults = UserDefaults.standard
452
+ defaults.set(geofenceBackendUrl?.absoluteString, forKey: geofenceUrlKey)
453
+ defaults.set(geofenceNotifyOnEntry, forKey: geofenceNotifyOnEntryKey)
454
+ defaults.set(geofenceNotifyOnExit, forKey: geofenceNotifyOnExitKey)
455
+ if JSONSerialization.isValidJSONObject(geofencePayload),
456
+ let data = try? JSONSerialization.data(withJSONObject: geofencePayload) {
457
+ defaults.set(data, forKey: geofencePayloadKey)
458
+ } else {
459
+ defaults.removeObject(forKey: geofencePayloadKey)
460
+ }
461
+ }
462
+
463
+ private func restoreGeofenceConfiguration() {
464
+ let defaults = UserDefaults.standard
465
+ if let urlString = defaults.string(forKey: geofenceUrlKey), !urlString.isEmpty {
466
+ geofenceBackendUrl = URL(string: urlString)
467
+ }
468
+ if defaults.object(forKey: geofenceNotifyOnEntryKey) != nil {
469
+ geofenceNotifyOnEntry = defaults.bool(forKey: geofenceNotifyOnEntryKey)
470
+ }
471
+ if defaults.object(forKey: geofenceNotifyOnExitKey) != nil {
472
+ geofenceNotifyOnExit = defaults.bool(forKey: geofenceNotifyOnExitKey)
473
+ }
474
+ if let data = defaults.data(forKey: geofencePayloadKey),
475
+ let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
476
+ geofencePayload = payload
477
+ }
478
+ }
479
+
480
+ private func persistGeofenceRegion(_ region: CLCircularRegion, payload: [String: Any]) {
481
+ let data: [String: Any] = [
482
+ "latitude": region.center.latitude,
483
+ "longitude": region.center.longitude,
484
+ "radius": region.radius,
485
+ "payload": payload
486
+ ]
487
+ if JSONSerialization.isValidJSONObject(data),
488
+ let encoded = try? JSONSerialization.data(withJSONObject: data) {
489
+ UserDefaults.standard.set(encoded, forKey: geofenceRegionPrefix + region.identifier)
490
+ }
491
+ }
492
+
493
+ private func persistedGeofenceRegion(_ identifier: String) -> [String: Any] {
494
+ guard let data = UserDefaults.standard.data(forKey: geofenceRegionPrefix + identifier),
495
+ let decoded = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
496
+ return [:]
497
+ }
498
+ return decoded
499
+ }
500
+
501
+ private func removePersistedGeofenceRegion(_ identifier: String) {
502
+ UserDefaults.standard.removeObject(forKey: geofenceRegionPrefix + identifier)
503
+ }
504
+
505
+ private func persistedGeofenceRegionIds() -> Set<String> {
506
+ Set(UserDefaults.standard.dictionaryRepresentation().keys.compactMap { key in
507
+ guard key.hasPrefix(geofenceRegionPrefix) else { return nil }
508
+ return String(key.dropFirst(geofenceRegionPrefix.count))
509
+ })
510
+ }
511
+
512
+ private func geofenceTransitionData(for region: CLRegion, enter: Bool) -> [String: Any] {
513
+ let persistedRegion = persistedGeofenceRegion(region.identifier)
514
+ let regionPayload = persistedRegion["payload"] as? [String: Any] ?? [:]
515
+ var payload = geofencePayload
516
+ for (key, value) in regionPayload {
517
+ payload[key] = value
518
+ }
519
+
520
+ var data = payload
521
+ data["identifier"] = region.identifier
522
+ data["transition"] = enter ? "enter" : "exit"
523
+ data["enter"] = enter
524
+ if let circularRegion = region as? CLCircularRegion {
525
+ data["latitude"] = circularRegion.center.latitude
526
+ data["longitude"] = circularRegion.center.longitude
527
+ data["radius"] = circularRegion.radius
528
+ } else {
529
+ data["latitude"] = persistedRegion["latitude"]
530
+ data["longitude"] = persistedRegion["longitude"]
531
+ data["radius"] = persistedRegion["radius"]
532
+ }
533
+ data["payload"] = payload
534
+ return data
535
+ }
536
+
537
+ private func handleGeofenceTransition(for region: CLRegion, enter: Bool) {
538
+ if let circularRegion = region as? CLCircularRegion {
539
+ if enter && !circularRegion.notifyOnEntry {
540
+ return
541
+ }
542
+ if !enter && !circularRegion.notifyOnExit {
543
+ return
544
+ }
545
+ }
546
+
547
+ let transition = enter ? "enter" : "exit"
548
+ if lastGeofenceTransition[region.identifier] == transition {
549
+ return
550
+ }
551
+ lastGeofenceTransition[region.identifier] = transition
552
+
553
+ let data = geofenceTransitionData(for: region, enter: enter)
554
+ notifyListeners("geofenceTransition", data: data, retainUntilConsumed: true)
555
+ postGeofenceTransition(data)
556
+ }
557
+
558
+ private func postGeofenceTransition(_ data: [String: Any]) {
559
+ guard let backendUrl = geofenceBackendUrl,
560
+ JSONSerialization.isValidJSONObject(data),
561
+ let body = try? JSONSerialization.data(withJSONObject: data) else {
562
+ return
563
+ }
564
+ var request = URLRequest(url: backendUrl)
565
+ request.httpMethod = "POST"
566
+ request.addValue("application/json", forHTTPHeaderField: "Content-Type")
567
+ request.addValue("application/json", forHTTPHeaderField: "Accept")
568
+ request.httpBody = body
569
+
570
+ var backgroundTask = UIBackgroundTaskIdentifier.invalid
571
+ backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "CapgoGeofenceTransition") {
572
+ if backgroundTask != .invalid {
573
+ UIApplication.shared.endBackgroundTask(backgroundTask)
574
+ backgroundTask = .invalid
575
+ }
576
+ }
577
+ URLSession.shared.dataTask(with: request) { _, _, _ in
578
+ if backgroundTask != .invalid {
579
+ UIApplication.shared.endBackgroundTask(backgroundTask)
580
+ backgroundTask = .invalid
581
+ }
582
+ }.resume()
583
+ }
584
+
228
585
  private func startUpdatingLocation() {
229
586
  // Avoid unnecessary calls to startUpdatingLocation, which can
230
587
  // result in extraneous invocations of didFailWithError.
@@ -353,7 +710,8 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
353
710
  _ manager: CLLocationManager,
354
711
  didFailWithError error: Error
355
712
  ) {
356
- guard let callbackId = activeCallbackId,
713
+ guard manager === locationManager,
714
+ let callbackId = activeCallbackId,
357
715
  let call = self.bridge?.savedCall(withID: callbackId) else {
358
716
  return
359
717
  }
@@ -378,7 +736,8 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
378
736
  _ manager: CLLocationManager,
379
737
  didUpdateLocations locations: [CLLocation]
380
738
  ) {
381
- guard let location = locations.last,
739
+ guard manager === locationManager,
740
+ let location = locations.last,
382
741
  let callbackId = activeCallbackId,
383
742
  let call = self.bridge?.savedCall(withID: callbackId) else {
384
743
  return
@@ -394,14 +753,76 @@ public class BackgroundGeolocation: CAPPlugin, CLLocationManagerDelegate, CAPBri
394
753
  _ manager: CLLocationManager,
395
754
  didChangeAuthorization status: CLAuthorizationStatus
396
755
  ) {
756
+ if let pendingCall = pendingGeofenceSetupCall {
757
+ if status == .authorizedAlways {
758
+ pendingGeofenceSetupTimeout?.cancel()
759
+ pendingGeofenceSetupTimeout = nil
760
+ pendingGeofenceSetupCall = nil
761
+ pendingCall.resolve()
762
+ } else if status == .denied || status == .restricted || status == .authorizedWhenInUse {
763
+ pendingGeofenceSetupTimeout?.cancel()
764
+ pendingGeofenceSetupTimeout = nil
765
+ pendingGeofenceSetupCall = nil
766
+ pendingCall.reject("Always location permission is required for geofencing", "NOT_AUTHORIZED")
767
+ }
768
+ }
769
+
397
770
  // If this method is called before the user decides on a permission, as
398
771
  // it is on iOS 14 when the permissions dialog is presented, we ignore
399
772
  // it.
400
- if status != .notDetermined {
773
+ if manager === locationManager && status != .notDetermined {
401
774
  startUpdatingLocation()
402
775
  }
403
776
  }
404
777
 
778
+ public func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
779
+ guard manager === geofenceLocationManager else { return }
780
+ if let pending = pendingGeofenceRegions.removeValue(forKey: region.identifier) {
781
+ persistGeofenceRegion(pending.region, payload: pending.payload)
782
+ pendingGeofenceAddCalls.removeValue(forKey: region.identifier)?.resolve()
783
+ }
784
+ manager.requestState(for: region)
785
+ }
786
+
787
+ public func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
788
+ guard manager === geofenceLocationManager, let region = region else { return }
789
+ pendingGeofenceRegions.removeValue(forKey: region.identifier)
790
+ removePersistedGeofenceRegion(region.identifier)
791
+ pendingGeofenceAddCalls.removeValue(forKey: region.identifier)?.reject(
792
+ "Could not start monitoring the geofence",
793
+ "MONITORING_FAILED",
794
+ error
795
+ )
796
+ let nsError = error as NSError
797
+ notifyListeners(
798
+ "geofenceError",
799
+ data: [
800
+ "identifier": region.identifier,
801
+ "message": error.localizedDescription,
802
+ "code": nsError.code,
803
+ "domain": nsError.domain
804
+ ],
805
+ retainUntilConsumed: true
806
+ )
807
+ }
808
+
809
+ public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
810
+ guard manager === geofenceLocationManager, region is CLCircularRegion else { return }
811
+ handleGeofenceTransition(for: region, enter: true)
812
+ }
813
+
814
+ public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
815
+ guard manager === geofenceLocationManager, region is CLCircularRegion else { return }
816
+ handleGeofenceTransition(for: region, enter: false)
817
+ }
818
+
819
+ public func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
820
+ guard manager === geofenceLocationManager, region is CLCircularRegion else { return }
821
+ if state == .inside {
822
+ handleGeofenceTransition(for: region, enter: true)
823
+ }
824
+ }
825
+
405
826
  @objc func getPluginVersion(_ call: CAPPluginCall) {
406
827
  call.resolve(["version": self.pluginVersion])
407
828
  }
@@ -39,6 +39,11 @@ class CapgoBackgroundGeolocationTests: XCTestCase {
39
39
  XCTAssertTrue(methodNames.contains("stop"))
40
40
  XCTAssertTrue(methodNames.contains("openSettings"))
41
41
  XCTAssertTrue(methodNames.contains("setPlannedRoute"))
42
+ XCTAssertTrue(methodNames.contains("setupGeofencing"))
43
+ XCTAssertTrue(methodNames.contains("addGeofence"))
44
+ XCTAssertTrue(methodNames.contains("removeGeofence"))
45
+ XCTAssertTrue(methodNames.contains("removeAllGeofences"))
46
+ XCTAssertTrue(methodNames.contains("getMonitoredGeofences"))
42
47
  XCTAssertTrue(methodNames.contains("getPluginVersion"))
43
48
  }
44
49
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capgo/background-geolocation",
3
- "version": "8.0.31",
4
- "description": "Receive accurate geolocation updates even while the app is in the background.",
3
+ "version": "8.0.33",
4
+ "description": "Accurate background geolocation and native geofencing for Capacitor apps on iOS and Android.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
7
7
  "types": "dist/esm/index.d.ts",
@@ -26,23 +26,23 @@
26
26
  },
27
27
  "homepage": "https://capgo.app/docs/plugins/background-geolocation/",
28
28
  "scripts": {
29
- "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
29
+ "verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
30
30
  "verify:ios": "xcodebuild -scheme CapgoBackgroundGeolocation -destination generic/platform=iOS",
31
31
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
32
- "verify:web": "npm run build",
33
- "test": "npm run test:ios && npm run test:android",
32
+ "verify:web": "bun run build",
33
+ "test": "bun run test:ios && bun run test:android",
34
34
  "test:ios": "./scripts/test-ios.sh",
35
35
  "test:android": "cd android && ./gradlew test && cd ..",
36
- "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
37
- "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
36
+ "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
37
+ "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
38
38
  "eslint": "eslint . --ext ts",
39
39
  "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
40
40
  "swiftlint": "node-swiftlint",
41
41
  "docgen": "docgen --api BackgroundGeolocationPlugin --output-readme README.md --output-json dist/docs.json",
42
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
42
+ "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
43
43
  "clean": "rimraf ./dist",
44
44
  "watch": "tsc --watch",
45
- "prepublishOnly": "npm run build"
45
+ "prepublishOnly": "bun run build"
46
46
  },
47
47
  "keywords": [
48
48
  "capacitor",
@@ -50,7 +50,14 @@
50
50
  "native",
51
51
  "accurate",
52
52
  "background",
53
- "geolocation"
53
+ "geolocation",
54
+ "geofence",
55
+ "geofencing",
56
+ "location",
57
+ "background-location",
58
+ "ios-geofencing",
59
+ "android-geofencing",
60
+ "webhook"
54
61
  ],
55
62
  "devDependencies": {
56
63
  "@capacitor/android": "^8.0.0",