@capgo/capacitor-contacts 8.0.19 → 8.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -24
- package/android/src/main/java/app/capgo/contacts/CapacitorContactsPlugin.java +231 -20
- package/dist/docs.json +71 -5
- package/dist/esm/definitions.d.ts +70 -0
- package/dist/esm/definitions.js +29 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js +30 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +30 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorContactsPlugin/CapacitorContactsPlugin.swift +190 -56
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import UIKit
|
|
|
5
5
|
|
|
6
6
|
@objc(CapacitorContactsPlugin)
|
|
7
7
|
public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
8
|
-
private let pluginVersion: String = "8.
|
|
8
|
+
private let pluginVersion: String = "8.1.2"
|
|
9
9
|
public let identifier = "CapacitorContactsPlugin"
|
|
10
10
|
public let jsName = "CapacitorContacts"
|
|
11
11
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -373,78 +373,191 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
373
373
|
private var currentPickerDelegate: NSObject?
|
|
374
374
|
|
|
375
375
|
@objc func pickContact(_ call: CAPPluginCall) {
|
|
376
|
+
presentContactPicker(call, multiple: false)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@objc func pickContacts(_ call: CAPPluginCall) {
|
|
380
|
+
let multiple = (call.options["multiple"] as? Bool) ?? true
|
|
381
|
+
presentContactPicker(call, multiple: multiple)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func presentContactPicker(_ call: CAPPluginCall, multiple: Bool) {
|
|
376
385
|
DispatchQueue.main.async {
|
|
377
|
-
// Re-entrancy guard: only one picker can be active at a time.
|
|
378
386
|
if self.currentPickerCall != nil {
|
|
379
387
|
call.reject("A contact picker is already open.")
|
|
380
388
|
return
|
|
381
389
|
}
|
|
382
|
-
// Validate bridge and viewController are available before proceeding.
|
|
383
390
|
guard let viewController = self.bridge?.viewController else {
|
|
384
391
|
call.reject("Unable to present contact picker: no view controller available.")
|
|
385
392
|
return
|
|
386
393
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
delegate.onSelect = { [weak self, call] contact in
|
|
393
|
-
guard let self else { return }
|
|
394
|
-
self.currentPickerCall = nil
|
|
395
|
-
self.currentPickerDelegate = nil
|
|
396
|
-
let membership = self.groupMembershipMap(for: [contact.identifier])
|
|
397
|
-
let serialized = self.serialize(contact: contact, fields: nil, membership: membership)
|
|
398
|
-
call.resolve(["contacts": [serialized]])
|
|
399
|
-
}
|
|
400
|
-
delegate.onCancel = { [weak self, call] in
|
|
401
|
-
guard let self else { return }
|
|
402
|
-
self.currentPickerCall = nil
|
|
403
|
-
self.currentPickerDelegate = nil
|
|
404
|
-
call.resolve(["contacts": []])
|
|
394
|
+
|
|
395
|
+
let property = call.options["property"] as? String
|
|
396
|
+
if let property, self.displayedPropertyKeys(for: property) == nil {
|
|
397
|
+
call.reject("Invalid property. Use phoneNumber, emailAddress, or postalAddress.")
|
|
398
|
+
return
|
|
405
399
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
picker
|
|
400
|
+
|
|
401
|
+
self.currentPickerCall = call
|
|
402
|
+
let picker = self.makeContactPicker(call: call, property: property, multiple: multiple)
|
|
409
403
|
viewController.present(picker, animated: true)
|
|
410
404
|
}
|
|
411
405
|
}
|
|
412
406
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
407
|
+
private func makeContactPicker(call: CAPPluginCall, property: String?, multiple: Bool) -> CNContactPickerViewController {
|
|
408
|
+
let picker = CNContactPickerViewController()
|
|
409
|
+
if let property, let keys = displayedPropertyKeys(for: property) {
|
|
410
|
+
picker.displayedPropertyKeys = keys
|
|
411
|
+
picker.predicateForEnablingContact = predicateForProperty(property)
|
|
412
|
+
picker.predicateForSelectionOfProperty = predicateForSelectionOfProperty(property)
|
|
413
|
+
picker.delegate = propertyPickerDelegate(call)
|
|
414
|
+
return picker
|
|
415
|
+
}
|
|
416
|
+
if multiple {
|
|
417
|
+
picker.delegate = multiPickerDelegate(call)
|
|
418
|
+
return picker
|
|
419
|
+
}
|
|
420
|
+
picker.delegate = singlePickerDelegate(call)
|
|
421
|
+
return picker
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private func resolvePicker(_ call: CAPPluginCall, contacts: [JSObject]) {
|
|
425
|
+
currentPickerCall = nil
|
|
426
|
+
currentPickerDelegate = nil
|
|
427
|
+
call.resolve(["contacts": contacts])
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private func propertyPickerDelegate(_ call: CAPPluginCall) -> PropertyContactPickerDelegate {
|
|
431
|
+
let delegate = PropertyContactPickerDelegate()
|
|
432
|
+
delegate.onSelect = { [weak self, call] contactProperty in
|
|
433
|
+
guard let self else { return }
|
|
434
|
+
self.resolvePicker(call, contacts: [self.serializePickedProperty(contactProperty)])
|
|
435
|
+
}
|
|
436
|
+
delegate.onCancel = { [weak self, call] in
|
|
437
|
+
self?.resolvePicker(call, contacts: [])
|
|
438
|
+
}
|
|
439
|
+
currentPickerDelegate = delegate
|
|
440
|
+
return delegate
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private func multiPickerDelegate(_ call: CAPPluginCall) -> MultiContactPickerDelegate {
|
|
444
|
+
let delegate = MultiContactPickerDelegate()
|
|
445
|
+
delegate.onSelect = { [weak self, call] contacts in
|
|
446
|
+
guard let self else { return }
|
|
447
|
+
let membership = self.groupMembershipMap(for: contacts.map(\.identifier))
|
|
448
|
+
let serialized = contacts.map { self.serialize(contact: $0, fields: nil, membership: membership) }
|
|
449
|
+
self.resolvePicker(call, contacts: serialized)
|
|
450
|
+
}
|
|
451
|
+
delegate.onCancel = { [weak self, call] in
|
|
452
|
+
self?.resolvePicker(call, contacts: [])
|
|
453
|
+
}
|
|
454
|
+
currentPickerDelegate = delegate
|
|
455
|
+
return delegate
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private func singlePickerDelegate(_ call: CAPPluginCall) -> SingleContactPickerDelegate {
|
|
459
|
+
let delegate = SingleContactPickerDelegate()
|
|
460
|
+
delegate.onSelect = { [weak self, call] contact in
|
|
461
|
+
guard let self else { return }
|
|
462
|
+
let membership = self.groupMembershipMap(for: [contact.identifier])
|
|
463
|
+
self.resolvePicker(call, contacts: [self.serialize(contact: contact, fields: nil, membership: membership)])
|
|
464
|
+
}
|
|
465
|
+
delegate.onCancel = { [weak self, call] in
|
|
466
|
+
self?.resolvePicker(call, contacts: [])
|
|
467
|
+
}
|
|
468
|
+
currentPickerDelegate = delegate
|
|
469
|
+
return delegate
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private func displayedPropertyKeys(for property: String) -> [String]? {
|
|
473
|
+
let nameKeys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactMiddleNameKey]
|
|
474
|
+
switch property {
|
|
475
|
+
case "phoneNumber":
|
|
476
|
+
return nameKeys + [CNContactPhoneNumbersKey]
|
|
477
|
+
case "emailAddress":
|
|
478
|
+
return nameKeys + [CNContactEmailAddressesKey]
|
|
479
|
+
case "postalAddress":
|
|
480
|
+
return nameKeys + [CNContactPostalAddressesKey]
|
|
481
|
+
default:
|
|
482
|
+
return nil
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private func predicateForSelectionOfProperty(_ property: String) -> NSPredicate? {
|
|
487
|
+
switch property {
|
|
488
|
+
case "phoneNumber":
|
|
489
|
+
return NSPredicate(format: "key == %@", CNContactPhoneNumbersKey)
|
|
490
|
+
case "emailAddress":
|
|
491
|
+
return NSPredicate(format: "key == %@", CNContactEmailAddressesKey)
|
|
492
|
+
case "postalAddress":
|
|
493
|
+
return NSPredicate(format: "key == %@", CNContactPostalAddressesKey)
|
|
494
|
+
default:
|
|
495
|
+
return nil
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private func predicateForProperty(_ property: String) -> NSPredicate? {
|
|
500
|
+
switch property {
|
|
501
|
+
case "phoneNumber":
|
|
502
|
+
return NSPredicate(format: "phoneNumbers.@count > 0")
|
|
503
|
+
case "emailAddress":
|
|
504
|
+
return NSPredicate(format: "emailAddresses.@count > 0")
|
|
505
|
+
case "postalAddress":
|
|
506
|
+
return NSPredicate(format: "postalAddresses.@count > 0")
|
|
507
|
+
default:
|
|
508
|
+
return nil
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private func serializePickedProperty(_ contactProperty: CNContactProperty) -> JSObject {
|
|
513
|
+
let contact = contactProperty.contact
|
|
514
|
+
var result: JSObject = [:]
|
|
515
|
+
result["id"] = contact.identifier
|
|
516
|
+
result["displayName"] = CNContactFormatter.string(from: contact, style: .fullName) ?? ""
|
|
517
|
+
|
|
518
|
+
switch contactProperty.key {
|
|
519
|
+
case CNContactPhoneNumbersKey:
|
|
520
|
+
if let phone = contactProperty.value as? CNPhoneNumber {
|
|
521
|
+
let (type, label) = mapPhoneLabel(contactProperty.label)
|
|
522
|
+
var entry: JSObject = [:]
|
|
523
|
+
entry["value"] = phone.stringValue
|
|
524
|
+
entry["type"] = type
|
|
525
|
+
entry["isPrimary"] = false
|
|
526
|
+
if let label { entry["label"] = label }
|
|
527
|
+
result["phoneNumbers"] = [entry]
|
|
424
528
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
let
|
|
433
|
-
|
|
434
|
-
let serialized = contacts.map { self.serialize(contact: $0, fields: nil, membership: membership) }
|
|
435
|
-
call.resolve(["contacts": serialized])
|
|
529
|
+
case CNContactEmailAddressesKey:
|
|
530
|
+
if let email = contactProperty.value as? String {
|
|
531
|
+
let (type, label) = mapEmailLabel(contactProperty.label)
|
|
532
|
+
var entry: JSObject = [:]
|
|
533
|
+
entry["value"] = email
|
|
534
|
+
entry["type"] = type
|
|
535
|
+
entry["isPrimary"] = false
|
|
536
|
+
if let label { entry["label"] = label }
|
|
537
|
+
result["emailAddresses"] = [entry]
|
|
436
538
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
539
|
+
case CNContactPostalAddressesKey:
|
|
540
|
+
if let postal = contactProperty.value as? CNPostalAddress {
|
|
541
|
+
let (type, label) = mapPostalLabel(contactProperty.label)
|
|
542
|
+
var entry: JSObject = [:]
|
|
543
|
+
entry["city"] = postal.city
|
|
544
|
+
entry["country"] = postal.country
|
|
545
|
+
entry["formatted"] = CNPostalAddressFormatter.string(from: postal, style: .mailingAddress)
|
|
546
|
+
entry["isoCountryCode"] = postal.isoCountryCode
|
|
547
|
+
entry["isPrimary"] = false
|
|
548
|
+
entry["neighborhood"] = postal.subLocality
|
|
549
|
+
entry["postalCode"] = postal.postalCode
|
|
550
|
+
entry["state"] = postal.state
|
|
551
|
+
entry["street"] = postal.street
|
|
552
|
+
entry["type"] = type
|
|
553
|
+
if let label { entry["label"] = label }
|
|
554
|
+
result["postalAddresses"] = [entry]
|
|
442
555
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
picker.delegate = delegate
|
|
446
|
-
viewController.present(picker, animated: true)
|
|
556
|
+
default:
|
|
557
|
+
break
|
|
447
558
|
}
|
|
559
|
+
|
|
560
|
+
return result
|
|
448
561
|
}
|
|
449
562
|
|
|
450
563
|
@objc func displayContactById(_ call: CAPPluginCall) {
|
|
@@ -608,7 +721,7 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
608
721
|
// No additional keys required, but this preserves the behaviour if custom keys are needed later.
|
|
609
722
|
}
|
|
610
723
|
|
|
611
|
-
if shouldFetchAll || fields!.contains("fullName") {
|
|
724
|
+
if shouldFetchAll || fields!.contains("fullName") || fields!.contains("displayName") {
|
|
612
725
|
keys.append(CNContactFormatter.descriptorForRequiredKeys(for: .fullName))
|
|
613
726
|
}
|
|
614
727
|
|
|
@@ -675,6 +788,9 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
675
788
|
if shouldInclude("fullName") {
|
|
676
789
|
result["fullName"] = CNContactFormatter.string(from: contact, style: .fullName) ?? ""
|
|
677
790
|
}
|
|
791
|
+
if shouldInclude("displayName") {
|
|
792
|
+
result["displayName"] = CNContactFormatter.string(from: contact, style: .fullName) ?? ""
|
|
793
|
+
}
|
|
678
794
|
|
|
679
795
|
if shouldInclude("emailAddresses") {
|
|
680
796
|
let emails: [JSObject] = contact.emailAddresses.map { labeledValue in
|
|
@@ -1030,6 +1146,24 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1030
1146
|
}
|
|
1031
1147
|
}
|
|
1032
1148
|
|
|
1149
|
+
private class PropertyContactPickerDelegate: NSObject, CNContactPickerDelegate {
|
|
1150
|
+
var onSelect: ((CNContactProperty) -> Void)?
|
|
1151
|
+
var onCancel: (() -> Void)?
|
|
1152
|
+
|
|
1153
|
+
func contactPicker(_ picker: CNContactPickerViewController, didSelect contactProperty: CNContactProperty) {
|
|
1154
|
+
onSelect?(contactProperty)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
|
|
1158
|
+
onCancel?()
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
deinit {
|
|
1162
|
+
onSelect = nil
|
|
1163
|
+
onCancel = nil
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1033
1167
|
// MARK: - Single-selection picker delegate (no checkboxes in UI)
|
|
1034
1168
|
// Only implementing the singular `didSelect contact:` tells iOS NOT to
|
|
1035
1169
|
// enable multi-selection mode on CNContactPickerViewController.
|