xref: /expo/packages/expo-contacts/src/Contacts.ts (revision c8170acc)
1import { PermissionResponse, PermissionStatus, UnavailabilityError, uuid } from 'expo-modules-core';
2import { Platform, Share } from 'react-native';
3
4import ExpoContacts from './ExpoContacts';
5
6export type CalendarFormatType = CalendarFormats | `${CalendarFormats}`;
7
8export type ContainerType = ContainerTypes | `${ContainerTypes}`;
9
10export type ContactType = ContactTypes | `${ContactTypes}`;
11
12export type FieldType = Fields | `${Fields}`;
13
14export type Date = {
15  /**
16   * Day.
17   */
18  day?: number;
19  /**
20   * Month - adjusted for JavaScript `Date` which starts at `0`.
21   */
22  month?: number;
23  /**
24   * Year.
25   */
26  year?: number;
27  /**
28   * Unique ID. This value will be generated by the OS.
29   */
30  id?: string;
31  /**
32   * Localized display name.
33   */
34  label: string;
35  /**
36   * Format for the input date.
37   */
38  format?: CalendarFormatType;
39};
40
41export type Relationship = {
42  /**
43   * Localized display name.
44   */
45  label: string;
46  /**
47   * Name of related contact.
48   */
49  name?: string;
50  /**
51   * Unique ID. This value will be generated by the OS.
52   */
53  id?: string;
54};
55
56export type Email = {
57  /**
58   * Email address.
59   */
60  email?: string;
61  /**
62   * Flag signifying if it is a primary email address.
63   */
64  isPrimary?: boolean;
65  /**
66   * Localized display name.
67   */
68  label: string;
69  /**
70   * Unique ID. This value will be generated by the OS.
71   */
72  id?: string;
73};
74
75export type PhoneNumber = {
76  /**
77   * Phone number.
78   */
79  number?: string;
80  /**
81   * Flag signifying if it is a primary phone number.
82   */
83  isPrimary?: boolean;
84  /**
85   * Phone number without format.
86   * @example `8674305`
87   */
88  digits?: string;
89  /**
90   * Country code.
91   * @example `+1`
92   */
93  countryCode?: string;
94  /**
95   * Localized display name.
96   */
97  label: string;
98  /**
99   * Unique ID. This value will be generated by the OS.
100   */
101  id?: string;
102};
103
104export type Address = {
105  /**
106   * Street name.
107   */
108  street?: string;
109  /**
110   * City name.
111   */
112  city?: string;
113  /**
114   * Country name
115   */
116  country?: string;
117  /**
118   * Region or state name.
119   */
120  region?: string;
121  /**
122   * Neighborhood name.
123   */
124  neighborhood?: string;
125  /**
126   * Local post code.
127   */
128  postalCode?: string;
129  /**
130   * P.O. Box.
131   */
132  poBox?: string;
133  /**
134   * [Standard country code](https://www.iso.org/iso-3166-country-codes.html).
135   */
136  isoCountryCode?: string;
137  /**
138   * Localized display name.
139   */
140  label: string;
141  /**
142   * Unique ID. This value will be generated by the OS.
143   */
144  id?: string;
145};
146
147/**
148 * @platform ios
149 */
150export type SocialProfile = {
151  /**
152   * Name of social app.
153   */
154  service?: string;
155  /**
156   * Localized profile name.
157   */
158  localizedProfile?: string;
159  /**
160   * Web URL.
161   */
162  url?: string;
163  /**
164   * Username in social app.
165   */
166  username?: string;
167  /**
168   * Username ID in social app.
169   */
170  userId?: string;
171  /**
172   * Localized display name.
173   */
174  label: string;
175  /**
176   * Unique ID. This value will be generated by the OS.
177   */
178  id?: string;
179};
180
181export type InstantMessageAddress = {
182  /**
183   * Name of instant messaging app.
184   */
185  service?: string;
186  /**
187   * Username in IM app.
188   */
189  username?: string;
190  /**
191   * Localized name of app.
192   */
193  localizedService?: string;
194  /**
195   * Localized display name.
196   */
197  label: string;
198  /**
199   * Unique ID. This value will be generated by the OS.
200   */
201  id?: string;
202};
203
204export type UrlAddress = {
205  /**
206   * Localized display name.
207   */
208  label: string;
209  /**
210   * Web URL.
211   */
212  url?: string;
213  /**
214   * Unique ID. This value will be generated by the OS.
215   */
216  id?: string;
217};
218
219// @needs-audit
220/**
221 * Information regarding thumbnail images.
222 * > On Android you can get dimensions using [`Image.getSize`](https://reactnative.dev/docs/image#getsize) method.
223 */
224export type Image = {
225  uri?: string;
226  /**
227   * Image width.
228   * @platform ios
229   */
230  width?: number;
231  /**
232   * Image height
233   * @platform ios
234   */
235  height?: number;
236  /**
237   * Image as Base64 string.
238   */
239  base64?: string;
240};
241
242/**
243 * A set of fields that define information about a single contact entity.
244 */
245export type Contact = {
246  /**
247   * Immutable identifier used for querying and indexing. This value will be generated by the OS when the contact is created.
248   */
249  id?: string;
250  /**
251   * Denoting a person or company.
252   */
253  contactType: ContactType;
254  /**
255   * Full name with proper format.
256   */
257  name: string;
258  /**
259   * Given name.
260   */
261  firstName?: string;
262  /**
263   * Middle name
264   */
265  middleName?: string;
266  /**
267   * Last name.
268   */
269  lastName?: string;
270  /**
271   * Maiden name.
272   */
273  maidenName?: string;
274  /**
275   * Dr. Mr. Mrs. ect…
276   */
277  namePrefix?: string;
278  /**
279   * Jr. Sr. ect…
280   */
281  nameSuffix?: string;
282  /**
283   * An alias to the proper name.
284   */
285  nickname?: string;
286  /**
287   * Pronunciation of the first name.
288   */
289  phoneticFirstName?: string;
290  /**
291   * Pronunciation of the middle name.
292   */
293  phoneticMiddleName?: string;
294  /**
295   * Pronunciation of the last name.
296   */
297  phoneticLastName?: string;
298  /**
299   * Organization the entity belongs to.
300   */
301  company?: string;
302  /**
303   * Job description.
304   */
305  jobTitle?: string;
306  /**
307   * Job department.
308   */
309  department?: string;
310  /**
311   * Additional information.
312   * > On iOS 13+, the `note` field [requires your app to request additional entitlements](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes).
313   * > The Expo Go app does not contain those entitlements, so in order to test this feature you will need to [request the entitlement from Apple](https://developer.apple.com/contact/request/contact-note-field),
314   * > set the [`ios.accessesContactNotes`](./../config/app/#accessescontactnotes) field in **app config** to `true`, and [create your development build](/develop/development-builds/create-a-build/).
315   */
316  note?: string;
317  /**
318   * Used for efficient retrieval of images.
319   */
320  imageAvailable?: boolean;
321  /**
322   * Thumbnail image. On iOS it size is set to 320×320px, on Android it may vary.
323   */
324  image?: Image;
325  /**
326   * Raw image without cropping, usually large.
327   */
328  rawImage?: Image;
329  /**
330   * Birthday information in Gregorian format.
331   */
332  birthday?: Date;
333  /**
334   * A labeled list of other relevant user dates in Gregorian format.
335   */
336  dates?: Date[];
337  /**
338   * Names of other relevant user connections.
339   */
340  relationships?: Relationship[];
341  /**
342   * Email addresses.
343   */
344  emails?: Email[];
345  /**
346   * Phone numbers.
347   */
348  phoneNumbers?: PhoneNumber[];
349  /**
350   * Locations.
351   */
352  addresses?: Address[];
353  /**
354   * Instant messaging connections.
355   */
356  instantMessageAddresses?: InstantMessageAddress[];
357  /**
358   * Associated web URLs.
359   */
360  urlAddresses?: UrlAddress[];
361  /**
362   * Birthday that doesn't conform to the Gregorian calendar format, interpreted based on the [calendar `format`](#date) setting.
363   * @platform ios
364   */
365  nonGregorianBirthday?: Date;
366  /**
367   * Social networks.
368   * @platform ios
369   */
370  socialProfiles?: SocialProfile[];
371};
372
373/**
374 * The return value for queried contact operations like `getContactsAsync`.
375 */
376export type ContactResponse = {
377  /**
378   * An array of contacts that match a particular query.
379   */
380  data: Contact[];
381  /**
382   * This will be `true` if there are more contacts to retrieve beyond what is returned.
383   */
384  hasNextPage: boolean;
385  /**
386   * This will be `true if there are previous contacts that weren't retrieved due to `pageOffset` limit.
387   */
388  hasPreviousPage: boolean;
389};
390
391export type ContactSort = `${SortTypes}`;
392
393/**
394 * Used to query contacts from the user's device.
395 */
396export type ContactQuery = {
397  /**
398   * The max number of contacts to return. If skipped or set to `0` all contacts will be returned.
399   */
400  pageSize?: number;
401  /**
402   * The number of contacts to skip before gathering contacts.
403   */
404  pageOffset?: number;
405  /**
406   * If specified, the defined fields will be returned. If skipped, all fields will be returned.
407   */
408  fields?: FieldType[];
409  /**
410   * Sort method used when gathering contacts.
411   */
412  sort?: ContactSort;
413  /**
414   * Get all contacts whose name contains the provided string (not case-sensitive).
415   */
416  name?: string;
417  /**
418   * Get contacts with a matching ID or array of IDs.
419   */
420  id?: string | string[];
421  /**
422   * Get all contacts that belong to the group matching this ID.
423   * @platform ios
424   */
425  groupId?: string;
426  /**
427   * Get all contacts that belong to the container matching this ID.
428   * @platform ios
429   */
430  containerId?: string;
431  /**
432   * Prevent unification of contacts when gathering.
433   * @default false
434   * @platform ios
435   */
436  rawContacts?: boolean;
437};
438
439/**
440 * Denotes the functionality of a native contact form.
441 */
442export type FormOptions = {
443  /**
444   * The properties that will be displayed. On iOS those properties does nothing while in editing mode.
445   */
446  displayedPropertyKeys?: FieldType[];
447  /**
448   * Controller title.
449   */
450  message?: string;
451  /**
452   * Used if contact doesn't have a name defined.
453   */
454  alternateName?: string;
455  /**
456   * Allows for contact mutation.
457   */
458  allowsEditing?: boolean;
459  /**
460   * Actions like share, add, create.
461   */
462  allowsActions?: boolean;
463  /**
464   * Show or hide the similar contacts.
465   */
466  shouldShowLinkedContacts?: boolean;
467  /**
468   * Present the new contact controller. If set to `false` the unknown controller will be shown.
469   */
470  isNew?: boolean;
471  /**
472   * The name of the left bar button.
473   */
474  cancelButtonTitle?: string;
475  /**
476   * Prevents the controller from animating in.
477   */
478  preventAnimation?: boolean;
479  /**
480   * The parent group for a new contact.
481   */
482  groupId?: string;
483};
484
485/**
486 * Used to query native contact groups.
487 * @platform ios
488 */
489export type GroupQuery = {
490  /**
491   * Query the group with a matching ID.
492   */
493  groupId?: string;
494  /**
495   * Query all groups matching a name.
496   */
497  groupName?: string;
498  /**
499   * Query all groups that belong to a certain container.
500   */
501  containerId?: string;
502};
503
504/**
505 * A parent to contacts. A contact can belong to multiple groups. Here are some query operations you can perform:
506 * - Child Contacts: `getContactsAsync({ groupId })`
507 * - Groups From Container: `getGroupsAsync({ containerId })`
508 * - Groups Named: `getContainersAsync({ groupName })`
509 * @platform ios
510 */
511export type Group = {
512  /**
513   * Immutable id representing the group.
514   */
515  name?: string;
516  /**
517   * The editable name of a group.
518   */
519  id?: string;
520};
521
522/**
523 * Used to query native contact containers.
524 * @platform ios
525 */
526export type ContainerQuery = {
527  /**
528   * Query all the containers that parent a contact.
529   */
530  contactId?: string;
531  /**
532   * Query all the containers that parent a group.
533   */
534  groupId?: string;
535  /**
536   * Query all the containers that matches ID or an array od IDs.
537   */
538  containerId?: string | string[];
539};
540
541export type Container = {
542  name: string;
543  id: string;
544  type: ContainerType;
545};
546
547export { PermissionStatus, PermissionResponse };
548
549/**
550 * Returns whether the Contacts API is enabled on the current device. This method does not check the app permissions.
551 * @returns A promise that fulfills with a `boolean`, indicating whether the Contacts API is available on the current device. It always resolves to `false` on web.
552 */
553export async function isAvailableAsync(): Promise<boolean> {
554  return !!ExpoContacts.getContactsAsync;
555}
556
557export async function shareContactAsync(
558  contactId: string,
559  message: string,
560  shareOptions: object = {}
561): Promise<any> {
562  if (Platform.OS === 'ios') {
563    const url = await writeContactToFileAsync({
564      id: contactId,
565    });
566    return await Share.share(
567      {
568        url,
569        message,
570      },
571      shareOptions
572    );
573  } else if (!ExpoContacts.shareContactAsync) {
574    throw new UnavailabilityError('Contacts', 'shareContactAsync');
575  }
576  return await ExpoContacts.shareContactAsync(contactId, message);
577}
578
579/**
580 * Return a list of contacts that fit a given criteria. You can get all of the contacts by passing no criteria.
581 * @param contactQuery Object used to query contacts.
582 * @return A promise that fulfills with `ContactResponse` object returned from the query.
583 * @example
584 * ```js
585 * const { data } = await Contacts.getContactsAsync({
586 *   fields: [Contacts.Fields.Emails],
587 * });
588 *
589 * if (data.length > 0) {
590 *   const contact = data[0];
591 *   console.log(contact);
592 * }
593 * ```
594 */
595export async function getContactsAsync(contactQuery: ContactQuery = {}): Promise<ContactResponse> {
596  if (!ExpoContacts.getContactsAsync) {
597    throw new UnavailabilityError('Contacts', 'getContactsAsync');
598  }
599  return await ExpoContacts.getContactsAsync(contactQuery);
600}
601
602export async function getPagedContactsAsync(
603  contactQuery: ContactQuery = {}
604): Promise<ContactResponse> {
605  const { pageSize, ...nOptions } = contactQuery;
606
607  if (pageSize && pageSize <= 0) {
608    throw new Error('Error: Contacts.getPagedContactsAsync: `pageSize` must be greater than 0');
609  }
610
611  return await getContactsAsync({
612    ...nOptions,
613    pageSize,
614  });
615}
616
617/**
618 * Used for gathering precise data about a contact. Returns a contact matching the given `id`.
619 * @param id The ID of a system contact.
620 * @param fields If specified, the fields defined will be returned. When skipped, all fields will be returned.
621 * @return A promise that fulfills with `Contact` object with ID matching the input ID, or `undefined` if there is no match.
622 * @example
623 * ```js
624 * const contact = await Contacts.getContactByIdAsync('161A368D-D614-4A15-8DC6-665FDBCFAE55');
625 * if (contact) {
626 *   console.log(contact);
627 * }
628 * ```
629 */
630export async function getContactByIdAsync(
631  id: string,
632  fields?: FieldType[]
633): Promise<Contact | undefined> {
634  if (!ExpoContacts.getContactsAsync) {
635    throw new UnavailabilityError('Contacts', 'getContactsAsync');
636  }
637
638  if (id == null) {
639    throw new Error('Error: Contacts.getContactByIdAsync: Please pass an ID as a parameter');
640  } else {
641    const results = await ExpoContacts.getContactsAsync({
642      pageSize: 1,
643      pageOffset: 0,
644      fields,
645      id,
646    });
647    if (results && results.data && results.data.length > 0) {
648      return results.data[0];
649    }
650  }
651  return undefined;
652}
653
654/**
655 * Creates a new contact and adds it to the system.
656 * > **Note**: For Android users, the Expo Go app does not have the required `WRITE_CONTACTS` permission to write to Contacts.
657 * > You will need to create a [development build](/develop/development-builds/create-a-build/) and add permission in there manually to use this method.
658 * @param contact A contact with the changes you wish to persist. The `id` parameter will not be used.
659 * @param containerId @tag-ios The container that will parent the contact.
660 * @return A promise that fulfills with ID of the new system contact.
661 * @example
662 * ```js
663 * const contact = {
664 *   [Contacts.Fields.FirstName]: 'Bird',
665 *   [Contacts.Fields.LastName]: 'Man',
666 *   [Contacts.Fields.Company]: 'Young Money',
667 * };
668 * const contactId = await Contacts.addContactAsync(contact);
669 * ```
670 */
671export async function addContactAsync(contact: Contact, containerId?: string): Promise<string> {
672  if (!ExpoContacts.addContactAsync) {
673    throw new UnavailabilityError('Contacts', 'addContactAsync');
674  }
675
676  const noIdContact = removeIds(contact);
677
678  return await ExpoContacts.addContactAsync(noIdContact, containerId);
679}
680
681/**
682 * Mutate the information of an existing contact. Due to an iOS bug, `nonGregorianBirthday` field cannot be modified.
683 * > **info** On Android, you can use [`presentFormAsync`](#contactspresentformasynccontactid-contact-formoptions) to make edits to contacts.
684 * @param contact A contact object including the wanted changes.
685 * @return A promise that fulfills with ID of the updated system contact if mutation was successful.
686 * @example
687 * ```js
688 * const contact = {
689 *   id: '161A368D-D614-4A15-8DC6-665FDBCFAE55',
690 *   [Contacts.Fields.FirstName]: 'Drake',
691 *   [Contacts.Fields.Company]: 'Young Money',
692 * };
693 * await Contacts.updateContactAsync(contact);
694 * ```
695 * @platform ios
696 */
697export async function updateContactAsync(contact: Contact): Promise<string> {
698  if (!ExpoContacts.updateContactAsync) {
699    throw new UnavailabilityError('Contacts', 'updateContactAsync');
700  }
701  return await ExpoContacts.updateContactAsync(contact);
702}
703
704// @needs-audit
705/**
706 * Delete a contact from the system.
707 * @param contactId ID of the contact you want to delete.
708 * @example
709 * ```js
710 * await Contacts.removeContactAsync('161A368D-D614-4A15-8DC6-665FDBCFAE55');
711 * ```
712 * @platform ios
713 */
714export async function removeContactAsync(contactId: string): Promise<any> {
715  if (!ExpoContacts.removeContactAsync) {
716    throw new UnavailabilityError('Contacts', 'removeContactAsync');
717  }
718  return await ExpoContacts.removeContactAsync(contactId);
719}
720
721/**
722 * Query a set of contacts and write them to a local URI that can be used for sharing.
723 * @param contactQuery Used to query contact you want to write.
724 * @return A promise that fulfills with shareable local URI, or `undefined` if there was no match.
725 * @example
726 * ```js
727 * const localUri = await Contacts.writeContactToFileAsync({
728 *   id: '161A368D-D614-4A15-8DC6-665FDBCFAE55',
729 * });
730 * Share.share({ url: localUri, message: 'Call me!' });
731 * ```
732 */
733export async function writeContactToFileAsync(
734  contactQuery: ContactQuery = {}
735): Promise<string | undefined> {
736  if (!ExpoContacts.writeContactToFileAsync) {
737    throw new UnavailabilityError('Contacts', 'writeContactToFileAsync');
738  }
739  return await ExpoContacts.writeContactToFileAsync(contactQuery);
740}
741
742// @needs-audit
743/**
744 * Present a native form for manipulating contacts.
745 * @param contactId The ID of a system contact.
746 * @param contact A contact with the changes you want to persist.
747 * @param formOptions Options for the native editor.
748 * @example
749 * ```js
750 * await Contacts.presentFormAsync('161A368D-D614-4A15-8DC6-665FDBCFAE55');
751 * ```
752 */
753export async function presentFormAsync(
754  contactId?: string | null,
755  contact?: Contact | null,
756  formOptions: FormOptions = {}
757): Promise<any> {
758  if (!ExpoContacts.presentFormAsync) {
759    throw new UnavailabilityError('Contacts', 'presentFormAsync');
760  }
761  if (Platform.OS === 'ios') {
762    const adjustedOptions = formOptions;
763
764    if (contactId) {
765      if (contact) {
766        contact = undefined;
767        console.log(
768          'Expo.Contacts.presentFormAsync: You should define either a `contact` or a `contactId` but not both.'
769        );
770      }
771      if (adjustedOptions.isNew !== undefined) {
772        console.log(
773          'Expo.Contacts.presentFormAsync: `formOptions.isNew` is not supported with `contactId`'
774        );
775      }
776    }
777    return await ExpoContacts.presentFormAsync(contactId, contact, adjustedOptions);
778  } else {
779    return await ExpoContacts.presentFormAsync(contactId, contact, formOptions);
780  }
781}
782
783// iOS Only
784
785/**
786 * Add a group to a container.
787 * @param groupId The group you want to target.
788 * @param containerId The container you want to add membership to.
789 * @example
790 * ```js
791 * await Contacts.addExistingGroupToContainerAsync(
792 *   '161A368D-D614-4A15-8DC6-665FDBCFAE55',
793 *   '665FDBCFAE55-D614-4A15-8DC6-161A368D'
794 * );
795 * ```
796 * @platform ios
797 */
798export async function addExistingGroupToContainerAsync(
799  groupId: string,
800  containerId: string
801): Promise<any> {
802  if (!ExpoContacts.addExistingGroupToContainerAsync) {
803    throw new UnavailabilityError('Contacts', 'addExistingGroupToContainerAsync');
804  }
805
806  return await ExpoContacts.addExistingGroupToContainerAsync(groupId, containerId);
807}
808
809/**
810 * Create a group with a name, and add it to a container. If the container is undefined, the default container will be targeted.
811 * @param name Name of the new group.
812 * @param containerId The container you to add membership to.
813 * @return A promise that fulfills with ID of the new group.
814 * @example
815 * ```js
816 * const groupId = await Contacts.createGroupAsync('Sailor Moon');
817 * ```
818 * @platform ios
819 */
820export async function createGroupAsync(name?: string, containerId?: string): Promise<string> {
821  if (!ExpoContacts.createGroupAsync) {
822    throw new UnavailabilityError('Contacts', 'createGroupAsync');
823  }
824
825  name = name || uuid.v4();
826  if (!containerId) {
827    containerId = await getDefaultContainerIdAsync();
828  }
829
830  return await ExpoContacts.createGroupAsync(name, containerId);
831}
832
833/**
834 * Change the name of an existing group.
835 * @param groupName New name for an existing group.
836 * @param groupId ID of the group you want to edit.
837 * @example
838 * ```js
839 * await Contacts.updateGroupName('Expo Friends', '161A368D-D614-4A15-8DC6-665FDBCFAE55');
840 * ```
841 * @platform ios
842 */
843export async function updateGroupNameAsync(groupName: string, groupId: string): Promise<any> {
844  if (!ExpoContacts.updateGroupNameAsync) {
845    throw new UnavailabilityError('Contacts', 'updateGroupNameAsync');
846  }
847
848  return await ExpoContacts.updateGroupNameAsync(groupName, groupId);
849}
850
851// @needs-audit
852/**
853 * Delete a group from the device.
854 * @param groupId ID of the group you want to remove.
855 * @example
856 * ```js
857 * await Contacts.removeGroupAsync('161A368D-D614-4A15-8DC6-665FDBCFAE55');
858 * ```
859 * @platform ios
860 */
861export async function removeGroupAsync(groupId: string): Promise<any> {
862  if (!ExpoContacts.removeGroupAsync) {
863    throw new UnavailabilityError('Contacts', 'removeGroupAsync');
864  }
865
866  return await ExpoContacts.removeGroupAsync(groupId);
867}
868
869// @needs-audit
870/**
871 * Add a contact as a member to a group. A contact can be a member of multiple groups.
872 * @param contactId ID of the contact you want to edit.
873 * @param groupId ID for the group you want to add membership to.
874 * @example
875 * ```js
876 * await Contacts.addExistingContactToGroupAsync(
877 *   '665FDBCFAE55-D614-4A15-8DC6-161A368D',
878 *   '161A368D-D614-4A15-8DC6-665FDBCFAE55'
879 * );
880 * ```
881 * @platform ios
882 */
883export async function addExistingContactToGroupAsync(
884  contactId: string,
885  groupId: string
886): Promise<any> {
887  if (!ExpoContacts.addExistingContactToGroupAsync) {
888    throw new UnavailabilityError('Contacts', 'addExistingContactToGroupAsync');
889  }
890
891  return await ExpoContacts.addExistingContactToGroupAsync(contactId, groupId);
892}
893
894// @needs-audit
895/**
896 * Remove a contact's membership from a given group. This will not delete the contact.
897 * @param contactId ID of the contact you want to remove.
898 * @param groupId ID for the group you want to remove membership of.
899 * @example
900 * ```js
901 * await Contacts.removeContactFromGroupAsync(
902 *   '665FDBCFAE55-D614-4A15-8DC6-161A368D',
903 *   '161A368D-D614-4A15-8DC6-665FDBCFAE55'
904 * );
905 * ```
906 * @platform ios
907 */
908export async function removeContactFromGroupAsync(
909  contactId: string,
910  groupId: string
911): Promise<any> {
912  if (!ExpoContacts.removeContactFromGroupAsync) {
913    throw new UnavailabilityError('Contacts', 'removeContactFromGroupAsync');
914  }
915
916  return await ExpoContacts.removeContactFromGroupAsync(contactId, groupId);
917}
918
919// @needs-audit
920/**
921 * Query and return a list of system groups.
922 * @param groupQuery Information regarding which groups you want to get.
923 * @example
924 * ```js
925 * const groups = await Contacts.getGroupsAsync({ groupName: 'sailor moon' });
926 * const allGroups = await Contacts.getGroupsAsync({});
927 * ```
928 * @return A promise that fulfills with array of groups that fit the query.
929 * @platform ios
930 */
931export async function getGroupsAsync(groupQuery: GroupQuery): Promise<Group[]> {
932  if (!ExpoContacts.getGroupsAsync) {
933    throw new UnavailabilityError('Contacts', 'getGroupsAsync');
934  }
935
936  return await ExpoContacts.getGroupsAsync(groupQuery);
937}
938
939/**
940 * Get the default container's ID.
941 * @return A promise that fulfills with default container ID.
942 * @example
943 * ```js
944 * const containerId = await Contacts.getDefaultContainerIdAsync();
945 * ```
946 * @platform ios
947 */
948export async function getDefaultContainerIdAsync(): Promise<string> {
949  if (!ExpoContacts.getDefaultContainerIdentifierAsync) {
950    throw new UnavailabilityError('Contacts', 'getDefaultContainerIdentifierAsync');
951  }
952
953  return await ExpoContacts.getDefaultContainerIdentifierAsync();
954}
955
956/**
957 * Query a list of system containers.
958 * @param containerQuery Information used to gather containers.
959 * @return A promise that fulfills with array of containers that fit the query.
960 * @example
961 * ```js
962 * const allContainers = await Contacts.getContainersAsync({
963 *   contactId: '665FDBCFAE55-D614-4A15-8DC6-161A368D',
964 * });
965 * ```
966 * @platform ios
967 */
968export async function getContainersAsync(containerQuery: ContainerQuery): Promise<Container[]> {
969  if (!ExpoContacts.getContainersAsync) {
970    throw new UnavailabilityError('Contacts', 'getContainersAsync');
971  }
972
973  return await ExpoContacts.getContainersAsync(containerQuery);
974}
975
976/**
977 * Checks user's permissions for accessing contacts data.
978 * @return A promise that resolves to a [PermissionResponse](#permissionresponse) object.
979 */
980export async function getPermissionsAsync(): Promise<PermissionResponse> {
981  if (!ExpoContacts.getPermissionsAsync) {
982    throw new UnavailabilityError('Contacts', 'getPermissionsAsync');
983  }
984
985  return await ExpoContacts.getPermissionsAsync();
986}
987
988/**
989 * Asks the user to grant permissions for accessing contacts data.
990 * @return A promise that resolves to a [PermissionResponse](#permissionresponse) object.
991 */
992export async function requestPermissionsAsync(): Promise<PermissionResponse> {
993  if (!ExpoContacts.requestPermissionsAsync) {
994    throw new UnavailabilityError('Contacts', 'requestPermissionsAsync');
995  }
996
997  return await ExpoContacts.requestPermissionsAsync();
998}
999
1000/** @private */
1001function removeIds(contact: Contact): Contact {
1002  const updatedContact = { ...contact };
1003  if (contact.id && __DEV__) {
1004    console.warn(
1005      `You have set an id = ${contact.id} for the contact. This value will be ignored, because the id will be generated by the OS`
1006    );
1007    delete updatedContact.id;
1008  }
1009
1010  for (const key of Object.keys(contact)) {
1011    if (Array.isArray(contact[key])) {
1012      updatedContact[key] = contact[key].map((item, index) => {
1013        if (item.id) {
1014          __DEV__ &&
1015            console.warn(
1016              `You have set an id "${item.id}" at index "${index}" for the key "${key}" of the contact. This value will be ignored, because the id will be generated by the OS`
1017            );
1018          return { ...item, id: null };
1019        }
1020        return item;
1021      });
1022    }
1023  }
1024  return updatedContact;
1025}
1026
1027/**
1028 * Possible fields to retrieve for a contact.
1029 */
1030export enum Fields {
1031  ID = 'id',
1032  ContactType = 'contactType',
1033  Name = 'name',
1034  FirstName = 'firstName',
1035  MiddleName = 'middleName',
1036  LastName = 'lastName',
1037  MaidenName = 'maidenName',
1038  NamePrefix = 'namePrefix',
1039  NameSuffix = 'nameSuffix',
1040  Nickname = 'nickname',
1041  PhoneticFirstName = 'phoneticFirstName',
1042  PhoneticMiddleName = 'phoneticMiddleName',
1043  PhoneticLastName = 'phoneticLastName',
1044  Birthday = 'birthday',
1045  /**
1046   * @platform ios
1047   */
1048  NonGregorianBirthday = 'nonGregorianBirthday',
1049  Emails = 'emails',
1050  PhoneNumbers = 'phoneNumbers',
1051  Addresses = 'addresses',
1052  /**
1053   * @platform ios
1054   */
1055  SocialProfiles = 'socialProfiles',
1056  InstantMessageAddresses = 'instantMessageAddresses',
1057  UrlAddresses = 'urlAddresses',
1058  Company = 'company',
1059  JobTitle = 'jobTitle',
1060  Department = 'department',
1061  ImageAvailable = 'imageAvailable',
1062  Image = 'image',
1063  RawImage = 'rawImage',
1064  ExtraNames = 'extraNames',
1065  Note = 'note',
1066  Dates = 'dates',
1067  Relationships = 'relationships',
1068}
1069
1070/**
1071 * This format denotes the common calendar format used to specify how a date is calculated in `nonGregorianBirthday` fields.
1072 */
1073export enum CalendarFormats {
1074  Gregorian = 'gregorian',
1075  /**
1076   * @platform ios
1077   */
1078  Buddhist = 'buddhist',
1079  /**
1080   * @platform ios
1081   */
1082  Chinese = 'chinese',
1083  /**
1084   * @platform ios
1085   */
1086  Coptic = 'coptic',
1087  /**
1088   * @platform ios
1089   */
1090  EthiopicAmeteMihret = 'ethiopicAmeteMihret',
1091  /**
1092   * @platform ios
1093   */
1094  EthiopicAmeteAlem = 'ethiopicAmeteAlem',
1095  /**
1096   * @platform ios
1097   */
1098  Hebrew = 'hebrew',
1099  /**
1100   * @platform ios
1101   */
1102  ISO8601 = 'iso8601',
1103  /**
1104   * @platform ios
1105   */
1106  Indian = 'indian',
1107  /**
1108   * @platform ios
1109   */
1110  Islamic = 'islamic',
1111  /**
1112   * @platform ios
1113   */
1114  IslamicCivil = 'islamicCivil',
1115  /**
1116   * @platform ios
1117   */
1118  Japanese = 'japanese',
1119  /**
1120   * @platform ios
1121   */
1122  Persian = 'persian',
1123  /**
1124   * @platform ios
1125   */
1126  RepublicOfChina = 'republicOfChina',
1127  /**
1128   * @platform ios
1129   */
1130  IslamicTabular = 'islamicTabular',
1131  /**
1132   * @platform ios
1133   */
1134  IslamicUmmAlQura = 'islamicUmmAlQura',
1135}
1136
1137/**
1138 * @platform ios
1139 */
1140export enum ContainerTypes {
1141  /**
1142   * A local non-iCloud container.
1143   */
1144  Local = 'local',
1145  /**
1146   * In association with email server.
1147   */
1148  Exchange = 'exchange',
1149  /**
1150   * With cardDAV protocol used for sharing.
1151   */
1152  CardDAV = 'cardDAV',
1153  /**
1154   * Unknown container.
1155   */
1156  Unassigned = 'unassigned',
1157}
1158
1159export enum SortTypes {
1160  /**
1161   * The user default method of sorting.
1162   * @platform android
1163   */
1164  UserDefault = 'userDefault',
1165  /**
1166   * Sort by first name in ascending order.
1167   */
1168  FirstName = 'firstName',
1169  /**
1170   * Sort by last name in ascending order.
1171   */
1172  LastName = 'lastName',
1173  /**
1174   * No sorting should be applied.
1175   */
1176  None = 'none',
1177}
1178
1179export enum ContactTypes {
1180  /**
1181   * Contact is a human.
1182   */
1183  Person = 'person',
1184  /**
1185   * Contact is group or company.
1186   */
1187  Company = 'company',
1188}
1189