Skip to content

Commit d5b66ca

Browse files
committed
feat: add explicit contact request function
1 parent 35f9f67 commit d5b66ca

File tree

4 files changed

+89
-13
lines changed

4 files changed

+89
-13
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ $ npm i node-mac-contacts
1111

1212
This Native Node Module allows you to create, read, update, and delete contact from users' contacts databases on macOS.
1313

14-
All methods invoking the [CNContactStore](https://developer.apple.com/documentation/contacts/cncontactstore) will require authorization, which will be requested the first time the functions themselves are invoked. You can verify authorization status with `contacts.getAuthStatus()` as outlined below.
14+
All methods invoking the [CNContactStore](https://developer.apple.com/documentation/contacts/cncontactstore) will require authorization, which you can request from users with the `requestAccess` method. You can verify authorization status with `contacts.getAuthStatus()` as outlined below.
1515

1616
In your app, you should put the reason you're requesting to manipulate user's contacts database in your `Info.plist` like so:
1717

@@ -22,6 +22,14 @@ In your app, you should put the reason you're requesting to manipulate user's co
2222

2323
## API
2424

25+
### `contacts.requestAccess()`
26+
27+
Returns `Promise<String>` - Can be one of 'Denied', 'Authorized'.
28+
29+
Requests access to the [CNContactStore](https://developer.apple.com/documentation/contacts/cncontactstore) via a dialog presented to the user.
30+
31+
If the user has previously denied the request, this method will open the Contacts pane within the Privacy section of System Preferences.
32+
2533
### `contacts.getAuthStatus()`
2634

2735
Returns `String` - Can be one of 'Not Determined', 'Denied', 'Authorized', or 'Restricted'.

contacts.mm

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#import <AppKit/AppKit.h>
12
#import <Contacts/Contacts.h>
23
#include <napi.h>
34

@@ -6,6 +7,11 @@
67

78
/***** HELPERS *****/
89

10+
// Dummy value to pass into function parameter for ThreadSafeFunction.
11+
Napi::Value NoOp(const Napi::CallbackInfo &info) {
12+
return info.Env().Undefined();
13+
}
14+
915
// Parses and returns an array of email addresses as strings.
1016
Napi::Array GetEmailAddresses(Napi::Env env, CNContact *cncontact) {
1117
int num_email_addresses = [[cncontact emailAddresses] count];
@@ -263,6 +269,22 @@ CNAuthorizationStatus AuthStatus() {
263269
return [CNContactStore authorizationStatusForEntityType:entityType];
264270
}
265271

272+
// Returns the authorization status as a string.
273+
std::string AuthStatusString() {
274+
std::string auth_status = "Not Determined";
275+
276+
CNAuthorizationStatus status_for_entity = AuthStatus();
277+
278+
if (status_for_entity == CNAuthorizationStatusAuthorized)
279+
auth_status = "Authorized";
280+
else if (status_for_entity == CNAuthorizationStatusDenied)
281+
auth_status = "Denied";
282+
else if (status_for_entity == CNAuthorizationStatusRestricted)
283+
auth_status = "Restricted";
284+
285+
return auth_status;
286+
}
287+
266288
// Returns the set of Contacts properties to retrieve from the CNContactStore.
267289
NSArray *GetContactKeys(Napi::Array requested_keys) {
268290
NSMutableArray *keys = [NSMutableArray arrayWithArray:@[
@@ -392,21 +414,54 @@ CNAuthorizationStatus AuthStatus() {
392414

393415
/***** EXPORTED FUNCTIONS *****/
394416

395-
// Returns the user's Contacts access consent status as a string.
396-
Napi::Value GetAuthStatus(const Napi::CallbackInfo &info) {
417+
// Request Contacts access.
418+
Napi::Promise RequestAccess(const Napi::CallbackInfo &info) {
397419
Napi::Env env = info.Env();
398-
std::string auth_status = "Not Determined";
399-
400-
CNAuthorizationStatus status_for_entity = AuthStatus();
420+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
421+
Napi::ThreadSafeFunction ts_fn = Napi::ThreadSafeFunction::New(
422+
env, Napi::Function::New(env, NoOp), "contactsCallback", 0, 1);
423+
424+
if (@available(macOS 10.11, *)) {
425+
std::string status = AuthStatusString();
426+
427+
if (status == "Not Determined") {
428+
__block Napi::ThreadSafeFunction tsfn = ts_fn;
429+
CNContactStore *store = [CNContactStore new];
430+
[store requestAccessForEntityType:CNEntityTypeContacts
431+
completionHandler:^(BOOL granted, NSError *error) {
432+
auto callback = [=](Napi::Env env, Napi::Function js_cb,
433+
const char *granted) {
434+
deferred.Resolve(Napi::String::New(env, granted));
435+
};
436+
tsfn.BlockingCall(granted ? "Authorized" : "Denied",
437+
callback);
438+
tsfn.Release();
439+
}];
440+
} else if (status == "Denied") {
441+
NSWorkspace *workspace = [[NSWorkspace alloc] init];
442+
NSString *pref_string = @"x-apple.systempreferences:com.apple.preference."
443+
@"security?Contacts";
444+
445+
[workspace openURL:[NSURL URLWithString:pref_string]];
446+
447+
ts_fn.Release();
448+
deferred.Resolve(Napi::String::New(env, "Denied"));
449+
} else {
450+
ts_fn.Release();
451+
deferred.Resolve(Napi::String::New(env, "Authorized"));
452+
}
453+
} else {
454+
ts_fn.Release();
455+
deferred.Resolve(Napi::String::New(env, "Authorized"));
456+
}
401457

402-
if (status_for_entity == CNAuthorizationStatusAuthorized)
403-
auth_status = "Authorized";
404-
else if (status_for_entity == CNAuthorizationStatusDenied)
405-
auth_status = "Denied";
406-
else if (status_for_entity == CNAuthorizationStatusRestricted)
407-
auth_status = "Restricted";
458+
return deferred.Promise();
459+
}
408460

409-
return Napi::Value::From(env, auth_status);
461+
// Returns the user's Contacts access consent status as a string.
462+
Napi::Value GetAuthStatus(const Napi::CallbackInfo &info) {
463+
Napi::Env env = info.Env();
464+
return Napi::Value::From(env, AuthStatusString());
410465
}
411466

412467
// Returns an array of all a user's Contacts as objects.
@@ -606,6 +661,8 @@ CNAuthorizationStatus AuthStatus() {
606661
Napi::Function::New(env, RemoveListener));
607662
exports.Set(Napi::String::New(env, "isListening"),
608663
Napi::Function::New(env, IsListening));
664+
exports.Set(Napi::String::New(env, "requestAccess"),
665+
Napi::Function::New(env, RequestAccess));
609666
exports.Set(Napi::String::New(env, "getAuthStatus"),
610667
Napi::Function::New(env, GetAuthStatus));
611668
exports.Set(Napi::String::New(env, "getAllContacts"),

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function deleteContact(contact) {
171171

172172
module.exports = {
173173
listener,
174+
requestAccess: contacts.requestAccess,
174175
getAuthStatus: contacts.getAuthStatus,
175176
getAllContacts,
176177
getContactsByName,

test/module.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ const {
77
deleteContact,
88
updateContact,
99
listener,
10+
requestAccess,
1011
} = require('../index')
1112

1213
const isCI = require('is-ci')
1314
const ifit = (condition) => (condition ? it : it.skip)
1415
const ifdescribe = (condition) => (condition ? describe : describe.skip)
1516

17+
if (!isCI) {
18+
requestAccess().then((status) => {
19+
if (status !== 'Authorized') {
20+
console.error('Access to Contacts not authorized - cannot proceed.')
21+
process.exit(1)
22+
}
23+
})
24+
}
25+
1626
describe('node-mac-contacts', () => {
1727
describe('getAuthStatus()', () => {
1828
it('should not throw', () => {

0 commit comments

Comments
 (0)