Contact Picker API

Unofficial Proposal Draft,

This version:
https://wicg.github.io/contact-api/spec
Issue Tracking:
GitHub
Editors:
(Google)
(Google)

Abstract

An API to give one-off access to a user’s contact information with full control over the shared data.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

Contact pickers are frequently seen in various desktop and native mobile applications for a variety of use cases. This specification defines an API to bring contact pickers to the web, which will enable new use cases for web apps, such as:

The contact picker model was chosen to give full control to users over the shared data, allowing users to choose exactly which contacts to provide to the website. The contact picker model gives websites one-off access to a user’s contacts, meaning developers have to request access to the user’s contacts every time they need it. This differs from some native contact APIs, but is necessary for ensuring users' contacts are not accessed without their knowledge and explicit consent.

1.1. Examples

Requesting contacts as a result of a user click.
selectRecipientsButton.addEventListener('click', async () => {
  const contacts = await navigator.contacts.select(['name', 'email'], {multiple: true});

  if (!contacts.length) {
    // No contacts were selected in the picker.
    return;
  }

  // Use the names and e-mail addresses in |contacts| to populate the
  // recipients field in the website’s UI.
  populateRecipients(contacts);
});

In the above example selectRecipientsButton is a HTMLButtonElement, and populateRecipients is a developer-defined function.

Requesting an address to deliver a gift to.
selectRecipientButton.addEventListener('click', async () => {

  // We are unsure if addresses are supported, or can be provided by the browser.
  if ((await navigator.contacts.getProperties()).includes('address')) {
    const contacts = await navigator.contacts.select(['address']);

    if (!contacts.length) {
      // No contacts were selected in the picker.
      return;
    }

    // length is 1 since we didn’t request multiple contacts.
    sendGiftToAddress(contacts[0].address);
  }

 // Fallback to a form. 
});

In the above example selectRecipientButton is a HTMLButtonElement, and sendGiftToAddress is a developer-defined function.

Requesting a name and an icon.
selectRecipientButton.addEventListener('click', async () => {

  // We are unsure if icons are supported, or can be provided by the browser.
  if ((await navigator.contacts.getProperties()).includes('icon')) {
    const contacts = await navigator.contacts.select(['name', 'icon']);

    if (!contacts.length) {
      // No contacts were selected in the picker.
      return;
    }

    if (!contacts[0].name.length || !contacts[0].icon.length) {
      // Info not found. Use fallback.
      return;
    }

    // We only need one name and one image.
    const name = contacts[0].name[0];
    const imgBlob = contacts[0].icon[0];

    // Display image.
    const url = URL.createObjectURL(imgBlob);
    imgContainer.onload = () => URL.revokeObjectURL(url);
    imgContainer.src = url;

    // Alternatively use a Bitmap.
    const imgBitmap = await createImageBitmap(imgBlob);

    // Upload icon.
    const response = await fetch('/contacticon', {method: 'POST', body: imgBlob});
  }
});

In the above example selectRecipientButton is a HTMLButtonElement, and imgContainer is a HTMLImageElement.

2. Privacy Considerations

Exposing contact information has a clear privacy impact, in terms of exposing PII of uninvolved parties. A picker model is enforced so that the user agent can offer a user experience that makes it clear what information is going to be shared with the website and when.

The following constraints are also enforced:

3. Realms

All platform objects are created in the context object's relevant Realm unless otherwise specified.

4. Infrastructure

The contact picker task source is a task source.

To queue a contact picker task on an optional eventLoop (an event loop, defaulting to the caller’s context object's relevant settings object's responsible event loop) with steps (steps), queue a task on eventLoop using the contact picker task source to run steps.

4.1. User contact

A user contact consists of:

A user contact contains data relating to a single user.

Note: The lists can be of different sizes, and entries with the same index don’t need to correspond to each other.

4.2. Contacts source

The contacts source is a service that provides the user’s contact information to the user agent.

A contacts source consists of:

Note: It is up to the user agent to choose the contacts source.

5. API Description

5.1. Extensions to Navigator

[Exposed=Window]
partial interface Navigator {
  [SecureContext, SameObject] readonly attribute ContactsManager contacts;
};
A Navigator has a contacts manager (a ContactsManager), initially a new ContactsManager.

The contacts attribute’s getter must return the context object's contacts manager.

The browsing context has a contact picker is showing flag, initially unset.

5.2. ContactProperty

enum ContactProperty { "address", "email", "icon", "name", "tel" };

A ContactProperty is considered to be available if its associated user contact field can be accessed by the user agent.

"address"

Associated with user contact's addresses.

"email"

Associated with user contact's emails.

"icon"

Associated with user contact's icons.

"name"

Associated with user contact's names.

"tel"

Associated with user contact's numbers.

5.3. ContactsManager

interface ContactAddress : PaymentAddress {};

dictionary ContactInfo {
    sequence<ContactAddress> address;
    sequence<DOMString> email;
    sequence<Blob> icon;
    sequence<DOMString> name;
    sequence<DOMString> tel;
};

dictionary ContactsSelectOptions {
    boolean multiple = false;
};

[Exposed=(Window,SecureContext)]
interface ContactsManager {
    Promise<sequence<ContactProperty>> getProperties();
    Promise<sequence<ContactInfo>> select(sequence<ContactProperty> properties, optional ContactsSelectOptions options);
};

5.3.1. getProperties()

The getProperties() method, when invoked, runs these steps:
  1. Let promise be a new promise.

  2. Run the following steps in parallel:

    1. Resolve promise with contacts source's supported properties.

  3. Return promise.

5.3.2. select()

The select(properties, options) method, when invoked, runs these steps:
  1. Let relevantBrowsingContext be the context object's relevant settings object's responsible browsing context.

  2. If relevantBrowsingContext is not a top-level browsing context, then return a promise rejected with an InvalidStateError DOMException.

  3. If the algorithm is not triggered by user activation then return a promise rejected with a SecurityError DOMException.

  4. If relevantBrowsingContext’s contact picker is showing flag is set then return a promise rejected with an InvalidStateError DOMException.

  5. If properties is empty, then return a promise rejected with a TypeError.

  6. For each property of properties:

    1. If contacts source's supported properties does not contain property, then return a promise rejected with a TypeError.

  7. Set relevantBrowsingContext’s contact picker is showing flag.

  8. Let promise be a new promise.

  9. Run the following steps in parallel:

    1. Let selectedContacts be be the result of launching a contact picker with optionsmultiple member and properties. If this fails, then:

      1. Return a promise rejected with an InvalidStateError DOMException.

      2. Unset relevantBrowsingContext’s contact picker is showing flag.

      3. Abort these steps.

    2. Unset relevantBrowsingContext’s contact picker is showing flag.

    3. Queue a contact picker task to run these steps:

      1. Let contacts be an empty list.

      2. For each selectedContact in selectedContacts:

        1. Let contact be a new ContactInfo with:

          address

          selectedContact’s addresses if properties contains "address", otherwise undefined.

          email

          selectedContact’s emails if properties contains "email", otherwise undefined.

          icon

          selectedContact’s icons if properties contains "icon", otherwise undefined.

          name

          selectedContact’s names if properties contains "name", otherwise undefined.

          tel

          selectedContact’s numbers if properties contains "tel", otherwise undefined.

        2. Append contact to contacts.

      3. Resolve promise with contacts.

  10. Return promise.

6. Contact Picker

To launch a contact picker with allowMultiple (a boolean), and properties (a list of DOMStrings), the user agent MUST present a user interface that follows these rules:
  • If presenting a user interface fails or accessing the contacts source's available contacts fails, then return failure.

  • The UI MUST prominently display the browsing context's origin.

  • The UI MUST make it clear which properties of the contact will be shared.

    NOTE: This information is derived from properties.

  • The UI MUST provide a way to select individual contacts. If allowMultiple is false, only one contact should be pickable.

  • The UI MUST provide an option to cancel/return without sharing any contacts, in which case remove the UI and return an empty list.

  • The UI MUST provide an a way for users to indicate that they are done selecting, in which case remove the UI and return a list of the selected contacts as user contacts.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FileAPI]
Marijn Kruisselbrink; Arun Ranganathan. File API. 31 May 2019. WD. URL: https://www.w3.org/TR/FileAPI/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PAYMENT-REQUEST]
Zach Koch; et al. Payment Request API. 16 April 2019. CR. URL: https://www.w3.org/TR/payment-request/
[PROMISES-GUIDE]
Domenic Denicola. Writing Promise-Using Specifications. 9 November 2018. TAG Finding. URL: https://www.w3.org/2001/tag/doc/promises-guide
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SECURE-CONTEXTS]
Mike West. Secure Contexts. 15 September 2016. CR. URL: https://www.w3.org/TR/secure-contexts/
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

Informative References

[MIMESNIFF]
Gordon P. Hemsley. MIME Sniffing Standard. Living Standard. URL: https://mimesniff.spec.whatwg.org/

IDL Index

[Exposed=Window]
partial interface Navigator {
  [SecureContext, SameObject] readonly attribute ContactsManager contacts;
};

enum ContactProperty { "address", "email", "icon", "name", "tel" };

interface ContactAddress : PaymentAddress {};

dictionary ContactInfo {
    sequence<ContactAddress> address;
    sequence<DOMString> email;
    sequence<Blob> icon;
    sequence<DOMString> name;
    sequence<DOMString> tel;
};

dictionary ContactsSelectOptions {
    boolean multiple = false;
};

[Exposed=(Window,SecureContext)]
interface ContactsManager {
    Promise<sequence<ContactProperty>> getProperties();
    Promise<sequence<ContactInfo>> select(sequence<ContactProperty> properties, optional ContactsSelectOptions options);
};