From 3ade8c0a91bcfb5c6255c61449fdd7e0e16717ec Mon Sep 17 00:00:00 2001 From: John <35761976+drash-course@users.noreply.github.com> Date: Sun, 10 Oct 2021 05:34:54 +0200 Subject: [PATCH] feat(iOS14): Support for the new auth status `PHAuthorizationStatusLimited` from iOS 14 (#326) * feat `limited` in the response of CameraRoll.getPhotos(...) if the permission is `PHAuthorizationStatusLimited` * update Flow types * add example usage of the limited info --- README.md | 1 + example/js/CameraRollView.js | 20 ++++++++++- ios/RNCCameraRollManager.m | 69 +++++++++++++++++++++++------------- js/CameraRoll.js | 2 ++ 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e64a315..796c78c 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ Returns a Promise which when resolved will be of the following shape: * `has_next_page`: {boolean} * `start_cursor`: {string} * `end_cursor`: {string} +* `limited` : {boolean | undefined} : true if the app can only access a subset of the gallery pictures (authorization is `PHAuthorizationStatusLimited`), false otherwise (iOS only) #### Example diff --git a/example/js/CameraRollView.js b/example/js/CameraRollView.js index bd99645..02409fd 100644 --- a/example/js/CameraRollView.js +++ b/example/js/CameraRollView.js @@ -20,6 +20,9 @@ const { Platform, StyleSheet, View, + TouchableOpacity, + Text, + Linking, } = ReactNative; import CameraRoll from '../../js/CameraRoll'; @@ -61,6 +64,7 @@ class CameraRollView extends React.Component { lastCursor: null, noMore: false, loadingMore: false, + isLimited: false, }; } @@ -148,6 +152,16 @@ class CameraRollView extends React.Component { if (!this.state.noMore) { return ; } + if (this.state.isLimited) { + return ( + + + Not all pictures are available. Tap here to go to Settings and + change which media the app is allowed to access. + + + ); + } return null; }; @@ -162,7 +176,7 @@ class CameraRollView extends React.Component { _appendAssets(data) { const assets = data.edges; - const newState = {loadingMore: false}; + const newState = {loadingMore: false, isLimited: data.limited}; if (!data.page_info.has_next_page) { newState.noMore = true; @@ -210,6 +224,10 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + footerText: { + padding: 20, + textAlign: 'center', + }, }); module.exports = CameraRollView; diff --git a/ios/RNCCameraRollManager.m b/ios/RNCCameraRollManager.m index 06f6fa3..cf0150b 100644 --- a/ios/RNCCameraRollManager.m +++ b/ios/RNCCameraRollManager.m @@ -49,7 +49,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ NSString *const lowercase = [mediaType lowercaseString]; NSMutableArray *format = [NSMutableArray new]; NSMutableArray *arguments = [NSMutableArray new]; - + if ([lowercase isEqualToString:@"photos"]) { [format addObject:@"mediaType = %d"]; [arguments addObject:@(PHAssetMediaTypeImage)]; @@ -62,7 +62,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ "'videos' or 'all'.", mediaType); } } - + if (fromTime > 0) { NSDate* fromDate = [NSDate dateWithTimeIntervalSince1970:fromTime/1000]; [format addObject:@"creationDate > %@"]; @@ -73,7 +73,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{ [format addObject:@"creationDate <= %@"]; [arguments addObject:toDate]; } - + // This case includes the "all" mediatype PHFetchOptions *const options = [PHFetchOptions new]; if ([format count] > 0) { @@ -96,18 +96,34 @@ static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD"; static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED"; static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED"; -typedef void (^PhotosAuthorizedBlock)(void); +typedef void (^PhotosAuthorizedBlock)(bool isLimited); static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) { - PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus]; + PHAuthorizationStatus authStatus; + if (@available(iOS 14, *)) { + authStatus = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]; + } else { + authStatus = [PHPhotoLibrary authorizationStatus]; + } if (authStatus == PHAuthorizationStatusRestricted) { reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil); } else if (authStatus == PHAuthorizationStatusAuthorized) { - authorizedBlock(); + authorizedBlock(false); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + } else if (authStatus == PHAuthorizationStatusLimited) { +#pragma clang diagnostic pop + authorizedBlock(true); } else if (authStatus == PHAuthorizationStatusNotDetermined) { - [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - requestPhotoLibraryAccess(reject, authorizedBlock); - }]; + if (@available(iOS 14, *)) { + [PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus status) { + requestPhotoLibraryAccess(reject, authorizedBlock); + }]; + } else { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { + requestPhotoLibraryAccess(reject, authorizedBlock); + }]; + } } else { reject(kErrorAuthDenied, @"Access to photo library was denied", nil); } @@ -159,7 +175,7 @@ RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request }; void (^saveWithOptions)(void) = ^void() { if (![options[@"album"] isEqualToString:@""]) { - + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; fetchOptions.predicate = [NSPredicate predicateWithFormat:@"title = %@", options[@"album"] ]; collection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum @@ -188,7 +204,7 @@ RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request } }; - void (^loadBlock)(void) = ^void() { + void (^loadBlock)(bool isLimited) = ^void(bool isLimited) { inputURI = request.URL; saveWithOptions(); }; @@ -220,14 +236,16 @@ RCT_EXPORT_METHOD(getAlbums:(NSDictionary *)params static void RCTResolvePromise(RCTPromiseResolveBlock resolve, NSArray *> *assets, - BOOL hasNextPage) + BOOL hasNextPage, + bool isLimited) { if (!assets.count) { resolve(@{ @"edges": assets, @"page_info": @{ @"has_next_page": @NO, - } + }, + @"limited": @(isLimited) }); return; } @@ -237,7 +255,8 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve, @"start_cursor": assets[0][@"node"][@"image"][@"uri"], @"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"], @"has_next_page": @(hasNextPage), - } + }, + @"limited": @(isLimited) }); } @@ -262,14 +281,14 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound; BOOL __block includeImageSize = [include indexOfObject:@"imageSize"] != NSNotFound; BOOL __block includePlayableDuration = [include indexOfObject:@"playableDuration"] != NSNotFound; - + // If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all // other groupTypes values require the "album" collection type. PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"] ? PHAssetCollectionTypeSmartAlbum : PHAssetCollectionTypeAlbum); PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes]; - + // Predicate for fetching assets within a collection PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime]; // We can directly set the limit if we guarantee every image fetched will be @@ -285,29 +304,29 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params assetFetchOptions.fetchLimit = first + 1; } assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; - + BOOL __block foundAfter = NO; BOOL __block hasNextPage = NO; BOOL __block resolvedPromise = NO; NSMutableArray *> *assets = [NSMutableArray new]; - + // Filter collection name ("group") PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new]; collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]]; if (groupName != nil) { collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName]; } - + BOOL __block stopCollections_; NSString __block *currentCollectionName; - requestPhotoLibraryAccess(reject, ^{ + requestPhotoLibraryAccess(reject, ^(bool isLimited){ void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) { NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]]; NSString *_Nullable originalFilename = NULL; PHAssetResource *_Nullable resource = NULL; NSNumber* fileSize = [NSNumber numberWithInt:0]; - + if (includeFilename || includeFileSize || [mimeTypes count] > 0) { // Get underlying resources of an asset - this includes files as well as details about edited PHAssets // This is required for the filename and mimeType filtering @@ -316,7 +335,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params originalFilename = resource.originalFilename; fileSize = [resource valueForKey:@"fileSize"]; } - + // WARNING: If you add any code to `collectAsset` that may skip adding an // asset to the `assets` output array, you should do it inside this // block and ensure the logic for `collectAssetMayOmitAsset` above is @@ -354,7 +373,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params stopCollections_ = YES; hasNextPage = YES; RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results."); - RCTResolvePromise(resolve, assets, hasNextPage); + RCTResolvePromise(resolve, assets, hasNextPage, isLimited); resolvedPromise = YES; return; } @@ -412,7 +431,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params // If we get this far and haven't resolved the promise yet, we reached the end of the list of photos if (!resolvedPromise) { hasNextPage = NO; - RCTResolvePromise(resolve, assets, hasNextPage); + RCTResolvePromise(resolve, assets, hasNextPage, isLimited); resolvedPromise = YES; } }); @@ -423,7 +442,7 @@ RCT_EXPORT_METHOD(deletePhotos:(NSArray*)assets reject:(RCTPromiseRejectBlock)reject) { NSMutableArray *convertedAssets = [NSMutableArray array]; - + for (NSString *asset in assets) { [convertedAssets addObject: [asset stringByReplacingOccurrencesOfString:@"ph://" withString:@""]]; } diff --git a/js/CameraRoll.js b/js/CameraRoll.js index a7a0f12..859060d 100644 --- a/js/CameraRoll.js +++ b/js/CameraRoll.js @@ -122,7 +122,9 @@ export type PhotoIdentifiersPage = { start_cursor?: string, end_cursor?: string, }, + limited?: boolean, }; + export type SaveToCameraRollOptions = { type?: 'photo' | 'video' | 'auto', album?: string,