-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathindex.bs
572 lines (456 loc) · 24.2 KB
/
index.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
<pre class="metadata">
Title: Contact Picker API
Shortname: contact-picker
Status: ED
Group: dap
Level: none
ED: https://w3c.github.io/contact-picker/
TR: https://www.w3.org/TR/contact-picker/
Editor: Peter Beverloo 44819, Google, [email protected]
Former Editor: Rayan Kanso 115452, Google, [email protected]
Abstract: An API to give one-off access to a user's contact information with full control over the shared data.
Markup Shorthands: css no, markdown yes
Indent: 2
text macro: JOINT yes
text macro: JOINTWEBAPPS yes
</pre>
<pre class=link-defaults>
spec:infra; type:dfn; text:list
spec:html; type:dfn; for:/; text:browsing context
spec:html; type:dfn; for:/; text:valid email address
spec:html; type:dfn; for:environment settings object; text:origin
</pre>
# Introduction # {#intro}
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:
* Bootstrapping a user's social graph for social networks.
* Selecting the recipients of a message within an e-mail application.
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.
## Examples ## {#examples}
<div class="example">
Requesting contacts as a result of a user click.
<pre class="lang-js">
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);
});
</pre>
In the above example `selectRecipientsButton` is a {{HTMLButtonElement}}, and `populateRecipients`
is a developer-defined function.
</div>
<div class="example">
Requesting an address to deliver a gift to.
<pre class="lang-js">
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.
});
</pre>
In the above example `selectRecipientButton` is a {{HTMLButtonElement}}, and `sendGiftToAddress`
is a developer-defined function.
</div>
<div class="example">
Requesting a name and an icon.
<pre class="lang-js">
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});
}
});
</pre>
In the above example `selectRecipientButton` is a {{HTMLButtonElement}}, and `imgContainer`
is a {{HTMLImageElement}}.
</div>
# Privacy Considerations # {#privacy}
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:
* A user gesture is needed to initiate the API, to disallow programmatic requests to the user's
* The API is only available in a [=navigable/top-level traversable=], which must also be a
[=secure context=]. These restrictions help ensure that the provided contact information reaches
its intended recipient.
* [=Transient activation=] is needed to initiate the API, to disallow programmatic requests to the user's
contacts.
# Security Considerations # {#security-considerations}
* The API is only available in a [=navigable/top-level traversables=], which must also be a
[=secure context=]. These restrictions help ensure that the provided contact information reaches
its intended recipient.
# Realms # {#realms}
All platform objects are created in the [=this=]'s [=relevant Realm=] unless otherwise
specified.
# Infrastructure # {#infrastructure}
The <dfn>contact picker task source</dfn> is a [=task source=].
<div algorithm>
To <dfn>queue a contact picker task</dfn> on an optional |eventLoop| (an [=/event loop=],
defaulting to the caller's [=this=]'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|.
</div>
## Physical address ## {#infrastructure-physical-address}
A <dfn export>physical address</dfn> consists of:
<div dfn-for="physical address">
* <dfn for="physical address" export>country</dfn>, a {{DOMString}} representing the country of the address as an
[[ISO3166-1]] alpha-2 code stored in its canonical uppercase form or the empty string. For
example, "JP".
* <dfn for="physical address" export>address line</dfn>, a [=list=] of {{DOMString}}s, containing the most specific part of the
address. It can include, for example, a street name, a house number, apartment number, a rural
delivery route, descriptive instructions, or a post office box number.
* <dfn for="physical address" export>region</dfn>, a {{DOMString}} representing the top level administrative subdivision of the
country. For example, this can be a state, a province, an oblast, or a prefecture.
* <dfn for="physical address" export>city</dfn>, a {{DOMString}} representing the city/town portion of the address.
* <dfn for="physical address" export>dependent locality</dfn>, a {{DOMString}} representing the dependent locality or sublocality
within a city. For example, neighborhoods, boroughs, districts, or UK dependent localities.
* <dfn for="physical address" export>postal code</dfn>, a {{DOMString}} representing the postal code or ZIP code, also known as
PIN code in India.
* <dfn for="physical address" export>sorting code</dfn>, a {{DOMString}} representing the sorting code system, such as the CEDEX
system used in France.
* <dfn for="physical address" export>organization</dfn>, a {{DOMString}} representing the organization, firm, company, or
institution at the address.
* <dfn for="physical address" export>recipient</dfn>, a {{DOMString}} representing the name of the recipient or contact person at
the address.
* <dfn for="physical address" export>phone number</dfn>, a {{DOMString}} representing the phone number of the recipient or contact
person at the address, optionally structured to adhere to [[E.164]].
</div>
## User contact ## {#infrastructure-user-contact}
A <dfn>user contact</dfn> consists of:
<div dfn-for="user contact">
* <dfn for="user contact">names</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique name
corresponding to the user.
* <dfn for="user contact">emails</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique
[=valid email address=] of the user.
* <dfn for="user contact">numbers</dfn>, a [=list=] of {{DOMString}}s, each [=list/item=] representing a unique phone
number of the user.
* <dfn for="user contact">addresses</dfn>, a [=list=] of {{ContactAddress}}es, each [=list/item=] representing a
unique [=physical address=] of the user.
* <dfn for="user contact">icons</dfn>, a [=list=] of {{Blob}}s, each [=list/item=] representing a unique image of the
user.
NOTE: An icon {{Blob}}'s {{Blob/type}} is an [=image mime type=].
</div>
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.
## Contacts source ## {#infrastructure-contacts-source}
The <dfn>contacts source</dfn> is a service that provides the user's contact information to
the user agent.
A [=contacts source=] consists of:
<div dfn-for="contacts source">
* <dfn>available contacts</dfn>, a [=list=] of [=user contacts=].
* <dfn>supported properties</dfn>, a [=list=] of [=available=] {{ContactProperty}} values.
</div>
Note: It is up to the user agent to choose the [=contacts source=].
</div>
# API Description # {#api}
## Extensions to {{Navigator}} ## {#extensions-to-navigator}
<script type="idl">
[Exposed=Window]
partial interface Navigator {
[SecureContext, SameObject] readonly attribute ContactsManager contacts;
};
</script>
<div dfn-for="Navigator">
A {{Navigator}} has a <dfn>contacts manager</dfn> (a {{ContactsManager}}), initially a new
{{ContactsManager}}.
The <dfn attribute>contacts</dfn> attribute's getter must return the [=this=]'s
[=Navigator/contacts manager=].
</div>
The [=Window/navigable=] has a <dfn>contact picker is showing flag</dfn>, initially unset.
## {{ContactProperty}} ## {#contact-property}
<script type="idl">
enum ContactProperty { "address", "email", "icon", "name", "tel" };
</script>
A {{ContactProperty}} is considered to be <dfn>available</dfn> if its associated [=user contact=]
field can be accessed by the user agent.
: "address"
:: Associated with [=user contact=]'s [=user contact/addresses=].
: "email"
:: Associated with [=user contact=]'s [=user contact/emails=].
: "icon"
:: Associated with [=user contact=]'s [=user contact/icons=].
: "name"
:: Associated with [=user contact=]'s [=user contact/names=].
: "tel"
:: Associated with [=user contact=]'s [=user contact/numbers=].
## {{ContactAddress}} ## {#contact-address}
<script type="idl">
[Exposed=Window]
interface ContactAddress {
[Default] object toJSON();
readonly attribute DOMString city;
readonly attribute DOMString country;
readonly attribute DOMString dependentLocality;
readonly attribute DOMString organization;
readonly attribute DOMString phone;
readonly attribute DOMString postalCode;
readonly attribute DOMString recipient;
readonly attribute DOMString region;
readonly attribute DOMString sortingCode;
readonly attribute FrozenArray<DOMString> addressLine;
};
</script>
The {{ContactAddress}} interface represents a [=physical address=].
<div dfn-for="ContactAddress">
A {{ContactAddress}} instance has:
* An <dfn>address</dfn> (a [=physical address=]).
The <dfn attribute>city</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/city=].
The <dfn attribute>country</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/country=].
The <dfn attribute>dependentLocality</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/dependent locality=].
The <dfn attribute>organization</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/organization=].
The <dfn attribute>phone</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/phone number=].
The <dfn attribute>postalCode</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/postal code=].
The <dfn attribute>recipient</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/recipient=].
The <dfn attribute>region</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/region=].
The <dfn attribute>sortingCode</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/sorting code=].
The <dfn attribute>addressLine</dfn> attribute's getter must return the [=this=]'s
[=ContactAddress/address=]' [=physical address/address line=].
</div>
## {{ContactsManager}} ## {#contacts-manager}
<script type="idl">
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 = {});
};
</script>
<div dfn-for="ContactsManager">
### {{ContactsManager/getProperties()}} ### {#contacts-manager-getproperties}
<div algorithm>
The <dfn method>getProperties()</dfn> method, when invoked, runs these steps:
1. Let |promise| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Resolve |promise| with [=contacts source=]'s [=contacts source/supported properties=].
1. Return |promise|.
</div>
### {{ContactsManager/select()}} ### {#contacts-manager-select}
<div algorithm>
The <dfn method>select(|properties|, |options|)</dfn> method, when invoked, runs these steps:
1. Let |global| be the [=this=]'s [=relevant global object=].
1. Let |navigable| be |global|'s [=Window/navigable=].
1. If |navigable| is not a [=navigable/top-level traversable=], then return
[=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}.
1. If |global| does not have [=transient activation=], then return
[=a promise rejected with=] a {{SecurityError}} {{DOMException}}.
1. Otherwise, [=consume user activation=] of the |global|.
1. If |navigable|'s [=contact picker is showing flag=] is set then return
[=a promise rejected with=] an {{InvalidStateError}} {{DOMException}}.
1. If |properties| is [=list/empty=], then return [=a promise rejected with=] a {{TypeError}}.
1. [=list/For each=] |property| of |properties|:
1. If [=contacts source=]'s [=contacts source/supported properties=] does not [=list/contain=]
|property|, then return [=a promise rejected with=] a {{TypeError}}.
1. Set |navigable|'s [=contact picker is showing flag=].
1. Let |promise| be [=a new promise=].
1. Run the following steps [=in parallel=]:
1. Let |selectedContacts| be be the result of [=launching a contact picker=] with |options|'
`multiple` member and |properties|. If this fails, then:
1. [=Queue a contact picker task=] to run these steps:
1. [=Reject=] |promise| an {{InvalidStateError}} {{DOMException}}.
1. Unset |navigable|'s [=contact picker is showing flag=].
1. Abort these steps.
1. Unset |navigable|'s [=contact picker is showing flag=].
1. [=Queue a contact picker task=] to run these steps:
1. Let |contacts| be an empty [=list=].
1. [=list/For each=] |selectedContact| in |selectedContacts|:
1. Let |contact| be a new {{ContactInfo}} with:
: {{ContactInfo/address}}
:: |selectedContact|'s [=user contact/addresses=] if |properties| [=list/contains=]
"`address`", otherwise undefined.
: {{ContactInfo/email}}
:: |selectedContact|'s [=user contact/emails=] if |properties| [=list/contains=]
"`email`", otherwise undefined.
: {{ContactInfo/icon}}
:: |selectedContact|'s [=user contact/icons=] if |properties| [=list/contains=]
"`icon`", otherwise undefined.
: {{ContactInfo/name}}
:: |selectedContact|'s [=user contact/names=] if |properties| [=list/contains=]
"`name`", otherwise undefined.
: {{ContactInfo/tel}}
:: |selectedContact|'s [=user contact/numbers=] if |properties| [=list/contains=]
"`tel`", otherwise undefined.
1. [=list/Append=] |contact| to |contacts|.
1. Resolve |promise| with |contacts|.
1. Return |promise|.
</div>
# Contact Picker # {#contact-picker}
<div algorithm>
To <dfn lt="launching a contact picker">launch</dfn> a contact picker with |allowMultiple| (a
[=boolean=]), and |properties| (a [=list=] of {{DOMString}}s), the user agent MUST present a user
interface that follows these rules:
* If presenting a user interface fails or accessing the [=contacts source=]'s
[=contacts source/available contacts=] fails, then return failure.
* The UI MUST prominently display the [=navigable/top-level traversable=]'s [=origin=].
* The UI MUST make it clear which `properties` of the contacts are requested.
NOTE: This information is derived from |properties|.
* The UI SHOULD provide a way for users to opt out of sharing certain contact information.
NOTE: If the user opts out, the appropriate [=user contact=] fields should be modified before
returning the selected contacts. It should be indistinguishable from the returned
[=user contact=]s whether the user opted out from sharing some information or if the
information was not present to begin with.
* The UI MUST make it clear which information will be shared.
* 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=].
</div>
# Creating a `ContactAddress` from user-provided input # {#creating-contactaddress}
The steps to <dfn export>create a `ContactAddress` from user-provided input</dfn>
are given by the following algorithm.
The algorithm optionally takes a [=list=] |redactList|.
If the |redactList| is not passed, it defaults to an [=list/empty=] [=list=].
NOTE: The |redactList| optionally gives user agents
the possibility to limit the amount of personal information
about the recipient that the API shares with the requesting application.
The resulting {{ContactAddress}} object provides enough information
to perform necessary operations
such as communication or service delivery,
but, in most cases,
not enough information to physically locate and uniquely identify the recipient.
Unfortunately, even with the |redactList|,
recipient anonymity cannot be assured.
This is because in some countries
postal codes are so fine-grained that they can uniquely identify a recipient.
<div class="algorithm">
1. Let |details| be the [=map=] «
"addressLine" → empty [=list=],
"country" → "",
"phone" → "",
"city" → "",
"dependentLocality" → "",
"organization" → "",
"postalCode" → "",
"recipient" → "",
"region" → "",
"sortingCode" → ""
».
1. If |redactList| doesn't [=list/contain=] "addressLine",
set |details|["addressLine"] to the result of splitting the user-provided address line into a [=list=].
NOTE: How to split an address line is locale dependent
and beyond the scope of this specification.
1. If |redactList| doesn't [=list/contain=] "country",
set |details|["country"] to the user-provided [=physical address/country=] as an upper case [[ISO3166-1]] alpha-2 code.
1. If |redactList| doesn't [=list/contain=] "phone",
set |details|["phone"] to the user-provided [=physical address/phone number=].
NOTE: To maintain users' privacy,
implementers need to be mindful
that a contact address's associated phone number
might be different or the same from that of the end user's.
As such,
implementers need to take care
to not provide the end user's phone number without the end user's consent.
1. If |redactList| doesn't [=list/contain=] "city",
set |details|["city"] to the user-provided [=physical address/city=].
1. If |redactList| doesn't [=list/contain=] "dependentLocality",
set |details|["dependentLocality"] to the user-provided [=physical address/dependent locality=].
1. If |redactList| doesn't [=list/contain=] "organization",
set |details|["organization"] to the user-provided recipient [=physical address/organization=].
1. If |redactList| doesn't [=list/contain=] "postalCode",
set |details|["postalCode"] to the user-provided [=physical address/postal code=].
Optionally, redact part of |details|["postalCode"].
NOTE: [=physical address/Postal codes=]
in certain countries can be so specific
as to uniquely identify an individual.
This being a privacy concern,
some user agents only return the part of a postal code
that they deem sufficient for the application's needs.
This varies across countries and regions,
and so the choice to redact part,
or all,
of the postal code is left to the discretion of implementers
in the interest of protecting users' privacy.
1. If |redactList| doesn't [=list/contain=] "recipient",
set |details|["recipient"] to the user-provided [=physical address/recipient=] of the contact information.
1. If |redactList| doesn't [=list/contain=] "region",
set |details|["region"] to the user-provided [=physical address/region=].
NOTE: In some countries (e.g., Belgium)
it is uncommon for users to include a [=physical address/region=]
as part of a [=physical address=]
(even if all the regions of a country are part of [[ISO3166-2]]).
As such,
when the user agent knows that the user is inputting the address
for a particular country,
it might not provide a field for the user to input a [=physical address/region=].
In such cases,
the user agent returns an empty string for both {{ContactAddress}}'s
{{ContactAddress/region}} attribute - but the address can still serve its intended purpose
(e.g., be valid for communication or service delivery).
1. If |redactList| doesn't [=list/contain=] "sortingCode",
set |details|["sortingCode"] to the user-provided [=physical address/sorting code=].
1. Return a newly created {{ContactAddress}} whose attribute's value's match those in |details|.
</div>
# Acknowledgments # {#acknowledgments}
There has been multiple earlier attempts to standardize a Contacts API for the web and this
API strives to learn from this rich history. Earlier attempts include Mozilla's
[Contacts API](https://wiki.mozilla.org/WebAPI/ContactsAPI),
[Contacts API](https://lists.w3.org/Archives/Public/public-device-apis/2009Apr/att-0001/contacts.html)
W3C Member submission and standardization efforts by W3C Working Groups:
[Contacts API](https://www.w3.org/TR/2010/WD-contacts-api-20100121/),
[Pick Contacts Intent](https://www.w3.org/TR/2014/NOTE-contacts-api-20140114/), and
[Contacts Manager API](https://www.w3.org/TR/2015/NOTE-contacts-manager-api-20150602/).
The Contact Picker API differs in its approach to privacy, which was the main emphasis of the API
when it was designed. Unlike previous attempts which allow for perpetual access after granted
permission, or include a vague privacy model, this spec enforces UI restrictions which give users
full control over shared data and limit abuse. For example, a picker model is enforced where the
user always acts as an intermediary of the shared contact info with full control every time
contacts are requested. For more historical context, please refer to the Status of the Document
sections of the earlier attempts.