-
-
Notifications
You must be signed in to change notification settings - Fork 836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle upgrading from location when in use to location always on iOS #716
Conversation
It looks like the promise resolve to early: RPReplay_Final1663665401.MP4RPReplay_Final1663665784.MP4 |
@zoontek that led me down a rabbit hole -- and the solution was a bit unorthodox but from my testing this new commit seems to cover every situation! |
Hey @zoontek just following up if you had any more thoughts on this! |
Unfortunately, it still fails in some cases. Allowing once, then requesting always doesn't resolve the first time: RPReplay_Final1665238739.MP4 |
@zoontek if you're talking about requesting Here's what I see:
allow_once.mp4
allow_when_in_use.mp4
when_in_use_then_always.mp4In all cases, I'm also not seeing the case where allowing once, then requesting always does not resolve. allow_once_and_request_always_resolves.mp4And on a real device: RPReplay_Final1665327190.MP4 |
I am new to using the library, but if it helps at all, I was able to test this successfully on an Iphone SE 2020 running iOS 15.6 and on simulator running iOS 16. So far it has resolved on the first time as expected, I'll be testing more today and tomorrow though. React native version 0.67.2 |
I was able to get this to work on iOS 15 (simulator) and 16 (real device). However, for iOS 14.5 and 12.4 on the simulator, the always prompt briefly appears and then disappears without user action. location_permission_disappears.mp4 |
Just a message to say that we are experiencing the same issue. I also join @tallpants opinion that iOS allows us asking for |
@ag-drivequant @ddarren @tallpants @zoontek to fix this issue with iOS < 15 you just need to modify/add this code (Many Thanks to @rformato for the contribution):
The fix is explained here https://stackoverflow.com/a/9474095 Any chance to merge this PR? |
any updates about merging this PR? |
can we get this one merged? I tried a patch but build is failing :/
|
@woodybury The current state of this feature doesn't work correctly, so I can't merge it (never resolving Promise) But good news, I can work on it (see #808)! If your company really needs it, contact me 🙂 |
thanks @zoontek got it to compile and "work" (meaning I can request user to change to always) with this simple patch:
but obviously I need the checks to work too :/ definitely +1 for this feature request. If I have the capacity to fix the checks I'll open a PR. Keep me posted - thanks! |
@zoontek @woodybury I have created a patch to handle this case: index e20d4fe..56a986e 100644
--- a/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
+++ b/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
@@ -3,9 +3,8 @@
@import CoreLocation;
@import UIKit;
-@interface RNPermissionHandlerLocationAlways() <CLLocationManagerDelegate>
+@interface RNPermissionHandlerLocationAlways()
-@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) void (^resolve)(RNPermissionStatus status);
@property (nonatomic, strong) void (^reject)(NSError *error);
@@ -13,6 +12,9 @@ @interface RNPermissionHandlerLocationAlways() <CLLocationManagerDelegate>
@implementation RNPermissionHandlerLocationAlways
+static NSString* SETTING_KEY = @"@RNPermissions:Requested";
+CLLocationManager *locationManager;
+
+ (NSArray<NSString *> * _Nonnull)usageDescriptionKeys {
return @[@"NSLocationAlwaysAndWhenInUseUsageDescription"];
}
@@ -28,7 +30,13 @@ - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
return resolve(RNPermissionStatusNotDetermined);
case kCLAuthorizationStatusRestricted:
return resolve(RNPermissionStatusRestricted);
- case kCLAuthorizationStatusAuthorizedWhenInUse:
+ case kCLAuthorizationStatusAuthorizedWhenInUse: {
+ BOOL requestedBefore = [self isFlaggedAsRequested:[[self class] handlerUniqueId]];
+ if (requestedBefore) {
+ return resolve(RNPermissionStatusDenied);
+ }
+ return resolve(RNPermissionStatusNotDetermined);
+ }
case kCLAuthorizationStatusDenied:
return resolve(RNPermissionStatusDenied);
case kCLAuthorizationStatusAuthorizedAlways:
@@ -38,22 +46,67 @@ - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
- (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
rejecter:(void (^ _Nonnull)(NSError * _Nonnull))reject {
- if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined) {
- return [self checkWithResolver:resolve rejecter:reject];
- }
+ CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus];
+ BOOL requestedBefore = [self isFlaggedAsRequested:[[self class] handlerUniqueId]];
+ if (authorizationStatus != kCLAuthorizationStatusNotDetermined && !(authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse && !requestedBefore)) {
+ return [self checkWithResolver:resolve rejecter:reject];
+ }
+
+ _resolve = resolve;
+ _reject = reject;
- _resolve = resolve;
- _reject = reject;
+ // When we request location always permission, if the user selects "Keep Only While Using", iOS
+ // won't trigger the locationManager:didChangeAuthorizationStatus: delegate method. This means we
+ // can't know when the user has responded to the permission prompt directly.
+ //
+ // We can get around this by listening for the UIApplicationDidBecomeActiveNotification event which posts
+ // when the application regains focus from the permission prompt. When this happens we'll
+ // trigger the applicationDidBecomeActive method on this class, and we'll check the authorization status and
+ // resolve the promise there -- letting us stay consistent with our promise-based API.
+ //
+ // References:
+ // ===========
+ // CLLocationManager requestAlwaysAuthorization:
+ // https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?language=objc
+ //
+ // NSNotificationCenter addObserver:
+ // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1415360-addobserver
+ //
+ // UIApplicationDidBecomeActiveNotification:
+ // https://developer.apple.com/documentation/uikit/uiapplicationdidbecomeactivenotification
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationDidBecomeActive)
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
- _locationManager = [CLLocationManager new];
- [_locationManager setDelegate:self];
- [_locationManager requestAlwaysAuthorization];
+ locationManager = [CLLocationManager new];
+ [locationManager requestAlwaysAuthorization];
+ [self flagAsRequested:[[self class] handlerUniqueId]];
}
-- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
- if (status != kCLAuthorizationStatusNotDetermined) {
- [_locationManager setDelegate:nil];
- [self checkWithResolver:_resolve rejecter:_reject];
+- (void)applicationDidBecomeActive {
+ [self checkWithResolver:_resolve rejecter:_reject];
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];}
+
+- (bool)isFlaggedAsRequested:(NSString * _Nonnull)handlerId {
+ NSArray<NSString *> *requested = [[NSUserDefaults standardUserDefaults] arrayForKey:SETTING_KEY];
+ return requested == nil ? false : [requested containsObject:handlerId];
+}
+
+- (void)flagAsRequested:(NSString * _Nonnull)handlerId {
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ NSMutableArray *requested = [[userDefaults arrayForKey:SETTING_KEY] mutableCopy];
+
+ if (requested == nil) {
+ requested = [NSMutableArray new];
+ }
+
+ if (![requested containsObject:handlerId]) {
+ [requested addObject:handlerId];
+ [userDefaults setObject:requested forKey:SETTING_KEY];
+ [userDefaults synchronize];
}
}
|
@zoontek, First off thank you for your hard work on this project. After reading through the discussion, it seems like this PR would not be an acceptable approach according to your criteria because of the observer's possible never ending promise. I have done surface level testing with @tallpants / @alessioemireni's patch and it works! Seems like you are expecting a different approach than to listen to the notification change. If so, should we close this PR? Or are you willing to consider this approach. From my bit of testing, the observer seems work well because the native popup can't be dismissed unless the phone is locked which seems to set it "Keep While Using app", so I don't see a case where the promise would be never ending (I could be completely wrong, but just trying to learn more native code). @tallpants @alessioemireni please chime in. I would love your feedback and thoughts. Thank you for your contributions. |
So after my testing of @alessioemireni's code. I had some great results! However, I did find one condition where if the user had selected "Allow once" then "Allow always" was requested the notification would not be posted and the promise would hang. No worries, I have a solution. I created a listener to track if the notification is posted (via UIApplicationWillResignActiveNotification). If after 0.25 seconds the notification has not been posted, then Would love to get some feedback. diff --git a/node_modules/react-native-permissions/ios/.DS_Store b/node_modules/react-native-permissions/ios/.DS_Store
new file mode 100644
index 0000000..85e3a0c
Binary files /dev/null and b/node_modules/react-native-permissions/ios/.DS_Store differ
diff --git a/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m b/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
index e20d4fe..f706695 100644
--- a/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
+++ b/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
@@ -3,9 +3,11 @@
@import CoreLocation;
@import UIKit;
-@interface RNPermissionHandlerLocationAlways() <CLLocationManagerDelegate>
+@interface RNPermissionHandlerLocationAlways()
+{
+ BOOL notified;
+}
-@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) void (^resolve)(RNPermissionStatus status);
@property (nonatomic, strong) void (^reject)(NSError *error);
@@ -13,6 +15,9 @@ @interface RNPermissionHandlerLocationAlways() <CLLocationManagerDelegate>
@implementation RNPermissionHandlerLocationAlways
+static NSString* SETTING_KEY = @"@RNPermissions:Requested";
+CLLocationManager *locationManager;
+
+ (NSArray<NSString *> * _Nonnull)usageDescriptionKeys {
return @[@"NSLocationAlwaysAndWhenInUseUsageDescription"];
}
@@ -28,7 +33,13 @@ - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
return resolve(RNPermissionStatusNotDetermined);
case kCLAuthorizationStatusRestricted:
return resolve(RNPermissionStatusRestricted);
- case kCLAuthorizationStatusAuthorizedWhenInUse:
+ case kCLAuthorizationStatusAuthorizedWhenInUse: {
+ BOOL requestedBefore = [self isFlaggedAsRequested:[[self class] handlerUniqueId]];
+ if (requestedBefore) {
+ return resolve(RNPermissionStatusDenied);
+ }
+ return resolve(RNPermissionStatusNotDetermined);
+ }
case kCLAuthorizationStatusDenied:
return resolve(RNPermissionStatusDenied);
case kCLAuthorizationStatusAuthorizedAlways:
@@ -38,21 +49,92 @@ - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
- (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
rejecter:(void (^ _Nonnull)(NSError * _Nonnull))reject {
- if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined) {
- return [self checkWithResolver:resolve rejecter:reject];
- }
+ CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus];
+ BOOL requestedBefore = [self isFlaggedAsRequested:[[self class] handlerUniqueId]];
+ if (authorizationStatus != kCLAuthorizationStatusNotDetermined && !(authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse && !requestedBefore)) {
+ return [self checkWithResolver:resolve rejecter:reject];
+ }
+
+ _resolve = resolve;
+ _reject = reject;
- _resolve = resolve;
- _reject = reject;
+ // When we request location always permission, if the user selects "Keep Only While Using", iOS
+ // won't trigger the locationManager:didChangeAuthorizationStatus: delegate method. This means we
+ // can't know when the user has responded to the permission prompt directly.
+ //
+ // We can get around this by listening for the UIApplicationDidBecomeActiveNotification event which posts
+ // when the application regains focus from the permission prompt. When this happens we'll
+ // trigger the applicationDidBecomeActive method on this class, and we'll check the authorization status and
+ // resolve the promise there -- letting us stay consistent with our promise-based API.
+ //
+ // In addition, we'll also set a timeout of 0.25 seconds to resolve the promise if the notification fails to occur.
+ // We check by listening to UIApplicationWillResignActiveNotification and setting a flag if the notification occurs.
+ // This is to handle the case where the user has selected "Allow once" and cannot be prompted to "Allow Always"
+ // which results in no notification being posted.
+ //
+ // References:
+ // ===========
+ // CLLocationManager requestAlwaysAuthorization:
+ // https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?language=objc
+ //
+ // NSNotificationCenter addObserver:
+ // https://developer.apple.com/documentation/foundation/nsnotificationcenter/1415360-addobserver
+ //
+ // UIApplicationDidBecomeActiveNotification:
+ // https://developer.apple.com/documentation/uikit/uiapplicationdidbecomeactivenotification
+ //
+ // UIApplicationWillResignActiveNotification:
+ // https://developer.apple.com/documentation/uikit/uiapplicationwillresignactivenotification
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(notificationOccurred:)
+ name:UIApplicationWillResignActiveNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(applicationDidBecomeActive)
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
+ [self performSelector:@selector(onRequestTimeout) withObject:nil afterDelay:0.25];
+
+ locationManager = [CLLocationManager new];
+ [locationManager requestAlwaysAuthorization];
+ [self flagAsRequested:[[self class] handlerUniqueId]];
+}
+
+- (void)notificationOccurred:(NSNotification *)notification {
+ notified = YES;
+}
- _locationManager = [CLLocationManager new];
- [_locationManager setDelegate:self];
- [_locationManager requestAlwaysAuthorization];
+- (void)applicationDidBecomeActive {
+ [self checkWithResolver:_resolve rejecter:_reject];
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];}
+
+- (bool)isFlaggedAsRequested:(NSString * _Nonnull)handlerId {
+ NSArray<NSString *> *requested = [[NSUserDefaults standardUserDefaults] arrayForKey:SETTING_KEY];
+ return requested == nil ? false : [requested containsObject:handlerId];
+}
+
+- (void)flagAsRequested:(NSString * _Nonnull)handlerId {
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ NSMutableArray *requested = [[userDefaults arrayForKey:SETTING_KEY] mutableCopy];
+
+ if (requested == nil) {
+ requested = [NSMutableArray new];
+ }
+
+ if (![requested containsObject:handlerId]) {
+ [requested addObject:handlerId];
+ [userDefaults setObject:requested forKey:SETTING_KEY];
+ [userDefaults synchronize];
+ }
}
-- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
- if (status != kCLAuthorizationStatusNotDetermined) {
- [_locationManager setDelegate:nil];
+- (void)onRequestTimeout {
+ if (!notified) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:UIApplicationDidBecomeActiveNotification
+ object:nil];
[self checkWithResolver:_resolve rejecter:_reject];
}
} |
@alexkev This might be a good solution 👍 I'm currently working on next major version (as iOS 18 and Android 15 are around the corner, I doubt they will not add breaking changes to permissions again 😅), if it works correctly I will include it in the beta. |
The feature landed in The idea is kinda similar to solutions proposed here, but without relying on storage (flagging permission as requested in It also comes with an improved documentation about the different flows |
Summary
LOCATION_ALWAYS
permission if you already hadLOCATION_WHEN_IN_USE
permission.LocationAlways
pod.Implementation Changes
check
:AuthorizedWhenInUse
, and we have not requestedLOCATION_ALWAYS
in the past, we returnNotDetermined
.AuthorizedWhenInUse
and we have requestedLOCATION_ALWAYS
in the past, we returnDenied
, since we're not allowed to prompt for this permission twice.request
:AuthorizedWhenInUse
and we have not requestedLOCATION_ALWAYS
in the past, we callrequestAlwaysAuthorization
and flag the permission as requested.AuthorizedWhenInUse
and we have requestedLOCATION_ALWAYS
in the past, then we return the current authorization result, since we're not allowed to prompt for this permission twice.Test Plan
LOCATION_WHEN_IN_USE
permission.LOCATION_ALWAYS
permission.Compatibility
Checklist
README.md
example/App.tsx
)