UNPKG

7.66 kBPlain TextView Raw
1// Copyright 2016-present 650 Industries. All rights reserved.
2
3#import <EXLocation/EXLocationPermissionRequester.h>
4#import <UMCore/UMUtilities.h>
5
6#import <objc/message.h>
7#import <CoreLocation/CLLocationManager.h>
8#import <CoreLocation/CLLocationManagerDelegate.h>
9
10static SEL alwaysAuthorizationSelector;
11static SEL whenInUseAuthorizationSelector;
12
13@interface EXLocationPermissionRequester () <CLLocationManagerDelegate>
14
15@property (nonatomic, strong) CLLocationManager *locMgr;
16@property (nonatomic, strong) UMPromiseResolveBlock resolve;
17@property (nonatomic, strong) UMPromiseRejectBlock reject;
18
19@end
20
21@implementation EXLocationPermissionRequester
22
23+ (NSString *)permissionType
24{
25 return @"location";
26}
27
28+ (void)load
29{
30 alwaysAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"AlwaysAuthorization"]);
31 whenInUseAuthorizationSelector = NSSelectorFromString([@"request" stringByAppendingString:@"WhenInUseAuthorization"]);
32}
33
34- (NSDictionary *)getPermissions
35{
36 UMPermissionStatus status;
37 NSString *scope = @"none";
38
39 CLAuthorizationStatus systemStatus;
40 if (![[self class] isConfiguredForAlwaysAuthorization] && ![[self class] isConfiguredForWhenInUseAuthorization]) {
41 UMFatal(UMErrorWithMessage(@"This app is missing usage descriptions, so location services will fail. Add one of the `NSLocation*UsageDescription` keys to your bundle's Info.plist. See https://bit.ly/2P5fEbG (https://docs.expo.io/versions/latest/guides/app-stores.html#system-permissions-dialogs-on-ios) for more information."));
42 systemStatus = kCLAuthorizationStatusDenied;
43 } else {
44 systemStatus = [CLLocationManager authorizationStatus];
45 }
46
47 switch (systemStatus) {
48 case kCLAuthorizationStatusAuthorizedWhenInUse: {
49 status = UMPermissionStatusGranted;
50 scope = @"whenInUse";
51 break;
52 }
53 case kCLAuthorizationStatusAuthorizedAlways: {
54 status = UMPermissionStatusGranted;
55 scope = @"always";
56 break;
57 }
58 case kCLAuthorizationStatusDenied: case kCLAuthorizationStatusRestricted: {
59 status = UMPermissionStatusDenied;
60 break;
61 }
62 case kCLAuthorizationStatusNotDetermined: default: {
63 status = UMPermissionStatusUndetermined;
64 break;
65 }
66 }
67
68 return @{
69 @"status": @(status),
70 @"scope": scope
71 };
72}
73
74- (void)requestPermissionsWithResolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject
75{
76 NSDictionary *existingPermissions = [self getPermissions];
77 if (existingPermissions && [existingPermissions[@"status"] intValue] != UMPermissionStatusUndetermined) {
78 // since permissions are already determined, the iOS request methods will be no-ops.
79 // just resolve with whatever existing permissions.
80 resolve(existingPermissions);
81 } else {
82 _resolve = resolve;
83 _reject = reject;
84
85 UM_WEAKIFY(self)
86 [UMUtilities performSynchronouslyOnMainThread:^{
87 UM_ENSURE_STRONGIFY(self)
88 self.locMgr = [[CLLocationManager alloc] init];
89 self.locMgr.delegate = self;
90 }];
91
92 // 1. Why do we call CLLocationManager methods by those dynamically created selectors?
93 //
94 // Most probably application code submitted to Apple Store is statically analyzed
95 // paying special attention to camelcase(request_always_location) being called on CLLocationManager.
96 // This lets Apple warn developers when it notices that location authorization may be requested
97 // while there is no NSLocationUsageDescription in Info.plist. Since we want to neither
98 // make Expo developers receive this kind of messages nor add our own default usage description,
99 // we try to fool the static analyzer and construct the selector in runtime.
100 // This way behavior of this requester is governed by provided NSLocationUsageDescriptions.
101 //
102 // 2. Why there's no way to call specifically whenInUse or always authorization?
103 //
104 // The requester sets itself as the delegate of the CLLocationManager, so when the user responds
105 // to a permission requesting dialog, manager calls `locationManager:didChangeAuthorizationStatus:` method.
106 // To be precise, manager calls this method in two circumstances:
107 // - right when `request*Authorization` method is called,
108 // - when `authorizationStatus` changes.
109 // With this behavior we aren't able to support the following use case:
110 // - app requests `whenInUse` authorization
111 // - user allows `whenInUse` authorization
112 // - `authorizationStatus` changes from `undetermined` to `whenInUse`, callback is called, promise is resolved
113 // - app wants to escalate authorization to `always`
114 // - user selects `whenInUse` authorization (iOS 11+)
115 // - `authorizationStatus` doesn't change, so callback is not called and requester can't know whether
116 // user responded to the dialog selecting `whenInUse` or is still deciding
117 // To support this use case we will have to change the way location authorization is requested
118 // from promise-based to listener-based.
119
120 if ([[self class] isConfiguredForAlwaysAuthorization] && [_locMgr respondsToSelector:alwaysAuthorizationSelector]) {
121 ((void (*)(id, SEL))objc_msgSend)(_locMgr, alwaysAuthorizationSelector);
122 } else if ([[self class] isConfiguredForWhenInUseAuthorization] && [_locMgr respondsToSelector:whenInUseAuthorizationSelector]) {
123 ((void (*)(id, SEL))objc_msgSend)(_locMgr, whenInUseAuthorizationSelector);
124 } else {
125 _reject(@"E_LOCATION_INFO_PLIST", @"One of the `NSLocation*UsageDescription` keys must be present in Info.plist to be able to use geolocation.", nil);
126 }
127 }
128}
129
130# pragma mark - internal
131
132+ (BOOL)isConfiguredForWhenInUseAuthorization
133{
134 return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"] != nil;
135}
136
137+ (BOOL)isConfiguredForAlwaysAuthorization
138{
139 if (@available(iOS 11.0, *)) {
140 return [self isConfiguredForWhenInUseAuthorization] && [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"];
141 }
142
143 // iOS 10 fallback
144 return [self isConfiguredForWhenInUseAuthorization] && [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"];
145}
146
147#pragma mark - CLLocationManagerDelegate
148
149- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
150{
151 if (_reject) {
152 _reject(@"E_LOCATION_ERROR_UNKNOWN", error.localizedDescription, error);
153 _resolve = nil;
154 _reject = nil;
155 }
156}
157
158- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
159{
160 // TODO: Permissions.LOCATION issue (search by this phrase)
161 // 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
162 // it happens if user requests more that one permission at the same time via Permissions.askAsync(...) and LOCATION dialog is not being called first
163 // to reproduce this find NCL code testing that
164 if (status == kCLAuthorizationStatusNotDetermined) {
165 // CLLocationManager calls this delegate method once on start with kCLAuthorizationNotDetermined even before the user responds
166 // to the "Don't Allow" / "Allow" dialog box. This isn't the event we care about so we skip it. See:
167 // http://stackoverflow.com/questions/30106341/swift-locationmanager-didchangeauthorizationstatus-always-called/30107511#30107511
168 return;
169 }
170 if (_resolve) {
171 _resolve([self getPermissions]);
172 _resolve = nil;
173 _reject = nil;
174 }
175}
176
177@end