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,