1// Copyright 2016-present 650 Industries. All rights reserved.
2
3#if __has_include(<ExpoModulesCore/EXPermissionsService.h>)
4#import <ExpoModulesCore/EXUtilities.h>
5#import <ExpoModulesCore/EXDefines.h>
6
7#import "EXScopedPermissions.h"
8
9@interface EXScopedPermissions ()
10
11@property (nonatomic, strong) NSString *scopeKey;
12@property (nonatomic, weak) id<EXPermissionsScopedModuleDelegate> permissionsService;
13@property (nonatomic, weak) id<EXUtilitiesInterface> utils;
14@property (nonatomic, weak) EXConstantsBinding *constantsBinding;
15
16@end
17
18@implementation EXScopedPermissions
19
20- (instancetype)initWithScopeKey:(NSString *)scopeKey andConstantsBinding:(EXConstantsBinding *)constantsBinding
21{
22  if (self = [super init]) {
23    _scopeKey = scopeKey;
24    _constantsBinding = constantsBinding;
25  }
26  return self;
27}
28
29- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
30{
31  [super setModuleRegistry:moduleRegistry];
32  _utils = [moduleRegistry getModuleImplementingProtocol:@protocol(EXUtilitiesInterface)];
33  _permissionsService = [moduleRegistry getSingletonModuleForName:@"Permissions"];
34}
35
36# pragma mark - permission requesters / getters
37
38// overriding EXPermission to inject scoped permission logic
39- (NSDictionary *)getPermissionUsingRequesterClass:(Class)requesterClass
40{
41  NSDictionary *globalPermission = [super getPermissionUsingRequesterClass:requesterClass];
42  return [self getScopedPermissionForType:[requesterClass permissionType] withGlobalPermission:globalPermission];
43}
44
45- (NSString *)getScopedPermissionStatus:(NSString *)permissionType {
46  if (!_permissionsService) {
47    return [[self class] permissionStringForStatus:EXPermissionStatusGranted];
48  }
49
50  return [[self class] permissionStringForStatus:[_permissionsService getPermission:permissionType forExperience:_scopeKey]];
51}
52
53- (BOOL)hasGrantedScopedPermission:(NSString *)permissionType
54{
55  if (!_permissionsService || ![self shouldVerifyScopedPermission:permissionType]) {
56    return YES;
57  }
58
59  return [_permissionsService getPermission:permissionType forExperience:_scopeKey] == EXPermissionStatusGranted;
60}
61
62- (void)askForPermissionUsingRequesterClass:(Class)requesterClass
63                                    resolve:(EXPromiseResolveBlock)resolve
64                                     reject:(EXPromiseRejectBlock)reject
65{
66  NSDictionary *globalPermissions = [super getPermissionUsingRequesterClass:requesterClass];
67  NSString *permissionType = [requesterClass permissionType];
68  EX_WEAKIFY(self)
69  if (![globalPermissions[@"status"] isEqualToString:@"granted"]) {
70    // first group
71    // ask for permission. If granted then save it as scope permission
72    void (^customOnResults)(NSDictionary *) = ^(NSDictionary *permission){
73      EX_ENSURE_STRONGIFY(self)
74      // if permission should be scoped save it
75      if ([self shouldVerifyScopedPermission:permissionType]) {
76        [self.permissionsService savePermission:permission ofType:permissionType forExperience:self.scopeKey];
77      }
78      resolve(permission);
79    };
80
81    return [self askForGlobalPermissionUsingRequesterClass:requesterClass withResolver:customOnResults withRejecter:reject];
82  } else if ([_constantsBinding.appOwnership isEqualToString:@"expo"] &&
83             ![self hasGrantedScopedPermission:permissionType]) {
84    // second group
85    // had to reinitilize UIAlertActions between alertShow invocations
86    UIAlertAction *allowAction = [UIAlertAction actionWithTitle:@"Allow" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
87      EX_ENSURE_STRONGIFY(self);
88      NSMutableDictionary *permission = [globalPermissions mutableCopy];
89      // try to save scoped permissions - if fails than permission is denied
90      if (![self.permissionsService savePermission:permission ofType:permissionType forExperience:self.scopeKey]) {
91        permission[@"status"] = [[self class] permissionStringForStatus:EXPermissionStatusDenied];
92        permission[@"granted"] = @(NO);
93      }
94      resolve(permission);
95    }];
96
97    UIAlertAction *denyAction = [UIAlertAction actionWithTitle:@"Deny" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
98      EX_ENSURE_STRONGIFY(self);
99      NSMutableDictionary *permission = [globalPermissions mutableCopy];
100      permission[@"status"] = [[self class] permissionStringForStatus:EXPermissionStatusDenied];
101      permission[@"granted"] = @(NO);
102      resolve([NSDictionary dictionaryWithDictionary:permission]);
103    }];
104
105    return [self showPermissionRequestAlert:permissionType withAllowAction:allowAction withDenyAction:denyAction];
106  }
107
108  resolve(globalPermissions); // third group
109}
110
111# pragma mark - helpers
112
113- (NSDictionary *)getScopedPermissionForType:(NSString *)permissionType withGlobalPermission:(NSDictionary *)globalPermission
114{
115  if (!globalPermission) {
116    return nil;
117  }
118  NSMutableDictionary *permission = [NSMutableDictionary dictionaryWithDictionary:globalPermission];
119
120  if ([_constantsBinding.appOwnership isEqualToString:@"expo"]
121      && [self shouldVerifyScopedPermission:permissionType]
122      && [EXPermissionsService statusForPermission:permission] == EXPermissionStatusGranted) {
123    permission[@"status"] = [self getScopedPermissionStatus:permissionType];
124    permission[@"granted"] = [permission[@"status"] isEqual:@"granted"] ? @YES : @NO;
125  }
126  return permission;
127}
128
129- (void)showPermissionRequestAlert:(NSString *)permissionType
130                   withAllowAction:(UIAlertAction *)allow
131                    withDenyAction:(UIAlertAction *)deny
132{
133  NSString *experienceName = self.scopeKey; // TODO: we might want to use name from the manifest?
134  NSString *messageTemplate = @"%1$@ needs permissions for %2$@. You\'ve already granted permission to another Expo experience. Allow %1$@ to also use it?";
135  NSString *permissionString = [[self class] textForPermissionType:permissionType];
136
137  NSString *message = [NSString stringWithFormat:messageTemplate, experienceName, permissionString];
138  UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Experience needs permissions"
139                                                                 message:message
140                                                          preferredStyle:UIAlertControllerStyleAlert];
141  [alert addAction:deny];
142  [alert addAction:allow];
143
144  EX_WEAKIFY(self);
145  [EXUtilities performSynchronouslyOnMainThread:^{
146    EX_ENSURE_STRONGIFY(self);
147    // TODO: below line is sometimes failing with: "Presenting view controllers on detached view controllers is discourage"
148    [self->_utils.currentViewController presentViewController:alert animated:YES completion:nil];
149  }];
150}
151
152+ (NSString *)textForPermissionType:(NSString *)type
153{
154  if ([type isEqualToString:@"audioRecording"]) {
155    return @"recording audio";
156  } else if ([type isEqualToString:@"cameraRoll"]) {
157    return @"photos";
158  }
159
160  return type;
161}
162
163- (BOOL)shouldVerifyScopedPermission:(NSString *)permissionType
164{
165  // temporarily exclude notifactions from permissions per experience; system brightness is always granted
166  return ![@[@"notifications", @"userFacingNotifications", @"systemBrightness"] containsObject:permissionType];
167}
168
169@end
170#endif
171