import { type PermissionResponse } from 'expo'; import { type EventSubscription, UnavailabilityError, uuid } from 'expo-modules-core'; import { Platform, Share, type ShareOptions } from 'react-native'; import ExpoContacts from './ExpoContacts'; export const onContactsChangeEventName = 'onContactsChange'; export type ContactsPermissionResponse = PermissionResponse & { /** * Indicates if your app has access to the whole or only part of the contact library. Possible values are: * - `'all'` if the user granted your app access to the whole contact library * - `'limited'` if the user granted your app access only to selected contacts (only available on iOS 18+) * - `'none'` */ accessPrivileges?: 'all' | 'limited' | 'none'; }; export type CalendarFormatType = CalendarFormats | `${CalendarFormats}`; export type ContainerType = ContainerTypes | `${ContainerTypes}`; export type ContactType = ContactTypes | `${ContactTypes}`; export type FieldType = Fields | `${Fields}`; export type Date = { /** * Day. */ day: number; /** * Month - adjusted for JavaScript `Date` which starts at `0`. */ month: number; /** * Year. */ year?: number; /** * Unique ID. This value will be generated by the OS. */ id?: string; /** * Localized display name. */ label?: string; /** * Format for the date. This is provided by the OS, do not set this manually. */ format?: CalendarFormatType; }; export type Relationship = { /** * Localized display name. */ label: string; /** * Name of related contact. */ name?: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; export type Email = { /** * Email address. */ email?: string; /** * Flag signifying if it is a primary email address. */ isPrimary?: boolean; /** * Localized display name. */ label: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; export type PhoneNumber = { /** * Phone number. */ number?: string; /** * Flag signifying if it is a primary phone number. */ isPrimary?: boolean; /** * Phone number without format. * @example * `8674305` */ digits?: string; /** * Country code. * @example * `us` */ countryCode?: string; /** * Localized display name. */ label: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; export type Address = { /** * Street name. */ street?: string; /** * City name. */ city?: string; /** * Country name */ country?: string; /** * Region or state name. */ region?: string; /** * Neighborhood name. */ neighborhood?: string; /** * Local post code. */ postalCode?: string; /** * P.O. Box. */ poBox?: string; /** * [Standard country code](https://www.iso.org/iso-3166-country-codes.html). */ isoCountryCode?: string; /** * Localized display name. */ label: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; /** */ export type SocialProfile = { /** * Name of social app. */ service?: string; /** * Localized profile name. */ localizedProfile?: string; /** * Web URL. */ url?: string; /** * Username in social app. */ username?: string; /** * Username ID in social app. */ userId?: string; /** * Localized display name. */ label: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; export type InstantMessageAddress = { /** * Name of instant messaging app. */ service?: string; /** * Username in IM app. */ username?: string; /** * Localized name of app. */ localizedService?: string; /** * Localized display name. */ label: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; export type UrlAddress = { /** * Localized display name. */ label: string; /** * Web URL. */ url?: string; /** * Unique ID. This value will be generated by the OS. */ id?: string; }; // @needs-audit /** */ export type Image = { /** * A local image URI. * > **Note**: If you have a remote URI, download it first using [`FileSystem.downloadAsync`](/versions/latest/sdk/filesystem/#filesystemdownloadasyncuri-fileuri-options). */ uri?: string; /** * Image width. * @platform ios */ width?: number; /** * Image height * @platform ios */ height?: number; /** * Image as Base64 string. */ base64?: string; }; /** */ export type Contact = { /** * Denoting a person or company. */ contactType: ContactType; /** * Full name with proper format. */ name: string; /** * Given name. */ firstName?: string; /** * Middle name */ middleName?: string; /** * Last name. */ lastName?: string; /** * Maiden name. */ maidenName?: string; /** * Dr., Mr., Mrs., and so on. */ namePrefix?: string; /** * Jr., Sr., and so on. */ nameSuffix?: string; /** * An alias to the proper name. */ nickname?: string; /** * Pronunciation of the first name. */ phoneticFirstName?: string; /** * Pronunciation of the middle name. */ phoneticMiddleName?: string; /** * Pronunciation of the last name. */ phoneticLastName?: string; /** * Organization the entity belongs to. */ company?: string; /** * Job description. */ jobTitle?: string; /** * Job department. */ department?: string; /** * Additional information. * > The `note` field [requires your app to request additional entitlements](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes). * > 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), * > set the [`ios.accessesContactNotes`](./../config/app/#accessescontactnotes) field in **app config** to `true`, and [create your development build](/develop/development-builds/create-a-build/). */ note?: string; /** * Used for efficient retrieval of images. */ imageAvailable?: boolean; /** * Thumbnail image. On iOS it size is set to 320×320px, on Android it may vary. */ image?: Image; /** * Raw image without cropping, usually large. */ rawImage?: Image; /** * Birthday information in Gregorian format. */ birthday?: Date; /** * A labeled list of other relevant user dates in Gregorian format. */ dates?: Date[]; /** * Names of other relevant user connections. */ relationships?: Relationship[]; /** * Email addresses. */ emails?: Email[]; /** * Phone numbers. */ phoneNumbers?: PhoneNumber[]; /** * Locations. */ addresses?: Address[]; /** * Instant messaging connections. */ instantMessageAddresses?: InstantMessageAddress[]; /** * Associated web URLs. */ urlAddresses?: UrlAddress[]; /** * Birthday that doesn't conform to the Gregorian calendar format, interpreted based on the [calendar `format`](#date) setting. * @platform ios */ nonGregorianBirthday?: Date; /** * Social networks. * @platform ios */ socialProfiles?: SocialProfile[]; /** * Whether the contact is starred. * @platform android */ isFavorite?: boolean; }; /** */ export type ExistingContact = Contact & { /** * Immutable identifier used for querying and indexing. This value will be generated by the OS when the contact is created. */ id: string; }; /** */ export type ContactResponse = { /** * An array of contacts that match a particular query. */ data: ExistingContact[]; /** * This will be `true` if there are more contacts to retrieve beyond what is returned. */ hasNextPage: boolean; /** * This will be `true` if there are previous contacts that weren't retrieved due to `pageOffset` limit. */ hasPreviousPage: boolean; }; export type ContactSort = `${SortTypes}`; /** */ export type ContactQuery = { /** * The max number of contacts to return. If skipped or set to `0` all contacts will be returned. */ pageSize?: number; /** * The number of contacts to skip before gathering contacts. */ pageOffset?: number; /** * If specified, the defined fields will be returned. If skipped, all fields will be returned. */ fields?: FieldType[]; /** * Sort method used when gathering contacts. */ sort?: ContactSort; /** * Get all contacts whose name contains the provided string (not case-sensitive). */ name?: string; /** * Get contacts with a matching ID or array of IDs. */ id?: string | string[]; /** * Get all contacts that belong to the group matching this ID. * @platform ios */ groupId?: string; /** * Get all contacts that belong to the container matching this ID. * @platform ios */ containerId?: string; /** * Prevent unification of contacts when gathering. * @default false * @platform ios */ rawContacts?: boolean; }; /** */ export type FormOptions = { /** * The properties that will be displayed when viewing a contact. */ displayedPropertyKeys?: FieldType[]; /** * The message displayed under the name of the contact. Only applies when editing an existing contact. */ message?: string; /** * Used if contact doesn't have a name defined. */ alternateName?: string; /** * Allows for contact mutation. */ allowsEditing?: boolean; /** * Actions like share, add, create. */ allowsActions?: boolean; /** * Show or hide the similar contacts. */ shouldShowLinkedContacts?: boolean; /** * Present the new contact controller. If set to `false` the unknown controller will be shown. */ isNew?: boolean; /** * The name of the left bar button. Only applies when editing an existing contact. */ cancelButtonTitle?: string; /** * Prevents the controller from animating in. */ preventAnimation?: boolean; /** * The parent group for a new contact. */ groupId?: string; }; /** */ export type GroupQuery = { /** * Query the group with a matching ID. */ groupId?: string; /** * Query all groups matching a name. */ groupName?: string; /** * Query all groups that belong to a certain container. */ containerId?: string; }; /** */ export type Group = { /** * Immutable id representing the group. */ name?: string; /** * The editable name of a group. */ id?: string; }; /** */ export type ContainerQuery = { /** * Query all the containers that parent a contact. */ contactId?: string; /** * Query all the containers that parent a group. */ groupId?: string; /** * Query all the containers that matches ID or an array od IDs. */ containerId?: string | string[]; }; export type Container = { name: string; id: string; type: ContainerType; }; // TODO(@kitten): Remove re-exports from EMC export { PermissionStatus, type PermissionResponse, type PermissionExpiration } from 'expo'; /** */ export async function isAvailableAsync(): Promise { return !!ExpoContacts.getContactsAsync; } /** */ export async function hasContactsAsync(): Promise { if (!ExpoContacts.hasContactsAsync) { throw new UnavailabilityError('Contacts', 'hasContactsAsync'); } return await ExpoContacts.hasContactsAsync(); } /** */ // @docsMissing export async function shareContactAsync( contactId: string, message: string, shareOptions: ShareOptions = {} ): Promise { if (Platform.OS === 'ios') { const url = await writeContactToFileAsync({ id: contactId, }); return await Share.share( { url, message, }, shareOptions ); } else if (!ExpoContacts.shareContactAsync) { throw new UnavailabilityError('Contacts', 'shareContactAsync'); } return await ExpoContacts.shareContactAsync(contactId, message); } /** * */ export async function getContactsAsync(contactQuery: ContactQuery = {}): Promise { if (!ExpoContacts.getContactsAsync) { throw new UnavailabilityError('Contacts', 'getContactsAsync'); } return await ExpoContacts.getContactsAsync(contactQuery); } /** */ export async function getPagedContactsAsync( contactQuery: ContactQuery = {} ): Promise { const { pageSize, ...nOptions } = contactQuery; if (pageSize && pageSize <= 0) { throw new Error('Error: Contacts.getPagedContactsAsync: `pageSize` must be greater than 0'); } return await getContactsAsync({ ...nOptions, pageSize, }); } /** */ export async function getContactByIdAsync( id: string, fields?: FieldType[] ): Promise { if (!ExpoContacts.getContactsAsync) { throw new UnavailabilityError('Contacts', 'getContactsAsync'); } if (id == null) { throw new Error('Error: Contacts.getContactByIdAsync: id is required'); } else { const results = await ExpoContacts.getContactsAsync({ pageSize: 1, pageOffset: 0, fields, id, }); if (results && results.data && results.data.length > 0) { return results.data[0]; } } return undefined; } /** * * * */ export async function addContactAsync(contact: Contact, containerId?: string): Promise { if (!ExpoContacts.addContactAsync) { throw new UnavailabilityError('Contacts', 'addContactAsync'); } return await ExpoContacts.addContactAsync(contact, containerId); } /** */ export async function updateContactAsync( contact: { id: string } & Partial ): Promise { if (!ExpoContacts.updateContactAsync) { throw new UnavailabilityError('Contacts', 'updateContactAsync'); } return await ExpoContacts.updateContactAsync(contact); } // @needs-audit /** */ export async function removeContactAsync(contactId: string): Promise { if (!ExpoContacts.removeContactAsync) { throw new UnavailabilityError('Contacts', 'removeContactAsync'); } return await ExpoContacts.removeContactAsync(contactId); } /** */ export async function writeContactToFileAsync( contactQuery: ContactQuery = {} ): Promise { if (!ExpoContacts.writeContactToFileAsync) { throw new UnavailabilityError('Contacts', 'writeContactToFileAsync'); } return await ExpoContacts.writeContactToFileAsync(contactQuery); } // @needs-audit /** */ export async function presentFormAsync( contactId?: string | null, contact?: Contact | null, formOptions: FormOptions = {} ): Promise { if (!ExpoContacts.presentFormAsync) { throw new UnavailabilityError('Contacts', 'presentFormAsync'); } if (Platform.OS === 'ios') { const adjustedOptions = formOptions; if (contactId) { if (contact) { contact = undefined; console.log( 'Expo.Contacts.presentFormAsync: You should define either a `contact` or a `contactId` but not both.' ); } if (adjustedOptions.isNew !== undefined) { console.log( 'Expo.Contacts.presentFormAsync: `formOptions.isNew` is not supported with `contactId`' ); } } return await ExpoContacts.presentFormAsync(contactId, contact, adjustedOptions); } else { return await ExpoContacts.presentFormAsync(contactId, contact, formOptions); } } // iOS Only /** */ export async function addExistingGroupToContainerAsync( groupId: string, containerId: string ): Promise { if (!ExpoContacts.addExistingGroupToContainerAsync) { throw new UnavailabilityError('Contacts', 'addExistingGroupToContainerAsync'); } return await ExpoContacts.addExistingGroupToContainerAsync(groupId, containerId); } /** */ export async function createGroupAsync(name?: string, containerId?: string): Promise { if (!ExpoContacts.createGroupAsync) { throw new UnavailabilityError('Contacts', 'createGroupAsync'); } name = name || uuid.v4(); if (!containerId) { containerId = await getDefaultContainerIdAsync(); } return await ExpoContacts.createGroupAsync(name, containerId); } /** */ export async function updateGroupNameAsync(groupName: string, groupId: string): Promise { if (!ExpoContacts.updateGroupNameAsync) { throw new UnavailabilityError('Contacts', 'updateGroupNameAsync'); } return await ExpoContacts.updateGroupNameAsync(groupName, groupId); } // @needs-audit /** */ export async function removeGroupAsync(groupId: string): Promise { if (!ExpoContacts.removeGroupAsync) { throw new UnavailabilityError('Contacts', 'removeGroupAsync'); } return await ExpoContacts.removeGroupAsync(groupId); } // @needs-audit /** */ export async function addExistingContactToGroupAsync( contactId: string, groupId: string ): Promise { if (!ExpoContacts.addExistingContactToGroupAsync) { throw new UnavailabilityError('Contacts', 'addExistingContactToGroupAsync'); } return await ExpoContacts.addExistingContactToGroupAsync(contactId, groupId); } // @needs-audit /** */ export async function removeContactFromGroupAsync( contactId: string, groupId: string ): Promise { if (!ExpoContacts.removeContactFromGroupAsync) { throw new UnavailabilityError('Contacts', 'removeContactFromGroupAsync'); } return await ExpoContacts.removeContactFromGroupAsync(contactId, groupId); } // @needs-audit /** */ export async function getGroupsAsync(groupQuery: GroupQuery): Promise { if (!ExpoContacts.getGroupsAsync) { throw new UnavailabilityError('Contacts', 'getGroupsAsync'); } return await ExpoContacts.getGroupsAsync(groupQuery); } /** */ export async function presentContactPickerAsync(): Promise { if (!ExpoContacts.presentContactPickerAsync) { throw new UnavailabilityError('Contacts', 'presentContactPickerAsync'); } return await ExpoContacts.presentContactPickerAsync(); } /** */ export async function getDefaultContainerIdAsync(): Promise { if (!ExpoContacts.getDefaultContainerIdentifierAsync) { throw new UnavailabilityError('Contacts', 'getDefaultContainerIdentifierAsync'); } return await ExpoContacts.getDefaultContainerIdentifierAsync(); } /** */ export async function getContainersAsync(containerQuery: ContainerQuery): Promise { if (!ExpoContacts.getContainersAsync) { throw new UnavailabilityError('Contacts', 'getContainersAsync'); } return await ExpoContacts.getContainersAsync(containerQuery); } /** */ export async function getPermissionsAsync(): Promise { if (!ExpoContacts.getPermissionsAsync) { throw new UnavailabilityError('Contacts', 'getPermissionsAsync'); } return await ExpoContacts.getPermissionsAsync(); } /** */ export async function requestPermissionsAsync(): Promise { if (!ExpoContacts.requestPermissionsAsync) { throw new UnavailabilityError('Contacts', 'requestPermissionsAsync'); } return await ExpoContacts.requestPermissionsAsync(); } /** */ export async function presentAccessPickerAsync(): Promise { return await ExpoContacts.presentAccessPickerAsync(); } /** * * * * * */ export function addContactsChangeListener(listener: () => void): EventSubscription { if (!ExpoContacts.addListener) { throw new UnavailabilityError('Contacts', 'addContactChangeListener'); } const eventSubscription = ExpoContacts.addListener(onContactsChangeEventName, listener); return { remove: () => { eventSubscription?.remove(); }, }; } /** */ export enum Fields { ID = 'id', ContactType = 'contactType', Name = 'name', FirstName = 'firstName', MiddleName = 'middleName', LastName = 'lastName', MaidenName = 'maidenName', NamePrefix = 'namePrefix', NameSuffix = 'nameSuffix', Nickname = 'nickname', PhoneticFirstName = 'phoneticFirstName', PhoneticMiddleName = 'phoneticMiddleName', PhoneticLastName = 'phoneticLastName', Birthday = 'birthday', /** * @platform ios */ NonGregorianBirthday = 'nonGregorianBirthday', Emails = 'emails', PhoneNumbers = 'phoneNumbers', Addresses = 'addresses', /** * @platform ios */ SocialProfiles = 'socialProfiles', InstantMessageAddresses = 'instantMessageAddresses', UrlAddresses = 'urlAddresses', Company = 'company', JobTitle = 'jobTitle', Department = 'department', ImageAvailable = 'imageAvailable', Image = 'image', RawImage = 'rawImage', ExtraNames = 'extraNames', Note = 'note', Dates = 'dates', Relationships = 'relationships', /** * @platform android */ IsFavorite = 'isFavorite', } /** */ export enum CalendarFormats { Gregorian = 'gregorian', /** * @platform ios */ Buddhist = 'buddhist', /** * @platform ios */ Chinese = 'chinese', /** * @platform ios */ Coptic = 'coptic', /** * @platform ios */ EthiopicAmeteMihret = 'ethiopicAmeteMihret', /** * @platform ios */ EthiopicAmeteAlem = 'ethiopicAmeteAlem', /** * @platform ios */ Hebrew = 'hebrew', /** * @platform ios */ ISO8601 = 'iso8601', /** * @platform ios */ Indian = 'indian', /** * @platform ios */ Islamic = 'islamic', /** * @platform ios */ IslamicCivil = 'islamicCivil', /** * @platform ios */ Japanese = 'japanese', /** * @platform ios */ Persian = 'persian', /** * @platform ios */ RepublicOfChina = 'republicOfChina', /** * @platform ios */ IslamicTabular = 'islamicTabular', /** * @platform ios */ IslamicUmmAlQura = 'islamicUmmAlQura', } /** */ export enum ContainerTypes { /** * A local non-iCloud container. */ Local = 'local', /** * In association with email server. */ Exchange = 'exchange', /** * With cardDAV protocol used for sharing. */ CardDAV = 'cardDAV', /** * Unknown container. */ Unassigned = 'unassigned', } export enum SortTypes { /** * The user default method of sorting. * @platform android */ UserDefault = 'userDefault', /** * Sort by first name in ascending order. */ FirstName = 'firstName', /** * Sort by last name in ascending order. */ LastName = 'lastName', /** * No sorting should be applied. */ None = 'none', } export enum ContactTypes { /** * Contact is a human. */ Person = 'person', /** * Contact is group or company. */ Company = 'company', }