expo-location 18.0.9 → 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
@@ -10,6 +10,13 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 18.0.10 — 2025-04-01
14
+
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
+
13
20
  ## 18.0.9 — 2025-03-31
14
21
 
15
22
  _This version does not introduce any user-facing changes._
@@ -31,6 +38,7 @@ _This version does not introduce any user-facing changes._
31
38
 
32
39
  ### 🐛 Bug fixes
33
40
 
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))
34
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))
35
43
 
36
44
  ## 18.0.6 — 2025-02-10
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '18.0.9'
4
+ version = '18.0.10'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.location"
15
15
  defaultConfig {
16
16
  versionCode 29
17
- versionName "18.0.9"
17
+ versionName "18.0.10"
18
18
  }
19
19
  }
20
20
 
@@ -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) {
@@ -68,11 +68,10 @@
68
68
  EXLocationAccuracy accuracy = [options[@"accuracy"] unsignedIntegerValue] ?: EXLocationAccuracyBalanced;
69
69
 
70
70
  locationManager.desiredAccuracy = [EXLocation CLLocationAccuracyFromOption:accuracy];
71
- locationManager.distanceFilter = [options[@"distanceInterval"] doubleValue] ?: kCLDistanceFilterNone;
72
- locationManager.activityType = [EXLocation CLActivityTypeFromOption:[options[@"activityType"] integerValue]];
73
- locationManager.pausesLocationUpdatesAutomatically = [options[@"pausesUpdatesAutomatically"] boolValue];
74
-
75
- locationManager.showsBackgroundLocationIndicator = [options[@"showsBackgroundLocationIndicator"] boolValue];
71
+ locationManager.distanceFilter = [self numberToDouble:options[@"distanceInterval"] defaultValue:kCLDistanceFilterNone];
72
+ locationManager.activityType = [EXLocation CLActivityTypeFromOption:[self numberToInteger:options[@"activityType"] defaultValue:CLActivityTypeOther]];
73
+ locationManager.pausesLocationUpdatesAutomatically = [self numberToBool:options[@"pausesUpdatesAutomatically"] defaultValue:true];
74
+ locationManager.showsBackgroundLocationIndicator = [self numberToBool:options[@"showsBackgroundLocationIndicator"] defaultValue:false];
76
75
 
77
76
  [locationManager startUpdatingLocation];
78
77
  [locationManager startMonitoringSignificantLocationChanges];
@@ -91,15 +90,7 @@
91
90
 
92
91
  - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
93
92
  {
94
- if (error.domain == kCLErrorDomain) {
95
- // This error might happen when the device is not able to find out the location. Try to restart monitoring location.
96
- [manager stopUpdatingLocation];
97
- [manager stopMonitoringSignificantLocationChanges];
98
- [manager startUpdatingLocation];
99
- [manager startMonitoringSignificantLocationChanges];
100
- } else {
101
- [_task executeWithData:nil withError:error];
102
- }
93
+ [_task executeWithData:nil withError:error];
103
94
  }
104
95
 
105
96
  # pragma mark - internal
@@ -172,7 +163,17 @@
172
163
 
173
164
  - (double)numberToDouble:(NSNumber *)number defaultValue:(double)defaultValue
174
165
  {
175
- return number == nil ? defaultValue : [number doubleValue];
166
+ return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number doubleValue];
167
+ }
168
+
169
+ - (NSInteger)numberToInteger:(NSNumber *)number defaultValue:(NSInteger)defaultValue
170
+ {
171
+ return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number integerValue];
172
+ }
173
+
174
+ - (BOOL)numberToBool:(NSNumber *)number defaultValue:(BOOL)defaultValue
175
+ {
176
+ return [number isEqual:[NSNull null]] || number == nil ? defaultValue : [number boolValue];
176
177
  }
177
178
 
178
179
  + (NSArray<NSDictionary *> *)_exportLocations:(NSArray<CLLocation *> *)locations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-location",
3
- "version": "18.0.9",
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",
@@ -44,5 +44,5 @@
44
44
  "peerDependencies": {
45
45
  "expo": "*"
46
46
  },
47
- "gitHead": "efc676ef6dbe6c2eaed19c1a31abb79a935f583a"
47
+ "gitHead": "b08a0bd52965f85871c12c31da16a45e2cd26c4c"
48
48
  }