Skip to content
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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

tallpants
Copy link

Summary

Implementation Changes

  • For check:
    • If the current authorization status is AuthorizedWhenInUse, and we have not requested LOCATION_ALWAYS in the past, we return NotDetermined.
    • If the current authorization status is AuthorizedWhenInUse and we have requested LOCATION_ALWAYS in the past, we return Denied, since we're not allowed to prompt for this permission twice.
  • For request:
    • If the current authorization status is AuthorizedWhenInUse and we have not requested LOCATION_ALWAYS in the past, we call requestAlwaysAuthorization and flag the permission as requested.
    • If the current authorization status is AuthorizedWhenInUse and we have requested LOCATION_ALWAYS in the past, then we return the current authorization result, since we're not allowed to prompt for this permission twice.

Test Plan

  • Request and grant LOCATION_WHEN_IN_USE permission.
  • Then request LOCATION_ALWAYS permission.
Before After
Before After

Compatibility

OS Implemented
iOS
Android N/A

Checklist

  • I have tested this on a device and a simulator
  • I added the documentation in README.md
  • I added a sample use of the API in the example project (example/App.tsx)

@zoontek
Copy link
Owner

zoontek commented Sep 20, 2022

It looks like the promise resolve to early:
Also, it doesn't seems that applicationDidBecameActive is called (at least, on iOS 16).

RPReplay_Final1663665401.MP4
RPReplay_Final1663665784.MP4

@tallpants
Copy link
Author

tallpants commented Sep 20, 2022

@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!

@tallpants
Copy link
Author

Hey @zoontek just following up if you had any more thoughts on this!

@zoontek
Copy link
Owner

zoontek commented Oct 8, 2022

Unfortunately, it still fails in some cases. Allowing once, then requesting always doesn't resolve the first time:

RPReplay_Final1665238739.MP4

@tallpants
Copy link
Author

@zoontek if you're talking about requesting LOCATION_ALWAYS, selecting "Allow Once", and then requesting again -- that appears to be the intended behaviour.

Here's what I see:

  1. On requesting LOCATION_ALWAYS, and selecting "Allow Once" -- LOCATION_WHEN_IN_USE is granted until the app is closed and opened again, at which point any location permission is removed, but is requestable again:
allow_once.mp4
  1. On requesting LOCATION_ALWAYS and selecting "Allow When In Use" -- the app receives provisional always authorization (described here: https://developer.apple.com/videos/play/wwdc2019/705/?time=195):
allow_when_in_use.mp4
  1. On requesting LOCATION_WHEN_IN_USE, granting it, and then requesting an LOCATION_ALWAYS -- the app correctly prompts the user to upgrade the when-in-use permission to always:
when_in_use_then_always.mp4

In all cases, LOCATION_ALWAYS can only be requested once as described here: https://developer.apple.com/documentation/bundleresources/information_property_list/protected_resources/choosing_the_location_services_authorization_to_request?language=objc

CleanShot 2022-10-09 at 10 46 16@2x

I'm also not seeing the case where allowing once, then requesting always does not resolve.
Here it is on the simulator:

allow_once_and_request_always_resolves.mp4

And on a real device:

RPReplay_Final1665327190.MP4

@jimfoambox
Copy link

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

@ddarren
Copy link

ddarren commented Nov 2, 2022

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

@ag-drivequant
Copy link

ag-drivequant commented Nov 10, 2022

Just a message to say that we are experiencing the same issue.

I also join @tallpants opinion that iOS allows us asking for LOCATION_ALWAYS only once, so asking twice for the permission will not work, and that's the expected behaviour. (from user experience point of view, this can be enhanced by checking the status and explaining why it will not prompt for the permission.)

@alessioemireni
Copy link

alessioemireni commented Mar 10, 2023

@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):

CLLocationManager *locationManager;

...

locationManager = [CLLocationManager new];
[locationManager requestAlwaysAuthorization];

The fix is explained here https://stackoverflow.com/a/9474095

Any chance to merge this PR?

@sayurimizuguchi
Copy link

any updates about merging this PR?

@woodybury
Copy link

woodybury commented Oct 4, 2023

can we get this one merged? I tried a patch but build is failing :/

  34 |         return resolve(RNPermissionStatusRestricted);
  35 |       case kCLAuthorizationStatusAuthorizedWhenInUse: {
> 36 |         BOOL requestedBefore = [RNPermissions isFlaggedAsRequested:[[self class] handlerUniqueId]];
     |                                 ^ use of undeclared identifier 'RNPermissions'
  37 |         if (requestedBefore) {
  38 |           return resolve(RNPermissionStatusDenied);
  39 |         }

@zoontek
Copy link
Owner

zoontek commented Oct 4, 2023

@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 🙂

@woodybury
Copy link

thanks @zoontek got it to compile and "work" (meaning I can request user to change to always) with this simple patch:

diff --git a/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m b/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
index 719e6be..241134d 100644
--- a/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
+++ b/node_modules/react-native-permissions/ios/LocationAlways/RNPermissionHandlerLocationAlways.m
@@ -48,9 +48,10 @@ - (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
     if (![CLLocationManager locationServicesEnabled]) {
       return resolve(RNPermissionStatusNotAvailable);
     }
-    if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined) {
-      return [self checkWithResolver:resolve rejecter:reject];
-    }
+// Removing the authorizationStatus check here
+//     if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusNotDetermined) {
+//       return [self checkWithResolver:resolve rejecter:reject];
+//     }
 
     self->_resolve = resolve;
     self->_reject = reject;

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!

@alessioemireni
Copy link

alessioemireni commented Feb 5, 2024

@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];
   }
 }
 

@alexkev
Copy link

alexkev commented Feb 26, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

iOS: Cannot request location always if we already have location when in use permission
9 participants