@tokamakdev/plugin-location 0.1.0-beta.107 → 0.1.0-beta.112
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.
- package/README.md +30 -13
- package/android/Bindings.kt +997 -0
- package/android/LocationPlugin.kt +3 -124
- package/android/bindings.ts +4150 -0
- package/apple/Bindings.swift +1057 -0
- package/apple/LocationPlugin.swift +28 -167
- package/apple/LocationRegions.swift +3 -1
- package/apple/bindings.ts +1314 -0
- package/events/index.ts +5 -5
- package/package.json +5 -3
- package/src/android.ts +138 -0
- package/src/apple.ts +117 -0
- package/src/errors.ts +34 -0
- package/src/index.ts +89 -64
- package/src/types.ts +12 -0
- package/tokamak-plugin.json +69 -2
- package/web/client.ts +11 -0
- package/src/checks.ts +0 -65
- package/src/options.ts +0 -54
|
@@ -23,19 +23,6 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
23
23
|
name: "NotAllowedError",
|
|
24
24
|
message: "Location permission was denied"
|
|
25
25
|
)
|
|
26
|
-
private static let timedOut = TokamakPluginError(
|
|
27
|
-
name: "TimeoutError",
|
|
28
|
-
message: "Location was not found in time"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
/// A current position request, timed from when locating starts.
|
|
32
|
-
private struct Request {
|
|
33
|
-
let reply: TokamakPluginReply
|
|
34
|
-
let maximumAge: TimeInterval
|
|
35
|
-
let timeout: TimeInterval
|
|
36
|
-
var timer: DispatchWorkItem?
|
|
37
|
-
}
|
|
38
|
-
|
|
39
26
|
/// A `requestPermission` waiting for the user to answer a prompt.
|
|
40
27
|
private struct Prompt {
|
|
41
28
|
/// The purpose key of the temporary precise location to ask for next.
|
|
@@ -45,14 +32,10 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
45
32
|
|
|
46
33
|
private let host: TokamakHost
|
|
47
34
|
private let defaults = UserDefaults.standard
|
|
48
|
-
///
|
|
49
|
-
private let
|
|
50
|
-
/// Takes single fixes for current position requests, apart from watching.
|
|
51
|
-
private let fixes = CLLocationManager()
|
|
35
|
+
/// Reports the app's authorization, and asks for permission.
|
|
36
|
+
private let authorization = CLLocationManager()
|
|
52
37
|
/// Delivers the significant changes and visits `setup` names.
|
|
53
38
|
private let monitoring = CLLocationManager()
|
|
54
|
-
private var current: [UUID: Request] = [:]
|
|
55
|
-
private var watchers: [UUID: TokamakPluginReply] = [:]
|
|
56
39
|
private var prompts: [UUID: Prompt] = [:]
|
|
57
40
|
/// The prompts waiting when the app resigned active, as a prompt showing
|
|
58
41
|
/// makes it.
|
|
@@ -73,10 +56,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
73
56
|
updates = TokamakLocationUpdates(events: events)
|
|
74
57
|
regions = TokamakLocationRegions(events: events)
|
|
75
58
|
super.init()
|
|
76
|
-
|
|
77
|
-
manager.delegate = self
|
|
78
|
-
manager.desiredAccuracy = kCLLocationAccuracyBest
|
|
79
|
-
}
|
|
59
|
+
authorization.delegate = self
|
|
80
60
|
monitoring.delegate = self
|
|
81
61
|
if let stored = defaults.data(forKey: setupKey),
|
|
82
62
|
let config = try? JSONSerialization.jsonObject(with: stored) as? [String: Any]
|
|
@@ -98,7 +78,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
98
78
|
method: String,
|
|
99
79
|
arguments: Any,
|
|
100
80
|
reply: @escaping TokamakPluginReply
|
|
101
|
-
) {
|
|
81
|
+
) -> (() -> Void)? {
|
|
102
82
|
let options = arguments as? [String: Any] ?? [:]
|
|
103
83
|
do throws(TokamakPluginError) {
|
|
104
84
|
switch method {
|
|
@@ -111,13 +91,6 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
111
91
|
apply(options)
|
|
112
92
|
defaults.set(try? JSONSerialization.data(withJSONObject: options), forKey: setupKey)
|
|
113
93
|
reply(.success(nil))
|
|
114
|
-
case "getCurrentPosition":
|
|
115
|
-
current[UUID()] = Request(
|
|
116
|
-
reply: reply,
|
|
117
|
-
maximumAge: (options["maximumAge"] as? Double ?? 0) / 1000,
|
|
118
|
-
timeout: (options["timeout"] as? Double ?? Double(UInt32.max)) / 1000
|
|
119
|
-
)
|
|
120
|
-
start()
|
|
121
94
|
case "permission":
|
|
122
95
|
reply(.success(permission()))
|
|
123
96
|
case "requestPermission":
|
|
@@ -142,7 +115,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
142
115
|
reply(.success(updates.options))
|
|
143
116
|
case "addRegion":
|
|
144
117
|
try requirePermission()
|
|
145
|
-
guard
|
|
118
|
+
guard authorization.accuracyAuthorization == .fullAccuracy else {
|
|
146
119
|
throw TokamakPluginError(
|
|
147
120
|
name: "NotAllowedError", message: "Regions need precise location")
|
|
148
121
|
}
|
|
@@ -170,6 +143,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
170
143
|
} catch {
|
|
171
144
|
reply(.failure(error))
|
|
172
145
|
}
|
|
146
|
+
return nil
|
|
173
147
|
}
|
|
174
148
|
|
|
175
149
|
func subscribe(
|
|
@@ -177,66 +151,41 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
177
151
|
arguments: Any,
|
|
178
152
|
reply: @escaping TokamakPluginReply
|
|
179
153
|
) throws(TokamakPluginError) -> (() -> Void) {
|
|
180
|
-
|
|
181
|
-
return remove
|
|
182
|
-
}
|
|
183
|
-
guard method == "watchPosition" else {
|
|
154
|
+
guard let remove = events.listen(method, reply) else {
|
|
184
155
|
throw .notSupported("\(id).\(method) is not supported")
|
|
185
156
|
}
|
|
186
|
-
|
|
187
|
-
watchers[subscription] = reply
|
|
188
|
-
start()
|
|
189
|
-
return { [weak self] in
|
|
190
|
-
self?.watchers.removeValue(forKey: subscription)
|
|
191
|
-
if self?.watchers.isEmpty == true {
|
|
192
|
-
self?.watching.stopUpdatingLocation()
|
|
193
|
-
}
|
|
194
|
-
}
|
|
157
|
+
return remove
|
|
195
158
|
}
|
|
196
159
|
|
|
197
|
-
/// Also called when each manager is created
|
|
198
|
-
///
|
|
199
|
-
/// `watching`'s calls handle.
|
|
160
|
+
/// Also called when each manager is created. Every manager reports the
|
|
161
|
+
/// app's one authorization, which `authorization`'s calls handle.
|
|
200
162
|
func locationManagerDidChangeAuthorization(
|
|
201
163
|
_ manager: CLLocationManager
|
|
202
164
|
) {
|
|
203
|
-
guard manager ===
|
|
165
|
+
guard manager === authorization else { return }
|
|
204
166
|
holdAlwaysSession()
|
|
205
167
|
guard manager.authorizationStatus != .notDetermined else { return }
|
|
206
168
|
answer(Array(prompts.keys))
|
|
207
|
-
if !current.isEmpty || !watchers.isEmpty {
|
|
208
|
-
serve(manager.authorizationStatus)
|
|
209
|
-
}
|
|
210
169
|
}
|
|
211
170
|
|
|
212
171
|
func locationManager(
|
|
213
172
|
_ manager: CLLocationManager,
|
|
214
173
|
didUpdateLocations locations: [CLLocation]
|
|
215
174
|
) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
return
|
|
175
|
+
guard manager === monitoring else { return }
|
|
176
|
+
for location in locations {
|
|
177
|
+
events.update(location)
|
|
221
178
|
}
|
|
222
|
-
guard let location = locations.last else { return }
|
|
223
|
-
deliver(.success(TokamakLocationEvents.position(location)), from: manager)
|
|
224
179
|
}
|
|
225
180
|
|
|
226
|
-
///
|
|
181
|
+
/// Monitoring continues while the position is not yet known.
|
|
227
182
|
func locationManager(
|
|
228
183
|
_ manager: CLLocationManager,
|
|
229
184
|
didFailWithError error: Error
|
|
230
185
|
) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
} else if manager === monitoring {
|
|
236
|
-
events.fail(locationError(error))
|
|
237
|
-
} else {
|
|
238
|
-
deliver(.failure(locationError(error)), from: manager)
|
|
239
|
-
}
|
|
186
|
+
guard manager === monitoring, (error as NSError).code != CLError.Code.locationUnknown.rawValue
|
|
187
|
+
else { return }
|
|
188
|
+
events.fail(locationError(error))
|
|
240
189
|
}
|
|
241
190
|
|
|
242
191
|
/// Core Location delivers each visit to every manager's delegate.
|
|
@@ -262,102 +211,14 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
262
211
|
}
|
|
263
212
|
}
|
|
264
213
|
|
|
265
|
-
/// Checks location services off the main thread, which the check can block.
|
|
266
|
-
private func start() {
|
|
267
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
268
|
-
let enabled = CLLocationManager.locationServicesEnabled()
|
|
269
|
-
DispatchQueue.main.async { self.start(servicesEnabled: enabled) }
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private func start(servicesEnabled: Bool) {
|
|
274
|
-
guard servicesEnabled else {
|
|
275
|
-
fail(.locationUnavailable("Location services are disabled"))
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
let status = watching.authorizationStatus
|
|
279
|
-
guard status == .notDetermined else {
|
|
280
|
-
serve(status)
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
do throws(TokamakPluginError) {
|
|
284
|
-
try host.requireUI()
|
|
285
|
-
watching.requestWhenInUseAuthorization()
|
|
286
|
-
} catch {
|
|
287
|
-
fail(error)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/// Locates for the waiting requests and watchers when `status` allows it,
|
|
292
|
-
/// and fails them when it forbids it.
|
|
293
|
-
private func serve(_ status: CLAuthorizationStatus) {
|
|
294
|
-
switch status {
|
|
295
|
-
case .authorizedAlways, .authorizedWhenInUse:
|
|
296
|
-
if !watchers.isEmpty {
|
|
297
|
-
watching.startUpdatingLocation()
|
|
298
|
-
}
|
|
299
|
-
locate()
|
|
300
|
-
case .denied, .restricted:
|
|
301
|
-
fail(Self.permissionDenied)
|
|
302
|
-
case .notDetermined:
|
|
303
|
-
break
|
|
304
|
-
@unknown default:
|
|
305
|
-
fail(.notSupported("Location authorization is not supported"))
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/// Answers each new request with a fix no older than its `maximumAge`, and
|
|
310
|
-
/// takes a fix for the rest, each failing after its `timeout`.
|
|
311
|
-
private func locate() {
|
|
312
|
-
for (key, request) in current where request.timer == nil {
|
|
313
|
-
if let fix = fixes.location, -fix.timestamp.timeIntervalSinceNow <= request.maximumAge {
|
|
314
|
-
finish(key, .success(TokamakLocationEvents.position(fix)))
|
|
315
|
-
continue
|
|
316
|
-
}
|
|
317
|
-
let timer = DispatchWorkItem { [weak self] in self?.finish(key, .failure(Self.timedOut)) }
|
|
318
|
-
current[key]?.timer = timer
|
|
319
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + request.timeout, execute: timer)
|
|
320
|
-
}
|
|
321
|
-
if !current.isEmpty {
|
|
322
|
-
fixes.requestLocation()
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/// Answers the waiting requests with a fix, or the watchers with an update.
|
|
327
|
-
private func deliver(_ result: Result<Any?, TokamakPluginError>, from manager: CLLocationManager)
|
|
328
|
-
{
|
|
329
|
-
guard manager === fixes else {
|
|
330
|
-
for reply in watchers.values {
|
|
331
|
-
reply(result)
|
|
332
|
-
}
|
|
333
|
-
return
|
|
334
|
-
}
|
|
335
|
-
for key in current.keys {
|
|
336
|
-
finish(key, result)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private func fail(_ error: TokamakPluginError) {
|
|
341
|
-
deliver(.failure(error), from: fixes)
|
|
342
|
-
deliver(.failure(error), from: watching)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private func finish(_ key: UUID, _ result: Result<Any?, TokamakPluginError>) {
|
|
346
|
-
guard let request = current.removeValue(forKey: key) else { return }
|
|
347
|
-
request.timer?.cancel()
|
|
348
|
-
request.reply(result)
|
|
349
|
-
if current.isEmpty {
|
|
350
|
-
fixes.stopUpdatingLocation()
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
214
|
/// The app's in-use and background permission, and the precision it gets.
|
|
355
215
|
private func permission() -> [String: Any] {
|
|
356
|
-
let status =
|
|
216
|
+
let status = authorization.authorizationStatus
|
|
357
217
|
return [
|
|
358
218
|
"inUse": Self.state(status),
|
|
359
219
|
"background": backgroundPermission(status),
|
|
360
|
-
"precision":
|
|
220
|
+
"precision": authorization.accuracyAuthorization == .reducedAccuracy
|
|
221
|
+
? "approximate" : "precise",
|
|
361
222
|
]
|
|
362
223
|
}
|
|
363
224
|
|
|
@@ -377,7 +238,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
377
238
|
}
|
|
378
239
|
|
|
379
240
|
private var hasPermission: Bool {
|
|
380
|
-
![.notDetermined, .denied, .restricted].contains(
|
|
241
|
+
![.notDetermined, .denied, .restricted].contains(authorization.authorizationStatus)
|
|
381
242
|
}
|
|
382
243
|
|
|
383
244
|
/// Throws `NotAllowedError` unless the app has location permission.
|
|
@@ -391,7 +252,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
391
252
|
private func holdAlwaysSession() {
|
|
392
253
|
#if os(iOS)
|
|
393
254
|
guard #available(iOS 18, *) else { return }
|
|
394
|
-
let always =
|
|
255
|
+
let always = authorization.authorizationStatus == .authorizedAlways
|
|
395
256
|
if always && alwaysSession == nil {
|
|
396
257
|
alwaysSession = CLServiceSession(authorization: .always)
|
|
397
258
|
} else if !always {
|
|
@@ -430,7 +291,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
430
291
|
"Background location needs the location plugin's background build setting")
|
|
431
292
|
}
|
|
432
293
|
let prompt = Prompt(purpose: try Self.purpose(options), reply: reply)
|
|
433
|
-
let status =
|
|
294
|
+
let status = authorization.authorizationStatus
|
|
434
295
|
let upgrades = always && canUpgradeToAlways(status)
|
|
435
296
|
guard status == .notDetermined || upgrades else {
|
|
436
297
|
respond(to: prompt)
|
|
@@ -440,9 +301,9 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
440
301
|
prompts[UUID()] = prompt
|
|
441
302
|
if always {
|
|
442
303
|
defaults.set(true, forKey: askedForAlwaysKey)
|
|
443
|
-
|
|
304
|
+
authorization.requestAlwaysAuthorization()
|
|
444
305
|
} else {
|
|
445
|
-
|
|
306
|
+
authorization.requestWhenInUseAuthorization()
|
|
446
307
|
}
|
|
447
308
|
}
|
|
448
309
|
|
|
@@ -490,7 +351,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
490
351
|
/// precise location for its purpose when the app has approximate location.
|
|
491
352
|
private func respond(to prompt: Prompt) {
|
|
492
353
|
guard let purpose = prompt.purpose, hasPermission,
|
|
493
|
-
|
|
354
|
+
authorization.accuracyAuthorization == .reducedAccuracy
|
|
494
355
|
else {
|
|
495
356
|
prompt.reply(.success(permission()))
|
|
496
357
|
return
|
|
@@ -501,7 +362,7 @@ final class TokamakLocationPlugin: NSObject, TokamakPlugin,
|
|
|
501
362
|
prompt.reply(.failure(error))
|
|
502
363
|
return
|
|
503
364
|
}
|
|
504
|
-
|
|
365
|
+
authorization.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purpose) { _ in
|
|
505
366
|
DispatchQueue.main.async { prompt.reply(.success(self.permission())) }
|
|
506
367
|
}
|
|
507
368
|
}
|
|
@@ -60,7 +60,8 @@ final class TokamakLocationRegions {
|
|
|
60
60
|
|
|
61
61
|
/// Monitors the region `region` describes, replacing one with the same id.
|
|
62
62
|
/// It starts outside, so a device already inside reports an entry, as on
|
|
63
|
-
/// Android.
|
|
63
|
+
/// Android. `CLMonitor` keeps the state of a condition added again under its
|
|
64
|
+
/// identifier, so the old condition is removed first.
|
|
64
65
|
func add(_ region: [String: Any]) async throws(TokamakPluginError) {
|
|
65
66
|
guard let id = region["id"] as? String, let radius = region["radius"] as? Double,
|
|
66
67
|
let latitude = region["latitude"] as? Double, let longitude = region["longitude"] as? Double
|
|
@@ -79,6 +80,7 @@ final class TokamakLocationRegions {
|
|
|
79
80
|
let condition = CLMonitor.CircularGeographicCondition(center: center, radius: radius)
|
|
80
81
|
regions[id] = region
|
|
81
82
|
inside.remove(id)
|
|
83
|
+
await monitor.remove(id)
|
|
82
84
|
await monitor.add(condition, identifier: id, assuming: .unsatisfied)
|
|
83
85
|
}
|
|
84
86
|
|