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