Skip to content

Commit 4eb156b

Browse files
committed
feat: support new PHPhotos access
Closes #44
1 parent 03d0b0d commit 4eb156b

File tree

4 files changed

+74
-19
lines changed

4 files changed

+74
-19
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Return Value Descriptions:
6767
* `restricted` - The application is not authorized to access `type` data. The user cannot change this application’s status, possibly due to active restrictions such as parental controls being in place.
6868
* `denied` - The user explicitly denied access to `type` data for the application.
6969
* `authorized` - The application is authorized to access `type` data.
70+
* `limited` - The application is authorized for limited access to `type` data. Currently only applicable to the `photos` type.
7071

7172
**Notes:**
7273
* Access to `bluetooth` will always return a status of `authorized` prior to macOS 10.15, as the underlying API was not introduced until that version.
@@ -330,7 +331,9 @@ askForMusicLibraryAccess().then(status => {
330331
})
331332
```
332333

333-
### `permissions.askForPhotosAccess()`
334+
### `permissions.askForPhotosAccess([accessLevel])`
335+
336+
* `accessLevel` String (optional) - The access level being requested of Photos. Can be either `add-only` or `read-write`. Only available on macOS 11 or higher.
334337

335338
Returns `Promise<String>` - Current permission status; can be `authorized`, `denied`, or `restricted`.
336339

@@ -347,6 +350,11 @@ Your app must provide an explanation for its use of the photo library using the
347350
<string>Your reason for wanting to access Photos</string>
348351
```
349352

353+
**Note:**
354+
355+
You should add the `PHPhotoLibraryPreventAutomaticLimitedAccessAlert` key with a Boolean value of `YES` to your app’s `Info.plist` file to prevent the system from automatically presenting the limited library selection prompt. See [`PHAuthorizationStatusLimited`](https://developer.apple.com/documentation/photokit/phauthorizationstatus/phauthorizationstatuslimited?language=objc) for more information.
356+
357+
350358
Example:
351359

352360
```js

index.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ function getAuthStatus(type) {
1212
'location',
1313
'microphone',
1414
'music-library',
15-
'photos',
15+
'photos-add-only',
16+
'photos-read-write',
1617
'reminders',
1718
'speech-recognition',
1819
'screen',
@@ -35,6 +36,14 @@ function askForFoldersAccess(folder) {
3536
return permissions.askForFoldersAccess.call(this, folder)
3637
}
3738

39+
function askForPhotosAccess(accessLevel = 'add-only') {
40+
if (!['add-only', 'read-write'].includes(accessLevel)) {
41+
throw new TypeError(`${accessLevel} must be one of either 'add-only' or 'read-write'`)
42+
}
43+
44+
return permissions.askForPhotosAccess.call(this, accessLevel)
45+
}
46+
3847
module.exports = {
3948
askForAccessibilityAccess: permissions.askForAccessibilityAccess,
4049
askForCalendarAccess: permissions.askForCalendarAccess,
@@ -46,7 +55,7 @@ module.exports = {
4655
askForRemindersAccess: permissions.askForRemindersAccess,
4756
askForMicrophoneAccess: permissions.askForMicrophoneAccess,
4857
askForMusicLibraryAccess: permissions.askForMusicLibraryAccess,
49-
askForPhotosAccess: permissions.askForPhotosAccess,
58+
askForPhotosAccess,
5059
askForSpeechRecognitionAccess: permissions.askForSpeechRecognitionAccess,
5160
askForScreenCaptureAccess: permissions.askForScreenCaptureAccess,
5261
getAuthStatus,

permissions.mm

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
const std::string kDenied{"denied"};
2222
const std::string kRestricted{"restricted"};
2323
const std::string kNotDetermined{"not determined"};
24+
const std::string kLimited{"limited"};
25+
26+
PHAccessLevel GetPHAccessLevel(const std::string &type)
27+
API_AVAILABLE(macosx(11.0)) {
28+
return type == "read-write" ? PHAccessLevelReadWrite : PHAccessLevelAddOnly;
29+
}
2430

2531
NSURL *URLForDirectory(NSSearchPathDirectory directory) {
2632
NSFileManager *fm = [NSFileManager defaultManager];
@@ -367,8 +373,16 @@ bool HasOpenSystemPreferencesDialog() {
367373

368374
// Returns a status indicating whether or not the user has authorized Photos
369375
// access.
370-
std::string PhotosAuthStatus() {
371-
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
376+
std::string PhotosAuthStatus(const std::string &access_level) {
377+
PHAuthorizationStatus status = PHAuthorizationStatusNotDetermined;
378+
379+
if (@available(macOS 11, *)) {
380+
PHAccessLevel level = GetPHAccessLevel(access_level);
381+
status = [PHPhotoLibrary authorizationStatusForAccessLevel:level];
382+
} else {
383+
status = [PHPhotoLibrary authorizationStatus];
384+
}
385+
372386
return StringFromPhotosStatus(status);
373387
}
374388

@@ -390,8 +404,10 @@ bool HasOpenSystemPreferencesDialog() {
390404
auth_status = FDAAuthStatus();
391405
} else if (type == "microphone") {
392406
auth_status = MediaAuthStatus("microphone");
393-
} else if (type == "photos") {
394-
auth_status = PhotosAuthStatus();
407+
} else if (type == "photos-add-only") {
408+
auth_status = PhotosAuthStatus("add-only");
409+
} else if (type == "photos-read-write") {
410+
auth_status = PhotosAuthStatus("read-write");
395411
} else if (type == "speech-recognition") {
396412
auth_status = SpeechRecognitionAuthStatus();
397413
} else if (type == "camera") {
@@ -603,18 +619,40 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) {
603619
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(
604620
env, Napi::Function::New(env, NoOp), "photosCallback", 0, 1);
605621

606-
std::string auth_status = PhotosAuthStatus();
622+
std::string access_level = info[0].As<Napi::String>().Utf8Value();
623+
std::string auth_status = PhotosAuthStatus(access_level);
624+
607625
if (auth_status == kNotDetermined) {
608626
__block Napi::ThreadSafeFunction tsfn = ts_fn;
609-
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
610-
auto callback = [=](Napi::Env env, Napi::Function js_cb,
611-
const char *granted) {
612-
deferred.Resolve(Napi::String::New(env, granted));
613-
};
614-
std::string auth_result = StringFromPhotosStatus(status);
615-
tsfn.BlockingCall(auth_result.c_str(), callback);
616-
tsfn.Release();
617-
}];
627+
if (@available(macOS 11, *)) {
628+
[PHPhotoLibrary
629+
requestAuthorizationForAccessLevel:GetPHAccessLevel(access_level)
630+
handler:^(PHAuthorizationStatus status) {
631+
auto callback =
632+
[=](Napi::Env env,
633+
Napi::Function js_cb,
634+
const char *granted) {
635+
deferred.Resolve(Napi::String::New(
636+
env, granted));
637+
};
638+
std::string auth_result =
639+
StringFromPhotosStatus(status);
640+
tsfn.BlockingCall(auth_result.c_str(),
641+
callback);
642+
tsfn.Release();
643+
}];
644+
} else {
645+
__block Napi::ThreadSafeFunction tsfn = ts_fn;
646+
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
647+
auto callback = [=](Napi::Env env, Napi::Function js_cb,
648+
const char *granted) {
649+
deferred.Resolve(Napi::String::New(env, granted));
650+
};
651+
std::string auth_result = StringFromPhotosStatus(status);
652+
tsfn.BlockingCall(auth_result.c_str(), callback);
653+
tsfn.Release();
654+
}];
655+
}
618656
} else if (auth_status == kDenied) {
619657
OpenPrefPane("Privacy_Photos");
620658

@@ -624,7 +662,6 @@ void AskForFullDiskAccess(const Napi::CallbackInfo &info) {
624662
ts_fn.Release();
625663
deferred.Resolve(Napi::String::New(env, auth_status));
626664
}
627-
628665
return deferred.Promise();
629666
}
630667

test/module.spec.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ describe('node-mac-permissions', () => {
2121
'location',
2222
'microphone',
2323
'music-library',
24-
'photos',
24+
'photos-add-only',
25+
'photos-read-write',
2526
'reminders',
2627
'speech-recognition',
2728
'screen',

0 commit comments

Comments
 (0)