@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.
@@ -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.0.19"
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
- self.currentPickerCall = call
388
- // Use a single-selection delegate so iOS does NOT show checkboxes.
389
- // CNContactPickerViewController enables multi-select UI only when the
390
- // delegate implements the plural didSelect(contacts:) method.
391
- let delegate = SingleContactPickerDelegate()
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
- self.currentPickerDelegate = delegate
407
- let picker = CNContactPickerViewController()
408
- picker.delegate = delegate
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
- @objc func pickContacts(_ call: CAPPluginCall) {
414
- DispatchQueue.main.async {
415
- // Re-entrancy guard: only one picker can be active at a time.
416
- if self.currentPickerCall != nil {
417
- call.reject("A contact picker is already open.")
418
- return
419
- }
420
- // Validate bridge and viewController are available before proceeding.
421
- guard let viewController = self.bridge?.viewController else {
422
- call.reject("Unable to present contact picker: no view controller available.")
423
- return
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
- self.currentPickerCall = call
426
- // Use a multi-selection delegate so iOS shows checkboxes as expected.
427
- let delegate = MultiContactPickerDelegate()
428
- delegate.onSelect = { [weak self, call] contacts in
429
- guard let self else { return }
430
- self.currentPickerCall = nil
431
- self.currentPickerDelegate = nil
432
- let contactIds = contacts.map(\.identifier)
433
- let membership = self.groupMembershipMap(for: contactIds)
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
- delegate.onCancel = { [weak self, call] in
438
- guard let self else { return }
439
- self.currentPickerCall = nil
440
- self.currentPickerDelegate = nil
441
- call.resolve(["contacts": []])
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
- self.currentPickerDelegate = delegate
444
- let picker = CNContactPickerViewController()
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-contacts",
3
- "version": "8.0.19",
3
+ "version": "8.1.2",
4
4
  "description": "Work with device contacts using Capacitor APIs",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",