An introduction to the Web Contact Picker API

Phil Nash - Apr 1 '20 - - Dev Community

If you're building a mobile application and you need access to the user's contacts you wouldn't give it a second thought, but on the web this feature was missing. The Chrome team saw this gap and got to work on an API that makes users' contacts available to developers with the security and privacy expected on the web. That API is now available in Chrome 80 on Android M or later.

In this post we will investigate the new Contact Picker API and put it to use it in a Twilio Client application to add contact selection for making browser phone calls.

The Contact Picker API

The Contact Picker API consists of the ContactsManager object, available as the contacts property on the navigator object. As it is only supported on Chrome on Android for now, the first thing we should concern ourselves with is checking for support. We can do so with this line:

const supportsContacts = ('contacts' in navigator && 'ContactsManager' in window);
Enter fullscreen mode Exit fullscreen mode

We should be sure to wrap any code that uses the Contact Picker API in a conditional test for support so that we don't cause JavaScript errors in browsers that don't support it.

Once we have checked we can use it, we turn our attention to the navigator.contacts.select function. It takes two arguments, an array of properties you want to retrieve about the contacts and an object of options. The properties available are "name", "email", and "tel". (though there is an origin trial available for two extra properties; "address" and "icon"). There is one available option for the second argument—"multiple"—which can be true or false depending on whether you want to be able to return one or multiple contacts.

select will show the user a modal with an interface that allows them to select contacts and then returns a promise. The promise resolves with an array of contacts (even if you only asked for one). Each contact will have an array property for each of the properties you requested (as contacts applications allow for more than one phone number or email address). For example:

navigator.contacts.select(["name", "tel"])
  .then(contacts => {
    console.log(contacts);
  })
  .catch(console.error);
//=> [{ "name": ["Phil Nash"], "tel": ["+61412345678", "+447123456789"]}]
Enter fullscreen mode Exit fullscreen mode

Since we are returning a promise you can also use async/await:

try {
  const contacts = await navigator.select(["name", "tel"]);
  console.log(contacts);
} catch(error) {
  console.error(error);
}
//=> [{ "name": ["Phil Nash"], "tel": ["+61412345678", "+447123456789"]}]
Enter fullscreen mode Exit fullscreen mode

It's up to your application to then display the contact and allow the user to select the properties of the contact to use within the application.

The Contact Picker API requires a user gesture to be activated and will only run on a secure domain, like other new web APIs that give access to potentially sensitive data. It should also be noted that every time you call the API it will show the contact selector modal, so there is no permanent access to the user's contacts and the user always has control over the data they share.

That's the theory done with, let's add this to an application to see it in action.

Using the Contact Picker API in an application

I've built, as a starter for this post, a simple Twilio Client based application that can make calls from the browser. We're going to add the ability to choose who we call from the device's contacts using the Contact Picker API.

Preparing the application

You will need a few things to run this application:

Once you have those, start by cloning or downloading the getting-started branch of the application from GitHub:

git clone https://github.com/philnash/contact-picker-twilio-client.git -b getting-started
cd contact-picker-twilio-client
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Copy the .env.example file to .env:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

You now need to fill in the .env file with your account credentials. You can find your Twilio Account SID in your Twilio console. You also need to generate an API key and collect both the SID and the secret (check out this video if you want to know more about API keys and secrets). For the caller ID, you can either buy a new phone number or verify your own phone number. The final thing you need is a TwiML App.

A TwiML App is a collection of webhook URLs that Twilio can use to connect calls to your application. For Twilio Client, when you initiate a call from the browser Twilio needs to know what to do with the call next, so consults a TwiML App to find a voice URL to make a request to. To set this up we need to make a tunnel to our local server using ngrok.

The application starts on port 3000, so run:

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Then grab the ngrok URL and create a TwiML App giving it the Voice URL https://YOUR_NGROK_SUBDOMAIN.ngrok.io/voice.

Enter your ngrok URL into the Voice request URL input when you create your TwiML App

That's all the config sorted, now run the application with:

npm start
Enter fullscreen mode Exit fullscreen mode

It will look like this:

The application has a header, a single input for the phone number and a Dial button.

Enter your phone number into the input field, click dial and you will receive a call.

Adding the Contact Picker API

Open up the project in your editor or IDE and load up client/app.js. This is all the code, aside from the Twilio Client JS library, that it takes to run this application.

To add the Contact Picker API to this we need to do a few things:

  1. Check whether we have support for the API
  2. Add a button to the interface to trigger the API
  3. Listen to the click event and call the Contact Picker API
  4. Handle the response from the API and fill in the input with the contact's number

To get this started, at the bottom of the init function let's do our check to see if the API is supported. If it is we have more code to write, but if it's not let's show an explanatory message.

    });
  });
  if ("contacts" in navigator && "ContactsManager" in window) {

  } else {
    const notSupported = document.createElement("p");
    notSupported.classList.add("error");
    notSupported.innerText = "Sorry, the contact picker API is not supported in your browser.";
    dialBtn.insertAdjacentElement("afterend", notSupported);
  }
};

window.addEventListener("DOMContentLoaded", init);
Enter fullscreen mode Exit fullscreen mode

Next up, we'll get a reference to the <main> element on the page, create a button and append it to the element.

  if ("contacts" in navigator && "ContactsManager" in window) {
    const mainElt = document.getElementsByTagName("main")[0];
    const contactsButton = document.createElement("button");
    contactsButton.innerText = "Choose contact";
    mainElt.appendChild(contactsButton);
  }
Enter fullscreen mode Exit fullscreen mode

We need to trigger the Contact Picker API when a user clicks on this button (note: the API requires an interaction like a click, so you can't trigger it on a page load). When we call the Contact Picker API we pass it an array of properties, in this case we just want the contact name and telephone number. We can also pass whether we want multiple contacts or not as an object.

We'll also use async/await to handle the asynchronous response from the API. For this our handler function will need to be declared as an async function. Add the event handler before the code to append the button to the page.

    contactsButton.innerText = "Choose contact";
    contactsButton.addEventListener("click", async () => {
      const contactProperties = ["name", "tel"];
      const options = { multiple: false };
      const contacts = await navigator.contacts.select(contactProperties, options);
    });
    mainElt.appendChild(contactsButton);
  }
Enter fullscreen mode Exit fullscreen mode

Once the call to the API resolves the contacts variable will be an array. If the user selected one contact it will have one item, if you passed the options { multiple: true } then it may have more than one item, but if the user didn't select a contact at all then it will be an empty array. Before we move on we should check that there is a contact in the array.

Once we are sure we have a contact, we can extract their name and telephone number too. A contact object will have a property for each of the properties we asked for, in this case "name" and "tel". Those properties will be arrays that could contain zero, one or more entries. During testing, I found that contacts may have blank entries in the array, so we'll want to filter those out too.

For this application if there is no phone number available we are going to ignore it, otherwise we'll add the phone number as the value of the input and add the name to the "Dial" button.

      const contacts = await navigator.contacts.select(contactProperties, options);
      if (contacts.length > 0) {
        const contact = contacts[0];
        const contactNumber = contact.tel.filter(tel => tel.length > 0)[0];
        const contactName = contact.name.filter(name => name.length > 0)[0];
        if (contactNumber) {
          phoneNumInput.value = contactNumber.replace(/\s/g, "");
          dialBtn.innerText = `Dial ${contactName}`;
        }
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

That is all the code you need to test this out. Open up your application on an Android device in Chrome (you can use your ngrok URL for this too). It should work like this:

An animation showing the contact picker working with the application.

The Contact Picker API is here

In this post we've seen an introduction to the Contact Picker API and an example of it in a web application that uses it to make calling contacts easier. You can see the complete version of this phone and contacts web application on GitHub.

We also saw that we should test for the existence of this API as it is currently only available on Chrome 80 on Android M and up. It remains to be seen whether other browsers will implement this, but you can progressively enhance the experience of some of your users with this API.

This API is useful not only for communications applications like we have built, but for sharing content with a user's contacts or even bootstrapping a social graph for a user. Do you have any ideas for what to build with this API? Share them with me in the comments below or on Twitter at @philnash.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .