@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 CHANGED
@@ -45,6 +45,50 @@ 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
+ - `pickContacts()` / `pickContact()` without `property` (full contact record)
73
+ - `getContacts()`, `getContactById()`, `countContacts()`, `getGroups()`, `getAccounts()`
74
+ - Syncing, backing up, or matching the whole address book
75
+
76
+ Those APIs keep working as before once `READ_CONTACTS` is in the app manifest and granted. Pass `property` only if you want to drop the permission.
77
+
78
+ If full-address-book access is a core feature, add the permission and file the Play Console declaration before pre-review checks start on 27 October 2026:
79
+
80
+ ```xml
81
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
82
+ ```
83
+
84
+ `WRITE_CONTACTS` is unchanged by this policy. Add it only if you create or update contacts in code.
85
+
86
+ Without `READ_CONTACTS`, `pickContacts()` without `property` can still return `id` and `displayName` (and, on Android 17+, picker-granted fields). Use `property` to get a phone, email, or address with no permission on every Android version.
87
+
88
+ ## iOS
89
+
90
+ `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).
91
+
48
92
  ## API
49
93
 
50
94
  <docgen-index>
@@ -73,6 +117,7 @@ npx cap sync
73
117
  * [`getPluginVersion()`](#getpluginversion)
74
118
  * [Interfaces](#interfaces)
75
119
  * [Type Aliases](#type-aliases)
120
+ * [Enums](#enums)
76
121
 
77
122
  </docgen-index>
78
123
 
@@ -359,6 +404,9 @@ pickContact(options?: PickContactsOptions | undefined) => Promise<PickContactRes
359
404
 
360
405
  <a href="#pick">Pick</a> a single contact using the native contact picker.
361
406
 
407
+ Same options as {@link pickContacts}. Without `property`, Android still
408
+ returns the full contact when `READ_CONTACTS` is granted.
409
+
362
410
  | Param | Type | Description |
363
411
  | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
364
412
  | **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
@@ -378,6 +426,14 @@ pickContacts(options?: PickContactsOptions | undefined) => Promise<PickContactsR
378
426
 
379
427
  <a href="#pick">Pick</a> one or more contacts using the native contact picker.
380
428
 
429
+ On iOS this never requires a contacts permission.
430
+
431
+ On Android, `pickContacts()` without `property` still returns the full
432
+ contact when the app has `READ_CONTACTS`. Pass `property` only when you
433
+ want a single phone number, email address, or postal address and can omit
434
+ that permission (required to target Android 17 / API 37 without a Play
435
+ Console contacts declaration).
436
+
381
437
  | Param | Type | Description |
382
438
  | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
383
439
  | **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
@@ -489,26 +545,27 @@ Options for creating a contact.
489
545
 
490
546
  <a href="#contact">Contact</a> information.
491
547
 
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 |
548
+ | Prop | Type | Description | Since |
549
+ | ---------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
550
+ | **`id`** | <code>string</code> | Unique identifier for the contact. | 1.0.0 |
551
+ | **`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 |
552
+ | **`account`** | <code><a href="#account">Account</a></code> | <a href="#account">Account</a> information for the contact. | 1.0.0 |
553
+ | **`birthday`** | <code><a href="#birthday">Birthday</a></code> | <a href="#birthday">Birthday</a> information for the contact. | 1.0.0 |
554
+ | **`emailAddresses`** | <code>EmailAddress[]</code> | Email addresses for the contact. | 1.0.0 |
555
+ | **`familyName`** | <code>string</code> | Family name (last name) of the contact. | 1.0.0 |
556
+ | **`fullName`** | <code>string</code> | Full name of the contact. | 1.0.0 |
557
+ | **`givenName`** | <code>string</code> | Given name (first name) of the contact. | 1.0.0 |
558
+ | **`groupIds`** | <code>string[]</code> | <a href="#group">Group</a> IDs the contact belongs to. | 1.0.0 |
559
+ | **`jobTitle`** | <code>string</code> | Job title of the contact. | 1.0.0 |
560
+ | **`middleName`** | <code>string</code> | Middle name of the contact. | 1.0.0 |
561
+ | **`namePrefix`** | <code>string</code> | Name prefix (e.g., "Dr.", "Mr.", "Ms.") of the contact. | 1.0.0 |
562
+ | **`nameSuffix`** | <code>string</code> | Name suffix (e.g., "Jr.", "Sr.", "III") of the contact. | 1.0.0 |
563
+ | **`note`** | <code>string</code> | Notes about the contact. | 1.0.0 |
564
+ | **`organizationName`** | <code>string</code> | Organization name of the contact. | 1.0.0 |
565
+ | **`phoneNumbers`** | <code>PhoneNumber[]</code> | Phone numbers for the contact. | 1.0.0 |
566
+ | **`photo`** | <code>string</code> | Base64-encoded photo of the contact. | 1.0.0 |
567
+ | **`postalAddresses`** | <code>PostalAddress[]</code> | Postal addresses for the contact. | 1.0.0 |
568
+ | **`urlAddresses`** | <code>UrlAddress[]</code> | URL addresses for the contact. | 1.0.0 |
512
569
 
513
570
 
514
571
  #### Account
@@ -774,10 +831,11 @@ Result from picking contacts.
774
831
 
775
832
  Options for picking contacts using the native contact picker.
776
833
 
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 |
834
+ | Prop | Type | Description | Since |
835
+ | -------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
836
+ | **`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 |
837
+ | **`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 |
838
+ | **`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
839
 
782
840
 
783
841
  #### UpdateContactByIdOptions
@@ -900,6 +958,18 @@ Type of contacts permission to request.
900
958
 
901
959
  <code>'readContacts' | 'writeContacts'</code>
902
960
 
961
+
962
+ ### Enums
963
+
964
+
965
+ #### ContactProperty
966
+
967
+ | Members | Value | Description | Since |
968
+ | ------------------- | ---------------------------- | ------------------------ | ----- |
969
+ | **`PhoneNumber`** | <code>'phoneNumber'</code> | A single phone number. | 8.1.0 |
970
+ | **`EmailAddress`** | <code>'emailAddress'</code> | A single email address. | 8.1.0 |
971
+ | **`PostalAddress`** | <code>'postalAddress'</code> | A single postal address. | 8.1.0 |
972
+
903
973
  </docgen-api>
904
974
 
905
975
  ### 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.19";
43
+ private final String pluginVersion = "8.1.2";
44
44
  private static final int BATCH_SIZE = 50;
45
45
 
46
46
  // MARK: - Implemented API surface
@@ -467,19 +467,48 @@ 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;
470
471
 
471
472
  @PluginMethod
472
473
  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);
474
+ launchContactPicker(call);
476
475
  }
477
476
 
478
477
  @PluginMethod
479
478
  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);
479
+ launchContactPicker(call);
480
+ }
481
+
482
+ private void launchContactPicker(PluginCall call) {
483
+ String property = call.getString("property");
484
+ if (property != null && contentUriForProperty(property) == null) {
485
+ call.reject("Invalid property. Use phoneNumber, emailAddress, or postalAddress.");
486
+ return;
487
+ }
488
+
489
+ currentPickerCall = call;
490
+ currentPickerProperty = property;
491
+ startActivityForResult(call, createPickIntent(property), PICK_CONTACT_REQUEST);
492
+ }
493
+
494
+ private Intent createPickIntent(String property) {
495
+ if (property != null) {
496
+ return new Intent(Intent.ACTION_PICK, contentUriForProperty(property));
497
+ }
498
+ return new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
499
+ }
500
+
501
+ private Uri contentUriForProperty(String property) {
502
+ if ("phoneNumber".equals(property)) {
503
+ return ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
504
+ }
505
+ if ("emailAddress".equals(property)) {
506
+ return ContactsContract.CommonDataKinds.Email.CONTENT_URI;
507
+ }
508
+ if ("postalAddress".equals(property)) {
509
+ return ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI;
510
+ }
511
+ return null;
483
512
  }
484
513
 
485
514
  @PluginMethod
@@ -546,6 +575,7 @@ public class CapacitorContactsPlugin extends Plugin {
546
575
  currentPickerCall = null;
547
576
 
548
577
  if (resultCode != android.app.Activity.RESULT_OK) {
578
+ currentPickerProperty = null;
549
579
  if (requestCode == PICK_CONTACT_REQUEST) {
550
580
  call.resolve(new JSObject().put("contacts", new JSArray()));
551
581
  } else if (requestCode == CREATE_CONTACT_REQUEST) {
@@ -557,20 +587,9 @@ public class CapacitorContactsPlugin extends Plugin {
557
587
  try {
558
588
  if (requestCode == PICK_CONTACT_REQUEST) {
559
589
  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
- }
590
+ resolvePickedUri(call, data.getData());
573
591
  } else {
592
+ currentPickerProperty = null;
574
593
  call.resolve(new JSObject().put("contacts", new JSArray()));
575
594
  }
576
595
  } else if (requestCode == CREATE_CONTACT_REQUEST) {
@@ -590,6 +609,192 @@ public class CapacitorContactsPlugin extends Plugin {
590
609
  }
591
610
  }
592
611
 
612
+ private void resolvePickedUri(PluginCall call, Uri uri) {
613
+ String property = currentPickerProperty;
614
+ currentPickerProperty = null;
615
+
616
+ if (property != null) {
617
+ JSObject contact = contactFromDataRowUri(uri, property);
618
+ JSArray contacts = new JSArray();
619
+ if (contact != null) {
620
+ contacts.put(contact);
621
+ }
622
+ call.resolve(new JSObject().put("contacts", contacts));
623
+ return;
624
+ }
625
+
626
+ call.resolve(new JSObject().put("contacts", contactsFromFullPick(uri, fieldsForPickedContact(call))));
627
+ }
628
+
629
+ private Set<String> fieldsForPickedContact(PluginCall call) {
630
+ Set<String> fields = parseFieldsArray(call);
631
+ if (fields == null) {
632
+ return null;
633
+ }
634
+ Set<String> withIdentity = new HashSet<>(fields);
635
+ withIdentity.add("id");
636
+ withIdentity.add("displayName");
637
+ return withIdentity;
638
+ }
639
+
640
+ private boolean isSessionUri(Uri uri) {
641
+ String authority = uri.getAuthority();
642
+ return authority != null && authority.contains("picker");
643
+ }
644
+
645
+ private JSArray contactsFromFullPick(Uri uri, Set<String> fields) {
646
+ JSArray contacts = new JSArray();
647
+ List<String> ids = contactIdsFromPickerUri(uri);
648
+
649
+ if (hasReadPermission()) {
650
+ for (String contactId : ids) {
651
+ try {
652
+ ContactBuilder builder = fetchContact(contactId, fields);
653
+ if (builder != null) {
654
+ if (builder.displayName == null) {
655
+ builder.displayName = builder.fullName;
656
+ }
657
+ contacts.put(builder.toJSObject(fields));
658
+ }
659
+ } catch (Exception ex) {
660
+ android.util.Log.w("CapacitorContacts", "Failed to fetch picked contact", ex);
661
+ }
662
+ }
663
+ }
664
+
665
+ if (contacts.length() > 0) {
666
+ return contacts;
667
+ }
668
+
669
+ if (isSessionUri(uri)) {
670
+ return contactsFromSessionUri(uri, fields);
671
+ }
672
+
673
+ if (!ids.isEmpty()) {
674
+ ContactBuilder builder = new ContactBuilder(ids.get(0));
675
+ String displayName = resolveDisplayNameFromUri(uri);
676
+ builder.displayName = displayName;
677
+ builder.fullName = displayName;
678
+ Set<String> thinFields = new HashSet<>();
679
+ thinFields.add("id");
680
+ thinFields.add("displayName");
681
+ contacts.put(builder.toJSObject(thinFields));
682
+ }
683
+ return contacts;
684
+ }
685
+
686
+ private List<String> contactIdsFromPickerUri(Uri uri) {
687
+ List<String> ids = new ArrayList<>();
688
+ if (isSessionUri(uri)) {
689
+ Set<String> seen = new HashSet<>();
690
+ ContentResolver resolver = getContext().getContentResolver();
691
+ try (Cursor cursor = resolver.query(uri, null, null, null, null)) {
692
+ if (cursor != null) {
693
+ while (cursor.moveToNext()) {
694
+ String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
695
+ if (contactId != null && seen.add(contactId)) {
696
+ ids.add(contactId);
697
+ }
698
+ }
699
+ }
700
+ } catch (Exception ex) {
701
+ android.util.Log.w("CapacitorContacts", "Failed to read picker session contact ids", ex);
702
+ }
703
+ return ids;
704
+ }
705
+
706
+ String contactId = getContactIdFromUri(uri);
707
+ if (contactId != null) {
708
+ ids.add(contactId);
709
+ }
710
+ return ids;
711
+ }
712
+
713
+ private JSArray contactsFromSessionUri(Uri sessionUri, Set<String> fields) {
714
+ JSArray contacts = new JSArray();
715
+ Map<String, ContactBuilder> builders = new java.util.LinkedHashMap<>();
716
+ ContentResolver resolver = getContext().getContentResolver();
717
+
718
+ try (Cursor cursor = resolver.query(sessionUri, null, null, null, null)) {
719
+ if (cursor == null) {
720
+ return contacts;
721
+ }
722
+ while (cursor.moveToNext()) {
723
+ String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
724
+ if (contactId == null) {
725
+ continue;
726
+ }
727
+ ContactBuilder builder = builders.get(contactId);
728
+ if (builder == null) {
729
+ builder = new ContactBuilder(contactId);
730
+ String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
731
+ builder.displayName = displayName;
732
+ builder.fullName = displayName;
733
+ builders.put(contactId, builder);
734
+ }
735
+ processDataRow(builder, cursor);
736
+ }
737
+ }
738
+
739
+ for (ContactBuilder builder : builders.values()) {
740
+ contacts.put(builder.toJSObject(fields));
741
+ }
742
+ return contacts;
743
+ }
744
+
745
+ private JSObject contactFromDataRowUri(Uri uri, String property) {
746
+ ContentResolver resolver = getContext().getContentResolver();
747
+ try (Cursor cursor = resolver.query(uri, null, null, null, null)) {
748
+ if (cursor == null || !cursor.moveToFirst()) {
749
+ return null;
750
+ }
751
+
752
+ String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
753
+ ContactBuilder builder = new ContactBuilder(contactId != null ? contactId : uri.getLastPathSegment());
754
+ String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
755
+ builder.displayName = displayName;
756
+ builder.fullName = displayName;
757
+ processDataRow(builder, cursor);
758
+
759
+ Set<String> fields = new HashSet<>();
760
+ fields.add("id");
761
+ fields.add("displayName");
762
+ if ("emailAddress".equals(property)) {
763
+ fields.add("emailAddresses");
764
+ } else if ("postalAddress".equals(property)) {
765
+ fields.add("postalAddresses");
766
+ } else {
767
+ fields.add("phoneNumbers");
768
+ }
769
+ return builder.toJSObject(fields);
770
+ } catch (Exception ex) {
771
+ android.util.Log.w("CapacitorContacts", "Failed to read picked contact data row", ex);
772
+ return null;
773
+ }
774
+ }
775
+
776
+ private String resolveDisplayNameFromUri(Uri contactUri) {
777
+ ContentResolver resolver = getContext().getContentResolver();
778
+ try (
779
+ Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }, null, null, null)
780
+ ) {
781
+ if (cursor != null && cursor.moveToFirst()) {
782
+ return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
783
+ }
784
+ } catch (Exception ex) {
785
+ // The picker URI grant may only expose a subset of columns.
786
+ }
787
+ return null;
788
+ }
789
+
790
+ private static String getStringColumn(Cursor cursor, String column) {
791
+ int index = cursor.getColumnIndex(column);
792
+ if (index < 0) {
793
+ return null;
794
+ }
795
+ return cursor.getString(index);
796
+ }
797
+
593
798
  private String getContactIdFromUri(Uri contactUri) {
594
799
  ContentResolver resolver = getContext().getContentResolver();
595
800
  try (Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts._ID }, null, null, null)) {
@@ -1110,6 +1315,7 @@ public class CapacitorContactsPlugin extends Plugin {
1110
1315
  String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID));
1111
1316
  String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
1112
1317
  ContactBuilder builder = new ContactBuilder(id);
1318
+ builder.displayName = displayName;
1113
1319
  builder.fullName = displayName;
1114
1320
  builderMap.put(id, builder);
1115
1321
  }
@@ -1292,6 +1498,9 @@ public class CapacitorContactsPlugin extends Plugin {
1292
1498
  if (builder.fullName == null) {
1293
1499
  builder.fullName = resolveDisplayName(contactId);
1294
1500
  }
1501
+ if (builder.displayName == null) {
1502
+ builder.displayName = builder.fullName;
1503
+ }
1295
1504
 
1296
1505
  return builder;
1297
1506
  }
@@ -1480,6 +1689,7 @@ public class CapacitorContactsPlugin extends Plugin {
1480
1689
  String jobTitle;
1481
1690
  String note;
1482
1691
  String fullName;
1692
+ String displayName;
1483
1693
  String photoBase64;
1484
1694
  String accountName;
1485
1695
  String accountType;
@@ -1612,6 +1822,7 @@ public class CapacitorContactsPlugin extends Plugin {
1612
1822
  if (includeAll || fields.contains("jobTitle")) contact.put("jobTitle", jobTitle);
1613
1823
  if (includeAll || fields.contains("note")) contact.put("note", note);
1614
1824
  if (includeAll || fields.contains("fullName")) contact.put("fullName", fullName);
1825
+ if (includeAll || fields.contains("displayName")) contact.put("displayName", displayName != null ? displayName : fullName);
1615
1826
  if (includeAll || fields.contains("photo")) contact.put("photo", photoBase64);
1616
1827
  if (includeAll || fields.contains("groupIds")) contact.put("groupIds", groupIds);
1617
1828
  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}. Without `property`, Android still\nreturns the full contact when `READ_CONTACTS` is granted.",
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, `pickContacts()` without `property` still returns the full\ncontact when the app has `READ_CONTACTS`. Pass `property` only when you\nwant a single phone number, email address, or postal address and can omit\nthat permission (required to target Android 17 / API 37 without a Play\nConsole contacts declaration).",
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",