Skip to content

Commit 3ade8c0

Browse files
authored
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
1 parent 1421c1d commit 3ade8c0

4 files changed

Lines changed: 66 additions & 26 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ Returns a Promise which when resolved will be of the following shape:
241241
* `has_next_page`: {boolean}
242242
* `start_cursor`: {string}
243243
* `end_cursor`: {string}
244+
* `limited` : {boolean | undefined} : true if the app can only access a subset of the gallery pictures (authorization is `PHAuthorizationStatusLimited`), false otherwise (iOS only)
244245

245246
#### Example
246247

example/js/CameraRollView.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const {
2020
Platform,
2121
StyleSheet,
2222
View,
23+
TouchableOpacity,
24+
Text,
25+
Linking,
2326
} = ReactNative;
2427

2528
import CameraRoll from '../../js/CameraRoll';
@@ -61,6 +64,7 @@ class CameraRollView extends React.Component {
6164
lastCursor: null,
6265
noMore: false,
6366
loadingMore: false,
67+
isLimited: false,
6468
};
6569
}
6670

@@ -148,6 +152,16 @@ class CameraRollView extends React.Component {
148152
if (!this.state.noMore) {
149153
return <ActivityIndicator />;
150154
}
155+
if (this.state.isLimited) {
156+
return (
157+
<TouchableOpacity onPress={Linking.openSettings}>
158+
<Text style={styles.footerText}>
159+
Not all pictures are available. Tap here to go to Settings and
160+
change which media the app is allowed to access.
161+
</Text>
162+
</TouchableOpacity>
163+
);
164+
}
151165
return null;
152166
};
153167

@@ -162,7 +176,7 @@ class CameraRollView extends React.Component {
162176

163177
_appendAssets(data) {
164178
const assets = data.edges;
165-
const newState = {loadingMore: false};
179+
const newState = {loadingMore: false, isLimited: data.limited};
166180

167181
if (!data.page_info.has_next_page) {
168182
newState.noMore = true;
@@ -210,6 +224,10 @@ const styles = StyleSheet.create({
210224
container: {
211225
flex: 1,
212226
},
227+
footerText: {
228+
padding: 20,
229+
textAlign: 'center',
230+
},
213231
});
214232

215233
module.exports = CameraRollView;

ios/RNCCameraRollManager.m

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ + (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType
4949
NSString *const lowercase = [mediaType lowercaseString];
5050
NSMutableArray *format = [NSMutableArray new];
5151
NSMutableArray *arguments = [NSMutableArray new];
52-
52+
5353
if ([lowercase isEqualToString:@"photos"]) {
5454
[format addObject:@"mediaType = %d"];
5555
[arguments addObject:@(PHAssetMediaTypeImage)];
@@ -62,7 +62,7 @@ + (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType
6262
"'videos' or 'all'.", mediaType);
6363
}
6464
}
65-
65+
6666
if (fromTime > 0) {
6767
NSDate* fromDate = [NSDate dateWithTimeIntervalSince1970:fromTime/1000];
6868
[format addObject:@"creationDate > %@"];
@@ -73,7 +73,7 @@ + (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType
7373
[format addObject:@"creationDate <= %@"];
7474
[arguments addObject:toDate];
7575
}
76-
76+
7777
// This case includes the "all" mediatype
7878
PHFetchOptions *const options = [PHFetchOptions new];
7979
if ([format count] > 0) {
@@ -96,18 +96,34 @@ @implementation RNCCameraRollManager
9696
static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED";
9797
static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED";
9898

99-
typedef void (^PhotosAuthorizedBlock)(void);
99+
typedef void (^PhotosAuthorizedBlock)(bool isLimited);
100100

101101
static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) {
102-
PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus];
102+
PHAuthorizationStatus authStatus;
103+
if (@available(iOS 14, *)) {
104+
authStatus = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
105+
} else {
106+
authStatus = [PHPhotoLibrary authorizationStatus];
107+
}
103108
if (authStatus == PHAuthorizationStatusRestricted) {
104109
reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil);
105110
} else if (authStatus == PHAuthorizationStatusAuthorized) {
106-
authorizedBlock();
111+
authorizedBlock(false);
112+
#pragma clang diagnostic push
113+
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
114+
} else if (authStatus == PHAuthorizationStatusLimited) {
115+
#pragma clang diagnostic pop
116+
authorizedBlock(true);
107117
} else if (authStatus == PHAuthorizationStatusNotDetermined) {
108-
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
109-
requestPhotoLibraryAccess(reject, authorizedBlock);
110-
}];
118+
if (@available(iOS 14, *)) {
119+
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(PHAuthorizationStatus status) {
120+
requestPhotoLibraryAccess(reject, authorizedBlock);
121+
}];
122+
} else {
123+
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
124+
requestPhotoLibraryAccess(reject, authorizedBlock);
125+
}];
126+
}
111127
} else {
112128
reject(kErrorAuthDenied, @"Access to photo library was denied", nil);
113129
}
@@ -159,7 +175,7 @@ static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthor
159175
};
160176
void (^saveWithOptions)(void) = ^void() {
161177
if (![options[@"album"] isEqualToString:@""]) {
162-
178+
163179
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
164180
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"title = %@", options[@"album"] ];
165181
collection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
@@ -188,7 +204,7 @@ static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthor
188204
}
189205
};
190206

191-
void (^loadBlock)(void) = ^void() {
207+
void (^loadBlock)(bool isLimited) = ^void(bool isLimited) {
192208
inputURI = request.URL;
193209
saveWithOptions();
194210
};
@@ -220,14 +236,16 @@ static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthor
220236

221237
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
222238
NSArray<NSDictionary<NSString *, id> *> *assets,
223-
BOOL hasNextPage)
239+
BOOL hasNextPage,
240+
bool isLimited)
224241
{
225242
if (!assets.count) {
226243
resolve(@{
227244
@"edges": assets,
228245
@"page_info": @{
229246
@"has_next_page": @NO,
230-
}
247+
},
248+
@"limited": @(isLimited)
231249
});
232250
return;
233251
}
@@ -237,7 +255,8 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
237255
@"start_cursor": assets[0][@"node"][@"image"][@"uri"],
238256
@"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"],
239257
@"has_next_page": @(hasNextPage),
240-
}
258+
},
259+
@"limited": @(isLimited)
241260
});
242261
}
243262

@@ -262,14 +281,14 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
262281
BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound;
263282
BOOL __block includeImageSize = [include indexOfObject:@"imageSize"] != NSNotFound;
264283
BOOL __block includePlayableDuration = [include indexOfObject:@"playableDuration"] != NSNotFound;
265-
284+
266285
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
267286
// other groupTypes values require the "album" collection type.
268287
PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"]
269288
? PHAssetCollectionTypeSmartAlbum
270289
: PHAssetCollectionTypeAlbum);
271290
PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes];
272-
291+
273292
// Predicate for fetching assets within a collection
274293
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime];
275294
// We can directly set the limit if we guarantee every image fetched will be
@@ -285,29 +304,29 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
285304
assetFetchOptions.fetchLimit = first + 1;
286305
}
287306
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
288-
307+
289308
BOOL __block foundAfter = NO;
290309
BOOL __block hasNextPage = NO;
291310
BOOL __block resolvedPromise = NO;
292311
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
293-
312+
294313
// Filter collection name ("group")
295314
PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new];
296315
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
297316
if (groupName != nil) {
298317
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
299318
}
300-
319+
301320
BOOL __block stopCollections_;
302321
NSString __block *currentCollectionName;
303322

304-
requestPhotoLibraryAccess(reject, ^{
323+
requestPhotoLibraryAccess(reject, ^(bool isLimited){
305324
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
306325
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
307326
NSString *_Nullable originalFilename = NULL;
308327
PHAssetResource *_Nullable resource = NULL;
309328
NSNumber* fileSize = [NSNumber numberWithInt:0];
310-
329+
311330
if (includeFilename || includeFileSize || [mimeTypes count] > 0) {
312331
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
313332
// This is required for the filename and mimeType filtering
@@ -316,7 +335,7 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
316335
originalFilename = resource.originalFilename;
317336
fileSize = [resource valueForKey:@"fileSize"];
318337
}
319-
338+
320339
// WARNING: If you add any code to `collectAsset` that may skip adding an
321340
// asset to the `assets` output array, you should do it inside this
322341
// block and ensure the logic for `collectAssetMayOmitAsset` above is
@@ -354,7 +373,7 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
354373
stopCollections_ = YES;
355374
hasNextPage = YES;
356375
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
357-
RCTResolvePromise(resolve, assets, hasNextPage);
376+
RCTResolvePromise(resolve, assets, hasNextPage, isLimited);
358377
resolvedPromise = YES;
359378
return;
360379
}
@@ -412,7 +431,7 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
412431
// If we get this far and haven't resolved the promise yet, we reached the end of the list of photos
413432
if (!resolvedPromise) {
414433
hasNextPage = NO;
415-
RCTResolvePromise(resolve, assets, hasNextPage);
434+
RCTResolvePromise(resolve, assets, hasNextPage, isLimited);
416435
resolvedPromise = YES;
417436
}
418437
});
@@ -423,7 +442,7 @@ static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
423442
reject:(RCTPromiseRejectBlock)reject)
424443
{
425444
NSMutableArray *convertedAssets = [NSMutableArray array];
426-
445+
427446
for (NSString *asset in assets) {
428447
[convertedAssets addObject: [asset stringByReplacingOccurrencesOfString:@"ph://" withString:@""]];
429448
}

js/CameraRoll.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ export type PhotoIdentifiersPage = {
122122
start_cursor?: string,
123123
end_cursor?: string,
124124
},
125+
limited?: boolean,
125126
};
127+
126128
export type SaveToCameraRollOptions = {
127129
type?: 'photo' | 'video' | 'auto',
128130
album?: string,

0 commit comments

Comments
 (0)