@bricks-soft/capacitor-location-tracking 8.1.0 → 8.2.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 (61) hide show
  1. package/BricksSoftCapacitorLocationTracking.podspec +18 -0
  2. package/Package.swift +13 -0
  3. package/README.md +107 -29
  4. package/android/build.gradle +3 -1
  5. package/dist/esm/definitions.d.ts +10 -4
  6. package/dist/esm/definitions.js.map +1 -1
  7. package/dist/esm/web/device.d.ts +1 -1
  8. package/dist/esm/web/device.js +1 -1
  9. package/dist/esm/web/device.js.map +1 -1
  10. package/dist/plugin.cjs.js +1 -1
  11. package/dist/plugin.cjs.js.map +1 -1
  12. package/dist/plugin.js +1 -1
  13. package/dist/plugin.js.map +1 -1
  14. package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
  15. package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
  16. package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
  17. package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
  18. package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
  19. package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
  20. package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
  21. package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
  22. package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
  23. package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
  24. package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
  25. package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
  26. package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
  27. package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
  28. package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
  29. package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
  30. package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
  31. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
  32. package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
  33. package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
  34. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
  35. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
  36. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
  37. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
  38. package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
  39. package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
  40. package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
  41. package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
  42. package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
  43. package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
  44. package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
  45. package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
  46. package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
  47. package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
  48. package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
  49. package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
  50. package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
  51. package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
  52. package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
  53. package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
  54. package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
  55. package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
  56. package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
  57. package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
  58. package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
  59. package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
  60. package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
  61. package/package.json +9 -3
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
3
+ Pod::Spec.new do |s|
4
+ s.name = 'BricksSoftCapacitorLocationTracking'
5
+ s.version = package['version']
6
+ s.summary = package['description']
7
+ s.license = package['license']
8
+ s.homepage = package['homepage']
9
+ s.author = package['author']
10
+ s.source = { :git => 'https://github.com/bricks-soft/location-tracking.git', :tag => s.version.to_s }
11
+ s.source_files = 'ios/Sources/LocationTrackingPlugin/**/*.swift'
12
+ s.resource_bundles = { 'LocationTrackingPrivacy' => ['ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy'] }
13
+ s.ios.deployment_target = '15.0'
14
+ s.module_name = 'LocationTrackingPlugin'
15
+ s.swift_version = '5.9'
16
+ s.dependency 'Capacitor', '>= 8.0.0', '< 9.0'
17
+ s.libraries = 'sqlite3'
18
+ end
package/Package.swift ADDED
@@ -0,0 +1,13 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "BricksSoftCapacitorLocationTracking",
6
+ platforms: [.iOS(.v15)],
7
+ products: [.library(name: "BricksSoftCapacitorLocationTracking", targets: ["LocationTrackingPlugin"])],
8
+ dependencies: [.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")],
9
+ targets: [
10
+ .target(name: "LocationTrackingPlugin", dependencies: [.product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm")], path: "ios/Sources/LocationTrackingPlugin", resources: [.copy("PrivacyInfo.xcprivacy")], linkerSettings: [.linkedLibrary("sqlite3")]),
11
+ .testTarget(name: "LocationTrackingPluginTests", dependencies: ["LocationTrackingPlugin"], path: "ios/Tests/LocationTrackingPluginTests")
12
+ ]
13
+ )
package/README.md CHANGED
@@ -74,7 +74,7 @@ whether the user really kept tracking on.
74
74
  | Platform | Status |
75
75
  |---|---|
76
76
  | Android (API 24+, Kotlin) | ✔ Supported |
77
- | iOS | Planned for a later phase. Until then every call rejects with "not implemented on ios". |
77
+ | iOS 15+ | Native route tracking, heartbeat, restore, positions and HTTP sync. Geofencing on iOS 17+, up to 20 geofences. |
78
78
  | Web | Stub for development only: positions through `navigator.geolocation`, an in-memory config, permissions and device info. Nothing is stored or uploaded, and tracking can't be started. Everything else rejects with `UNIMPLEMENTED`. See [Web stub](#web-stub). |
79
79
 
80
80
  ## Installation
@@ -914,7 +914,7 @@ How to measure the cost on a real phone: procedure M-04 in the
914
914
 
915
915
  ## Geofencing
916
916
 
917
- - Up to **100** geofences (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
917
+ - Up to **100** geofences on Android and **20** on iOS (`TOO_MANY_GEOFENCES` beyond that). An identifier is 1–100 characters; adding a geofence
918
918
  with an existing identifier replaces it. `loiteringDelay` must be ≥ 0. The identifier `__lt_stationary__` is
919
919
  reserved for the plugin's stationary region: `addGeofence` rejects it with `INVALID_ARGUMENT`.
920
920
  - The stationary region (see [Battery](#battery)) does not count against the 100. GMS accepts at most 100 geofences
@@ -1150,7 +1150,7 @@ Every rejected promise has a `code` (see the `ErrorCode` type) and a `message`.
1150
1150
  | `NO_URL` | `sync()` was called without a valid `http.url`. |
1151
1151
  | `HTTP_ERROR` | `sync()` got a non-2xx response. |
1152
1152
  | `NETWORK_ERROR` | `sync()` failed with a network or I/O error. |
1153
- | `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 (or the OS geofence limit reported by GMS/HMS). |
1153
+ | `TOO_MANY_GEOFENCES` | Adding the geofences would exceed 100 on Android, 20 on iOS (or the OS geofence limit reported by GMS/HMS). |
1154
1154
  | `NO_ACTIVITY` | `emailLog()` was called while no Activity is attached. (The `open*Settings` methods work without one.) |
1155
1155
  | `IO_ERROR` | Storage or file error (database, log files). |
1156
1156
  | `UNIMPLEMENTED` | Not available on this platform (web stub, iOS). |
@@ -1359,10 +1359,13 @@ Node ignores it.)
1359
1359
  CI runs the emulator suites through `.github/scripts/run-e2e.sh` in `.github/workflows/e2e-android.yml`, only when
1360
1360
  started by hand (Actions tab or `gh workflow run`; pushes and pull requests start no run, to save Actions minutes):
1361
1361
  the plugin suite on API 34 (with smaller runs on API 29 and 35), P-P08 on an image without Google Play services, the
1362
- field-force suite on API 34, and the long scenarios (with the `include-long` input). The build workflow
1363
- `.github/workflows/ci.yml`, also started by hand only, type-checks the kit and runs its unit tests,
1364
- type-checks and dry-runs both suites, runs the field-force Node tests, and checks that the Google Play build of the
1365
- field-force app supports 16 KB memory pages (`.github/scripts/check-16kb.py`).
1362
+ field-force suite on API 34, and the long scenarios (with the `include-long` input).
1363
+ `.github/workflows/typescript.yml` runs on every push: it type-checks the kit and runs its unit tests, type-checks and
1364
+ dry-runs both suites, and runs the field-force Node tests. `.github/workflows/android.yml`, also started by hand only,
1365
+ runs the Android unit tests, builds the example apps and checks that the Google Play build of the field-force app
1366
+ supports 16 KB memory pages (`.github/scripts/check-16kb.py`). `.github/workflows/ios.yml`, started by hand only, runs
1367
+ XCTest, the UI tests and the shared scenarios P-S01 to P-S07 on iOS simulators; on a Mac the same steps run with
1368
+ `.github/scripts/ios/check.sh all` ([docs/e2e-runbook.md](docs/e2e-runbook.md), 6.11).
1366
1369
 
1367
1370
  The field-force suite, together with the field-force app and the kit, is written so that it can move into the Bricks
1368
1371
  app as its integration test. The contract behind the tests (scenario ids, debug commands, mock back office) is
@@ -1396,13 +1399,15 @@ Each later release, with `scripts/release.sh` (bash; run it from the repository
1396
1399
  1. While you work, write the changes under `## [Unreleased]` in `CHANGELOG.md`.
1397
1400
  2. `scripts/release.sh check <version>` runs every check and changes nothing: `master` is clean and equal to
1398
1401
  `origin/master`; the version is new and greater than the one on npm, and its major version is Capacitor's; the
1399
- versions in `package.json`, `package-lock.json` and `src/web/device.ts` agree; `[Unreleased]` is not empty; git
1402
+ versions in `package.json`, `package-lock.json`, `src/web/device.ts` and
1403
+ `ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift` agree; `[Unreleased]` is not empty; git
1400
1404
  can sign; `npm run docgen` leaves the README unchanged; the changes since the last tag contain none of the
1401
- private-name patterns (a file outside the repository, see the script); the steps of `ci.yml` pass on this
1402
- computer; and `npm pack` contains only the files listed in `package.json`.
1405
+ private-name patterns (a file outside the repository, see the script); the steps of `typescript.yml` and
1406
+ `android.yml` pass on this computer, and on macOS those of `ios.yml` (elsewhere it warns that the iOS checks were
1407
+ skipped); and `npm pack` contains only the files listed in `package.json`.
1403
1408
  3. `scripts/release.sh run <version>` runs the checks, renames `[Unreleased]` to `[<version>] - <date>`, runs
1404
- `npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json` and `PLUGIN_VERSION` in
1405
- `src/web/device.ts`; `android/build.gradle` reads `package.json` itself), and makes a signed commit and a signed tag
1409
+ `npm version <version> --no-git-tag-version` (it sets `package.json`, `package-lock.json`, `PLUGIN_VERSION` in
1410
+ `src/web/device.ts` and `PluginInfo.version` in Swift; `android/build.gradle` reads `package.json` itself), and makes a signed commit and a signed tag
1406
1411
  `v<version>`. After you type `yes`, it pushes both and creates the GitHub release with the CHANGELOG section as
1407
1412
  notes. The release starts the workflow (it can also be started by hand from the Actions tab), which builds, runs
1408
1413
  `npm test` and runs `npm stage publish`; the script waits for it.
@@ -2489,12 +2494,13 @@ Posted when Android refuses to restore tracking from the background (after a reb
2489
2494
  restart), typically on Android 14+ without "Allow all the time". A tap resumes the session
2490
2495
  (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
2491
2496
 
2492
- | Prop | Type | Default |
2493
- | ----------------- | -------------------- | ---------------------------------------------------------- |
2494
- | **`enabled`** | <code>boolean</code> | <code>false</code> |
2495
- | **`title`** | <code>string</code> | <code>app label</code> |
2496
- | **`text`** | <code>string</code> | <code>'Location tracking is paused. Tap to resume.'</code> |
2497
- | **`channelName`** | <code>string</code> | <code>'Paused location tracking'</code> |
2497
+ | Prop | Type | Description | Default |
2498
+ | ----------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
2499
+ | **`delay`** | <code>number</code> | iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification; raised to `heartbeat.maxInterval + 60` when lower. | <code>600</code> |
2500
+ | **`enabled`** | <code>boolean</code> | | <code>false</code> |
2501
+ | **`title`** | <code>string</code> | | <code>app label</code> |
2502
+ | **`text`** | <code>string</code> | | <code>'Location tracking is paused. Tap to resume.'</code> |
2503
+ | **`channelName`** | <code>string</code> | | <code>'Paused location tracking'</code> |
2498
2504
 
2499
2505
 
2500
2506
  #### GeofenceConfig
@@ -2589,14 +2595,14 @@ restart), typically on Android 14+ without "Allow all the time". A tap resumes t
2589
2595
 
2590
2596
  Scheduling metadata of a `heartbeat` record (snake_case = wire format).
2591
2597
 
2592
- | Prop | Type | Description |
2593
- | -------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
2594
- | **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced'</code> | |
2595
- | **`min_interval`** | <code>number</code> | heartbeat.minInterval (s) when the heartbeat was created |
2596
- | **`max_interval`** | <code>number</code> | heartbeat.maxInterval (s) when the heartbeat was created |
2597
- | **`next_at`** | <code>string \| null</code> | when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown |
2598
- | **`battery_exempt`** | <code>boolean</code> | the app is exempt from battery optimization |
2599
- | **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
2598
+ | Prop | Type | Description |
2599
+ | -------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
2600
+ | **`strategy`** | <code>'exact' \| 'listener_with_backup' \| 'idle_paced' \| 'timer'</code> | |
2601
+ | **`min_interval`** | <code>number</code> | heartbeat.minInterval (s) when the heartbeat was created |
2602
+ | **`max_interval`** | <code>number</code> | heartbeat.maxInterval (s) when the heartbeat was created |
2603
+ | **`next_at`** | <code>string \| null</code> | when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown |
2604
+ | **`battery_exempt`** | <code>boolean</code> | the app is exempt from battery optimization |
2605
+ | **`device_idle`** | <code>boolean</code> | the device was in deep Doze |
2600
2606
 
2601
2607
 
2602
2608
  #### DeviceReadings
@@ -2712,7 +2718,7 @@ the computed enclosing circle.
2712
2718
 
2713
2719
  | Prop | Type | Description |
2714
2720
  | --------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
2715
- | **`platform`** | <code>'android' \| 'web'</code> | |
2721
+ | **`platform`** | <code>'android' \| 'web' \| 'ios'</code> | |
2716
2722
  | **`manufacturer`** | <code>string</code> | |
2717
2723
  | **`model`** | <code>string</code> | |
2718
2724
  | **`brand`** | <code>string</code> | |
@@ -2897,7 +2903,7 @@ A record could not be stored because the device storage is full.
2897
2903
 
2898
2904
  #### LocationBackend
2899
2905
 
2900
- <code>'gms' | 'hms' | 'android' | 'web'</code>
2906
+ <code>'gms' | 'hms' | 'android' | 'web' | 'ios'</code>
2901
2907
 
2902
2908
 
2903
2909
  #### DesiredAccuracy
@@ -2985,7 +2991,7 @@ Make all properties in T optional
2985
2991
 
2986
2992
  #### HeartbeatStrategy
2987
2993
 
2988
- <code>'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled'</code>
2994
+ <code>'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled'</code>
2989
2995
 
2990
2996
 
2991
2997
  #### PermissionState
@@ -3009,3 +3015,75 @@ Make all properties in T optional
3009
3015
  ## License
3010
3016
 
3011
3017
  MIT, © 2026 Bricks Soft. See [LICENSE](LICENSE).
3018
+
3019
+ ## iOS setup
3020
+
3021
+ The iOS implementation supports native route tracking, motion detection, heartbeat audit records, restore, check-in
3022
+ notifications, configuration, SQLite queueing, HTTP sync, permissions, current positions and watches. Geofencing needs
3023
+ iOS 17; below it, `startGeofences` and the geofence methods reject with `UNAVAILABLE`.
3024
+
3025
+ The deployment target is iOS 15. Install `@capacitor/ios` 8 and run `npx cap sync ios`. Both SPM and CocoaPods are supported.
3026
+ In the host app's Info.plist, add:
3027
+
3028
+ ```xml
3029
+ <key>UIBackgroundModes</key><array><string>location</string><string>fetch</string></array>
3030
+ <key>BGTaskSchedulerPermittedIdentifiers</key><array><string>com.brickssoft.locationtracking.refresh</string></array>
3031
+ <key>NSLocationWhenInUseUsageDescription</key><string>Explain your foreground tracking purpose.</string>
3032
+ <key>NSLocationAlwaysAndWhenInUseUsageDescription</key><string>Explain your optional background tracking purpose.</string>
3033
+ <key>NSMotionUsageDescription</key><string>Explain how motion selects the tracking profile.</string>
3034
+ ```
3035
+
3036
+ Call the native bootstrap before `application(_:didFinishLaunchingWithOptions:)` returns, so the background refresh
3037
+ handler is registered during launch:
3038
+
3039
+ ```swift
3040
+ import LocationTrackingPlugin
3041
+
3042
+ LocationTrackingNative.bootstrap()
3043
+ ```
3044
+
3045
+ For apps that track only while they are in use, request `{ permissions: ['location'] }`. Always is optional, and the
3046
+ Always purpose string is still required because the library references that API. When the user grants approximate
3047
+ location, `ProviderState.accuracy` is `'approximate'` and a `providerchange` event follows; stored geofences stay inactive
3048
+ and a `PERMISSION_DENIED` `locationerror` says why. Explain it in the app and call `openAppSettings()` so the user can
3049
+ turn on Precise Location, which stays on.
3050
+
3051
+ Queued records and configuration use Application Support with backup exclusion and protection until first unlock.
3052
+ Authorization is held in Keychain with the same unlock requirement. HTTP uses an ephemeral session, refuses redirects,
3053
+ and treats every 2xx as success; keep your upload endpoint on HTTPS. A refresh task is best effort and does not keep
3054
+ the app continuously running. `stop()` leaves position watches active; clear each watch explicitly.
3055
+
3056
+ While moving, the route uses the configured accuracy. While stationary, it keeps continuous 100 m updates so that
3057
+ movement can resume tracking without Motion permission. Core Motion confidence is 33, 66 or 100; the default confidence
3058
+ threshold accepts only high-confidence samples. Location update intervals are controlled by iOS.
3059
+
3060
+ Heartbeats use an in-process timer and are not backfilled after a suspension or kill. When In Use tracking must start
3061
+ in the foreground; enabled intent restores when the app next becomes active. Always authorization adds significant-change
3062
+ monitoring as a relaunch trigger. iOS does not relaunch a force-quit app for location events. If Location Services are
3063
+ switched off for the whole device, enabled tracking keeps its intent and original deadline and resumes when they are
3064
+ switched back on; a revoked app permission still stops it with `permission_denied`.
3065
+
3066
+ Enable `notification.resume` and request `notifications` permission for a pending check-in after tracking goes quiet.
3067
+ Its iOS `delay` defaults to 600 seconds and is raised to `heartbeat.maxInterval + 60` when lower. Fixes and records move
3068
+ one pending notification, at most once a minute. Stop and session expiry cancel it. A tap opens the app and restores the
3069
+ original session deadline. The plugin leaves Capacitor's notification-center delegate in place.
3070
+
3071
+ Geofences use one [`CLMonitor`](https://developer.apple.com/documentation/corelocation/clmonitor), opened again under the
3072
+ same name at every launch, and are registered while tracking runs. A polygon registers its enclosing circle and is
3073
+ hit-tested against fixes while the device is inside that circle. DWELL is synthesized from the time inside, so it fires
3074
+ only while the app runs. With Always authorization on iOS 18 and later, the plugin holds a
3075
+ [`CLServiceSession`](https://developer.apple.com/documentation/corelocation/clservicesession) so events also arrive when
3076
+ the app is not in use; with When In Use they arrive only while it runs. While location accuracy is approximate, stored
3077
+ geofences stay inactive and a `locationerror` with `PERMISSION_DENIED` says so.
3078
+
3079
+ `stopOnTerminate` handles the best-effort termination callback. An OS kill without that callback preserves enabled intent,
3080
+ even when `stopOnTerminate` is true. `startOnBoot` is ignored. Deadlines use the continuous elapsed clock within the process.
3081
+ On restore, matching saved wall/elapsed checkpoints preserve elapsed timing; a detected reboot or clock discontinuity uses
3082
+ the original wall-clock start. A wall-clock change while the process is absent cannot be distinguished reliably from a
3083
+ reboot on current iOS, which [no longer exposes a boot-session identifier](https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-18-release-notes).
3084
+ Fixes are ordered by the elapsed clock at delivery, so a wall-clock correction does not make new fixes look stale.
3085
+
3086
+ `example/ios` is the example host. Run `npm run copy-vendor` and `npx cap sync ios` from `example/` before building.
3087
+ The iOS workflow installs the npm tarball into a fresh app through each package manager and archives both unsigned and ad-hoc signed Release
3088
+ IPAs. The ad-hoc identity is `-`, with no provisioning profile; it does not provide Apple device authorization. Installing it on an iPhone still requires an Apple signing certificate/private key, a matching provisioning profile
3089
+ and entitlements for its bundle identifier, or a device-cloud service that re-signs uploaded apps.
@@ -64,7 +64,9 @@ android {
64
64
  all {
65
65
  maxHeapSize = '1024m'
66
66
  maxParallelForks = 1
67
- jvmArgs '-XX:+UseSerialGC', '-XX:+EnableDynamicAgentLoading'
67
+ // IPv4 only: where `localhost` is also ::1 (GitHub's Ubuntu runners), OkHttp tries ::1 after a failed try
68
+ // to MockWebServer (bound to 127.0.0.1), and the client does not fall back to the next address.
69
+ jvmArgs '-XX:+UseSerialGC', '-XX:+EnableDynamicAgentLoading', '-Djava.net.preferIPv4Stack=true'
68
70
  testLogging {
69
71
  events 'failed'
70
72
  exceptionFormat 'full'
@@ -2,7 +2,7 @@ import type { PermissionState, PluginListenerHandle } from '@capacitor/core';
2
2
  export type CallbackID = string;
3
3
  export type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';
4
4
  export type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';
5
- export type LocationBackend = 'gms' | 'hms' | 'android' | 'web';
5
+ export type LocationBackend = 'gms' | 'hms' | 'android' | 'web' | 'ios';
6
6
  export type TrackingMode = 'location' | 'geofences';
7
7
  export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';
8
8
  export type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';
@@ -12,7 +12,7 @@ export type HttpMethod = 'POST' | 'PUT' | 'PATCH';
12
12
  export type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';
13
13
  export type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';
14
14
  export type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';
15
- export type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled';
15
+ export type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled';
16
16
  /** Rejection `code` of every failed promise. */
17
17
  export type ErrorCode = 'NOT_READY' | 'PERMISSION_DENIED' | 'LOCATION_DISABLED' | 'TIMEOUT' | 'UNAVAILABLE' | 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'NO_URL' | 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TOO_MANY_GEOFENCES' | 'NO_ACTIVITY' | 'IO_ERROR' | 'UNIMPLEMENTED' | 'INTERNAL';
18
18
  export interface LocationFilterConfig {
@@ -253,6 +253,12 @@ export interface NotificationConfig {
253
253
  * (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.
254
254
  */
255
255
  export interface ResumeNotificationConfig {
256
+ /**
257
+ * iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification;
258
+ * raised to `heartbeat.maxInterval + 60` when lower.
259
+ * @default 600
260
+ */
261
+ delay?: number;
256
262
  /** @default false */
257
263
  enabled?: boolean;
258
264
  /** @default app label */
@@ -433,7 +439,7 @@ export interface DeviceReadings {
433
439
  }
434
440
  /** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */
435
441
  export interface HeartbeatMeta {
436
- strategy: 'exact' | 'listener_with_backup' | 'idle_paced';
442
+ strategy: 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer';
437
443
  /** heartbeat.minInterval (s) when the heartbeat was created */
438
444
  min_interval: number;
439
445
  /** heartbeat.maxInterval (s) when the heartbeat was created */
@@ -621,7 +627,7 @@ export interface PowerManagerInfo {
621
627
  available: boolean;
622
628
  }
623
629
  export interface DeviceInfo {
624
- platform: 'android' | 'web';
630
+ platform: 'android' | 'web' | 'ios';
625
631
  manufacturer: string;
626
632
  model: string;
627
633
  brand: string;
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CallbackID = string;\nexport type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';\nexport type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';\nexport type LocationBackend = 'gms' | 'hms' | 'android' | 'web';\nexport type TrackingMode = 'location' | 'geofences';\nexport type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';\nexport type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';\nexport type RecordEvent =\n | 'location'\n | 'motionchange'\n | 'current_position'\n | 'watch_position'\n | 'heartbeat'\n | 'geofence'\n | 'tracking_start'\n | 'tracking_stop'\n | 'providerchange';\nexport type GeofenceAction = 'ENTER' | 'EXIT' | 'DWELL';\nexport type HttpMethod = 'POST' | 'PUT' | 'PATCH';\nexport type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';\nexport type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';\nexport type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';\nexport type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'disabled';\n/** Rejection `code` of every failed promise. */\nexport type ErrorCode =\n | 'NOT_READY'\n | 'PERMISSION_DENIED'\n | 'LOCATION_DISABLED'\n | 'TIMEOUT'\n | 'UNAVAILABLE'\n | 'INVALID_ARGUMENT'\n | 'NOT_FOUND'\n | 'NO_URL'\n | 'HTTP_ERROR'\n | 'NETWORK_ERROR'\n | 'TOO_MANY_GEOFENCES'\n | 'NO_ACTIVITY'\n | 'IO_ERROR'\n | 'UNIMPLEMENTED'\n | 'INTERNAL';\n\n/* ================= CONFIG (all optional; defaults shown) ================= */\n\nexport interface LocationFilterConfig {\n /** @default false */\n useKalman?: boolean;\n /**\n * Reject fixes with accuracy worse than this (m).\n * @default 100\n */\n trackingAccuracyThreshold?: number;\n /**\n * Reject fixes implying speed above this (m/s); 0 = off.\n * @default 80\n */\n maxImpliedSpeed?: number;\n /**\n * Odometer ignores fixes worse than this (m).\n * @default 20\n */\n odometerAccuracyThreshold?: number;\n /** @default false */\n allowIdenticalLocations?: boolean;\n /**\n * Drop mock fixes entirely (otherwise they are kept with mock:true).\n * @default false\n */\n rejectMockLocations?: boolean;\n}\n\nexport interface GeolocationConfig {\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /**\n * meters\n * @default 10\n */\n distanceFilter?: number;\n /**\n * ms\n * @default 1000\n */\n locationUpdateInterval?: number;\n /**\n * ms\n * @default 500\n */\n fastestLocationUpdateInterval?: number;\n /** @default false */\n disableElasticity?: boolean;\n /** @default 1 */\n elasticityMultiplier?: number;\n /**\n * meters\n * @default 25\n */\n stationaryRadius?: number;\n /**\n * minutes\n * @default 5\n */\n stopTimeout?: number;\n /**\n * minutes, 0 = off\n * @default 0\n */\n stopAfterElapsedMinutes?: number;\n /** @default false */\n stopOnStationary?: boolean;\n /**\n * default getCurrentPosition timeout, ms\n * @default 30000\n */\n locationTimeout?: number;\n filter?: LocationFilterConfig;\n}\n\nexport interface ActivityConfig {\n /** @default false */\n disableMotionActivityUpdates?: boolean;\n /**\n * ms\n * @default 10000\n */\n activityRecognitionInterval?: number;\n /**\n * 0-100\n * @default 75\n */\n minimumActivityRecognitionConfidence?: number;\n /**\n * ms\n * @default 0\n */\n motionTriggerDelay?: number;\n /** @default false */\n disableStopDetection?: boolean;\n}\n\nexport interface HeartbeatConfig {\n /** @default true */\n enabled?: boolean;\n /**\n * seconds, min 60\n * @default 180\n */\n minInterval?: number;\n /**\n * seconds, >= minInterval\n * @default 300\n */\n maxInterval?: number;\n /**\n * seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this\n * long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake.\n * @default 10\n */\n uploadKeepAwake?: number;\n}\n\nexport interface AuthorizationConfig {\n /** @default 'JWT' */\n strategy?: 'JWT';\n accessToken?: string;\n refreshToken?: string;\n refreshUrl?: string;\n /**\n * String values may contain `{refreshToken}`.\n * @default {}\n */\n refreshPayload?: Record<string, string>;\n refreshHeaders?: Record<string, string>;\n /** @default 'json' */\n refreshPayloadEncoding?: 'json' | 'form';\n /**\n * access-token expiry, epoch ms; -1 = unknown\n * @default -1\n */\n expires?: number;\n}\n\nexport interface HttpConfig {\n /** No url => nothing is uploaded (records stay queued). */\n url?: string | null;\n /** @default 'POST' */\n method?: HttpMethod;\n /** @default {} */\n headers?: Record<string, string>;\n /**\n * merged into the ROOT of every request body\n * @default {}\n */\n params?: Record<string, unknown>;\n /** @default true */\n autoSync?: boolean;\n /**\n * upload when queue >= threshold; 0 = every record\n * @default 0\n */\n autoSyncThreshold?: number;\n /**\n * s; upload normal records once the oldest queued one is this old; 0 = off\n * @default 0\n */\n syncInterval?: number;\n /** @default false */\n batchSync?: boolean;\n /** @default 100 */\n maxBatchSize?: number;\n /** @default false */\n disableAutoSyncOnCellular?: boolean;\n /**\n * '.' = no wrapping\n * @default 'location'\n */\n rootProperty?: string;\n /** JSON text with `<%= name %>` placeholders */\n locationTemplate?: string | null;\n /** used for event 'geofence'; falls back to locationTemplate */\n geofenceTemplate?: string | null;\n /**\n * ms\n * @default 60000\n */\n timeout?: number;\n /**\n * seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads.\n * @default 3600\n */\n maxRetryAfter?: number;\n authorization?: AuthorizationConfig | null;\n}\n\nexport interface PersistenceConfig {\n /** @default 7 */\n maxDaysToPersist?: number;\n /**\n * -1 = unlimited\n * @default -1\n */\n maxRecordsToPersist?: number;\n /**\n * merged into `extras` of every record at creation time\n * @default {}\n */\n extras?: Record<string, unknown>;\n}\n\nexport interface AppConfig {\n /** @default true */\n stopOnTerminate?: boolean;\n /** @default false */\n startOnBoot?: boolean;\n}\n\nexport interface NotificationActionButton {\n id: string;\n label: string;\n}\n\nexport interface NotificationConfig {\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is active' */\n text?: string;\n /**\n * 'drawable/name' | 'mipmap/name'\n * @default plugin icon 'drawable/lt_ic_notification'\n */\n smallIcon?: string;\n largeIcon?: string;\n /** '#RRGGBB' */\n color?: string;\n /** @default 'default' */\n priority?: NotificationPriority;\n /** @default 'location_tracking' */\n channelId?: string;\n /** @default 'Location tracking' */\n channelName?: string;\n /** max 3; tap => 'notificationaction' event */\n actions?: NotificationActionButton[];\n /** notification that resumes tracking when Android refuses to restore it from the background (Android 14+) */\n resume?: ResumeNotificationConfig;\n}\n\n/**\n * Posted when Android refuses to restore tracking from the background (after a reboot, an app update or a process\n * restart), typically on Android 14+ without \"Allow all the time\". A tap resumes the session\n * (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.\n */\nexport interface ResumeNotificationConfig {\n /** @default false */\n enabled?: boolean;\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is paused. Tap to resume.' */\n text?: string;\n /** @default 'Paused location tracking' */\n channelName?: string;\n}\n\nexport interface GeofenceConfig {\n /** @default true */\n initialTriggerEntry?: boolean;\n}\n\nexport interface LoggerConfig {\n /** @default 'info' */\n logLevel?: LogLevel;\n /** @default 3 */\n logMaxDays?: number;\n}\n\nexport interface BackgroundPermissionRationale {\n /** defaults from plugin string resources */\n title?: string;\n message?: string;\n positiveAction?: string;\n negativeAction?: string;\n}\n\nexport interface Config {\n geolocation?: GeolocationConfig;\n activity?: ActivityConfig;\n heartbeat?: HeartbeatConfig;\n http?: HttpConfig;\n persistence?: PersistenceConfig;\n app?: AppConfig;\n notification?: NotificationConfig;\n geofence?: GeofenceConfig;\n logger?: LoggerConfig;\n backgroundPermissionRationale?: BackgroundPermissionRationale;\n /**\n * Android only\n * @default 'auto'\n */\n locationProvider?: LocationProviderSetting;\n}\n\nexport interface ReadyOptions {\n config?: Config;\n /**\n * true: config = defaults + given config (every launch). false: given config applied only on the very first\n * ready(); afterwards persisted config wins.\n * @default true\n */\n reset?: boolean;\n}\n\nexport interface State {\n enabled: boolean;\n trackingMode: TrackingMode;\n isMoving: boolean;\n odometer: number;\n backend: LocationBackend;\n lastRecordAt: string | null;\n /** reason of the last `tracking_stop` record (the `tracking_stop` reasons of `Location.reason`); null if none */\n lastStopReason: string | null;\n /** when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none */\n lastStopAt: string | null;\n /**\n * fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the\n * values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values\n * longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value\n * back to `setConfig` keeps the stored one. The web stub returns them as given.\n */\n config: Config;\n /**\n * `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably\n * wrong (for example an `http://` upload URL).\n */\n warnings?: string[];\n}\n\n/* ================= RECORDS (snake_case = identical to wire format) ================= */\n\nexport interface Coords {\n latitude: number;\n longitude: number;\n accuracy: number;\n altitude: number | null;\n altitude_accuracy: number | null;\n speed: number | null;\n speed_accuracy: number | null;\n heading: number | null;\n heading_accuracy: number | null;\n}\n\nexport interface ProviderState {\n enabled: boolean;\n gps: boolean;\n network: boolean;\n permission: 'always' | 'when_in_use' | 'denied';\n accuracy: 'precise' | 'approximate' | 'none';\n backend: LocationBackend;\n}\n\nexport interface Location {\n uuid: string;\n event: RecordEvent;\n /** fix time (ISO-8601 UTC ms); null if no location ever known */\n timestamp: string | null;\n /** record creation time */\n recorded_at: string;\n /** only present in HTTP bodies */\n sent_at?: string;\n elapsed_realtime_ms: number;\n /** -1 if unavailable */\n boot_count: number;\n is_moving: boolean;\n odometer: number;\n mock: boolean;\n coords: Coords | null;\n activity: { type: ActivityType; confidence: number };\n /**\n * level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device\n * reports them.\n */\n battery: {\n level: number;\n is_charging: boolean;\n temperature?: number;\n health?: 'good' | 'overheat' | 'dead' | 'over_voltage' | 'cold' | 'failure' | 'unknown';\n plugged?: 'ac' | 'usb' | 'wireless' | 'dock' | 'none' | 'other';\n };\n backend: LocationBackend | null;\n extras?: Record<string, unknown>;\n /** event 'geofence' only */\n geofence?: { identifier: string; action: GeofenceAction; extras?: Record<string, unknown> };\n /** provider state when the record was created; the new state for event 'providerchange' */\n provider?: ProviderState;\n /**\n * tracking_start: start|start_geofences|boot|restore|package_replaced|resume_notification;\n * tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced|user_stopped\n */\n reason?: string;\n /** event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) */\n stopped_at?: string;\n /** event 'heartbeat' only, optional: how the heartbeat is scheduled */\n heartbeat?: HeartbeatMeta;\n /** connection type when the record was created */\n network?: ConnectivityType;\n /** the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see `DeviceReadings`) */\n device?: DeviceReadings;\n}\n\n/**\n * The `device` object of records; a reading is present only when the device provides it.\n * Every record: `device_id`.\n * Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.\n * `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.\n */\nexport interface DeviceReadings {\n /**\n * the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id.\n * Absent in records queued by an older version. Declare \"Device or other IDs\" in Google Play's Data safety form.\n */\n device_id?: string;\n /** the user set the app's battery use to \"Restricted\" (Android 9+) */\n background_restricted?: boolean;\n /** App Standby bucket (Android 9+) */\n standby_bucket?: 'exempted' | 'active' | 'working_set' | 'frequent' | 'rare' | 'restricted';\n /** free space on the storage that holds the app's files */\n disk_free_bytes?: number;\n /** Android 10+ */\n thermal_status?: 'none' | 'light' | 'moderate' | 'severe' | 'critical' | 'emergency' | 'shutdown' | 'unknown';\n manufacturer?: string;\n model?: string;\n /** Android version, e.g. \"14\" */\n os_version?: string;\n sdk_int?: number;\n low_ram_device?: boolean;\n ram_total_bytes?: number;\n}\n\n/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */\nexport interface HeartbeatMeta {\n strategy: 'exact' | 'listener_with_backup' | 'idle_paced';\n /** heartbeat.minInterval (s) when the heartbeat was created */\n min_interval: number;\n /** heartbeat.maxInterval (s) when the heartbeat was created */\n max_interval: number;\n /** when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown */\n next_at: string | null;\n /** the app is exempt from battery optimization */\n battery_exempt: boolean;\n /** the device was in deep Doze */\n device_idle: boolean;\n}\n\nexport type LocationRecord = Location;\n\nexport interface InsertLocationInput {\n /** `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. */\n coords: Pick<Coords, 'latitude' | 'longitude' | 'accuracy'> & Partial<Coords>;\n timestamp?: string;\n event?: RecordEvent;\n is_moving?: boolean;\n extras?: Record<string, unknown>;\n}\n\n/* ================= GEOFENCES ================= */\n\n/**\n * Either (latitude, longitude, radius) = circle, or vertices = polygon. For polygons, getGeofences() also returns\n * the computed enclosing circle.\n */\nexport interface Geofence {\n identifier: string;\n latitude?: number;\n longitude?: number;\n /** m */\n radius?: number;\n /** [[lat,lng], ...] >= 3 points */\n vertices?: [number, number][];\n /** @default true */\n notifyOnEntry?: boolean;\n /** @default true */\n notifyOnExit?: boolean;\n /** @default false */\n notifyOnDwell?: boolean;\n /**\n * ms\n * @default 30000\n */\n loiteringDelay?: number;\n extras?: Record<string, unknown>;\n}\n\n/* ================= EVENTS ================= */\n\nexport interface MotionChangeEvent {\n isMoving: boolean;\n location: Location;\n}\nexport interface ActivityChangeEvent {\n activity: ActivityType;\n confidence: number;\n}\nexport interface HeartbeatEvent {\n location: Location;\n}\nexport interface GeofenceEvent {\n identifier: string;\n action: GeofenceAction;\n location: Location;\n extras?: Record<string, unknown>;\n}\nexport interface GeofencesChangeEvent {\n on: Geofence[];\n off: string[];\n}\nexport interface HttpEvent {\n success: boolean;\n status: number;\n responseText: string;\n uuids: string[];\n}\nexport interface ConnectivityChangeEvent {\n connected: boolean;\n type: ConnectivityType;\n}\nexport interface PowerSaveChangeEvent {\n isPowerSaveMode: boolean;\n}\nexport interface EnabledChangeEvent {\n enabled: boolean;\n /**\n * why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a\n * `tracking_stop` reason when it was switched off; the same values as `Location.reason`\n */\n reason: string | null;\n}\n/** The location request failed (no permission, Play services or HMS Core refused it, no usable provider). */\nexport interface LocationErrorEvent {\n code: ErrorCode;\n message: string;\n backend: LocationBackend;\n /** the plugin requests location updates again after this many ms; null if it does not */\n retryInMs: number | null;\n}\n/** A record could not be stored because the device storage is full. */\nexport interface StorageFullEvent {\n uuid: string;\n event: RecordEvent;\n diskFreeBytes: number | null;\n}\nexport interface NotificationActionEvent {\n id: string;\n}\nexport interface AuthorizationEvent {\n success: boolean;\n status: number;\n error?: string;\n response?: Record<string, unknown>;\n}\nexport interface LocationTrackingEventMap {\n location: Location;\n motionchange: MotionChangeEvent;\n activitychange: ActivityChangeEvent;\n providerchange: ProviderState;\n heartbeat: HeartbeatEvent;\n geofence: GeofenceEvent;\n geofenceschange: GeofencesChangeEvent;\n http: HttpEvent;\n connectivitychange: ConnectivityChangeEvent;\n powersavechange: PowerSaveChangeEvent;\n enabledchange: EnabledChangeEvent;\n notificationaction: NotificationActionEvent;\n authorization: AuthorizationEvent;\n locationerror: LocationErrorEvent;\n storagefull: StorageFullEvent;\n}\nexport type LocationTrackingEventName = keyof LocationTrackingEventMap;\n\n/* ================= MISC ================= */\n\nexport interface CurrentPositionOptions {\n /** @default 3 */\n samples?: number;\n /**\n * ms\n * @default geolocation.locationTimeout\n */\n timeout?: number;\n /**\n * ms\n * @default 0\n */\n maximumAge?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default true */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport interface WatchPositionOptions {\n /**\n * ms\n * @default 1000\n */\n interval?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default false */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport type WatchPositionCallback = (location: Location | null, error?: { code: ErrorCode; message: string }) => void;\n\nexport interface HeartbeatStatus {\n enabled: boolean;\n minInterval: number;\n maxInterval: number;\n lastRecordAt: string | null;\n lastHeartbeatAt: string | null;\n nextHeartbeatAt: string | null;\n strategy: HeartbeatStrategy;\n canScheduleExactAlarms: boolean;\n isIgnoringBatteryOptimizations: boolean;\n isDeviceIdleMode: boolean;\n isPowerSaveMode: boolean;\n pendingHeartbeats: number;\n}\n\nexport interface BatteryOptimizationStatus {\n isIgnoringBatteryOptimizations: boolean;\n canScheduleExactAlarms: boolean;\n isDeviceIdleMode: boolean;\n}\n\nexport interface PowerManagerInfo {\n manufacturer: string;\n available: boolean;\n}\n\nexport interface DeviceInfo {\n platform: 'android' | 'web';\n manufacturer: string;\n model: string;\n brand: string;\n osVersion: string;\n sdkInt: number;\n pluginVersion: string;\n gmsAvailable: boolean;\n hmsAvailable: boolean;\n backend: LocationBackend;\n packagedProviders: string[];\n /** why a better location backend is not used (for example \"gms: unavailable (status 2)\"); null when none is better */\n backendFallbackReason: string | null;\n /** raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet */\n gmsStatus: number | null;\n /** raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet */\n hmsStatus: number | null;\n /** true when HMS Core is older than 6.3.0.301; null if HMS is not packaged */\n hmsCoreOutdated: boolean | null;\n /** last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none */\n locationAvailableHint: boolean | null;\n /** number of CPU cores */\n cpuCores: number;\n /** CPU types (Build.SUPPORTED_ABIS), e.g. [\"arm64-v8a\"] */\n supportedAbis: string[];\n}\n\nexport interface Sensors {\n accelerometer: boolean;\n gyroscope: boolean;\n magnetometer: boolean;\n significantMotion: boolean;\n stepCounter: boolean;\n stepDetector: boolean;\n barometer: boolean;\n}\n\nexport interface PermissionStatus {\n location: PermissionState;\n backgroundLocation: PermissionState;\n activityRecognition: PermissionState;\n notifications: PermissionState;\n}\n\nexport interface LogQuery {\n start?: number;\n end?: number;\n level?: LogLevel;\n limit?: number;\n order?: 'asc' | 'desc';\n}\n\nexport interface LocationTrackingPlugin {\n /** Loads persisted config and state. Must resolve before most other methods (see NOT_READY rule). */\n ready(options?: ReadyOptions): Promise<State>;\n /** deep merge; arrays replaced; null resets key to default */\n setConfig(options: { config: Config }): Promise<State>;\n /** Resets the config to defaults, then applies the given config. */\n reset(options?: { config?: Config }): Promise<State>;\n getState(): Promise<State>;\n /** Starts location tracking (mode 'location'). */\n start(): Promise<State>;\n /** Starts geofence-only tracking (mode 'geofences'). */\n startGeofences(): Promise<State>;\n stop(): Promise<State>;\n /** Forces the motion state to moving or stationary. */\n changePace(options: { isMoving: boolean }): Promise<void>;\n\n getCurrentPosition(options?: CurrentPositionOptions): Promise<Location>;\n watchPosition(options: WatchPositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n clearWatch(options: { id: CallbackID }): Promise<void>;\n\n getOdometer(): Promise<{ odometer: number }>;\n setOdometer(options: { odometer: number }): Promise<{ odometer: number }>;\n resetOdometer(): Promise<{ odometer: number }>;\n\n getLocations(options?: { limit?: number }): Promise<{ locations: Location[] }>;\n getCount(): Promise<{ count: number }>;\n insertLocation(options: { location: InsertLocationInput }): Promise<{ uuid: string }>;\n destroyLocations(): Promise<{ count: number }>;\n destroyLocation(options: { uuid: string }): Promise<{ deleted: boolean }>;\n /** uploads whole queue now; resolves with uploaded records */\n sync(): Promise<{ locations: Location[] }>;\n\n addGeofence(options: { geofence: Geofence }): Promise<void>;\n addGeofences(options: { geofences: Geofence[] }): Promise<void>;\n removeGeofence(options: { identifier: string }): Promise<void>;\n /** identifiers omitted (or null) = remove all; an empty array removes nothing */\n removeGeofences(options?: { identifiers?: string[] }): Promise<void>;\n getGeofences(): Promise<{ geofences: Geofence[] }>;\n getGeofence(options: { identifier: string }): Promise<{ geofence: Geofence | null }>;\n geofenceExists(options: { identifier: string }): Promise<{ exists: boolean }>;\n\n getHeartbeatStatus(): Promise<HeartbeatStatus>;\n\n getProviderState(): Promise<ProviderState>;\n isPowerSaveMode(): Promise<{ isPowerSaveMode: boolean }>;\n getBatteryOptimizationStatus(): Promise<BatteryOptimizationStatus>;\n openBatteryOptimizationSettings(): Promise<{ opened: boolean }>;\n getPowerManagerInfo(): Promise<PowerManagerInfo>;\n openPowerManagerSettings(): Promise<{ opened: boolean }>;\n openLocationSettings(): Promise<{ opened: boolean }>;\n openAppSettings(): Promise<{ opened: boolean }>;\n getDeviceInfo(): Promise<DeviceInfo>;\n getSensors(): Promise<Sensors>;\n\n checkPermissions(): Promise<PermissionStatus>;\n /** default all four, order: location, notifications, activityRecognition, backgroundLocation */\n requestPermissions(options?: { permissions?: PermissionType[] }): Promise<PermissionStatus>;\n\n log(options: { level: Exclude<LogLevel, 'off'>; message: string }): Promise<void>;\n getLog(options?: LogQuery): Promise<{ log: string }>;\n destroyLog(): Promise<void>;\n uploadLog(options: {\n url: string;\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n }): Promise<{ success: boolean; status: number }>;\n emailLog(options: { email: string; subject?: string }): Promise<void>;\n\n addListener(eventName: 'location', listenerFunc: (e: Location) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'motionchange', listenerFunc: (e: MotionChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'activitychange',\n listenerFunc: (e: ActivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'providerchange', listenerFunc: (e: ProviderState) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'heartbeat', listenerFunc: (e: HeartbeatEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'geofence', listenerFunc: (e: GeofenceEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'geofenceschange',\n listenerFunc: (e: GeofencesChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'http', listenerFunc: (e: HttpEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'connectivitychange',\n listenerFunc: (e: ConnectivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'powersavechange',\n listenerFunc: (e: PowerSaveChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'notificationaction',\n listenerFunc: (e: NotificationActionEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CallbackID = string;\nexport type DesiredAccuracy = 'high' | 'balanced' | 'low' | 'passive';\nexport type LocationProviderSetting = 'auto' | 'gms' | 'hms' | 'android';\nexport type LocationBackend = 'gms' | 'hms' | 'android' | 'web' | 'ios';\nexport type TrackingMode = 'location' | 'geofences';\nexport type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'verbose';\nexport type ActivityType = 'still' | 'on_foot' | 'walking' | 'running' | 'on_bicycle' | 'in_vehicle' | 'unknown';\nexport type RecordEvent =\n | 'location'\n | 'motionchange'\n | 'current_position'\n | 'watch_position'\n | 'heartbeat'\n | 'geofence'\n | 'tracking_start'\n | 'tracking_stop'\n | 'providerchange';\nexport type GeofenceAction = 'ENTER' | 'EXIT' | 'DWELL';\nexport type HttpMethod = 'POST' | 'PUT' | 'PATCH';\nexport type NotificationPriority = 'min' | 'low' | 'default' | 'high' | 'max';\nexport type PermissionType = 'location' | 'backgroundLocation' | 'activityRecognition' | 'notifications';\nexport type ConnectivityType = 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none';\nexport type HeartbeatStrategy = 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer' | 'disabled';\n/** Rejection `code` of every failed promise. */\nexport type ErrorCode =\n | 'NOT_READY'\n | 'PERMISSION_DENIED'\n | 'LOCATION_DISABLED'\n | 'TIMEOUT'\n | 'UNAVAILABLE'\n | 'INVALID_ARGUMENT'\n | 'NOT_FOUND'\n | 'NO_URL'\n | 'HTTP_ERROR'\n | 'NETWORK_ERROR'\n | 'TOO_MANY_GEOFENCES'\n | 'NO_ACTIVITY'\n | 'IO_ERROR'\n | 'UNIMPLEMENTED'\n | 'INTERNAL';\n\n/* ================= CONFIG (all optional; defaults shown) ================= */\n\nexport interface LocationFilterConfig {\n /** @default false */\n useKalman?: boolean;\n /**\n * Reject fixes with accuracy worse than this (m).\n * @default 100\n */\n trackingAccuracyThreshold?: number;\n /**\n * Reject fixes implying speed above this (m/s); 0 = off.\n * @default 80\n */\n maxImpliedSpeed?: number;\n /**\n * Odometer ignores fixes worse than this (m).\n * @default 20\n */\n odometerAccuracyThreshold?: number;\n /** @default false */\n allowIdenticalLocations?: boolean;\n /**\n * Drop mock fixes entirely (otherwise they are kept with mock:true).\n * @default false\n */\n rejectMockLocations?: boolean;\n}\n\nexport interface GeolocationConfig {\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /**\n * meters\n * @default 10\n */\n distanceFilter?: number;\n /**\n * ms\n * @default 1000\n */\n locationUpdateInterval?: number;\n /**\n * ms\n * @default 500\n */\n fastestLocationUpdateInterval?: number;\n /** @default false */\n disableElasticity?: boolean;\n /** @default 1 */\n elasticityMultiplier?: number;\n /**\n * meters\n * @default 25\n */\n stationaryRadius?: number;\n /**\n * minutes\n * @default 5\n */\n stopTimeout?: number;\n /**\n * minutes, 0 = off\n * @default 0\n */\n stopAfterElapsedMinutes?: number;\n /** @default false */\n stopOnStationary?: boolean;\n /**\n * default getCurrentPosition timeout, ms\n * @default 30000\n */\n locationTimeout?: number;\n filter?: LocationFilterConfig;\n}\n\nexport interface ActivityConfig {\n /** @default false */\n disableMotionActivityUpdates?: boolean;\n /**\n * ms\n * @default 10000\n */\n activityRecognitionInterval?: number;\n /**\n * 0-100\n * @default 75\n */\n minimumActivityRecognitionConfidence?: number;\n /**\n * ms\n * @default 0\n */\n motionTriggerDelay?: number;\n /** @default false */\n disableStopDetection?: boolean;\n}\n\nexport interface HeartbeatConfig {\n /** @default true */\n enabled?: boolean;\n /**\n * seconds, min 60\n * @default 180\n */\n minInterval?: number;\n /**\n * seconds, >= minInterval\n * @default 300\n */\n maxInterval?: number;\n /**\n * seconds, 0..60. After a heartbeat is queued, keep the phone awake until its upload has finished, or at most this\n * long, so it reaches the server on time while tracking is on. 0 = do not keep the phone awake.\n * @default 10\n */\n uploadKeepAwake?: number;\n}\n\nexport interface AuthorizationConfig {\n /** @default 'JWT' */\n strategy?: 'JWT';\n accessToken?: string;\n refreshToken?: string;\n refreshUrl?: string;\n /**\n * String values may contain `{refreshToken}`.\n * @default {}\n */\n refreshPayload?: Record<string, string>;\n refreshHeaders?: Record<string, string>;\n /** @default 'json' */\n refreshPayloadEncoding?: 'json' | 'form';\n /**\n * access-token expiry, epoch ms; -1 = unknown\n * @default -1\n */\n expires?: number;\n}\n\nexport interface HttpConfig {\n /** No url => nothing is uploaded (records stay queued). */\n url?: string | null;\n /** @default 'POST' */\n method?: HttpMethod;\n /** @default {} */\n headers?: Record<string, string>;\n /**\n * merged into the ROOT of every request body\n * @default {}\n */\n params?: Record<string, unknown>;\n /** @default true */\n autoSync?: boolean;\n /**\n * upload when queue >= threshold; 0 = every record\n * @default 0\n */\n autoSyncThreshold?: number;\n /**\n * s; upload normal records once the oldest queued one is this old; 0 = off\n * @default 0\n */\n syncInterval?: number;\n /** @default false */\n batchSync?: boolean;\n /** @default 100 */\n maxBatchSize?: number;\n /** @default false */\n disableAutoSyncOnCellular?: boolean;\n /**\n * '.' = no wrapping\n * @default 'location'\n */\n rootProperty?: string;\n /** JSON text with `<%= name %>` placeholders */\n locationTemplate?: string | null;\n /** used for event 'geofence'; falls back to locationTemplate */\n geofenceTemplate?: string | null;\n /**\n * ms\n * @default 60000\n */\n timeout?: number;\n /**\n * seconds, >= 0. The longest wait that a `Retry-After` header of a 429 or 503 answer can impose on automatic uploads.\n * @default 3600\n */\n maxRetryAfter?: number;\n authorization?: AuthorizationConfig | null;\n}\n\nexport interface PersistenceConfig {\n /** @default 7 */\n maxDaysToPersist?: number;\n /**\n * -1 = unlimited\n * @default -1\n */\n maxRecordsToPersist?: number;\n /**\n * merged into `extras` of every record at creation time\n * @default {}\n */\n extras?: Record<string, unknown>;\n}\n\nexport interface AppConfig {\n /** @default true */\n stopOnTerminate?: boolean;\n /** @default false */\n startOnBoot?: boolean;\n}\n\nexport interface NotificationActionButton {\n id: string;\n label: string;\n}\n\nexport interface NotificationConfig {\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is active' */\n text?: string;\n /**\n * 'drawable/name' | 'mipmap/name'\n * @default plugin icon 'drawable/lt_ic_notification'\n */\n smallIcon?: string;\n largeIcon?: string;\n /** '#RRGGBB' */\n color?: string;\n /** @default 'default' */\n priority?: NotificationPriority;\n /** @default 'location_tracking' */\n channelId?: string;\n /** @default 'Location tracking' */\n channelName?: string;\n /** max 3; tap => 'notificationaction' event */\n actions?: NotificationActionButton[];\n /** notification that resumes tracking when Android refuses to restore it from the background (Android 14+) */\n resume?: ResumeNotificationConfig;\n}\n\n/**\n * Posted when Android refuses to restore tracking from the background (after a reboot, an app update or a process\n * restart), typically on Android 14+ without \"Allow all the time\". A tap resumes the session\n * (`tracking_start` reason `resume_notification`). Uses the tracking notification's small icon and color.\n */\nexport interface ResumeNotificationConfig {\n /**\n * iOS only (Android warns about an unknown key): seconds from the last activity to the check-in notification;\n * raised to `heartbeat.maxInterval + 60` when lower.\n * @default 600\n */\n delay?: number;\n /** @default false */\n enabled?: boolean;\n /** @default app label */\n title?: string;\n /** @default 'Location tracking is paused. Tap to resume.' */\n text?: string;\n /** @default 'Paused location tracking' */\n channelName?: string;\n}\n\nexport interface GeofenceConfig {\n /** @default true */\n initialTriggerEntry?: boolean;\n}\n\nexport interface LoggerConfig {\n /** @default 'info' */\n logLevel?: LogLevel;\n /** @default 3 */\n logMaxDays?: number;\n}\n\nexport interface BackgroundPermissionRationale {\n /** defaults from plugin string resources */\n title?: string;\n message?: string;\n positiveAction?: string;\n negativeAction?: string;\n}\n\nexport interface Config {\n geolocation?: GeolocationConfig;\n activity?: ActivityConfig;\n heartbeat?: HeartbeatConfig;\n http?: HttpConfig;\n persistence?: PersistenceConfig;\n app?: AppConfig;\n notification?: NotificationConfig;\n geofence?: GeofenceConfig;\n logger?: LoggerConfig;\n backgroundPermissionRationale?: BackgroundPermissionRationale;\n /**\n * Android only\n * @default 'auto'\n */\n locationProvider?: LocationProviderSetting;\n}\n\nexport interface ReadyOptions {\n config?: Config;\n /**\n * true: config = defaults + given config (every launch). false: given config applied only on the very first\n * ready(); afterwards persisted config wins.\n * @default true\n */\n reset?: boolean;\n}\n\nexport interface State {\n enabled: boolean;\n trackingMode: TrackingMode;\n isMoving: boolean;\n odometer: number;\n backend: LocationBackend;\n lastRecordAt: string | null;\n /** reason of the last `tracking_stop` record (the `tracking_stop` reasons of `Location.reason`); null if none */\n lastStopReason: string | null;\n /** when tracking stopped for `lastStopReason` (ISO-8601 UTC ms); null if none */\n lastStopAt: string | null;\n /**\n * fully populated with defaults. On Android the `http.authorization` secrets (`accessToken`, `refreshToken`, the\n * values of `refreshPayload` and `refreshHeaders`) are masked: `***`, or `***` plus the last 4 characters for values\n * longer than 8 characters; a `refreshPayload` value that contains `{refreshToken}` is shown. Passing a masked value\n * back to `setConfig` keeps the stored one. The web stub returns them as given.\n */\n config: Config;\n /**\n * `ready()`, `setConfig()` and `reset()` only: what the given config contains that was accepted but is probably\n * wrong (for example an `http://` upload URL).\n */\n warnings?: string[];\n}\n\n/* ================= RECORDS (snake_case = identical to wire format) ================= */\n\nexport interface Coords {\n latitude: number;\n longitude: number;\n accuracy: number;\n altitude: number | null;\n altitude_accuracy: number | null;\n speed: number | null;\n speed_accuracy: number | null;\n heading: number | null;\n heading_accuracy: number | null;\n}\n\nexport interface ProviderState {\n enabled: boolean;\n gps: boolean;\n network: boolean;\n permission: 'always' | 'when_in_use' | 'denied';\n accuracy: 'precise' | 'approximate' | 'none';\n backend: LocationBackend;\n}\n\nexport interface Location {\n uuid: string;\n event: RecordEvent;\n /** fix time (ISO-8601 UTC ms); null if no location ever known */\n timestamp: string | null;\n /** record creation time */\n recorded_at: string;\n /** only present in HTTP bodies */\n sent_at?: string;\n elapsed_realtime_ms: number;\n /** -1 if unavailable */\n boot_count: number;\n is_moving: boolean;\n odometer: number;\n mock: boolean;\n coords: Coords | null;\n activity: { type: ActivityType; confidence: number };\n /**\n * level 0..1, -1 unknown. Heartbeat records also carry `temperature` (°C), `health` and `plugged` when the device\n * reports them.\n */\n battery: {\n level: number;\n is_charging: boolean;\n temperature?: number;\n health?: 'good' | 'overheat' | 'dead' | 'over_voltage' | 'cold' | 'failure' | 'unknown';\n plugged?: 'ac' | 'usb' | 'wireless' | 'dock' | 'none' | 'other';\n };\n backend: LocationBackend | null;\n extras?: Record<string, unknown>;\n /** event 'geofence' only */\n geofence?: { identifier: string; action: GeofenceAction; extras?: Record<string, unknown> };\n /** provider state when the record was created; the new state for event 'providerchange' */\n provider?: ProviderState;\n /**\n * tracking_start: start|start_geofences|boot|restore|package_replaced|resume_notification;\n * tracking_stop: stop|stop_on_stationary|stop_after_elapsed|terminate|permission_denied|service_start_failed|reboot|package_replaced|user_stopped\n */\n reason?: string;\n /** event 'tracking_stop' with reason 'user_stopped' only: when the user stopped the app (ISO-8601 UTC ms) */\n stopped_at?: string;\n /** event 'heartbeat' only, optional: how the heartbeat is scheduled */\n heartbeat?: HeartbeatMeta;\n /** connection type when the record was created */\n network?: ConnectivityType;\n /** the device id on every record; more readings on 'heartbeat' and 'tracking_start' records (see `DeviceReadings`) */\n device?: DeviceReadings;\n}\n\n/**\n * The `device` object of records; a reading is present only when the device provides it.\n * Every record: `device_id`.\n * Heartbeat records: `background_restricted`, `standby_bucket`, `disk_free_bytes`, `thermal_status`.\n * `tracking_start` records: `manufacturer`, `model`, `os_version`, `sdk_int`, `low_ram_device`, `ram_total_bytes`.\n */\nexport interface DeviceReadings {\n /**\n * the device id: on Android, `Settings.Secure.ANDROID_ID`; other platforms will fill it with their own id.\n * Absent in records queued by an older version. Declare \"Device or other IDs\" in Google Play's Data safety form.\n */\n device_id?: string;\n /** the user set the app's battery use to \"Restricted\" (Android 9+) */\n background_restricted?: boolean;\n /** App Standby bucket (Android 9+) */\n standby_bucket?: 'exempted' | 'active' | 'working_set' | 'frequent' | 'rare' | 'restricted';\n /** free space on the storage that holds the app's files */\n disk_free_bytes?: number;\n /** Android 10+ */\n thermal_status?: 'none' | 'light' | 'moderate' | 'severe' | 'critical' | 'emergency' | 'shutdown' | 'unknown';\n manufacturer?: string;\n model?: string;\n /** Android version, e.g. \"14\" */\n os_version?: string;\n sdk_int?: number;\n low_ram_device?: boolean;\n ram_total_bytes?: number;\n}\n\n/** Scheduling metadata of a `heartbeat` record (snake_case = wire format). */\nexport interface HeartbeatMeta {\n strategy: 'exact' | 'listener_with_backup' | 'idle_paced' | 'timer';\n /** heartbeat.minInterval (s) when the heartbeat was created */\n min_interval: number;\n /** heartbeat.maxInterval (s) when the heartbeat was created */\n max_interval: number;\n /** when the next heartbeat is expected (ISO-8601 UTC ms); null if unknown */\n next_at: string | null;\n /** the app is exempt from battery optimization */\n battery_exempt: boolean;\n /** the device was in deep Doze */\n device_idle: boolean;\n}\n\nexport type LocationRecord = Location;\n\nexport interface InsertLocationInput {\n /** `accuracy` (m) is required and must be greater than 0; otherwise the call rejects with `INVALID_ARGUMENT`. */\n coords: Pick<Coords, 'latitude' | 'longitude' | 'accuracy'> & Partial<Coords>;\n timestamp?: string;\n event?: RecordEvent;\n is_moving?: boolean;\n extras?: Record<string, unknown>;\n}\n\n/* ================= GEOFENCES ================= */\n\n/**\n * Either (latitude, longitude, radius) = circle, or vertices = polygon. For polygons, getGeofences() also returns\n * the computed enclosing circle.\n */\nexport interface Geofence {\n identifier: string;\n latitude?: number;\n longitude?: number;\n /** m */\n radius?: number;\n /** [[lat,lng], ...] >= 3 points */\n vertices?: [number, number][];\n /** @default true */\n notifyOnEntry?: boolean;\n /** @default true */\n notifyOnExit?: boolean;\n /** @default false */\n notifyOnDwell?: boolean;\n /**\n * ms\n * @default 30000\n */\n loiteringDelay?: number;\n extras?: Record<string, unknown>;\n}\n\n/* ================= EVENTS ================= */\n\nexport interface MotionChangeEvent {\n isMoving: boolean;\n location: Location;\n}\nexport interface ActivityChangeEvent {\n activity: ActivityType;\n confidence: number;\n}\nexport interface HeartbeatEvent {\n location: Location;\n}\nexport interface GeofenceEvent {\n identifier: string;\n action: GeofenceAction;\n location: Location;\n extras?: Record<string, unknown>;\n}\nexport interface GeofencesChangeEvent {\n on: Geofence[];\n off: string[];\n}\nexport interface HttpEvent {\n success: boolean;\n status: number;\n responseText: string;\n uuids: string[];\n}\nexport interface ConnectivityChangeEvent {\n connected: boolean;\n type: ConnectivityType;\n}\nexport interface PowerSaveChangeEvent {\n isPowerSaveMode: boolean;\n}\nexport interface EnabledChangeEvent {\n enabled: boolean;\n /**\n * why: a `tracking_start` reason when tracking was switched on (start, start_geofences, resume_notification), a\n * `tracking_stop` reason when it was switched off; the same values as `Location.reason`\n */\n reason: string | null;\n}\n/** The location request failed (no permission, Play services or HMS Core refused it, no usable provider). */\nexport interface LocationErrorEvent {\n code: ErrorCode;\n message: string;\n backend: LocationBackend;\n /** the plugin requests location updates again after this many ms; null if it does not */\n retryInMs: number | null;\n}\n/** A record could not be stored because the device storage is full. */\nexport interface StorageFullEvent {\n uuid: string;\n event: RecordEvent;\n diskFreeBytes: number | null;\n}\nexport interface NotificationActionEvent {\n id: string;\n}\nexport interface AuthorizationEvent {\n success: boolean;\n status: number;\n error?: string;\n response?: Record<string, unknown>;\n}\nexport interface LocationTrackingEventMap {\n location: Location;\n motionchange: MotionChangeEvent;\n activitychange: ActivityChangeEvent;\n providerchange: ProviderState;\n heartbeat: HeartbeatEvent;\n geofence: GeofenceEvent;\n geofenceschange: GeofencesChangeEvent;\n http: HttpEvent;\n connectivitychange: ConnectivityChangeEvent;\n powersavechange: PowerSaveChangeEvent;\n enabledchange: EnabledChangeEvent;\n notificationaction: NotificationActionEvent;\n authorization: AuthorizationEvent;\n locationerror: LocationErrorEvent;\n storagefull: StorageFullEvent;\n}\nexport type LocationTrackingEventName = keyof LocationTrackingEventMap;\n\n/* ================= MISC ================= */\n\nexport interface CurrentPositionOptions {\n /** @default 3 */\n samples?: number;\n /**\n * ms\n * @default geolocation.locationTimeout\n */\n timeout?: number;\n /**\n * ms\n * @default 0\n */\n maximumAge?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default true */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport interface WatchPositionOptions {\n /**\n * ms\n * @default 1000\n */\n interval?: number;\n /** @default 'high' */\n desiredAccuracy?: DesiredAccuracy;\n /** @default false */\n persist?: boolean;\n extras?: Record<string, unknown>;\n}\n\nexport type WatchPositionCallback = (location: Location | null, error?: { code: ErrorCode; message: string }) => void;\n\nexport interface HeartbeatStatus {\n enabled: boolean;\n minInterval: number;\n maxInterval: number;\n lastRecordAt: string | null;\n lastHeartbeatAt: string | null;\n nextHeartbeatAt: string | null;\n strategy: HeartbeatStrategy;\n canScheduleExactAlarms: boolean;\n isIgnoringBatteryOptimizations: boolean;\n isDeviceIdleMode: boolean;\n isPowerSaveMode: boolean;\n pendingHeartbeats: number;\n}\n\nexport interface BatteryOptimizationStatus {\n isIgnoringBatteryOptimizations: boolean;\n canScheduleExactAlarms: boolean;\n isDeviceIdleMode: boolean;\n}\n\nexport interface PowerManagerInfo {\n manufacturer: string;\n available: boolean;\n}\n\nexport interface DeviceInfo {\n platform: 'android' | 'web' | 'ios';\n manufacturer: string;\n model: string;\n brand: string;\n osVersion: string;\n sdkInt: number;\n pluginVersion: string;\n gmsAvailable: boolean;\n hmsAvailable: boolean;\n backend: LocationBackend;\n packagedProviders: string[];\n /** why a better location backend is not used (for example \"gms: unavailable (status 2)\"); null when none is better */\n backendFallbackReason: string | null;\n /** raw GoogleApiAvailability result (0 = success); null if GMS is not packaged or not checked yet */\n gmsStatus: number | null;\n /** raw HuaweiApiAvailability result (0 = success); null if HMS is not packaged or not checked yet */\n hmsStatus: number | null;\n /** true when HMS Core is older than 6.3.0.301; null if HMS is not packaged */\n hmsCoreOutdated: boolean | null;\n /** last LocationAvailability reported by GMS or HMS; only a hint (it can be wrong both ways); null if none */\n locationAvailableHint: boolean | null;\n /** number of CPU cores */\n cpuCores: number;\n /** CPU types (Build.SUPPORTED_ABIS), e.g. [\"arm64-v8a\"] */\n supportedAbis: string[];\n}\n\nexport interface Sensors {\n accelerometer: boolean;\n gyroscope: boolean;\n magnetometer: boolean;\n significantMotion: boolean;\n stepCounter: boolean;\n stepDetector: boolean;\n barometer: boolean;\n}\n\nexport interface PermissionStatus {\n location: PermissionState;\n backgroundLocation: PermissionState;\n activityRecognition: PermissionState;\n notifications: PermissionState;\n}\n\nexport interface LogQuery {\n start?: number;\n end?: number;\n level?: LogLevel;\n limit?: number;\n order?: 'asc' | 'desc';\n}\n\nexport interface LocationTrackingPlugin {\n /** Loads persisted config and state. Must resolve before most other methods (see NOT_READY rule). */\n ready(options?: ReadyOptions): Promise<State>;\n /** deep merge; arrays replaced; null resets key to default */\n setConfig(options: { config: Config }): Promise<State>;\n /** Resets the config to defaults, then applies the given config. */\n reset(options?: { config?: Config }): Promise<State>;\n getState(): Promise<State>;\n /** Starts location tracking (mode 'location'). */\n start(): Promise<State>;\n /** Starts geofence-only tracking (mode 'geofences'). */\n startGeofences(): Promise<State>;\n stop(): Promise<State>;\n /** Forces the motion state to moving or stationary. */\n changePace(options: { isMoving: boolean }): Promise<void>;\n\n getCurrentPosition(options?: CurrentPositionOptions): Promise<Location>;\n watchPosition(options: WatchPositionOptions, callback: WatchPositionCallback): Promise<CallbackID>;\n clearWatch(options: { id: CallbackID }): Promise<void>;\n\n getOdometer(): Promise<{ odometer: number }>;\n setOdometer(options: { odometer: number }): Promise<{ odometer: number }>;\n resetOdometer(): Promise<{ odometer: number }>;\n\n getLocations(options?: { limit?: number }): Promise<{ locations: Location[] }>;\n getCount(): Promise<{ count: number }>;\n insertLocation(options: { location: InsertLocationInput }): Promise<{ uuid: string }>;\n destroyLocations(): Promise<{ count: number }>;\n destroyLocation(options: { uuid: string }): Promise<{ deleted: boolean }>;\n /** uploads whole queue now; resolves with uploaded records */\n sync(): Promise<{ locations: Location[] }>;\n\n addGeofence(options: { geofence: Geofence }): Promise<void>;\n addGeofences(options: { geofences: Geofence[] }): Promise<void>;\n removeGeofence(options: { identifier: string }): Promise<void>;\n /** identifiers omitted (or null) = remove all; an empty array removes nothing */\n removeGeofences(options?: { identifiers?: string[] }): Promise<void>;\n getGeofences(): Promise<{ geofences: Geofence[] }>;\n getGeofence(options: { identifier: string }): Promise<{ geofence: Geofence | null }>;\n geofenceExists(options: { identifier: string }): Promise<{ exists: boolean }>;\n\n getHeartbeatStatus(): Promise<HeartbeatStatus>;\n\n getProviderState(): Promise<ProviderState>;\n isPowerSaveMode(): Promise<{ isPowerSaveMode: boolean }>;\n getBatteryOptimizationStatus(): Promise<BatteryOptimizationStatus>;\n openBatteryOptimizationSettings(): Promise<{ opened: boolean }>;\n getPowerManagerInfo(): Promise<PowerManagerInfo>;\n openPowerManagerSettings(): Promise<{ opened: boolean }>;\n openLocationSettings(): Promise<{ opened: boolean }>;\n openAppSettings(): Promise<{ opened: boolean }>;\n getDeviceInfo(): Promise<DeviceInfo>;\n getSensors(): Promise<Sensors>;\n\n checkPermissions(): Promise<PermissionStatus>;\n /** default all four, order: location, notifications, activityRecognition, backgroundLocation */\n requestPermissions(options?: { permissions?: PermissionType[] }): Promise<PermissionStatus>;\n\n log(options: { level: Exclude<LogLevel, 'off'>; message: string }): Promise<void>;\n getLog(options?: LogQuery): Promise<{ log: string }>;\n destroyLog(): Promise<void>;\n uploadLog(options: {\n url: string;\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n }): Promise<{ success: boolean; status: number }>;\n emailLog(options: { email: string; subject?: string }): Promise<void>;\n\n addListener(eventName: 'location', listenerFunc: (e: Location) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'motionchange', listenerFunc: (e: MotionChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'activitychange',\n listenerFunc: (e: ActivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'providerchange', listenerFunc: (e: ProviderState) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'heartbeat', listenerFunc: (e: HeartbeatEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'geofence', listenerFunc: (e: GeofenceEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'geofenceschange',\n listenerFunc: (e: GeofencesChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'http', listenerFunc: (e: HttpEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'connectivitychange',\n listenerFunc: (e: ConnectivityChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'powersavechange',\n listenerFunc: (e: PowerSaveChangeEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'enabledchange', listenerFunc: (e: EnabledChangeEvent) => void): Promise<PluginListenerHandle>;\n addListener(\n eventName: 'notificationaction',\n listenerFunc: (e: NotificationActionEvent) => void,\n ): Promise<PluginListenerHandle>;\n addListener(eventName: 'authorization', listenerFunc: (e: AuthorizationEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'locationerror', listenerFunc: (e: LocationErrorEvent) => void): Promise<PluginListenerHandle>;\n addListener(eventName: 'storagefull', listenerFunc: (e: StorageFullEvent) => void): Promise<PluginListenerHandle>;\n removeAllListeners(): Promise<void>;\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import type { DeviceInfo } from '../definitions';
2
2
  /** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
3
- export declare const PLUGIN_VERSION = "8.1.0";
3
+ export declare const PLUGIN_VERSION = "8.2.0";
4
4
  /**
5
5
  * Best-effort `DeviceInfo` for a browser, parsed from `navigator.userAgent`.
6
6
  * `brand` is the browser, `model` the device when the user agent names one (else `navigator.platform`),
@@ -1,5 +1,5 @@
1
1
  /** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
2
- export const PLUGIN_VERSION = '8.1.0';
2
+ export const PLUGIN_VERSION = '8.2.0';
3
3
  /** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */
4
4
  const NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];
5
5
  /** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */
@@ -1 +1 @@
1
- {"version":3,"file":"device.js","sourceRoot":"","sources":["../../../src/web/device.ts"],"names":[],"mappings":"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","sourcesContent":["// Owned by Unit 1 (TS wrapper + web).\nimport type { DeviceInfo } from '../definitions';\n\n/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */\nexport const PLUGIN_VERSION = '8.1.0';\n\n/** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */\nconst NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];\n\ninterface OsInfo {\n osVersion: string;\n manufacturer: string;\n model: string | undefined;\n}\n\n/** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */\nfunction androidModel(tokens: string[]): string | undefined {\n for (const token of tokens) {\n const model = token.replace(/\\s*Build\\/.*$/, '').trim();\n const skip =\n !model ||\n NOT_A_MODEL.indexOf(model) >= 0 ||\n /^[a-z]{2}(?:[-_][a-z]{2})?$/i.test(model) || // locale, e.g. en-us\n /^(?:rv:|HMSCore)/.test(model);\n if (!skip) {\n return model;\n }\n }\n return undefined;\n}\n\nfunction parseOs(ua: string): OsInfo {\n let match = /\\(([^)]*?)Android\\s*([\\d.]*)([^)]*)\\)/.exec(ua);\n if (match) {\n return {\n osVersion: match[2] ? `Android ${match[2]}` : 'Android',\n manufacturer: 'unknown',\n model: androidModel(match[3].split(';')),\n };\n }\n match = /(iPhone|iPad|iPod).*?OS (\\d+(?:_\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `iOS ${match[2].replace(/_/g, '.')}`, manufacturer: 'Apple', model: match[1] };\n }\n match = /Mac OS X (\\d+(?:[_.]\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `macOS ${match[1].replace(/_/g, '.')}`, manufacturer: 'Apple', model: 'Macintosh' };\n }\n match = /Windows NT (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `Windows ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n match = /CrOS \\S+ (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `ChromeOS ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n if (/Linux/.test(ua)) {\n return { osVersion: 'Linux', manufacturer: 'unknown', model: undefined };\n }\n return { osVersion: 'unknown', manufacturer: 'unknown', model: undefined };\n}\n\n/** Browser name and major version, e.g. `Chrome 120`. Order matters: many browsers also claim Chrome or Safari. */\nfunction parseBrowser(ua: string): string {\n const browsers: [string, RegExp][] = [\n ['Edge', /Edg(?:e|A|iOS)?\\/(\\d+)/],\n ['Opera', /(?:OPR|Opera)\\/(\\d+)/],\n ['Samsung Internet', /SamsungBrowser\\/(\\d+)/],\n ['Huawei Browser', /HuaweiBrowser\\/(\\d+)/],\n ['Firefox', /(?:Firefox|FxiOS)\\/(\\d+)/],\n ['Chrome', /(?:Chrome|CriOS)\\/(\\d+)/],\n ['Safari', /Version\\/(\\d+)[\\d.]*.*Safari\\//],\n ['Node.js', /Node\\.js\\/(\\d+)/],\n ];\n for (const [name, pattern] of browsers) {\n const match = pattern.exec(ua);\n if (match) {\n return `${name} ${match[1]}`;\n }\n }\n return 'unknown';\n}\n\n/**\n * Best-effort `DeviceInfo` for a browser, parsed from `navigator.userAgent`.\n * `brand` is the browser, `model` the device when the user agent names one (else `navigator.platform`),\n * `osVersion` includes the OS name, and `sdkInt` is -1 because there is no Android API level.\n */\nexport function webDeviceInfo(userAgent: string, platform?: string): DeviceInfo {\n const os = parseOs(userAgent);\n return {\n platform: 'web',\n manufacturer: os.manufacturer,\n model: os.model || platform || 'unknown',\n brand: parseBrowser(userAgent),\n osVersion: os.osVersion,\n sdkInt: -1,\n pluginVersion: PLUGIN_VERSION,\n gmsAvailable: false,\n hmsAvailable: false,\n backend: 'web',\n packagedProviders: [],\n backendFallbackReason: null,\n gmsStatus: null,\n hmsStatus: null,\n hmsCoreOutdated: null,\n locationAvailableHint: null,\n cpuCores: typeof navigator !== 'undefined' && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 0,\n supportedAbis: [],\n };\n}\n"]}
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../../../src/web/device.ts"],"names":[],"mappings":"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","sourcesContent":["// Owned by Unit 1 (TS wrapper + web).\nimport type { DeviceInfo } from '../definitions';\n\n/** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */\nexport const PLUGIN_VERSION = '8.2.0';\n\n/** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */\nconst NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];\n\ninterface OsInfo {\n osVersion: string;\n manufacturer: string;\n model: string | undefined;\n}\n\n/** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */\nfunction androidModel(tokens: string[]): string | undefined {\n for (const token of tokens) {\n const model = token.replace(/\\s*Build\\/.*$/, '').trim();\n const skip =\n !model ||\n NOT_A_MODEL.indexOf(model) >= 0 ||\n /^[a-z]{2}(?:[-_][a-z]{2})?$/i.test(model) || // locale, e.g. en-us\n /^(?:rv:|HMSCore)/.test(model);\n if (!skip) {\n return model;\n }\n }\n return undefined;\n}\n\nfunction parseOs(ua: string): OsInfo {\n let match = /\\(([^)]*?)Android\\s*([\\d.]*)([^)]*)\\)/.exec(ua);\n if (match) {\n return {\n osVersion: match[2] ? `Android ${match[2]}` : 'Android',\n manufacturer: 'unknown',\n model: androidModel(match[3].split(';')),\n };\n }\n match = /(iPhone|iPad|iPod).*?OS (\\d+(?:_\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `iOS ${match[2].replace(/_/g, '.')}`, manufacturer: 'Apple', model: match[1] };\n }\n match = /Mac OS X (\\d+(?:[_.]\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `macOS ${match[1].replace(/_/g, '.')}`, manufacturer: 'Apple', model: 'Macintosh' };\n }\n match = /Windows NT (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `Windows ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n match = /CrOS \\S+ (\\d+(?:\\.\\d+)*)/.exec(ua);\n if (match) {\n return { osVersion: `ChromeOS ${match[1]}`, manufacturer: 'unknown', model: undefined };\n }\n if (/Linux/.test(ua)) {\n return { osVersion: 'Linux', manufacturer: 'unknown', model: undefined };\n }\n return { osVersion: 'unknown', manufacturer: 'unknown', model: undefined };\n}\n\n/** Browser name and major version, e.g. `Chrome 120`. Order matters: many browsers also claim Chrome or Safari. */\nfunction parseBrowser(ua: string): string {\n const browsers: [string, RegExp][] = [\n ['Edge', /Edg(?:e|A|iOS)?\\/(\\d+)/],\n ['Opera', /(?:OPR|Opera)\\/(\\d+)/],\n ['Samsung Internet', /SamsungBrowser\\/(\\d+)/],\n ['Huawei Browser', /HuaweiBrowser\\/(\\d+)/],\n ['Firefox', /(?:Firefox|FxiOS)\\/(\\d+)/],\n ['Chrome', /(?:Chrome|CriOS)\\/(\\d+)/],\n ['Safari', /Version\\/(\\d+)[\\d.]*.*Safari\\//],\n ['Node.js', /Node\\.js\\/(\\d+)/],\n ];\n for (const [name, pattern] of browsers) {\n const match = pattern.exec(ua);\n if (match) {\n return `${name} ${match[1]}`;\n }\n }\n return 'unknown';\n}\n\n/**\n * Best-effort `DeviceInfo` for a browser, parsed from `navigator.userAgent`.\n * `brand` is the browser, `model` the device when the user agent names one (else `navigator.platform`),\n * `osVersion` includes the OS name, and `sdkInt` is -1 because there is no Android API level.\n */\nexport function webDeviceInfo(userAgent: string, platform?: string): DeviceInfo {\n const os = parseOs(userAgent);\n return {\n platform: 'web',\n manufacturer: os.manufacturer,\n model: os.model || platform || 'unknown',\n brand: parseBrowser(userAgent),\n osVersion: os.osVersion,\n sdkInt: -1,\n pluginVersion: PLUGIN_VERSION,\n gmsAvailable: false,\n hmsAvailable: false,\n backend: 'web',\n packagedProviders: [],\n backendFallbackReason: null,\n gmsStatus: null,\n hmsStatus: null,\n hmsCoreOutdated: null,\n locationAvailableHint: null,\n cpuCores: typeof navigator !== 'undefined' && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 0,\n supportedAbis: [],\n };\n}\n"]}
@@ -322,7 +322,7 @@ function webState(config) {
322
322
  }
323
323
 
324
324
  /** `npm version` copies the `package.json` version here (scripts/sync-version.mjs); a test checks it. */
325
- const PLUGIN_VERSION = '8.1.0';
325
+ const PLUGIN_VERSION = '8.2.0';
326
326
  /** Tokens after `Android x` that do not name a device ('K' is Chrome's reduced user agent). */
327
327
  const NOT_A_MODEL = ['K', 'U', 'wv', 'Mobile', 'Tablet', 'HarmonyOS'];
328
328
  /** The device model among the `;`-separated tokens that follow `Android x` in the user agent's comment. */