1. Software
  2. Web API
  3. PWA

6 min read

Interacting with Contacts on the Web

If you are a Safari user please continue reading the guide below. If you are a developer start here.

Enable the Contact Picker in Safari iOS

In April 2021, Safari added the ability for a web app to select and read native contacts. However, this feature is hidden behind a flag which means your favorite web apps can’t live up to their full potential.

As we close out of 2023, this feature is still hidden. Apple is probably working through some security stuff with this API as usual or it’s just not a priority for them (more likely). The good news is turning this feature on in Safari is pretty simple so let’s walk through that now.

First, open up your iPhone, go to settings, and click on Safari:

iOS Settings

Scroll to the very bottom and tap Advanced. In the Advanced settings, scroll to the bottom again and tap Feature Flags.

You should now see many different settings with the ability to switch them on and off. Scroll down to the setting called Contact Picker API:

Safari Feature Flags

Flip that switch like you’re turning on the disco lights, and voila, the contact picker should work! 🎉

Now, if this upcoming developer lingo makes your eyes glaze over faster than a Dunkin donut, feel free to exit stage left. Otherwise, continue reading for some goodies.

Contact Picker API

Accessing a user’s contacts has been made available to Native applications for years. With the release of the contact picker spec spec, this is now possible in Web Applications!

This API gives one-off access to a user’s contact information. As of 2023, the spec only includes reading contacts but I am hoping they expand this in the future to eventually give write or sync access as well.

This API has many use cases like selecting a contact to import into the application, selecting a contact to message or email, or discovering existing contacts in a system.

Browser support is lacking as of 2023. Only Chrome on Android has this feature enabled by default. Safari on iOS can be enabled as shown in the user guide above. Check MDN’s compatibility table for more details.

Contact Picker Interface

The Contact Picker API currently has one main interface called ContactsManager.

TIP: Remember to be as progressive as possible and check if this API is available in the browser before showing the option to the user: const isSupported = 'contacts' in navigator && 'ContactsManager' in window

This interface has one main function called select that returns a Promise with an array of contact results. This function takes a list of contact properties as its first argument and some options as its second argument. Here is an example:

const contactResults = await navigator.contacts.select(
	['icon', 'name', 'email', 'tel'],
	{ multiple: true }
)

If we were to create a TS type for contactResults it would look something like this:

type Contact = {
	icon: Blob[]
	name: string[]
	email: string[]
	tel: string[]
}

All possible property options at the time of this writing are:

  1. address
  2. email
  3. icon
  4. name
  5. tel

See the spec for an updated list.

The only option for the second argument in the select function at the time of this writing is the boolean multiple. Setting multiple to true will change the behavior of the native UI and allow the user to select multiple contacts vs just selecting one.

You can select all properties supported by the current user agent with code like this:

const supportedProperties = await navigator.contacts.getProperties()
const contactResults = await navigator.contacts.select(supportedProperties, {
	multiple: true
})
console.log(contactResults)

The property names are straightforward aside from the icon property. This property refers to an avatar or profile image that might be assigned to a contact. As shown above, this property is of type Blob.

If you want to display this property on an img tag, create a local url with URL.createObjectURL(). You can pass the result to the src attribute. Here is an example in Svelte:

{#each contactResults as contact}
	<div>
		{#if contact.icon.length}
			<img src={URL.createObjectURL(contact.icon[0])} />
		{/if}
		<p>{contact.name[0]}</p>
	</div>
{/each}

Those are the basics to using this API as there isn’t much to it yet. Now go out and try it today in your web apps on iOS and Android!

Bonus: Adapter Pattern

You may be using this API in some import contacts flow alongside providers like Google or Microsoft. If this sounds like you, may I recommend reaching into your bag of patterns and pulling out the adapter?

Creating a thin mapping interface between these contact providers and your application will allow your application to transform the output of these various services into a structured output that your code can understand. We can adapt the output into a uniform interface.

Let’s define what the interface could look like in TypeScript:

export type Address = {
	streetAddress: string
	city: string
	region: string
	postalCode: string
	country: string
}
export type ContactAttribute = {
	name: string
} & (
	| {
			type: 'email'
			value: string
	  }
	| {
			type: 'phone'
			value: string
	  }
	| {
			type: 'address'
			value: Address
	  }
)
export type Contact = {
	id: string
	name: string
	avatarUrl: string
	attributes: ContactAttribute[]
}

Now any code in our application can take a Contact as an input and work with that specific interface. Now we just need to adapt outputs from different contact providers to this interface. We can use the native contact picker output as an example:

async function getNativeContacts(): Promise<Contact[]> {
	const isContactPickerSupported =
		'contacts' in navigator && 'ContactsManager' in window
	if (!isContactPickerSupported) return []

	try {
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		const contacts: any[] = await (navigator as any).contacts.select(
			['name', 'tel', 'icon'],
			{
				multiple: true
			}
		)
		return contacts.map((c, idx) => {
			const attributes: ContactAttribute[] = []

			if (c.tel.length) {
				c.tel.forEach((tel: string, idx: number) =>
					attributes.push({ type: 'phone', name: `Phone ${idx + 1}`, value: tel })
				)
			}

			if (c.email.length) {
				c.email.forEach((email: string, idx: number) =>
					attributes.push({ type: 'email', name: `Email ${idx + 1}`, value: email })
				)
			}

			if (c.address.length) {
				c.address.forEach((address: any) =>
					attribute.push({
						type: 'address',
						name: `Address ${idx + 1}`,
						value: {
							streetAddress: address.addressLine,
							city: address.city,
							region: address.region,
							postalCode: address.postalCode,
							country: address.country
						} satisfies Address
					})
				)
			}

			return {
				id: idx.toString(),
				name: c.name[0],
				avatarUrl: URL.createObjectUrl(c.icon[0]),
				attributes: attributes
			}
		})
	} catch (err) {
		console.log(err)
		return []
	}
}

You can do something similar to this with the output from Google’s People Api.

Conclusion

I hope this guide helped you get started with the Contact Picker API.

Let me know if you think I missed something or want to dive deeper into a certain topic like the adapter pattern.

If you liked this content please check out my other blog posts and subscribe to my RSS feed.

I am also active on Mastodon.

Comments