diff --git a/README.md b/README.md index 6432e817..1e05ac14 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ target 'YourAwesomeProject' do pod 'Permission-Motion', :path => "#{permissions_path}/Motion.podspec" pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications.podspec" pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary.podspec" + pod 'Permission-PhotoLibraryAddOnly', :path => "#{permissions_path}/PhotoLibraryAddOnly.podspec" pod 'Permission-Reminders', :path => "#{permissions_path}/Reminders.podspec" pod 'Permission-Siri', :path => "#{permissions_path}/Siri.podspec" pod 'Permission-SpeechRecognition', :path => "#{permissions_path}/SpeechRecognition.podspec" @@ -93,6 +94,8 @@ Then update your `Info.plist` with wanted permissions usage descriptions: YOUR TEXT NSPhotoLibraryUsageDescription YOUR TEXT + NSPhotoLibraryAddUsageDescription + YOUR TEXT NSRemindersUsageDescription YOUR TEXT NSSpeechRecognitionUsageDescription @@ -391,6 +394,7 @@ PERMISSIONS.IOS.MEDIA_LIBRARY; PERMISSIONS.IOS.MICROPHONE; PERMISSIONS.IOS.MOTION; PERMISSIONS.IOS.PHOTO_LIBRARY; +PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY; PERMISSIONS.IOS.REMINDERS; PERMISSIONS.IOS.SIRI; PERMISSIONS.IOS.SPEECH_RECOGNITION; @@ -407,12 +411,18 @@ Permission checks and requests resolve into one of these statuses: | `RESULTS.DENIED` | The permission has not been requested / is denied but requestable | | `RESULTS.GRANTED` | The permission is granted | | `RESULTS.BLOCKED` | The permission is denied and not requestable anymore | +| `RESULTS.LIMITED` | The permission is granted but with limitations | ### Methods ```ts // type used in usage examples -type PermissionStatus = 'unavailable' | 'denied' | 'blocked' | 'granted'; +type PermissionStatus = + | 'unavailable' + | 'denied' + | 'blocked' + | 'granted' + | 'limited'; ``` #### check @@ -620,6 +630,24 @@ import {openSettings} from 'react-native-permissions'; openSettings().catch(() => console.warn('cannot open settings')); ``` +--- + +#### presentLimitedLibraryPicker + +On iOS, open a picker to update the photo selection when limited permissions are given. This is a no-op on Android, and when full permissions are given. + +```ts +function presentLimitedLibraryPicker(): Promise; +``` + +```js +import {presentLimitedLibraryPicker} from 'react-native-permissions'; + +presentLimitedLibraryPicker().catch(() => + console.warn('cannot open presentLimitedLibraryPicker'), +); +``` + ## Migrating from v1.x.x If you are currently using the version `1.x.x` and would like to switch to `v2.x.x`, the only thing you really need to know is that it's now required to select the wanted permission **per platform**. diff --git a/example/App.tsx b/example/App.tsx index cf31f0f8..c17c04c7 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -28,6 +28,7 @@ const colors: {[key: string]: string} = { denied: '#ff9800', granted: '#43a047', blocked: '#e53935', + limited: '#ffdd00', }; const icons: {[key: string]: string} = { @@ -35,6 +36,7 @@ const icons: {[key: string]: string} = { denied: 'alert-circle', granted: 'check-circle', blocked: 'close-circle', + limited: 'check-circle-outline', }; const PermissionRow = ({ @@ -117,6 +119,12 @@ export default class App extends React.Component<{}, State> { RNPermissions.openSettings(); }} /> + { + RNPermissions.presentLimitedLibraryPicker(); + }} + /> "#{permissions_path}/Motion.podspec" pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications.podspec" pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary.podspec" + pod 'Permission-PhotoLibraryAddOnly', :path => "#{permissions_path}/PhotoLibraryAddOnly.podspec" pod 'Permission-Reminders', :path => "#{permissions_path}/Reminders.podspec" # pod 'Permission-Siri', :path => "#{permissions_path}/Siri.podspec" pod 'Permission-SpeechRecognition', :path => "#{permissions_path}/SpeechRecognition.podspec" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index d25a45d5..ea1087fc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -96,6 +96,8 @@ PODS: - RNPermissions - Permission-PhotoLibrary (2.2.0): - RNPermissions + - Permission-PhotoLibraryAddOnly (2.2.0): + - RNPermissions - Permission-Reminders (2.2.0): - RNPermissions - Permission-SpeechRecognition (2.2.0): @@ -370,6 +372,7 @@ DEPENDENCIES: - Permission-Motion (from `../node_modules/react-native-permissions/ios/Motion.podspec`) - Permission-Notifications (from `../node_modules/react-native-permissions/ios/Notifications.podspec`) - Permission-PhotoLibrary (from `../node_modules/react-native-permissions/ios/PhotoLibrary.podspec`) + - Permission-PhotoLibraryAddOnly (from `../node_modules/react-native-permissions/ios/PhotoLibraryAddOnly.podspec`) - Permission-Reminders (from `../node_modules/react-native-permissions/ios/Reminders.podspec`) - Permission-SpeechRecognition (from `../node_modules/react-native-permissions/ios/SpeechRecognition.podspec`) - Permission-StoreKit (from `../node_modules/react-native-permissions/ios/StoreKit.podspec`) @@ -451,6 +454,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-permissions/ios/Notifications.podspec" Permission-PhotoLibrary: :path: "../node_modules/react-native-permissions/ios/PhotoLibrary.podspec" + Permission-PhotoLibraryAddOnly: + :path: "../node_modules/react-native-permissions/ios/PhotoLibraryAddOnly.podspec" Permission-Reminders: :path: "../node_modules/react-native-permissions/ios/Reminders.podspec" Permission-SpeechRecognition: @@ -531,7 +536,8 @@ SPEC CHECKSUMS: Permission-Microphone: 06462b9d979ada12095bcff8d5ee1aeeeaf20b5d Permission-Motion: b8f1f0e7baf8fb136fc54aabf6a8669e95265d16 Permission-Notifications: 572d4e94121e57dbecc71a0933abd8ed61108e92 - Permission-PhotoLibrary: e3ff899f87a3eda29427c142ca93837e8802d228 + Permission-PhotoLibrary: e7419100010711b9c1cf002c1483653d66e6c246 + Permission-PhotoLibraryAddOnly: a8f8e696158be0d7652dc14a3ce8f1d9d738e7ab Permission-Reminders: 82dfcc66d1afdaef20a0084350753c3ff2691e4a Permission-SpeechRecognition: 0dc3e7a65fa38beb9d249cd37ef1b1565f72a494 Permission-StoreKit: 0fc596a9c7d2c3fbebfd4b1c89222db3ce94eba2 @@ -559,6 +565,6 @@ SPEC CHECKSUMS: Yoga: 3ebccbdd559724312790e7742142d062476b698e YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 33d4013cd5154539040dffe614d959a536bc61f4 +PODFILE CHECKSUM: 8f91a3a16ccf05cf62965b5febfce149cb72da42 COCOAPODS: 1.9.3 diff --git a/example/ios/RNPermissionsExample/Info.plist b/example/ios/RNPermissionsExample/Info.plist index 16261e7c..48cb9607 100644 --- a/example/ios/RNPermissionsExample/Info.plist +++ b/example/ios/RNPermissionsExample/Info.plist @@ -61,6 +61,8 @@ Let me use the microphone NSMotionUsageDescription Let me use your motion data + NSPhotoLibraryAddUsageDescription + Let me add photos NSPhotoLibraryUsageDescription Let me use your photo library NSRemindersUsageDescription diff --git a/ios/PhotoLibrary.podspec b/ios/PhotoLibrary.podspec index d71a55ac..6cff6e78 100644 --- a/ios/PhotoLibrary.podspec +++ b/ios/PhotoLibrary.podspec @@ -18,4 +18,5 @@ Pod::Spec.new do |s| s.source = { :git => package["repository"]["url"], :tag => s.version } s.source_files = "PhotoLibrary/*.{h,m}" + s.frameworks = "PhotosUI" end diff --git a/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.h b/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.h index de1f8625..5bed2c11 100644 --- a/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.h +++ b/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.h @@ -2,4 +2,6 @@ @interface RNPermissionHandlerPhotoLibrary : NSObject +- (void)presentLimitedLibraryPickerFromViewController API_AVAILABLE(ios(14)); + @end diff --git a/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.m b/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.m index a931b4d3..69344ee7 100644 --- a/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.m +++ b/ios/PhotoLibrary/RNPermissionHandlerPhotoLibrary.m @@ -1,6 +1,7 @@ #import "RNPermissionHandlerPhotoLibrary.h" @import Photos; +@import PhotosUI; @implementation RNPermissionHandlerPhotoLibrary @@ -14,7 +15,14 @@ + (NSString * _Nonnull)handlerUniqueId { - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve rejecter:(void (__unused ^ _Nonnull)(NSError * _Nonnull))reject { - switch ([PHPhotoLibrary authorizationStatus]) { + PHAuthorizationStatus status; + if (@available(iOS 14.0, *)) { + status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]; + } else { + status = [PHPhotoLibrary authorizationStatus]; + } + + switch (status) { case PHAuthorizationStatusNotDetermined: return resolve(RNPermissionStatusNotDetermined); case PHAuthorizationStatusRestricted: @@ -23,14 +31,30 @@ - (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve return resolve(RNPermissionStatusDenied); case PHAuthorizationStatusAuthorized: return resolve(RNPermissionStatusAuthorized); + case PHAuthorizationStatusLimited: + return resolve(RNPermissionStatusLimited); } } - (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve rejecter:(void (^ _Nonnull)(NSError * _Nonnull))reject { + + if (@available(iOS 14.0, *)) { + [PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(__unused PHAuthorizationStatus status) { + [self checkWithResolver:resolve rejecter:reject]; + }]; + return; + } + [PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) { [self checkWithResolver:resolve rejecter:reject]; }]; } +- (void)presentLimitedLibraryPickerFromViewController { + UIViewController* rootViewController = [[UIApplication sharedApplication].keyWindow rootViewController]; + + [[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:rootViewController]; +} + @end diff --git a/ios/PhotoLibraryAddOnly.podspec b/ios/PhotoLibraryAddOnly.podspec new file mode 100644 index 00000000..b738400e --- /dev/null +++ b/ios/PhotoLibraryAddOnly.podspec @@ -0,0 +1,21 @@ +require 'json' +package = JSON.parse(File.read('../package.json')) + +Pod::Spec.new do |s| + s.name = "Permission-PhotoLibraryAddOnly" + s.dependency "RNPermissions" + + s.version = package["version"] + s.license = package["license"] + s.summary = package["description"] + s.authors = package["author"] + s.homepage = package["homepage"] + + s.platform = :ios, "9.0" + s.ios.deployment_target = "9.0" + s.tvos.deployment_target = "11.0" + s.requires_arc = true + + s.source = { :git => package["repository"]["url"], :tag => s.version } + s.source_files = "PhotoLibraryAddOnly/*.{h,m}" +end diff --git a/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.h b/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.h new file mode 100644 index 00000000..b598882d --- /dev/null +++ b/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.h @@ -0,0 +1,5 @@ +#import "RNPermissions.h" + +@interface RNPermissionHandlerPhotoLibraryAddOnly : NSObject + +@end diff --git a/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.m b/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.m new file mode 100644 index 00000000..9cfe95e9 --- /dev/null +++ b/ios/PhotoLibraryAddOnly/RNPermissionHandlerPhotoLibraryAddOnly.m @@ -0,0 +1,53 @@ +#import "RNPermissionHandlerPhotoLibraryAddOnly.h" + +@import Photos; + +@implementation RNPermissionHandlerPhotoLibraryAddOnly + ++ (NSArray * _Nonnull)usageDescriptionKeys { + return @[@"NSPhotoLibraryUsageDescription"]; +} + ++ (NSString * _Nonnull)handlerUniqueId { + return @"ios.permission.PHOTO_LIBRARY_ADD_ONLY"; +} + +- (void)checkWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve + rejecter:(void (__unused ^ _Nonnull)(NSError * _Nonnull))reject { + PHAuthorizationStatus status; + if (@available(iOS 14.0, *)) { + status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelAddOnly]; + } else { + status = [PHPhotoLibrary authorizationStatus]; + } + + switch (status) { + case PHAuthorizationStatusNotDetermined: + return resolve(RNPermissionStatusNotDetermined); + case PHAuthorizationStatusRestricted: + return resolve(RNPermissionStatusRestricted); + case PHAuthorizationStatusDenied: + return resolve(RNPermissionStatusDenied); + case PHAuthorizationStatusAuthorized: + return resolve(RNPermissionStatusAuthorized); + case PHAuthorizationStatusLimited: + return resolve(RNPermissionStatusLimited); + } +} + +- (void)requestWithResolver:(void (^ _Nonnull)(RNPermissionStatus))resolve + rejecter:(void (^ _Nonnull)(NSError * _Nonnull))reject { + + if (@available(iOS 14.0, *)) { + [PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelAddOnly handler:^(__unused PHAuthorizationStatus status) { + [self checkWithResolver:resolve rejecter:reject]; + }]; + return; + } + + [PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) { + [self checkWithResolver:resolve rejecter:reject]; + }]; +} + +@end diff --git a/ios/RNPermissions.h b/ios/RNPermissions.h index 68a28901..96c28649 100644 --- a/ios/RNPermissions.h +++ b/ios/RNPermissions.h @@ -51,6 +51,9 @@ typedef NS_ENUM(NSInteger, RNPermission) { #if __has_include("RNPermissionHandlerAppTrackingTransparency.h") RNPermissionAppTrackingTransparency = 16, #endif +#if __has_include("RNPermissionHandlerPhotoLibraryAddOnly.h") + RNPermissionAppPhotoLibraryAddOnly = 17, +#endif }; @interface RCTConvert (RNPermission) @@ -62,6 +65,7 @@ typedef enum { RNPermissionStatusRestricted = 2, RNPermissionStatusDenied = 3, RNPermissionStatusAuthorized = 4, + RNPermissionStatusLimited = 5, } RNPermissionStatus; @protocol RNPermissionHandler diff --git a/ios/RNPermissions.m b/ios/RNPermissions.m index 96d20b4e..6e60e31b 100644 --- a/ios/RNPermissions.m +++ b/ios/RNPermissions.m @@ -52,6 +52,9 @@ #if __has_include("RNPermissionHandlerAppTrackingTransparency.h") #import "RNPermissionHandlerAppTrackingTransparency.h" #endif +#if __has_include("RNPermissionHandlerPhotoLibraryAddOnly.h") +#import "RNPermissionHandlerPhotoLibraryAddOnly.h" +#endif static NSString* SETTING_KEY = @"@RNPermissions:Requested"; @@ -106,6 +109,9 @@ @implementation RCTConvert(RNPermission) #if __has_include("RNPermissionHandlerAppTrackingTransparency.h") [RNPermissionHandlerAppTrackingTransparency handlerUniqueId]: @(RNPermissionAppTrackingTransparency), #endif +#if __has_include("RNPermissionHandlerPhotoLibraryAddOnly.h") + [RNPermissionHandlerPhotoLibraryAddOnly handlerUniqueId]: @(RNPermissionAppPhotoLibraryAddOnly), +#endif }), RNPermissionUnknown, integerValue); @end @@ -182,6 +188,9 @@ - (NSDictionary *)constantsToExport { #if __has_include("RNPermissionHandlerAppTrackingTransparency.h") [available addObject:[RNPermissionHandlerAppTrackingTransparency handlerUniqueId]]; #endif +#if __has_include("RNPermissionHandlerPhotoLibraryAddOnly.h") + [available addObject:[RNPermissionHandlerPhotoLibraryAddOnly handlerUniqueId]]; +#endif #if RCT_DEV if ([available count] == 0) { @@ -282,6 +291,11 @@ - (NSDictionary *)constantsToExport { case RNPermissionAppTrackingTransparency: handler = [RNPermissionHandlerAppTrackingTransparency new]; break; +#endif +#if __has_include("RNPermissionHandlerPhotoLibraryAddOnly.h") + case RNPermissionAppPhotoLibraryAddOnly: + handler = [RNPermissionHandlerPhotoLibraryAddOnly new]; + break; #endif case RNPermissionUnknown: break; // RCTConvert prevents this case @@ -310,6 +324,8 @@ - (NSString *)stringForStatus:(RNPermissionStatus)status { return @"blocked"; case RNPermissionStatusAuthorized: return @"granted"; + case RNPermissionStatusLimited: + return @"limited"; } } @@ -441,4 +457,21 @@ + (void)flagAsRequested:(NSString * _Nonnull)handlerId { #endif } +RCT_REMAP_METHOD(presentLimitedLibraryPicker, + presentLimitedLibraryPickerWithResolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { +#if __has_include("RNPermissionHandlerPhotoLibrary.h") + if (@available(iOS 14.0, *)) { + RNPermissionHandlerPhotoLibrary *handler = [RNPermissionHandlerPhotoLibrary new]; + [handler presentLimitedLibraryPickerFromViewController]; + resolve(@true); + } else { + reject(@"cannot_open_limited_picker", @"Limited picker is only available on iOS 14 or higher.", nil); + } +#else + reject(@"photos_pod_missing", @"Photo permission pod is missing", nil); +#endif +} + + @end diff --git a/src/constants.ts b/src/constants.ts index 5eb21451..416e5788 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,6 +43,7 @@ export const IOS = Object.freeze({ MICROPHONE: 'ios.permission.MICROPHONE' as const, MOTION: 'ios.permission.MOTION' as const, PHOTO_LIBRARY: 'ios.permission.PHOTO_LIBRARY' as const, + PHOTO_LIBRARY_ADD_ONLY: 'ios.permission.PHOTO_LIBRARY_ADD_ONLY' as const, REMINDERS: 'ios.permission.REMINDERS' as const, SIRI: 'ios.permission.SIRI' as const, SPEECH_RECOGNITION: 'ios.permission.SPEECH_RECOGNITION' as const, @@ -56,4 +57,5 @@ export const RESULTS = Object.freeze({ DENIED: 'denied' as const, BLOCKED: 'blocked' as const, GRANTED: 'granted' as const, + LIMITED: 'limited' as const, }); diff --git a/src/contract.ts b/src/contract.ts index 7186e36d..2c8f108d 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -9,6 +9,8 @@ import { export interface Contract { openSettings(): Promise; + presentLimitedLibraryPicker(): Promise; + check(permission: Permission): Promise; request( diff --git a/src/index.ts b/src/index.ts index de3f747c..9742fdea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +19,7 @@ export const checkNotifications = module.checkNotifications; export const requestNotifications = module.requestNotifications; export const checkMultiple = module.checkMultiple; export const requestMultiple = module.requestMultiple; +export const presentLimitedLibraryPicker = module.presentLimitedLibraryPicker; export default { PERMISSIONS, @@ -30,4 +31,5 @@ export default { requestNotifications, checkMultiple, requestMultiple, + presentLimitedLibraryPicker, }; diff --git a/src/module.android.ts b/src/module.android.ts index d77ffcde..658b7810 100644 --- a/src/module.android.ts +++ b/src/module.android.ts @@ -34,6 +34,10 @@ function coreStatusToStatus(status: CoreStatus): PermissionStatus { } } +async function presentLimitedLibraryPicker(): Promise { + return; +} + async function openSettings(): Promise { await RNP.openSettings(); } @@ -152,4 +156,5 @@ export const module: Contract = { requestNotifications: checkNotifications, checkMultiple, requestMultiple, + presentLimitedLibraryPicker, }; diff --git a/src/module.ios.ts b/src/module.ios.ts index 4fb8465b..0287a366 100644 --- a/src/module.ios.ts +++ b/src/module.ios.ts @@ -17,6 +17,7 @@ const RNP: { options: NotificationOption[], ) => Promise; openSettings: () => Promise; + presentLimitedLibraryPicker: () => Promise; check: (permission: Permission) => Promise; request: (permission: Permission) => Promise; } = NativeModules.RNPermissions; @@ -25,6 +26,10 @@ async function openSettings(): Promise { await RNP.openSettings(); } +async function presentLimitedLibraryPicker(): Promise { + await RNP.presentLimitedLibraryPicker(); +} + async function check(permission: Permission): Promise { return RNP.available.includes(permission) ? RNP.check(permission) @@ -88,4 +93,5 @@ export const module: Contract = { requestNotifications, checkMultiple, requestMultiple, + presentLimitedLibraryPicker, }; diff --git a/src/module.ts b/src/module.ts index cbb30ce5..89bae5f7 100644 --- a/src/module.ts +++ b/src/module.ts @@ -21,6 +21,7 @@ async function checkMultiple

( export const module: Contract = { openSettings: Promise.reject, + presentLimitedLibraryPicker: Promise.reject, check, request: check, checkNotifications,