expo-location 18.0.10-canary-20250331-817737a → 18.0.10

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/CHANGELOG.md CHANGED
@@ -8,47 +8,54 @@
8
8
 
9
9
  ### 🐛 Bug fixes
10
10
 
11
- - [iOS] Remove restarting all services when CLLocationManager reports an error ([#35478](https://github.com/expo/expo/pull/35478) by [@chrfalch](https://github.com/chrfalch))
12
- - [Android] Add missing ProGuard rule to fix task consumer failed ([#34098](https://github.com/expo/expo/pull/34098) by [@cornejobarraza](https://github.com/cornejobarraza))
13
- - [iOS] `startLocationUpdatesAsync` should not require background permissions ([#33617](https://github.com/expo/expo/pull/33617) by [@andrejpavlovic](https://github.com/andrejpavlovic)
14
-
15
11
  ### 💡 Others
16
12
 
17
- - On Android, remove dependency on `smart-location-lib`. ([#33609](https://github.com/expo/expo/pull/33609) by [@alanjhughes](https://github.com/alanjhughes))
18
- - [Android] Started using expo modules gradle plugin. ([#34176](https://github.com/expo/expo/pull/34176) by [@lukmccall](https://github.com/lukmccall))
13
+ ## 18.0.10 2025-04-01
19
14
 
20
- ## 18.0.9 - 2025-03-31
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed issue with some permission request flows resolving too soon on iOS. ([#35693](https://github.com/expo/expo/pull/35693) by [@chrfalch](https://github.com/chrfalch))
18
+ - [iOS] Remove restarting all services when CLLocationManager reports an error ([#35478](https://github.com/expo/expo/pull/35478) by [@chrfalch](https://github.com/chrfalch))
19
+
20
+ ## 18.0.9 — 2025-03-31
21
21
 
22
22
  _This version does not introduce any user-facing changes._
23
23
 
24
- ## 18.0.8 - 2025-03-14
24
+ ## 18.0.8 2025-03-14
25
25
 
26
26
  ### 💡 Others
27
27
 
28
28
  - On iOS, added setting the scope value as per our documentation. ([#35452](https://github.com/expo/expo/pull/35452) by [@chrfalch](https://github.com/chrfalch))
29
29
 
30
+ ## 18.0.7 — 2025-02-19
31
+
32
+ - On Android, remove dependency on `smart-location-lib`. ([#33609](https://github.com/expo/expo/pull/33609) by [@alanjhughes](https://github.com/alanjhughes))
33
+ - [Android] Started using expo modules gradle plugin. ([#34176](https://github.com/expo/expo/pull/34176) by [@lukmccall](https://github.com/lukmccall))
34
+
30
35
  ## 18.0.7 - 2025-02-19
31
36
 
37
+ - ([iOS][location] Add scope to foreground and background iOS permissions as pr. documentation (#35452))
38
+
32
39
  ### 🐛 Bug fixes
33
40
 
34
41
  - [iOS] Added guards to avoid task options to crash the app. ([#35477](https://github.com/expo/expo/pull/35477) by [@chrfalch](https://github.com/chrfalch))
35
42
  - [iOS] Added error handler to the streaming location/heading methods since these can fail while streaming ([#35004](https://github.com/expo/expo/pull/35004) by [@chrfalch](https://github.com/chrfalch))
36
43
 
37
- ## 18.0.6 - 2025-02-10
44
+ ## 18.0.6 2025-02-10
38
45
 
39
46
  ### 🐛 Bug fixes
40
47
 
41
48
  - [Android] Use less specific exception in catch block of `resolveUserSettingsForRequest`. ([#34784](https://github.com/expo/expo/pull/34784) by [@alanjhughes](https://github.com/alanjhughes))
42
49
 
43
- ## 18.0.5 - 2025-01-10
50
+ ## 18.0.5 2025-01-10
44
51
 
45
52
  _This version does not introduce any user-facing changes._
46
53
 
47
- ## 18.0.4 - 2024-12-10
54
+ ## 18.0.4 2024-12-10
48
55
 
49
56
  _This version does not introduce any user-facing changes._
50
57
 
51
- ## 18.0.3 - 2024-11-29
58
+ ## 18.0.3 2024-11-29
52
59
 
53
60
  _This version does not introduce any user-facing changes._
54
61
 
@@ -1,20 +1,24 @@
1
- plugins {
2
- id 'com.android.library'
3
- id 'expo-module-gradle-plugin'
4
- }
1
+ apply plugin: 'com.android.library'
5
2
 
6
3
  group = 'host.exp.exponent'
7
- version = '18.0.2'
4
+ version = '18.0.10'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useDefaultAndroidSdkVersions()
11
+ useExpoPublishing()
8
12
 
9
13
  android {
10
14
  namespace "expo.modules.location"
11
15
  defaultConfig {
12
16
  versionCode 29
13
- versionName "18.0.2"
14
- consumerProguardFiles("proguard-rules.pro")
17
+ versionName "18.0.10"
15
18
  }
16
19
  }
17
20
 
18
21
  dependencies {
19
22
  api 'com.google.android.gms:play-services-location:21.0.1'
23
+ implementation project(":${project.name}\$io.nlopez.smartlocation-jetified-aar")
20
24
  }
@@ -16,6 +16,8 @@ import expo.modules.location.records.LocationLastKnownOptions
16
16
  import expo.modules.location.records.LocationOptions
17
17
  import expo.modules.location.records.LocationResponse
18
18
  import expo.modules.location.records.PermissionRequestResponse
19
+ import io.nlopez.smartlocation.location.config.LocationAccuracy
20
+ import io.nlopez.smartlocation.location.config.LocationParams
19
21
  import kotlin.coroutines.resume
20
22
  import kotlin.coroutines.resumeWithException
21
23
  import kotlin.coroutines.suspendCoroutine
@@ -108,16 +110,16 @@ class LocationHelpers {
108
110
 
109
111
  private fun mapOptionsToLocationParams(options: LocationOptions): LocationParams {
110
112
  val accuracy = options.accuracy
111
- val locationParams = buildLocationParamsForAccuracy(accuracy)
113
+ val locationParamsBuilder = buildLocationParamsForAccuracy(accuracy)
112
114
 
113
115
  options.timeInterval?.let {
114
- locationParams.interval = it
116
+ locationParamsBuilder.setInterval(it)
115
117
  }
116
118
  options.distanceInterval?.let {
117
- locationParams.distance = it.toFloat()
119
+ locationParamsBuilder.setDistance(it.toFloat())
118
120
  }
119
121
 
120
- return locationParams
122
+ return locationParamsBuilder.build()
121
123
  }
122
124
 
123
125
  private fun mapAccuracyToPriority(accuracy: Int): Int {
@@ -129,15 +131,42 @@ class LocationHelpers {
129
131
  }
130
132
  }
131
133
 
132
- private fun buildLocationParamsForAccuracy(accuracy: Int): LocationParams {
134
+ private fun buildLocationParamsForAccuracy(accuracy: Int): LocationParams.Builder {
133
135
  return when (accuracy) {
134
- LocationModule.ACCURACY_LOWEST -> LocationParams(accuracy = LocationAccuracy.LOWEST, distance = 3000f, interval = 10000)
135
- LocationModule.ACCURACY_LOW -> LocationParams(accuracy = LocationAccuracy.LOW, distance = 1000f, interval = 5000)
136
- LocationModule.ACCURACY_BALANCED -> LocationParams(accuracy = LocationAccuracy.MEDIUM, distance = 100f, interval = 3000)
137
- LocationModule.ACCURACY_HIGH -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 50f, interval = 2000)
138
- LocationModule.ACCURACY_HIGHEST -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 25f, interval = 1000)
139
- LocationModule.ACCURACY_BEST_FOR_NAVIGATION -> LocationParams(accuracy = LocationAccuracy.HIGH, distance = 0f, interval = 500)
140
- else -> LocationParams(accuracy = LocationAccuracy.MEDIUM, distance = 100f, interval = 3000)
136
+ LocationModule.ACCURACY_LOWEST -> LocationParams.Builder()
137
+ .setAccuracy(LocationAccuracy.LOWEST)
138
+ .setDistance(3000f)
139
+ .setInterval(10000)
140
+
141
+ LocationModule.ACCURACY_LOW -> LocationParams.Builder()
142
+ .setAccuracy(LocationAccuracy.LOW)
143
+ .setDistance(1000f)
144
+ .setInterval(5000)
145
+
146
+ LocationModule.ACCURACY_BALANCED -> LocationParams.Builder()
147
+ .setAccuracy(LocationAccuracy.MEDIUM)
148
+ .setDistance(100f)
149
+ .setInterval(3000)
150
+
151
+ LocationModule.ACCURACY_HIGH -> LocationParams.Builder()
152
+ .setAccuracy(LocationAccuracy.HIGH)
153
+ .setDistance(50f)
154
+ .setInterval(2000)
155
+
156
+ LocationModule.ACCURACY_HIGHEST -> LocationParams.Builder()
157
+ .setAccuracy(LocationAccuracy.HIGH)
158
+ .setDistance(25f)
159
+ .setInterval(1000)
160
+
161
+ LocationModule.ACCURACY_BEST_FOR_NAVIGATION -> LocationParams.Builder()
162
+ .setAccuracy(LocationAccuracy.HIGH)
163
+ .setDistance(0f)
164
+ .setInterval(500)
165
+
166
+ else -> LocationParams.Builder()
167
+ .setAccuracy(LocationAccuracy.MEDIUM)
168
+ .setDistance(100f)
169
+ .setInterval(3000)
141
170
  }
142
171
  }
143
172
 
@@ -4,7 +4,6 @@ import android.Manifest
4
4
  import android.app.Activity
5
5
  import android.content.Context
6
6
  import android.content.Intent
7
- import android.content.pm.PackageManager
8
7
  import android.hardware.GeomagneticField
9
8
  import android.hardware.Sensor
10
9
  import android.hardware.SensorEvent
@@ -12,14 +11,11 @@ import android.hardware.SensorEventListener
12
11
  import android.hardware.SensorManager
13
12
  import android.location.Geocoder
14
13
  import android.location.Location
15
- import android.location.LocationManager
16
14
  import android.os.Build
17
15
  import android.os.Bundle
18
16
  import android.os.Looper
19
17
  import android.util.Log
20
18
  import androidx.annotation.ChecksSdkIntAtLeast
21
- import androidx.core.app.ActivityCompat
22
- import androidx.core.location.LocationManagerCompat
23
19
  import androidx.core.os.bundleOf
24
20
  import com.google.android.gms.common.api.ApiException
25
21
  import com.google.android.gms.common.api.CommonStatusCodes
@@ -55,6 +51,9 @@ import expo.modules.location.records.ReverseGeocodeLocation
55
51
  import expo.modules.location.records.ReverseGeocodeResponse
56
52
  import expo.modules.location.taskConsumers.GeofencingTaskConsumer
57
53
  import expo.modules.location.taskConsumers.LocationTaskConsumer
54
+ import io.nlopez.smartlocation.SmartLocation
55
+ import io.nlopez.smartlocation.geocoding.utils.LocationAddress
56
+ import io.nlopez.smartlocation.location.config.LocationParams
58
57
  import java.util.Locale
59
58
  import kotlin.coroutines.resume
60
59
  import kotlin.coroutines.resumeWithException
@@ -158,12 +157,21 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
158
157
  }
159
158
 
160
159
  AsyncFunction<LocationProviderStatus>("getProviderStatusAsync") {
161
- return@AsyncFunction getProviderStatus()
160
+ val state = SmartLocation.with(mContext).location().state()
161
+
162
+ return@AsyncFunction LocationProviderStatus().apply {
163
+ backgroundModeEnabled = state.locationServicesEnabled()
164
+ gpsAvailable = state.isGpsAvailable
165
+ networkAvailable = state.isNetworkAvailable
166
+ locationServicesEnabled = state.locationServicesEnabled()
167
+ passiveAvailable = state.isPassiveAvailable
168
+ }
162
169
  }
163
170
 
164
171
  AsyncFunction("watchDeviceHeading") { watchId: Int ->
165
172
  mHeadingId = watchId
166
- return@AsyncFunction startHeadingUpdate()
173
+ startHeadingUpdate()
174
+ return@AsyncFunction
167
175
  }
168
176
 
169
177
  AsyncFunction("watchPositionImplAsync") { watchId: Int, options: LocationOptions, promise: Promise ->
@@ -206,6 +214,7 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
206
214
  } else {
207
215
  removeLocationUpdatesForRequest(watchId)
208
216
  }
217
+ return@AsyncFunction
209
218
  }
210
219
 
211
220
  AsyncFunction("geocodeAsync") Coroutine { address: String ->
@@ -332,23 +341,6 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
332
341
  } ?: throw NoPermissionsModuleException()
333
342
  }
334
343
 
335
- private fun getProviderStatus(): LocationProviderStatus {
336
- val manager = mContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
337
-
338
- val isGpsAvailable = manager.isProviderEnabled(LocationManager.GPS_PROVIDER)
339
- val isNetworkAvailable = manager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
340
- val isLocationServicesEnabled = LocationManagerCompat.isLocationEnabled(manager)
341
- val isPassiveAvailable = manager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER)
342
-
343
- return LocationProviderStatus().apply {
344
- backgroundModeEnabled = isLocationServicesEnabled
345
- gpsAvailable = isGpsAvailable
346
- networkAvailable = isNetworkAvailable
347
- locationServicesEnabled = isLocationServicesEnabled
348
- passiveAvailable = isPassiveAvailable
349
- }
350
- }
351
-
352
344
  private suspend fun requestBackgroundPermissionsAsync(): PermissionRequestResponse {
353
345
  if (!isBackgroundPermissionInManifest()) {
354
346
  throw NoPermissionInManifestException("ACCESS_BACKGROUND_LOCATION")
@@ -508,54 +500,27 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
508
500
  }
509
501
 
510
502
  private fun startHeadingUpdate() {
511
- val locationManager = mContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
512
- if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
513
- ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
514
- ) {
515
- return
516
- }
517
- val lastLocation =
518
- locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
519
- ?: locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
520
-
521
- if (lastLocation != null) {
503
+ val locationControl = SmartLocation.with(mContext).location().oneFix().config(LocationParams.BEST_EFFORT)
504
+ val currLoc = locationControl.lastLocation
505
+ if (currLoc != null) {
522
506
  mGeofield = GeomagneticField(
523
- lastLocation.latitude.toFloat(),
524
- lastLocation.longitude.toFloat(),
525
- lastLocation.altitude.toFloat(),
507
+ currLoc.latitude.toFloat(), currLoc.longitude.toFloat(), currLoc.altitude.toFloat(),
526
508
  System.currentTimeMillis()
527
509
  )
528
510
  } else {
529
- val locationRequest = LocationRequest.Builder(
530
- LocationRequest.PRIORITY_HIGH_ACCURACY,
531
- 0L
532
- ).setMaxUpdates(1)
533
- .build()
534
-
535
- val locationCallback = object : LocationCallback() {
536
- override fun onLocationResult(locationResult: LocationResult) {
537
- locationResult.lastLocation?.let {
538
- mGeofield = GeomagneticField(
539
- it.latitude.toFloat(),
540
- it.longitude.toFloat(),
541
- it.altitude.toFloat(),
542
- System.currentTimeMillis()
543
- )
544
- }
545
- }
511
+ locationControl.start { location: Location ->
512
+ mGeofield = GeomagneticField(
513
+ location.latitude.toFloat(), location.longitude.toFloat(), location.altitude.toFloat(),
514
+ System.currentTimeMillis()
515
+ )
546
516
  }
547
- mLocationProvider.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
548
517
  }
549
518
  mSensorManager.registerListener(
550
519
  this,
551
520
  mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
552
521
  SensorManager.SENSOR_DELAY_NORMAL
553
522
  )
554
- mSensorManager.registerListener(
555
- this,
556
- mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
557
- SensorManager.SENSOR_DELAY_NORMAL
558
- )
523
+ mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL)
559
524
  }
560
525
 
561
526
  private fun sendUpdate() {
@@ -633,6 +598,7 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
633
598
  private fun stopWatching() {
634
599
  // if permissions not granted it won't work anyway, but this can be invoked when permission dialog appears
635
600
  if (Geocoder.isPresent() && !isMissingForegroundPermissions()) {
601
+ SmartLocation.with(mContext).geocoding().stop()
636
602
  mGeocoderPaused = true
637
603
  }
638
604
  for (requestId in mLocationCallbacks.keys) {
@@ -699,10 +665,8 @@ class LocationModule : Module(), LifecycleEventListener, SensorEventListener, Ac
699
665
  locations?.let { location ->
700
666
  location.let {
701
667
  val results = it.mapNotNull { address ->
702
- val newLocation = Location(LocationManager.GPS_PROVIDER)
703
- newLocation.latitude = address.latitude
704
- newLocation.longitude = address.longitude
705
- GeocodeResponse.from(newLocation)
668
+ val locationAddress = LocationAddress(address)
669
+ GeocodeResponse.from(locationAddress.location)
706
670
  }
707
671
  continuation.resume(results)
708
672
  }
@@ -1,2 +1,2 @@
1
- export declare const LocationEventEmitter: import("expo").EventEmitterType<Record<never, never>>;
1
+ export declare const LocationEventEmitter: import("expo-modules-core/types").EventEmitter<Record<never, never>>;
2
2
  //# sourceMappingURL=LocationEventEmitter.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LocationEventEmitter.web.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.web.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,uDAAqB,CAAC"}
1
+ {"version":3,"file":"LocationEventEmitter.web.d.ts","sourceRoot":"","sources":["../src/LocationEventEmitter.web.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,oBAAoB,sEAAqB,CAAC"}
@@ -1,9 +1,22 @@
1
1
  {
2
- "platforms": ["apple", "android"],
2
+ "platforms": [
3
+ "apple",
4
+ "android"
5
+ ],
3
6
  "apple": {
4
- "modules": ["LocationModule"]
7
+ "modules": [
8
+ "LocationModule"
9
+ ]
5
10
  },
6
11
  "android": {
7
- "modules": ["expo.modules.location.LocationModule"]
12
+ "modules": [
13
+ "expo.modules.location.LocationModule"
14
+ ],
15
+ "gradleAarProjects": [
16
+ {
17
+ "name": "io.nlopez.smartlocation-jetified-aar",
18
+ "aarFilePath": "android/libs/io.nlopez.smartlocation-3.3.3-jetified.aar"
19
+ }
20
+ ]
8
21
  }
9
22
  }
@@ -163,13 +163,9 @@ public final class LocationModule: Module {
163
163
  // Background location
164
164
 
165
165
  AsyncFunction("startLocationUpdatesAsync") { (taskName: String, options: [String: Any]) in
166
- // There are two ways of starting this service.
167
- // 1. As a background location service, this requires the background location permission.
168
- // 2. As a user-initiated foreground service, this does NOT require the background location permission.
169
- // Unfortunately, we cannot distinguish between those cases.
170
- // So we only check foreground permission which needs to be granted in both cases.
171
166
  try ensureLocationServicesEnabled()
172
167
  try ensureForegroundLocationPermissions(appContext)
168
+ try ensureBackgroundLocationPermissions(appContext)
173
169
 
174
170
  guard CLLocationManager.significantLocationChangeMonitoringAvailable() else {
175
171
  throw Exceptions.LocationUpdatesUnavailable()
@@ -10,6 +10,7 @@ static SEL alwaysAuthorizationSelector;
10
10
  @interface EXBackgroundLocationPermissionRequester ()
11
11
 
12
12
  @property (nonatomic, assign) bool wasAsked;
13
+ @property (nonatomic, assign) bool isWaitingForTimeout;
13
14
 
14
15
  @end
15
16
 
@@ -19,6 +20,7 @@ static SEL alwaysAuthorizationSelector;
19
20
  {
20
21
  if (self = [super init]) {
21
22
  _wasAsked = false;
23
+ _isWaitingForTimeout = false;
22
24
  }
23
25
  return self;
24
26
  }
@@ -37,10 +39,28 @@ static SEL alwaysAuthorizationSelector;
37
39
  {
38
40
  if ([EXBaseLocationRequester isConfiguredForAlwaysAuthorization] && [self.locationManager respondsToSelector:alwaysAuthorizationSelector]) {
39
41
  _wasAsked = true;
40
- [[NSNotificationCenter defaultCenter] addObserver:self
41
- selector:@selector(handleAppBecomingActive)
42
- name:UIApplicationDidBecomeActiveNotification
43
- object:nil];
42
+ CLAuthorizationStatus status = [self.locationManager authorizationStatus];
43
+
44
+ if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
45
+ // We already have a foreground permission granted:
46
+ // When asking for background location, we might or might not have asked for foreground permission
47
+ // before we get here. An issue here is if the user has a temporary permission ("Allow once") - which
48
+ // results in the status being "kCLAuthorizationStatusAuthorizedWhenInUse" - without us knowing.
49
+ // We need to handle this special case which is not possible to detect through the API.
50
+ // What we do is that we'll wait 1.5 seconds on an UIApplicationWillResignActiveNotification
51
+ // notification (which will be emitted almost directly if the permission dialog is displayed). If the permission
52
+ // dialog is not displayed we'll timeout and can resolve the waiting promise with an updated denied status.
53
+ [[NSNotificationCenter defaultCenter] addObserver:self
54
+ selector:@selector(handleAppBecomingInactive)
55
+ name:UIApplicationWillResignActiveNotification
56
+ object:nil];
57
+
58
+ // Setup timeout - if no permission dialog was displayed we can just stop listening and deny
59
+ // the request
60
+ [self setupAppInactivateTimeout];
61
+ }
62
+
63
+ // Request permissions
44
64
  ((void (*)(id, SEL))objc_msgSend)(self.locationManager, alwaysAuthorizationSelector);
45
65
  } else {
46
66
  self.reject(@"ERR_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil);
@@ -50,9 +70,6 @@ static SEL alwaysAuthorizationSelector;
50
70
  }
51
71
  }
52
72
 
53
- // If user selects "Keep Only While Using" option, the `locationManagerDidChangeAuthorization` won't be called.
54
- // So we don't know when we should resolve promise.
55
- // Hovewer, we can check for `UIApplicationDidBecomeActiveNotification` event which is called when permissions modal disappears.
56
73
  - (void)handleAppBecomingActive
57
74
  {
58
75
  [[NSNotificationCenter defaultCenter] removeObserver:self];
@@ -63,6 +80,47 @@ static SEL alwaysAuthorizationSelector;
63
80
  }
64
81
  }
65
82
 
83
+ - (void)handleAppBecomingInactive
84
+ {
85
+ // Let's wait until the app becomes inactive - this happens when OS displays the
86
+ // permission dialog - then we can cancel the timeout handler.
87
+
88
+ _isWaitingForTimeout = false;
89
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
90
+
91
+ // When the app is inactive it means that a permission dialog is showing and we should ask to be
92
+ // notified when the dialog is closed:
93
+ [[NSNotificationCenter defaultCenter] addObserver:self
94
+ selector:@selector(handleAppBecomingActive)
95
+ name:UIApplicationDidBecomeActiveNotification
96
+ object:nil];
97
+ }
98
+
99
+ - (void)setupAppInactivateTimeout
100
+ {
101
+ _isWaitingForTimeout = true;
102
+
103
+ // Obtain a reference to the current queue
104
+ dispatch_queue_t currentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
105
+
106
+ // Calculate the time for the delay
107
+ dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC));
108
+
109
+ EX_WEAKIFY(self);
110
+
111
+ // Schedule the block to be executed after the delay
112
+ dispatch_after(delayTime, currentQueue, ^{
113
+ EX_ENSURE_STRONGIFY(self)
114
+ // Check if we are still waiting - ie. we haven't seen a permission dialog
115
+ if (self.isWaitingForTimeout && self.resolve) {
116
+ self.isWaitingForTimeout = false;
117
+ self.resolve([self getPermissions]);
118
+ self.resolve = nil;
119
+ self.reject = nil;
120
+ }
121
+ });
122
+ }
123
+
66
124
  - (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus
67
125
  {
68
126
  EXPermissionStatus status;
@@ -3,8 +3,9 @@
3
3
  #import <CoreLocation/CLLocationManager.h>
4
4
 
5
5
  #import <ExpoModulesCore/EXPermissionsInterface.h>
6
+ #import <CoreLocation/CLLocationManagerDelegate.h>
6
7
 
7
- @interface EXBaseLocationRequester : NSObject<EXPermissionsRequester>
8
+ @interface EXBaseLocationRequester : NSObject<EXPermissionsRequester, CLLocationManagerDelegate>
8
9
 
9
10
  @property (nonatomic, strong) CLLocationManager *locationManager;
10
11
  @property (nonatomic, strong) EXPromiseResolveBlock resolve;
@@ -16,6 +17,4 @@
16
17
  - (void)requestLocationPermissions;
17
18
  - (NSDictionary *)parsePermissions:(CLAuthorizationStatus)systemStatus;
18
19
 
19
- - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
20
-
21
20
  @end
@@ -4,12 +4,11 @@
4
4
  #import <ExpoModulesCore/EXUtilities.h>
5
5
 
6
6
  #import <objc/message.h>
7
- #import <CoreLocation/CLLocationManagerDelegate.h>
8
7
 
9
8
  @interface EXBaseLocationRequester () <CLLocationManagerDelegate>
10
9
 
11
10
  @property (nonatomic, assign) bool locationManagerWasCalled;
12
-
11
+ @property (nonatomic, assign) CLAuthorizationStatus beginStatus;
13
12
 
14
13
  @end
15
14
 
@@ -73,25 +72,47 @@
73
72
  // make Expo developers receive this kind of messages nor add our own default usage description,
74
73
  // we try to fool the static analyzer and construct the selector in runtime.
75
74
  // This way behavior of this requester is governed by provided NSLocationUsageDescriptions.
75
+
76
+ // 2. Location permission request types
76
77
  //
77
- // 2. Why there's no way to call specifically whenInUse or always authorization?
78
+ // Foreground
79
+ // - "Allow once"
80
+ // - "Allow while using App"
81
+ // - "Don't allow"
78
82
  //
79
- // The requester sets itself as the delegate of the CLLocationManager, so when the user responds
80
- // to a permission requesting dialog, manager calls `locationManager:didChangeAuthorizationStatus:` method.
81
- // To be precise, manager calls this method in two circumstances:
82
- // - right when `request*Authorization` method is called,
83
- // - when `authorizationStatus` changes.
84
- // With this behavior we aren't able to support the following use case:
85
- // - app requests `whenInUse` authorization
86
- // - user allows `whenInUse` authorization
87
- // - `authorizationStatus` changes from `undetermined` to `whenInUse`, callback is called, promise is resolved
88
- // - app wants to escalate authorization to `always`
89
- // - user selects `whenInUse` authorization (iOS 11+)
90
- // - `authorizationStatus` doesn't change, so callback is not called and requester can't know whether
91
- // user responded to the dialog selecting `whenInUse` or is still deciding
92
- // To support this use case we will have to change the way location authorization is requested
93
- // from promise-based to listener-based.
94
-
83
+ // Background
84
+ // - "Keep only while using"
85
+ // - "Change to always allow"
86
+ //
87
+ // Requesting background permissions directly without first asking for foreground permissions is the
88
+ // same as asking for foreground permissions and then asking for background permissions.
89
+ //
90
+ // "Allow once" is a temporary permission (limited to the current app session). It is not possible to get
91
+ // info from the API about wether or not the current permission is temporary. You cannot request background
92
+ // permissions with a temporary token - a background request will then return denied.
93
+ //
94
+ // Requesting background permissions directly and "Allow while using the App" gives you a provisional
95
+ // background permission that can later be elevated to a full "Always allow" permission.
96
+ // You will be asked at a later point if you want to convert to "Always allow". The system waits until
97
+ // you have started using the newly aquired permission before showing the permission dialog.
98
+ //
99
+ // Test the following scenarios in BareExpo -> APIs -> Location
100
+ // ------------------------------------------------------------
101
+ // (before tests, make sure to clear any location permissions and restart the app)
102
+ //
103
+ // rfp = requestForegroundPermissionsAsync, fp: Actual foreground permission given
104
+ // rbp = requestBackgroundPermissionsAsync, bg: Actual background permission given
105
+ //
106
+ // - rfp -> "Allow once", then rbp -> no dialog = (fp: granted (temporary), bg: denied after 1.5 seconds)
107
+ // - rfp -> "Allow while using App", then rbp -> "Keep only while using" = (fp: granted, bg: denied)
108
+ // - rfp -> "Allow while using App", then rbp -> "Change to always allow" = (fp: granted, bg: granted)
109
+ // - rfp -> "Don't allow", then rbp -> no dialog = (fp: denied, bg: denied)
110
+ // - rbp -> "Allow once", no more dalogs = (fp: granted (temporary), bg: denied)
111
+ // - rbp -> "Allow while using App", no more dialogs = (fp: granted, bg: granted (provisional))
112
+ // - rbp -> "Don't allow" = (fp: denied, bg: denied)
113
+
114
+ // Save start statue and call requestLocationPermissions
115
+ _beginStatus = [self.locationManager authorizationStatus];
95
116
  [self requestLocationPermissions];
96
117
  }
97
118
  }
@@ -107,38 +128,15 @@
107
128
  }
108
129
  }
109
130
 
110
- - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
111
- {
112
- // TODO: Permissions.LOCATION issue (search by this phrase)
113
- // if Permissions.LOCATION is being called for the first time on iOS devide and prompts for user action it might not call this callback at all
114
- // it happens if user requests more that one permission at the same time via Permissions.askAsync(...) and LOCATION dialog is not being called first
115
- // to reproduce this find NCL code testing that
116
- if (status == kCLAuthorizationStatusNotDetermined || !_locationManagerWasCalled) {
117
- // CLLocationManager calls this delegate method once on start with kCLAuthorizationNotDetermined even before the user responds
118
- // to the "Don't Allow" / "Allow" dialog box. This isn't the event we care about so we skip it. See:
119
- // http://stackoverflow.com/questions/30106341/swift-locationmanager-didchangeauthorizationstatus-always-called/30107511#30107511
120
- _locationManagerWasCalled = true;
121
- return;
122
- }
123
-
124
- if (_resolve) {
125
- _resolve([self getPermissions]);
126
- _resolve = nil;
127
- _reject = nil;
128
- }
129
- }
130
-
131
131
  - (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager
132
132
  {
133
- CLAuthorizationStatus status = [manager authorizationStatus];
134
- if (status == kCLAuthorizationStatusNotDetermined || !_locationManagerWasCalled) {
133
+ CLAuthorizationStatus nextState = [manager authorizationStatus];
134
+ if (_beginStatus == nextState && !_locationManagerWasCalled) {
135
135
  // CLLocationManager calls this delegate method once on start with kCLAuthorizationNotDetermined even before the user responds
136
136
  // to the "Don't Allow" / "Allow" dialog box. This isn't the event we care about so we skip it. See:
137
137
  // http://stackoverflow.com/questions/30106341/swift-locationmanager-didchangeauthorizationstatus-always-called/30107511#30107511
138
138
  _locationManagerWasCalled = true;
139
- if (status != kCLAuthorizationStatusAuthorizedWhenInUse) {
140
- return;
141
- }
139
+ return;
142
140
  }
143
141
 
144
142
  if (_resolve) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-location",
3
- "version": "18.0.10-canary-20250331-817737a",
3
+ "version": "18.0.10",
4
4
  "description": "Allows reading geolocation information from the device. Your app can poll for the current location or subscribe to location update events.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -39,9 +39,10 @@
39
39
  "preset": "expo-module-scripts"
40
40
  },
41
41
  "devDependencies": {
42
- "expo-module-scripts": "4.0.5-canary-20250331-817737a"
42
+ "expo-module-scripts": "^4.0.4"
43
43
  },
44
44
  "peerDependencies": {
45
- "expo": "53.0.0-canary-20250331-817737a"
46
- }
45
+ "expo": "*"
46
+ },
47
+ "gitHead": "b08a0bd52965f85871c12c31da16a45e2cd26c4c"
47
48
  }
@@ -1,3 +0,0 @@
1
- # https://github.com/expo/expo/issues/3918
2
-
3
- -keep class expo.modules.location.taskConsumers.** { *; }
@@ -1,14 +0,0 @@
1
- package expo.modules.location
2
-
3
- data class LocationParams(
4
- val accuracy: LocationAccuracy,
5
- var distance: Float,
6
- var interval: Long
7
- )
8
-
9
- enum class LocationAccuracy {
10
- LOWEST,
11
- LOW,
12
- MEDIUM,
13
- HIGH
14
- }