@capgo/capacitor-contacts 8.0.18 → 8.1.0

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 CHANGED
@@ -45,6 +45,48 @@ npm install @capgo/capacitor-contacts
45
45
  npx cap sync
46
46
  ```
47
47
 
48
+ ## Android
49
+
50
+ Google Play's Contacts Permissions policy (effective 27 January 2027) allows `READ_CONTACTS` only when the Android Contact Picker cannot cover your core feature, for apps that target Android 17 (API 37) or later.
51
+
52
+ This plugin does **not** declare `READ_CONTACTS` or `WRITE_CONTACTS`. Add them in your app manifest only if you need them.
53
+
54
+ **Pick a phone, email, or address without `READ_CONTACTS`:**
55
+
56
+ ```typescript
57
+ import { CapacitorContacts, ContactProperty } from '@capgo/capacitor-contacts';
58
+
59
+ const { contacts } = await CapacitorContacts.pickContacts({
60
+ property: ContactProperty.PhoneNumber,
61
+ });
62
+
63
+ const number = contacts[0]?.phoneNumbers?.[0]?.value;
64
+ ```
65
+
66
+ `property` works on every Android version. The picker returns a data-row URI the plugin can read without a permission. The result contains `id`, `displayName`, and the selected property only.
67
+
68
+ Use `ContactProperty.EmailAddress` or `ContactProperty.PostalAddress` the same way.
69
+
70
+ **When you still need `READ_CONTACTS`:**
71
+
72
+ - `getContacts()`, `getContactById()`, `countContacts()`, `getGroups()`, `getAccounts()`
73
+ - Picking a full contact without `property` on Android 16 and below
74
+ - Syncing, backing up, or matching the whole address book
75
+
76
+ If those are core features, add the permission and file the Play Console declaration before pre-review checks start on 27 October 2026:
77
+
78
+ ```xml
79
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
80
+ ```
81
+
82
+ `WRITE_CONTACTS` is unchanged by this policy. Add it only if you create or update contacts in code.
83
+
84
+ On Android 17 and later, picking a full contact without `property` also works without `READ_CONTACTS` because the system picker returns a session URI. Keep using `property` if you still support older Android versions and want the permission out of the manifest entirely.
85
+
86
+ ## iOS
87
+
88
+ `pickContact()` / `pickContacts()` never require a contacts permission. Add `NSContactsUsageDescription` only if you call methods that read or write the address book (`getContacts`, `createContact`, and similar).
89
+
48
90
  ## API
49
91
 
50
92
  <docgen-index>
@@ -73,6 +115,7 @@ npx cap sync
73
115
  * [`getPluginVersion()`](#getpluginversion)
74
116
  * [Interfaces](#interfaces)
75
117
  * [Type Aliases](#type-aliases)
118
+ * [Enums](#enums)
76
119
 
77
120
  </docgen-index>
78
121
 
@@ -359,6 +402,9 @@ pickContact(options?: PickContactsOptions | undefined) => Promise<PickContactRes
359
402
 
360
403
  <a href="#pick">Pick</a> a single contact using the native contact picker.
361
404
 
405
+ Same options as {@link pickContacts}. Pass `property` to select one phone
406
+ number, email address, or postal address without `READ_CONTACTS` on Android.
407
+
362
408
  | Param | Type | Description |
363
409
  | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
364
410
  | **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
@@ -378,6 +424,18 @@ pickContacts(options?: PickContactsOptions | undefined) => Promise<PickContactsR
378
424
 
379
425
  <a href="#pick">Pick</a> one or more contacts using the native contact picker.
380
426
 
427
+ On iOS this never requires a contacts permission.
428
+
429
+ On Android, pass `property` to select a single phone number, email address,
430
+ or postal address with no `READ_CONTACTS` permission. That works on every
431
+ Android version and is the way to ship without declaring the permission
432
+ when targeting Android 17 (API 37) or later.
433
+
434
+ Picking a full contact without `property` still needs `READ_CONTACTS` below
435
+ Android 17, because the picker URI exposes no phone, email, or structured
436
+ name. On Android 17 and later the system contact picker returns a session
437
+ URI the plugin reads without that permission.
438
+
381
439
  | Param | Type | Description |
382
440
  | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
383
441
  | **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
@@ -489,26 +547,27 @@ Options for creating a contact.
489
547
 
490
548
  <a href="#contact">Contact</a> information.
491
549
 
492
- | Prop | Type | Description | Since |
493
- | ---------------------- | --------------------------------------------- | ------------------------------------------------------------- | ----- |
494
- | **`id`** | <code>string</code> | Unique identifier for the contact. | 1.0.0 |
495
- | **`account`** | <code><a href="#account">Account</a></code> | <a href="#account">Account</a> information for the contact. | 1.0.0 |
496
- | **`birthday`** | <code><a href="#birthday">Birthday</a></code> | <a href="#birthday">Birthday</a> information for the contact. | 1.0.0 |
497
- | **`emailAddresses`** | <code>EmailAddress[]</code> | Email addresses for the contact. | 1.0.0 |
498
- | **`familyName`** | <code>string</code> | Family name (last name) of the contact. | 1.0.0 |
499
- | **`fullName`** | <code>string</code> | Full name of the contact. | 1.0.0 |
500
- | **`givenName`** | <code>string</code> | Given name (first name) of the contact. | 1.0.0 |
501
- | **`groupIds`** | <code>string[]</code> | <a href="#group">Group</a> IDs the contact belongs to. | 1.0.0 |
502
- | **`jobTitle`** | <code>string</code> | Job title of the contact. | 1.0.0 |
503
- | **`middleName`** | <code>string</code> | Middle name of the contact. | 1.0.0 |
504
- | **`namePrefix`** | <code>string</code> | Name prefix (e.g., "Dr.", "Mr.", "Ms.") of the contact. | 1.0.0 |
505
- | **`nameSuffix`** | <code>string</code> | Name suffix (e.g., "Jr.", "Sr.", "III") of the contact. | 1.0.0 |
506
- | **`note`** | <code>string</code> | Notes about the contact. | 1.0.0 |
507
- | **`organizationName`** | <code>string</code> | Organization name of the contact. | 1.0.0 |
508
- | **`phoneNumbers`** | <code>PhoneNumber[]</code> | Phone numbers for the contact. | 1.0.0 |
509
- | **`photo`** | <code>string</code> | Base64-encoded photo of the contact. | 1.0.0 |
510
- | **`postalAddresses`** | <code>PostalAddress[]</code> | Postal addresses for the contact. | 1.0.0 |
511
- | **`urlAddresses`** | <code>UrlAddress[]</code> | URL addresses for the contact. | 1.0.0 |
550
+ | Prop | Type | Description | Since |
551
+ | ---------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
552
+ | **`id`** | <code>string</code> | Unique identifier for the contact. | 1.0.0 |
553
+ | **`displayName`** | <code>string</code> | Formatted name shown by the device for this contact. Derived from `CNContactFormatter` on iOS and `DISPLAY_NAME` on Android. Read-only: setting this when creating or updating a contact has no effect. Use `givenName` and `familyName` to write a name. | 8.1.0 |
554
+ | **`account`** | <code><a href="#account">Account</a></code> | <a href="#account">Account</a> information for the contact. | 1.0.0 |
555
+ | **`birthday`** | <code><a href="#birthday">Birthday</a></code> | <a href="#birthday">Birthday</a> information for the contact. | 1.0.0 |
556
+ | **`emailAddresses`** | <code>EmailAddress[]</code> | Email addresses for the contact. | 1.0.0 |
557
+ | **`familyName`** | <code>string</code> | Family name (last name) of the contact. | 1.0.0 |
558
+ | **`fullName`** | <code>string</code> | Full name of the contact. | 1.0.0 |
559
+ | **`givenName`** | <code>string</code> | Given name (first name) of the contact. | 1.0.0 |
560
+ | **`groupIds`** | <code>string[]</code> | <a href="#group">Group</a> IDs the contact belongs to. | 1.0.0 |
561
+ | **`jobTitle`** | <code>string</code> | Job title of the contact. | 1.0.0 |
562
+ | **`middleName`** | <code>string</code> | Middle name of the contact. | 1.0.0 |
563
+ | **`namePrefix`** | <code>string</code> | Name prefix (e.g., "Dr.", "Mr.", "Ms.") of the contact. | 1.0.0 |
564
+ | **`nameSuffix`** | <code>string</code> | Name suffix (e.g., "Jr.", "Sr.", "III") of the contact. | 1.0.0 |
565
+ | **`note`** | <code>string</code> | Notes about the contact. | 1.0.0 |
566
+ | **`organizationName`** | <code>string</code> | Organization name of the contact. | 1.0.0 |
567
+ | **`phoneNumbers`** | <code>PhoneNumber[]</code> | Phone numbers for the contact. | 1.0.0 |
568
+ | **`photo`** | <code>string</code> | Base64-encoded photo of the contact. | 1.0.0 |
569
+ | **`postalAddresses`** | <code>PostalAddress[]</code> | Postal addresses for the contact. | 1.0.0 |
570
+ | **`urlAddresses`** | <code>UrlAddress[]</code> | URL addresses for the contact. | 1.0.0 |
512
571
 
513
572
 
514
573
  #### Account
@@ -774,10 +833,11 @@ Result from picking contacts.
774
833
 
775
834
  Options for picking contacts using the native contact picker.
776
835
 
777
- | Prop | Type | Description | Since |
778
- | -------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----- |
779
- | **`fields`** | <code>(keyof <a href="#contact">Contact</a>)[]</code> | Optional list of specific fields to retrieve. If not specified, all fields are returned. | 1.0.0 |
780
- | **`multiple`** | <code>boolean</code> | Whether to allow selecting multiple contacts. Default is false. | 1.0.0 |
836
+ | Prop | Type | Description | Since |
837
+ | -------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
838
+ | **`fields`** | <code>(keyof <a href="#contact">Contact</a>)[]</code> | Optional list of specific fields to retrieve. If not specified, all fields are returned. Ignored when {@link <a href="#pickcontactsoptions">PickContactsOptions.property</a>} is set. The result then contains only `id`, `displayName`, and the selected property. | 1.0.0 |
839
+ | **`multiple`** | <code>boolean</code> | Whether to allow selecting multiple contacts. Default is false. Ignored when {@link <a href="#pickcontactsoptions">PickContactsOptions.property</a>} is set. Property picking always returns a single value. | 1.0.0 |
840
+ | **`property`** | <code><a href="#contactproperty">ContactProperty</a></code> | Restrict the picker to a single contact property. On Android this launches the picker against the phone, email, or postal address table and reads the granted data row. That requires no `READ_CONTACTS` permission on any Android version, which is required for apps that only pick a contact detail and target Android 17 (API 37) or later under the Google Play Contacts Permissions policy. On iOS the picker lets the user choose one phone number, email address, or postal address. It never requires a contacts permission. | 8.1.0 |
781
841
 
782
842
 
783
843
  #### UpdateContactByIdOptions
@@ -900,6 +960,18 @@ Type of contacts permission to request.
900
960
 
901
961
  <code>'readContacts' | 'writeContacts'</code>
902
962
 
963
+
964
+ ### Enums
965
+
966
+
967
+ #### ContactProperty
968
+
969
+ | Members | Value | Description | Since |
970
+ | ------------------- | ---------------------------- | ------------------------ | ----- |
971
+ | **`PhoneNumber`** | <code>'phoneNumber'</code> | A single phone number. | 8.1.0 |
972
+ | **`EmailAddress`** | <code>'emailAddress'</code> | A single email address. | 8.1.0 |
973
+ | **`PostalAddress`** | <code>'postalAddress'</code> | A single postal address. | 8.1.0 |
974
+
903
975
  </docgen-api>
904
976
 
905
977
  ### Credit
@@ -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.18";
43
+ private final String pluginVersion = "8.1.0";
44
44
  private static final int BATCH_SIZE = 50;
45
45
 
46
46
  // MARK: - Implemented API surface
@@ -467,19 +467,87 @@ public class CapacitorContactsPlugin extends Plugin {
467
467
  private static final int EDIT_CONTACT_REQUEST = 7004;
468
468
 
469
469
  private PluginCall currentPickerCall;
470
+ private String currentPickerProperty;
471
+
472
+ // Android 17 (API 37) system contact picker. Constants are inlined so the
473
+ // plugin still compiles against compileSdk 36.
474
+ private static final int ANDROID_API_17 = 37;
475
+ private static final String ACTION_PICK_CONTACTS = "android.provider.action.PICK_CONTACTS";
476
+ private static final String EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS = "android.provider.extra.PICK_CONTACTS_REQUESTED_DATA_FIELDS";
477
+ private static final String EXTRA_PICK_CONTACTS_SELECTION_LIMIT = "android.provider.extra.PICK_CONTACTS_SELECTION_LIMIT";
470
478
 
471
479
  @PluginMethod
472
480
  public void pickContact(PluginCall call) {
473
- currentPickerCall = call;
474
- Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
475
- startActivityForResult(call, intent, PICK_CONTACT_REQUEST);
481
+ launchContactPicker(call);
476
482
  }
477
483
 
478
484
  @PluginMethod
479
485
  public void pickContacts(PluginCall call) {
480
- // Android doesn't have a native multi-select contact picker
481
- // Fall back to single selection
482
- pickContact(call);
486
+ launchContactPicker(call);
487
+ }
488
+
489
+ private void launchContactPicker(PluginCall call) {
490
+ String property = call.getString("property");
491
+ if (property != null && contentUriForProperty(property) == null) {
492
+ call.reject("Invalid property. Use phoneNumber, emailAddress, or postalAddress.");
493
+ return;
494
+ }
495
+
496
+ currentPickerCall = call;
497
+ currentPickerProperty = property;
498
+ startActivityForResult(call, createPickIntent(call, property), PICK_CONTACT_REQUEST);
499
+ }
500
+
501
+ private Intent createPickIntent(PluginCall call, String property) {
502
+ if (property != null) {
503
+ return new Intent(Intent.ACTION_PICK, contentUriForProperty(property));
504
+ }
505
+
506
+ if (android.os.Build.VERSION.SDK_INT >= ANDROID_API_17) {
507
+ Intent intent = new Intent(ACTION_PICK_CONTACTS);
508
+ intent.putStringArrayListExtra(EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS, requestedMimeTypes(call));
509
+ boolean multiple = Boolean.TRUE.equals(call.getBoolean("multiple", false));
510
+ if (!multiple) {
511
+ intent.putExtra(EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 1);
512
+ } else {
513
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
514
+ }
515
+ if (intent.resolveActivity(getContext().getPackageManager()) != null) {
516
+ return intent;
517
+ }
518
+ }
519
+
520
+ return new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
521
+ }
522
+
523
+ private Uri contentUriForProperty(String property) {
524
+ if ("phoneNumber".equals(property)) {
525
+ return ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
526
+ }
527
+ if ("emailAddress".equals(property)) {
528
+ return ContactsContract.CommonDataKinds.Email.CONTENT_URI;
529
+ }
530
+ if ("postalAddress".equals(property)) {
531
+ return ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI;
532
+ }
533
+ return null;
534
+ }
535
+
536
+ private ArrayList<String> requestedMimeTypes(PluginCall call) {
537
+ Set<String> fields = parseFieldsArray(call);
538
+ if (fields != null) {
539
+ return new ArrayList<>(getMimeTypesForFields(fields));
540
+ }
541
+ ArrayList<String> mimeTypes = new ArrayList<>();
542
+ mimeTypes.add(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
543
+ mimeTypes.add(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
544
+ mimeTypes.add(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
545
+ mimeTypes.add(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
546
+ mimeTypes.add(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
547
+ mimeTypes.add(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
548
+ mimeTypes.add(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
549
+ mimeTypes.add(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
550
+ return mimeTypes;
483
551
  }
484
552
 
485
553
  @PluginMethod
@@ -546,6 +614,7 @@ public class CapacitorContactsPlugin extends Plugin {
546
614
  currentPickerCall = null;
547
615
 
548
616
  if (resultCode != android.app.Activity.RESULT_OK) {
617
+ currentPickerProperty = null;
549
618
  if (requestCode == PICK_CONTACT_REQUEST) {
550
619
  call.resolve(new JSObject().put("contacts", new JSArray()));
551
620
  } else if (requestCode == CREATE_CONTACT_REQUEST) {
@@ -557,20 +626,9 @@ public class CapacitorContactsPlugin extends Plugin {
557
626
  try {
558
627
  if (requestCode == PICK_CONTACT_REQUEST) {
559
628
  if (data != null && data.getData() != null) {
560
- String contactId = getContactIdFromUri(data.getData());
561
- if (contactId != null) {
562
- ContactBuilder builder = fetchContact(contactId, null);
563
- if (builder != null) {
564
- JSArray contacts = new JSArray();
565
- contacts.put(builder.toJSObject(null));
566
- call.resolve(new JSObject().put("contacts", contacts));
567
- } else {
568
- call.resolve(new JSObject().put("contacts", new JSArray()));
569
- }
570
- } else {
571
- call.resolve(new JSObject().put("contacts", new JSArray()));
572
- }
629
+ resolvePickedUri(call, data.getData());
573
630
  } else {
631
+ currentPickerProperty = null;
574
632
  call.resolve(new JSObject().put("contacts", new JSArray()));
575
633
  }
576
634
  } else if (requestCode == CREATE_CONTACT_REQUEST) {
@@ -590,6 +648,168 @@ public class CapacitorContactsPlugin extends Plugin {
590
648
  }
591
649
  }
592
650
 
651
+ private void resolvePickedUri(PluginCall call, Uri uri) {
652
+ String property = currentPickerProperty;
653
+ currentPickerProperty = null;
654
+
655
+ if (isSessionUri(uri)) {
656
+ call.resolve(new JSObject().put("contacts", contactsFromSessionUri(uri, fieldsForPickedContact(call))));
657
+ return;
658
+ }
659
+
660
+ if (property != null || isDataRowUri(uri)) {
661
+ JSObject contact = contactFromDataRowUri(uri, property);
662
+ JSArray contacts = new JSArray();
663
+ if (contact != null) {
664
+ contacts.put(contact);
665
+ }
666
+ call.resolve(new JSObject().put("contacts", contacts));
667
+ return;
668
+ }
669
+
670
+ call.resolve(new JSObject().put("contacts", contactsFromContactUri(uri, fieldsForPickedContact(call))));
671
+ }
672
+
673
+ private Set<String> fieldsForPickedContact(PluginCall call) {
674
+ Set<String> fields = parseFieldsArray(call);
675
+ if (fields == null) {
676
+ return null;
677
+ }
678
+ Set<String> withIdentity = new HashSet<>(fields);
679
+ withIdentity.add("id");
680
+ withIdentity.add("displayName");
681
+ return withIdentity;
682
+ }
683
+
684
+ private boolean isSessionUri(Uri uri) {
685
+ String authority = uri.getAuthority();
686
+ return authority != null && authority.contains("picker");
687
+ }
688
+
689
+ private boolean isDataRowUri(Uri uri) {
690
+ List<String> segments = uri.getPathSegments();
691
+ return segments != null && !segments.isEmpty() && "data".equals(segments.get(0));
692
+ }
693
+
694
+ private JSArray contactsFromSessionUri(Uri sessionUri, Set<String> fields) {
695
+ JSArray contacts = new JSArray();
696
+ Map<String, ContactBuilder> builders = new java.util.LinkedHashMap<>();
697
+ ContentResolver resolver = getContext().getContentResolver();
698
+
699
+ try (Cursor cursor = resolver.query(sessionUri, null, null, null, null)) {
700
+ if (cursor == null) {
701
+ return contacts;
702
+ }
703
+ while (cursor.moveToNext()) {
704
+ String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
705
+ if (contactId == null) {
706
+ continue;
707
+ }
708
+ ContactBuilder builder = builders.get(contactId);
709
+ if (builder == null) {
710
+ builder = new ContactBuilder(contactId);
711
+ String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
712
+ builder.displayName = displayName;
713
+ builder.fullName = displayName;
714
+ builders.put(contactId, builder);
715
+ }
716
+ processDataRow(builder, cursor);
717
+ }
718
+ }
719
+
720
+ for (ContactBuilder builder : builders.values()) {
721
+ contacts.put(builder.toJSObject(fields));
722
+ }
723
+ return contacts;
724
+ }
725
+
726
+ private JSObject contactFromDataRowUri(Uri uri, String property) {
727
+ ContentResolver resolver = getContext().getContentResolver();
728
+ try (Cursor cursor = resolver.query(uri, null, null, null, null)) {
729
+ if (cursor == null || !cursor.moveToFirst()) {
730
+ return null;
731
+ }
732
+
733
+ String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
734
+ ContactBuilder builder = new ContactBuilder(contactId != null ? contactId : uri.getLastPathSegment());
735
+ String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
736
+ builder.displayName = displayName;
737
+ builder.fullName = displayName;
738
+ processDataRow(builder, cursor);
739
+
740
+ Set<String> fields = new HashSet<>();
741
+ fields.add("id");
742
+ fields.add("displayName");
743
+ if ("emailAddress".equals(property)) {
744
+ fields.add("emailAddresses");
745
+ } else if ("postalAddress".equals(property)) {
746
+ fields.add("postalAddresses");
747
+ } else {
748
+ fields.add("phoneNumbers");
749
+ }
750
+ return builder.toJSObject(fields);
751
+ } catch (Exception ex) {
752
+ android.util.Log.w("CapacitorContacts", "Failed to read picked contact data row", ex);
753
+ return null;
754
+ }
755
+ }
756
+
757
+ private JSArray contactsFromContactUri(Uri contactUri, Set<String> fields) {
758
+ JSArray contacts = new JSArray();
759
+ String contactId = getContactIdFromUri(contactUri);
760
+ if (contactId == null) {
761
+ return contacts;
762
+ }
763
+
764
+ if (hasReadPermission()) {
765
+ try {
766
+ ContactBuilder builder = fetchContact(contactId, fields);
767
+ if (builder != null) {
768
+ if (builder.displayName == null) {
769
+ builder.displayName = builder.fullName;
770
+ }
771
+ contacts.put(builder.toJSObject(fields));
772
+ }
773
+ return contacts;
774
+ } catch (Exception ex) {
775
+ android.util.Log.w("CapacitorContacts", "Failed to fetch picked contact", ex);
776
+ }
777
+ }
778
+
779
+ ContactBuilder builder = new ContactBuilder(contactId);
780
+ String displayName = resolveDisplayNameFromUri(contactUri);
781
+ builder.displayName = displayName;
782
+ builder.fullName = displayName;
783
+ Set<String> thinFields = new HashSet<>();
784
+ thinFields.add("id");
785
+ thinFields.add("displayName");
786
+ thinFields.add("fullName");
787
+ contacts.put(builder.toJSObject(thinFields));
788
+ return contacts;
789
+ }
790
+
791
+ private String resolveDisplayNameFromUri(Uri contactUri) {
792
+ ContentResolver resolver = getContext().getContentResolver();
793
+ try (
794
+ Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }, null, null, null)
795
+ ) {
796
+ if (cursor != null && cursor.moveToFirst()) {
797
+ return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
798
+ }
799
+ } catch (Exception ex) {
800
+ // The picker URI grant may only expose a subset of columns.
801
+ }
802
+ return null;
803
+ }
804
+
805
+ private static String getStringColumn(Cursor cursor, String column) {
806
+ int index = cursor.getColumnIndex(column);
807
+ if (index < 0) {
808
+ return null;
809
+ }
810
+ return cursor.getString(index);
811
+ }
812
+
593
813
  private String getContactIdFromUri(Uri contactUri) {
594
814
  ContentResolver resolver = getContext().getContentResolver();
595
815
  try (Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts._ID }, null, null, null)) {
@@ -1110,6 +1330,7 @@ public class CapacitorContactsPlugin extends Plugin {
1110
1330
  String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID));
1111
1331
  String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
1112
1332
  ContactBuilder builder = new ContactBuilder(id);
1333
+ builder.displayName = displayName;
1113
1334
  builder.fullName = displayName;
1114
1335
  builderMap.put(id, builder);
1115
1336
  }
@@ -1292,6 +1513,9 @@ public class CapacitorContactsPlugin extends Plugin {
1292
1513
  if (builder.fullName == null) {
1293
1514
  builder.fullName = resolveDisplayName(contactId);
1294
1515
  }
1516
+ if (builder.displayName == null) {
1517
+ builder.displayName = builder.fullName;
1518
+ }
1295
1519
 
1296
1520
  return builder;
1297
1521
  }
@@ -1480,6 +1704,7 @@ public class CapacitorContactsPlugin extends Plugin {
1480
1704
  String jobTitle;
1481
1705
  String note;
1482
1706
  String fullName;
1707
+ String displayName;
1483
1708
  String photoBase64;
1484
1709
  String accountName;
1485
1710
  String accountType;
@@ -1612,6 +1837,7 @@ public class CapacitorContactsPlugin extends Plugin {
1612
1837
  if (includeAll || fields.contains("jobTitle")) contact.put("jobTitle", jobTitle);
1613
1838
  if (includeAll || fields.contains("note")) contact.put("note", note);
1614
1839
  if (includeAll || fields.contains("fullName")) contact.put("fullName", fullName);
1840
+ if (includeAll || fields.contains("displayName")) contact.put("displayName", displayName != null ? displayName : fullName);
1615
1841
  if (includeAll || fields.contains("photo")) contact.put("photo", photoBase64);
1616
1842
  if (includeAll || fields.contains("groupIds")) contact.put("groupIds", groupIds);
1617
1843
  if (includeAll || fields.contains("emailAddresses")) contact.put("emailAddresses", emailAddresses);
package/dist/docs.json CHANGED
@@ -475,7 +475,7 @@
475
475
  "text": "1.0.0"
476
476
  }
477
477
  ],
478
- "docs": "Pick a single contact using the native contact picker.",
478
+ "docs": "Pick a single contact using the native contact picker.\n\nSame options as {@link pickContacts}. Pass `property` to select one phone\nnumber, email address, or postal address without `READ_CONTACTS` on Android.",
479
479
  "complexTypes": [
480
480
  "PickContactsResult",
481
481
  "PickContactOptions",
@@ -508,7 +508,7 @@
508
508
  "text": "1.0.0"
509
509
  }
510
510
  ],
511
- "docs": "Pick one or more contacts using the native contact picker.",
511
+ "docs": "Pick one or more contacts using the native contact picker.\n\nOn iOS this never requires a contacts permission.\n\nOn Android, pass `property` to select a single phone number, email address,\nor postal address with no `READ_CONTACTS` permission. That works on every\nAndroid version and is the way to ship without declaring the permission\nwhen targeting Android 17 (API 37) or later.\n\nPicking a full contact without `property` still needs `READ_CONTACTS` below\nAndroid 17, because the picker URI exposes no phone, email, or structured\nname. On Android 17 and later the system contact picker returns a session\nURI the plugin reads without that permission.",
512
512
  "complexTypes": [
513
513
  "PickContactsResult",
514
514
  "PickContactsOptions"
@@ -727,6 +727,18 @@
727
727
  "complexTypes": [],
728
728
  "type": "string | undefined"
729
729
  },
730
+ {
731
+ "name": "displayName",
732
+ "tags": [
733
+ {
734
+ "text": "8.1.0",
735
+ "name": "since"
736
+ }
737
+ ],
738
+ "docs": "Formatted name shown by the device for this contact.\n\nDerived from `CNContactFormatter` on iOS and `DISPLAY_NAME` on Android.\nRead-only: setting this when creating or updating a contact has no effect.\nUse `givenName` and `familyName` to write a name.",
739
+ "complexTypes": [],
740
+ "type": "string | undefined"
741
+ },
730
742
  {
731
743
  "name": "account",
732
744
  "tags": [
@@ -1971,7 +1983,7 @@
1971
1983
  "name": "since"
1972
1984
  }
1973
1985
  ],
1974
- "docs": "Optional list of specific fields to retrieve. If not specified, all fields are returned.",
1986
+ "docs": "Optional list of specific fields to retrieve. If not specified, all fields are returned.\n\nIgnored when {@link PickContactsOptions.property} is set. The result then\ncontains only `id`, `displayName`, and the selected property.",
1975
1987
  "complexTypes": [
1976
1988
  "ContactField"
1977
1989
  ],
@@ -1985,9 +1997,23 @@
1985
1997
  "name": "since"
1986
1998
  }
1987
1999
  ],
1988
- "docs": "Whether to allow selecting multiple contacts. Default is false.",
2000
+ "docs": "Whether to allow selecting multiple contacts. Default is false.\n\nIgnored when {@link PickContactsOptions.property} is set. Property picking\nalways returns a single value.",
1989
2001
  "complexTypes": [],
1990
2002
  "type": "boolean | undefined"
2003
+ },
2004
+ {
2005
+ "name": "property",
2006
+ "tags": [
2007
+ {
2008
+ "text": "8.1.0",
2009
+ "name": "since"
2010
+ }
2011
+ ],
2012
+ "docs": "Restrict the picker to a single contact property.\n\nOn Android this launches the picker against the phone, email, or postal\naddress table and reads the granted data row. That requires no\n`READ_CONTACTS` permission on any Android version, which is required for\napps that only pick a contact detail and target Android 17 (API 37) or\nlater under the Google Play Contacts Permissions policy.\n\nOn iOS the picker lets the user choose one phone number, email address,\nor postal address. It never requires a contacts permission.",
2013
+ "complexTypes": [
2014
+ "ContactProperty"
2015
+ ],
2016
+ "type": "ContactProperty"
1991
2017
  }
1992
2018
  ]
1993
2019
  },
@@ -2103,7 +2129,47 @@
2103
2129
  ]
2104
2130
  }
2105
2131
  ],
2106
- "enums": [],
2132
+ "enums": [
2133
+ {
2134
+ "name": "ContactProperty",
2135
+ "slug": "contactproperty",
2136
+ "members": [
2137
+ {
2138
+ "name": "PhoneNumber",
2139
+ "value": "'phoneNumber'",
2140
+ "tags": [
2141
+ {
2142
+ "text": "8.1.0",
2143
+ "name": "since"
2144
+ }
2145
+ ],
2146
+ "docs": "A single phone number."
2147
+ },
2148
+ {
2149
+ "name": "EmailAddress",
2150
+ "value": "'emailAddress'",
2151
+ "tags": [
2152
+ {
2153
+ "text": "8.1.0",
2154
+ "name": "since"
2155
+ }
2156
+ ],
2157
+ "docs": "A single email address."
2158
+ },
2159
+ {
2160
+ "name": "PostalAddress",
2161
+ "value": "'postalAddress'",
2162
+ "tags": [
2163
+ {
2164
+ "text": "8.1.0",
2165
+ "name": "since"
2166
+ }
2167
+ ],
2168
+ "docs": "A single postal address."
2169
+ }
2170
+ ]
2171
+ }
2172
+ ],
2107
2173
  "typeAliases": [
2108
2174
  {
2109
2175
  "name": "Omit",