@capgo/capacitor-contacts 8.0.16 → 8.0.18

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.
@@ -40,7 +40,7 @@ import java.util.Set;
40
40
  )
41
41
  public class CapacitorContactsPlugin extends Plugin {
42
42
 
43
- private final String pluginVersion = "8.0.16";
43
+ private final String pluginVersion = "8.0.18";
44
44
  private static final int BATCH_SIZE = 50;
45
45
 
46
46
  // MARK: - Implemented API surface
@@ -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.16"
8
+ private let pluginVersion: String = "8.0.18"
9
9
  public let identifier = "CapacitorContactsPlugin"
10
10
  public let jsName = "CapacitorContacts"
11
11
  public let pluginMethods: [CAPPluginMethod] = [
@@ -368,22 +368,82 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
368
368
  // MARK: - UI picker and display operations
369
369
 
370
370
  private var currentPickerCall: CAPPluginCall?
371
+ // Holds a strong reference to the picker delegate so it isn't deallocated
372
+ // before the picker dismisses (CNContactPickerViewController delegates are weak).
373
+ private var currentPickerDelegate: NSObject?
371
374
 
372
375
  @objc func pickContact(_ call: CAPPluginCall) {
373
376
  DispatchQueue.main.async {
377
+ // Re-entrancy guard: only one picker can be active at a time.
378
+ if self.currentPickerCall != nil {
379
+ call.reject("A contact picker is already open.")
380
+ return
381
+ }
382
+ // Validate bridge and viewController are available before proceeding.
383
+ guard let viewController = self.bridge?.viewController else {
384
+ call.reject("Unable to present contact picker: no view controller available.")
385
+ return
386
+ }
374
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": []])
405
+ }
406
+ self.currentPickerDelegate = delegate
375
407
  let picker = CNContactPickerViewController()
376
- picker.delegate = self
377
- self.bridge?.viewController?.present(picker, animated: true)
408
+ picker.delegate = delegate
409
+ viewController.present(picker, animated: true)
378
410
  }
379
411
  }
380
412
 
381
413
  @objc func pickContacts(_ call: CAPPluginCall) {
382
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
424
+ }
383
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])
436
+ }
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": []])
442
+ }
443
+ self.currentPickerDelegate = delegate
384
444
  let picker = CNContactPickerViewController()
385
- picker.delegate = self
386
- self.bridge?.viewController?.present(picker, animated: true)
445
+ picker.delegate = delegate
446
+ viewController.present(picker, animated: true)
387
447
  }
388
448
  }
389
449
 
@@ -413,19 +473,29 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
413
473
 
414
474
  @objc func displayCreateContact(_ call: CAPPluginCall) {
415
475
  DispatchQueue.main.async {
476
+ // Re-entrancy guard: only one picker can be active at a time.
477
+ if self.currentPickerCall != nil {
478
+ call.reject("A contact picker is already open.")
479
+ return
480
+ }
481
+ // Validate bridge and viewController are available before proceeding.
482
+ guard let viewController = self.bridge?.viewController else {
483
+ call.reject("Unable to present contact editor: no view controller available.")
484
+ return
485
+ }
416
486
  let newContact = CNMutableContact()
417
487
 
418
488
  if let contactData = call.options["contact"] as? JSObject {
419
489
  self.populateContact(newContact, from: contactData)
420
490
  }
421
491
 
422
- let viewController = CNContactViewController(forNewContact: newContact)
423
- viewController.delegate = self
424
- viewController.contactStore = self.contactStore
492
+ let contactViewController = CNContactViewController(forNewContact: newContact)
493
+ contactViewController.delegate = self
494
+ contactViewController.contactStore = self.contactStore
425
495
 
426
- let navController = UINavigationController(rootViewController: viewController)
496
+ let navController = UINavigationController(rootViewController: contactViewController)
427
497
  self.currentPickerCall = call
428
- self.bridge?.viewController?.present(navController, animated: true)
498
+ viewController.present(navController, animated: true)
429
499
  }
430
500
  }
431
501
 
@@ -960,35 +1030,47 @@ public class CapacitorContactsPlugin: CAPPlugin, CAPBridgedPlugin {
960
1030
  }
961
1031
  }
962
1032
 
963
- // MARK: - CNContactPickerDelegate
1033
+ // MARK: - Single-selection picker delegate (no checkboxes in UI)
1034
+ // Only implementing the singular `didSelect contact:` tells iOS NOT to
1035
+ // enable multi-selection mode on CNContactPickerViewController.
964
1036
 
965
- extension CapacitorContactsPlugin: CNContactPickerDelegate {
966
- public func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
967
- guard let call = currentPickerCall else { return }
968
- currentPickerCall = nil
1037
+ private class SingleContactPickerDelegate: NSObject, CNContactPickerDelegate {
1038
+ var onSelect: ((CNContact) -> Void)?
1039
+ var onCancel: (() -> Void)?
969
1040
 
970
- let includeGroupIds = true
971
- var membership: [String: [String]] = [:]
972
- let contactIds = contacts.map(\.identifier)
973
- membership = groupMembershipMap(for: contactIds)
1041
+ func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
1042
+ onSelect?(contact)
1043
+ }
974
1044
 
975
- let serialized = contacts.map { serialize(contact: $0, fields: nil, membership: membership) }
976
- call.resolve(["contacts": serialized])
1045
+ func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
1046
+ onCancel?()
977
1047
  }
978
1048
 
979
- public func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
980
- guard let call = currentPickerCall else { return }
981
- currentPickerCall = nil
1049
+ deinit {
1050
+ onSelect = nil
1051
+ onCancel = nil
1052
+ }
1053
+ }
1054
+
1055
+ // MARK: - Multi-selection picker delegate (checkboxes enabled in UI)
1056
+ // Implementing the plural `didSelect contacts:` is what tells iOS to
1057
+ // enable multi-selection mode (checkboxes) on CNContactPickerViewController.
982
1058
 
983
- let membership = groupMembershipMap(for: [contact.identifier])
984
- let serialized = serialize(contact: contact, fields: nil, membership: membership)
985
- call.resolve(["contacts": [serialized]])
1059
+ private class MultiContactPickerDelegate: NSObject, CNContactPickerDelegate {
1060
+ var onSelect: (([CNContact]) -> Void)?
1061
+ var onCancel: (() -> Void)?
1062
+
1063
+ func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {
1064
+ onSelect?(contacts)
986
1065
  }
987
1066
 
988
- public func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
989
- guard let call = currentPickerCall else { return }
990
- currentPickerCall = nil
991
- call.resolve(["contacts": []])
1067
+ func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
1068
+ onCancel?()
1069
+ }
1070
+
1071
+ deinit {
1072
+ onSelect = nil
1073
+ onCancel = nil
992
1074
  }
993
1075
  }
994
1076
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-contacts",
3
- "version": "8.0.16",
3
+ "version": "8.0.18",
4
4
  "description": "Work with device contacts using Capacitor APIs",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -52,8 +52,7 @@
52
52
  "prepublishOnly": "bun run build",
53
53
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
54
54
  "example:install": "cd example-app && bun install --frozen-lockfile",
55
- "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
56
- "example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
55
+ "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
57
56
  },
58
57
  "devDependencies": {
59
58
  "@capacitor/android": "^8.0.0",