Search, pick, create, and edit address book contacts
One.Contacts reads and writes the system address book: search by name,
let the user pick one contact, or create, update, and delete entries. The
picker exposes only the chosen contact and needs no address book
permission, so prefer it for one-shot flows like inviting a friend; use
search and writes when your app manages contacts itself.
On iOS it uses CNContactStore from the Contacts framework and
CNContactPickerViewController from ContactsUI. On Android it queries
ContactsContract through the content resolver. On web the address book
stays closed, but browsers with the Contact Picker API can still pick one
contact from a user gesture.
import { One } from 'one'
const permission = await One.Contacts.requestPermission()if (permission === 'authorized' || permission === 'limited') { const matches = await One.Contacts.search('Ada', 20) console.log(matches.map((contact) => `${contact.givenName} ${contact.familyName}`))}Search needs address book permission; the grant can be full or, on iOS, limited to the contacts the user shared. Results carry names, phone numbers, emails, and postal addresses.
Set native.app.contacts.usage before prebuild. It becomes
NSContactsUsageDescription on iOS and stamps READ_CONTACTS plus
WRITE_CONTACTS into the Android manifest. Without it, permission and
read calls reject with E_CONTACTS_MANIFEST. The status is
not determined, restricted, denied, authorized, or limited; Android has no
limited tier. Search and writes run off the UI thread: a background queue
on iOS, a single worker thread on Android.
search takes a nonempty name and a whole-number limit from 1 to 100
(default 100) and returns matches with an identifier, given and family
names, phone and email string arrays, and structured postal addresses.
create saves a new contact and returns its identifier. update edits
any combination of fields and returns the updated contact; omitted fields
keep their values, while supplied phone, email, and address arrays replace
their whole field, so an empty array clears one. Blank phone and email
entries are rejected, and a contact must keep at least one given or family
name when either name changes. delete removes a contact the app may
change. Ask the user before changing their contacts. Contact notes need
an Apple-granted entitlement and are not exposed.
pickContact presents the system picker from the visible screen and
resolves with the chosen contact in the search shape, or undefined on
cancel. It never asks for address book permission. Only one picker runs
at a time; a second call, or a missing presentation surface, rejects with
E_CONTACTS_PICKER. On web the browser picker returns the display name
as givenName with an empty family name and identifier, since the
browser exposes neither separate name fields nor a stable id, and phone,
email, and address fields are requested only when supported. Browsers
without the picker API keep the native-build rejection, and search,
create, update, and delete keep their unavailable contracts everywhere
off native: denied status, empty searches, and rejections for calls that
return a contact, while deletion silently resolves.
| Methods | Type | Description |
|---|---|---|
getPermissionStatus() => ContactsPermissionStatusThe address book status, without prompting. | () => ContactsPermissionStatus | The address book status, without prompting. |
requestPermission() => Promise<ContactsPermissionStatus>Ask for address book access. | () => Promise<ContactsPermissionStatus> | Ask for address book access. |
pickContact() => Promise<ContactInfo | undefined>Pick one contact. Needs no permission; undefined on cancel. | () => Promise<ContactInfo | undefined> | Pick one contact. Needs no permission; undefined on cancel. |
search(name: string, limit: number) => Promise<ContactInfo[]>Match contacts by name. Limit defaults to 100. | (name: string, limit: number) => Promise<ContactInfo[]> | Match contacts by name. Limit defaults to 100. |
create(input: ContactInput) => Promise<string>Save a contact. Resolves with its identifier. | (input: ContactInput) => Promise<string> | Save a contact. Resolves with its identifier. |
update(identifier: string, changes: ContactChanges) => Promise<ContactInfo>Edit a contact. Resolves with the updated contact. | (identifier: string, changes: ContactChanges) => Promise<ContactInfo> | Edit a contact. Resolves with the updated contact. |
delete(identifier: string) => Promise<void>Remove a contact the app may change. | (identifier: string) => Promise<void> | Remove a contact the app may change. |
| Contact | Type | Description |
|---|---|---|
identifierstringThe stable id for update and delete. | string | The stable id for update and delete. |
givenNamestringThe first name. | string | The first name. |
familyNamestringThe last name. | string | The last name. |
phoneNumbersstring[]Phone numbers as plain strings. | string[] | Phone numbers as plain strings. |
emailAddressesstring[]Email addresses as plain strings. | string[] | Email addresses as plain strings. |
postalAddressesContactPostalAddress[]Structured postal addresses. | ContactPostalAddress[] | Structured postal addresses. |
| New contact | Type | Description |
|---|---|---|
givenNamestringThe first name. | string | The first name. |
familyNamestringThe last name. | string | The last name. |
phoneNumbersstring[]Phone numbers to store. | string[] | Phone numbers to store. |
emailAddressesstring[]Email addresses to store. | string[] | Email addresses to store. |
postalAddressesContactPostalAddressInput[]Addresses to store. Optional. | ContactPostalAddressInput[] | Addresses to store. Optional. |
| Contact changes | Type | Description |
|---|---|---|
givenNamestringA new first name, or omit to keep it. | string | A new first name, or omit to keep it. |
familyNamestringA new last name, or omit to keep it. | string | A new last name, or omit to keep it. |
phoneNumbersstring[]Replaces all numbers. Empty clears them. | string[] | Replaces all numbers. Empty clears them. |
emailAddressesstring[]Replaces all emails. Empty clears them. | string[] | Replaces all emails. Empty clears them. |
postalAddressesContactPostalAddressInput[]Replaces all addresses. Empty clears them. | ContactPostalAddressInput[] | Replaces all addresses. Empty clears them. |
| Postal address | Type | Description |
|---|---|---|
labelstringThe native label, such as home. | string | The native label, such as home. |
streetstringThe street lines. | string | The street lines. |
subLocalitystringThe neighborhood or district. | string | The neighborhood or district. |
citystringThe city. | string | The city. |
subAdministrativeAreastringThe county. | string | The county. |
statestringThe state or province. | string | The state or province. |
postalCodestringThe postal code. | string | The postal code. |
countrystringThe country name. | string | The country name. |
isoCountryCodestringThe two-letter country code. | string | The two-letter country code. |
| Postal address input | Type | Description |
|---|---|---|
labelstringThe label. Home when omitted on create. | string | The label. Home when omitted on create. |
streetstringThe street lines. | string | The street lines. |
subLocalitystringThe neighborhood or district. | string | The neighborhood or district. |
citystringThe city. | string | The city. |
subAdministrativeAreastringThe county. | string | The county. |
statestringThe state or province. | string | The state or province. |
postalCodestringThe postal code. | string | The postal code. |
countrystringThe country name. | string | The country name. |
isoCountryCodestringThe two-letter country code. | string | The two-letter country code. |
| iOS | Android | Web | |
|---|---|---|---|
| Store | Contacts store | Contacts provider | None, picker only |
| Access | Full or limited | Full only | One pick only |
| Search | By name | By name | Empty list |
| Writes | Full editing | Full editing | Reject, delete silent |
| Picker | Needs no rights | Needs no rights | Needs a gesture |
E_CONTACTS_INPUT.undefined on cancel. Treat a missing contact
as a closed picker, not an error.pickContact while one is open
rejects with E_CONTACTS_PICKER.import { useState } from 'react'import { Button, Text } from 'react-native'import { One } from 'one'
function InviteButton({ invite }: { invite: (phone: string) => void }) { const [name, setName] = useState('')
async function pick() { const contact = await One.Contacts.pickContact() const phone = contact?.phoneNumbers[0] if (!contact || !phone) return setName(`${contact.givenName} ${contact.familyName}`.trim()) invite(phone) }
return ( <> <Button title="Invite a contact" onPress={pick} /> {name ? <Text>Inviting {name}</Text> : null} </> )}The picker needs no permission prompt, so inviting stays a one-tap flow. A cancel or a contact without a number returns quietly to the idle state.
import { One } from 'one'
async function fixSpelling(query: string, givenName: string) { const permission = await One.Contacts.requestPermission() if (permission !== 'authorized' && permission !== 'limited') return const [match] = await One.Contacts.search(query, 5) if (!match) return const updated = await One.Contacts.update(match.identifier, { givenName }) console.log('Renamed to', updated.givenName, updated.familyName)}Only the changed field is passed; every omitted field keeps its value. The returned contact confirms the new state without a second search.
import { One } from 'one'
async function saveFriend() { const identifier = await One.Contacts.create({ givenName: 'Ada', familyName: 'Lovelace', phoneNumbers: ['+1 555-0100'], emailAddresses: [], postalAddresses: [ { label: 'home', street: '12 Analytical Way', city: 'London', postalCode: 'E1 6AN', country: 'United Kingdom', isoCountryCode: 'GB', }, ], }) console.log('Saved contact', identifier)}Phone and email arrays are required even when empty; addresses stay optional. The resolved identifier is the handle for later updates.
Edit this page on GitHub.