Skip to content

Commit ece7b6a

Browse files
committed
feat: add Focus Status permission support
Add support for checking and requesting Focus Status (Do Not Disturb) authorization via INFocusStatusCenter, available since macOS 12.0. New API: - getAuthStatus('focus-status') - check current authorization - askForFocusStatusAccess() - request authorization
1 parent 5d16563 commit ece7b6a

6 files changed

Lines changed: 105 additions & 1 deletion

File tree

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [`permissions.askForCalendarAccess([accessLevel])`](#permissionsaskforcalendaraccessaccesslevel)
1515
- [`permissions.askForSpeechRecognitionAccess()`](#permissionsaskforspeechrecognitionaccess)
1616
- [`permissions.askForRemindersAccess()`](#permissionsaskforremindersaccess)
17+
- [`permissions.askForFocusStatusAccess()`](#permissionsaskforfocusstatusaccess)
1718
- [`permissions.askForFoldersAccess(folder)`](#permissionsaskforfoldersaccessfolder)
1819
- [`permissions.askForFullDiskAccess()`](#permissionsaskforfulldiskaccess)
1920
- [`permissions.askForCameraAccess()`](#permissionsaskforcameraaccess)
@@ -38,6 +39,7 @@ This native Node.js module allows you to manage an app's access to:
3839
- Calendar
3940
- Camera
4041
- Contacts
42+
- Focus Status
4143
- Full Disk Access
4244
- Input Monitoring
4345
- Location
@@ -60,7 +62,7 @@ If you're using macOS 12.3 or newer, you'll need to ensure you have Python insta
6062

6163
### `permissions.getAuthStatus(type)`
6264

63-
- `type` String - The type of system component to which you are requesting access. Can be one of `accessibility`, `bluetooth`, `calendar`, `camera`, `contacts`, `full-disk-access`, `input-monitoring`, `location`, `microphone`,`notifications`, `photos`, `reminders`, `screen`, or `speech-recognition`.
65+
- `type` String - The type of system component to which you are requesting access. Can be one of `accessibility`, `bluetooth`, `calendar`, `camera`, `contacts`, `focus-status`, `full-disk-access`, `input-monitoring`, `location`, `microphone`, `notifications`, `photos`, `reminders`, `screen`, or `speech-recognition`.
6466

6567
Returns `String` - Can be one of `not determined`, `denied`, `authorized`, `limited`, `provisional`, or `restricted`.
6668

@@ -84,6 +86,7 @@ const types = [
8486
'calendar',
8587
'camera',
8688
'contacts',
89+
'focus-status',
8790
'full-disk-access',
8891
'input-monitoring',
8992
'location',
@@ -237,6 +240,28 @@ requesting Reminders access.
237240
<string>Your reason for wanting access to read and write Reminders data.</string>
238241
```
239242

243+
### `permissions.askForFocusStatusAccess()`
244+
245+
Returns `Promise<String>` - Whether or not the request succeeded or failed; can be `authorized`, `denied`, `restricted`, or `not determined`.
246+
247+
Checks the authorization status for Focus Status access. If the status check returns:
248+
249+
- `not determined` - The Focus Status access authorization will prompt the user to authorize or deny. The Promise is resolved after the user selection.
250+
- `denied` - The Promise is resolved as `denied`.
251+
- `restricted` - The Promise is resolved as `restricted`.
252+
253+
**Note:** Requires macOS 12.0 or higher. On older versions, the Promise resolves as `not determined`.
254+
255+
Example:
256+
257+
```js
258+
const { askForFocusStatusAccess } = require('node-mac-permissions')
259+
260+
askForFocusStatusAccess().then(status => {
261+
console.log(`Access to Focus Status is ${status}`)
262+
})
263+
```
264+
240265
### `permissions.askForFoldersAccess(folder)`
241266

242267
- `type` String - The folder to which you are requesting access. Can be one of `desktop`, `documents`, or `downloads`.

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"-framework CoreGraphics",
3030
"-framework Contacts",
3131
"-framework EventKit",
32+
"-framework Intents",
3233
"-framework IOKit",
3334
"-framework Photos",
3435
"-framework Speech",

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export function askForAccessibilityAccess(): undefined
66
export function askForCalendarAccess(accessType?: 'write-only' | 'full'): Promise<Omit<PermissionType, 'restricted'>>
77
export function askForCameraAccess(): Promise<PermissionType>
88
export function askForContactsAccess(): Promise<Omit<PermissionType, 'restricted'>>
9+
export function askForFocusStatusAccess(): Promise<PermissionType>
910
export function askForFoldersAccess(): Promise<Omit<PermissionType, 'restricted'>>
1011
export function askForFullDiskAccess(): undefined
1112
export function askForInputMonitoringAccess(accessType?: 'listen' | 'post'): Promise<Omit<PermissionType, 'restricted'>>
@@ -23,6 +24,7 @@ export type AuthType =
2324
| 'calendar'
2425
| 'camera'
2526
| 'contacts'
27+
| 'focus-status'
2628
| 'full-disk-access'
2729
| 'input-monitoring'
2830
| 'location'

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function getAuthStatus(type) {
88
'calendar',
99
'camera',
1010
'contacts',
11+
'focus-status',
1112
'full-disk-access',
1213
'input-monitoring',
1314
'location',
@@ -97,6 +98,7 @@ module.exports = {
9798
askForLocationAccess: askForLocationAccess,
9899
askForCameraAccess: permissions.askForCameraAccess,
99100
askForContactsAccess: permissions.askForContactsAccess,
101+
askForFocusStatusAccess: permissions.askForFocusStatusAccess,
100102
askForFoldersAccess,
101103
askForFullDiskAccess: permissions.askForFullDiskAccess,
102104
askForInputMonitoringAccess,

permissions.mm

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import <EventKit/EventKit.h>
1111
#import <Foundation/Foundation.h>
1212
#import <IOKit/hidsystem/IOHIDLib.h>
13+
#import <Intents/INFocusStatusCenter.h>
1314
#import <Photos/Photos.h>
1415
#import <Speech/Speech.h>
1516
#import <StoreKit/StoreKit.h>
@@ -402,6 +403,25 @@ bool HasOpenSystemPreferencesDialog() {
402403
return StringFromPhotosStatus(status);
403404
}
404405

406+
// Returns a status indicating whether the user has authorized Focus Status
407+
// access.
408+
std::string FocusStatusAuthStatus() {
409+
if (@available(macOS 12.0, *)) {
410+
switch ([INFocusStatusCenter defaultCenter].authorizationStatus) {
411+
case INFocusStatusAuthorizationStatusAuthorized:
412+
return kAuthorized;
413+
case INFocusStatusAuthorizationStatusDenied:
414+
return kDenied;
415+
case INFocusStatusAuthorizationStatusRestricted:
416+
return kRestricted;
417+
default:
418+
return kNotDetermined;
419+
}
420+
}
421+
422+
return kNotDetermined;
423+
}
424+
405425
/***** EXPORTED FUNCTIONS *****/
406426

407427
// Returns the user's access consent status as a string.
@@ -442,6 +462,8 @@ bool HasOpenSystemPreferencesDialog() {
442462
auth_status = InputMonitoringAuthStatus();
443463
} else if (type == "notifications") {
444464
auth_status = NotificationAuthStatus();
465+
} else if (type == "focus-status") {
466+
auth_status = FocusStatusAuthStatus();
445467
}
446468

447469
return Napi::Value::From(env, auth_status);
@@ -815,6 +837,55 @@ void AskForAccessibilityAccess(const Napi::CallbackInfo &info) {
815837
}
816838
}
817839

840+
// Request Focus Status access.
841+
Napi::Promise AskForFocusStatusAccess(const Napi::CallbackInfo &info) {
842+
Napi::Env env = info.Env();
843+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
844+
845+
if (@available(macOS 12.0, *)) {
846+
Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
847+
env, Napi::Function::New(env, NoOp), "focusStatusCallback", 0, 1);
848+
849+
auto callback = [=](Napi::Env env, Napi::Function js_cb,
850+
const char *status) {
851+
deferred.Resolve(Napi::String::New(env, status));
852+
};
853+
854+
std::string auth_status = FocusStatusAuthStatus();
855+
856+
if (auth_status == kNotDetermined) {
857+
[[INFocusStatusCenter defaultCenter]
858+
requestAuthorizationWithCompletionHandler:^(
859+
INFocusStatusAuthorizationStatus status) {
860+
const char *result;
861+
switch (status) {
862+
case INFocusStatusAuthorizationStatusAuthorized:
863+
result = "authorized";
864+
break;
865+
case INFocusStatusAuthorizationStatusDenied:
866+
result = "denied";
867+
break;
868+
case INFocusStatusAuthorizationStatusRestricted:
869+
result = "restricted";
870+
break;
871+
default:
872+
result = "not determined";
873+
break;
874+
}
875+
tsfn.BlockingCall(result, callback);
876+
tsfn.Release();
877+
}];
878+
} else {
879+
tsfn.Release();
880+
deferred.Resolve(Napi::String::New(env, auth_status));
881+
}
882+
} else {
883+
deferred.Resolve(Napi::String::New(env, kNotDetermined));
884+
}
885+
886+
return deferred.Promise();
887+
}
888+
818889
// Initializes all functions exposed to JS
819890
Napi::Object Init(Napi::Env env, Napi::Object exports) {
820891
exports.Set(Napi::String::New(env, "askForAccessibilityAccess"),
@@ -827,6 +898,8 @@ void AskForAccessibilityAccess(const Napi::CallbackInfo &info) {
827898
Napi::Function::New(env, AskForCameraAccess));
828899
exports.Set(Napi::String::New(env, "askForContactsAccess"),
829900
Napi::Function::New(env, AskForContactsAccess));
901+
exports.Set(Napi::String::New(env, "askForFocusStatusAccess"),
902+
Napi::Function::New(env, AskForFocusStatusAccess));
830903
exports.Set(Napi::String::New(env, "askForFoldersAccess"),
831904
Napi::Function::New(env, AskForFoldersAccess));
832905
exports.Set(Napi::String::New(env, "askForFullDiskAccess"),

test/module.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('node-mac-permissions', () => {
2424
'calendar',
2525
'camera',
2626
'contacts',
27+
'focus-status',
2728
'full-disk-access',
2829
'input-monitoring',
2930
'location',

0 commit comments

Comments
 (0)