One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Contacts

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.

Usage

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.

How it works

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.

API reference

MethodsTypeDescription
getPermissionStatus() => ContactsPermissionStatusThe address book status, without prompting.
() => ContactsPermissionStatusThe 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.
ContactTypeDescription
identifierstringThe stable id for update and delete.
stringThe stable id for update and delete.
givenNamestringThe first name.
stringThe first name.
familyNamestringThe last name.
stringThe 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 contactTypeDescription
givenNamestringThe first name.
stringThe first name.
familyNamestringThe last name.
stringThe 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 changesTypeDescription
givenNamestringA new first name, or omit to keep it.
stringA new first name, or omit to keep it.
familyNamestringA new last name, or omit to keep it.
stringA 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 addressTypeDescription
labelstringThe native label, such as home.
stringThe native label, such as home.
streetstringThe street lines.
stringThe street lines.
subLocalitystringThe neighborhood or district.
stringThe neighborhood or district.
citystringThe city.
stringThe city.
subAdministrativeAreastringThe county.
stringThe county.
statestringThe state or province.
stringThe state or province.
postalCodestringThe postal code.
stringThe postal code.
countrystringThe country name.
stringThe country name.
isoCountryCodestringThe two-letter country code.
stringThe two-letter country code.
Postal address inputTypeDescription
labelstringThe label. Home when omitted on create.
stringThe label. Home when omitted on create.
streetstringThe street lines.
stringThe street lines.
subLocalitystringThe neighborhood or district.
stringThe neighborhood or district.
citystringThe city.
stringThe city.
subAdministrativeAreastringThe county.
stringThe county.
statestringThe state or province.
stringThe state or province.
postalCodestringThe postal code.
stringThe postal code.
countrystringThe country name.
stringThe country name.
isoCountryCodestringThe two-letter country code.
stringThe two-letter country code.

Platform differences

iOSAndroidWeb
StoreContacts storeContacts providerNone, picker only
AccessFull or limitedFull onlyOne pick only
SearchBy nameBy nameEmpty list
WritesFull editingFull editingReject, delete silent
PickerNeeds no rightsNeeds no rightsNeeds a gesture

Limits and gotchas

Examples

Invite someone from the address book

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.

Search and rename a contact

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.

Save a new contact with an address

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.