# Contacts - JavaScript bridge API

> Reading, searching, picking and writing - the picker needs no permission at all.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/contacts

### `navigator.contacts`

```js
navigator.contacts.select(properties, options) — Contact Picker
```

**Example**

The standard Contact Picker API: the phone's own contact picker opens, and the page gets only the person the user chose - no contacts permission.

**Returns:** a Promise of an array: `[{ name: [string], tel: [string] }]`, or `[]` when the user closes the picker. **Needs:** nothing to switch on; call it from a tap (a user gesture is required, else `SecurityError`).

```js
document.getElementById('pick').addEventListener('click', async () => {
  if (!('contacts' in navigator)) { showManualEntry(); return; }
  const props = await navigator.contacts.getProperties();   // ['name', 'tel']
  const [person] = await navigator.contacts.select(props, { multiple: false });
  if (!person) return;                                       // closed without choosing
  document.getElementById('name').value = person.name[0] || '';
  document.getElementById('phone').value = person.tel[0] || '';
});
```

**Notes:** the app supports `name` and `tel` - the number the user tapped. `email`, `address` and `icon` would need the full contacts permission, so they are not offered and asking for them rejects with `TypeError`. One person per pick (`multiple` is allowed but the Android picker returns one). Works in the top page only. For listing or searching all contacts use `WebToApk.listContacts` (needs the Contacts switch).

### `addContact`

```js
window.WebToApk.addContact(requestId: String, contactJson: String)
```

The system picker (no permission needed) and writing a new contact (WRITE_CONTACTS). The read side - listing and searching - is under the Contacts group above.

_Described by its group, Contacts: pick and add, rather than on its own._

**Example**

Saves a new contact straight into the phone's address book, without opening the Contacts app.

**Returns:** nothing now. The answer is the `appmint:contacts` event (and `window.onAppMintContacts`) with `detail = { requestId, ok: true, id: '123' }` on success, `{ requestId, ok: false, id: null }` when the save failed, or `{ requestId, error }`. **Needs:** turn on **Edit Contacts** in Step 4 (Access) when you build; the user is asked for permission on the first call.

**Save a customer.** The contact is passed as a JSON string.

```js
function saveCustomer(name, phone, email) {
  if (!(window.WebToApk && window.WebToApk.addContact)) return;
  var id = 'add-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error === 'not_enabled') { showMessage('Turn on Edit Contacts when you build'); return; }
    if (e.detail.error) { showMessage('Not saved: ' + e.detail.error); return; }
    showMessage(e.detail.ok ? 'Saved to contacts' : 'Could not save the contact');
  });

  WebToApk.addContact(id, JSON.stringify({
    displayName: name,
    phones: [{ number: phone, type: 'mobile' }],   // type: mobile | home | work
    emails: [email]
  }));
}
```

**Short form.** Single values are accepted too: `{ name: 'Ali', phone: '+911234567890', email: 'ali@example.com' }`.

**Notes:** Errors: `not_enabled`, `permission_denied`. The returned `id` is the new raw-contact id. A phone `type` other than `home` or `work` is saved as `mobile`. The contact is saved on the device (no account). Writing contacts is a sensitive permission: your Play Store listing must declare it in the Data safety form and privacy policy.

### `getContact`

```js
window.WebToApk.getContact(requestId: String, contactId: String)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

**Example**

Reads one contact again by its `id` (the `id` from `listContacts` or `searchContacts`).

**Returns:** nothing now. The answer is the `appmint:contacts` event (and `window.onAppMintContacts`) with `detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }`. `contact` is `null` when that contact no longer exists. On failure: `{ requestId, error }`. **Needs:** turn on **Contacts** in Step 4 (Access) when you build.

**Refresh a saved contact** using the page hook instead of an event listener.

```js
var pendingId = '';

window.onAppMintContacts = function (detail) {
  if (detail.requestId !== pendingId) return;
  if (detail.error) { showMessage('Could not read the contact: ' + detail.error); return; }
  if (!detail.contact) { showMessage('This contact was deleted.'); return; }

  var c = detail.contact;
  document.getElementById('name').textContent = c.displayName;
  document.getElementById('phones').textContent =
    c.phones.map(function (p) { return p.type + ': ' + p.number; }).join(', ');
  document.getElementById('emails').textContent = c.emails.join(', ');
};

function openContact(contactId) {
  if (!(window.WebToApk && window.WebToApk.getContact)) return;
  pendingId = 'one-' + Date.now();
  WebToApk.getContact(pendingId, String(contactId));
}
```

**Notes:** `window.onAppMintContacts` receives every contacts answer (list, search, pick, add), so always check `requestId`. You can use it or `addEventListener('appmint:contacts', ...)` - both fire. Errors: `not_enabled`, `permission_denied`, `foreign_origin`. The `id` from `pickContact` is a phone-row id, not a contact id, so it does not work here. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

### `listContacts`

```js
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

**Example**

Reads the phone's address book, page by page, sorted A to Z by name.

**Returns:** nothing now. The answer comes later as the `appmint:contacts` event (and `window.onAppMintContacts`) with `detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['a@b.com'] }] }`, or `{ requestId, error }`. **Needs:** turn on **Contacts** in Step 4 (Access) when you build; the user is asked for permission on the first call.

**Load the first 100 contacts.** Match the answer on your own `requestId`.

```js
function loadContacts() {
  if (!(window.WebToApk && window.WebToApk.listContacts)) {
    showMessage('Contacts work only inside the app.');
    return;
  }
  var id = 'contacts-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;   // another call's answer
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error) { showMessage('Could not read contacts: ' + e.detail.error); return; }

    var list = document.getElementById('list');
    list.innerHTML = '';
    e.detail.contacts.forEach(function (c) {
      var li = document.createElement('li');
      var num = c.phones.length ? c.phones[0].number + ' (' + c.phones[0].type + ')' : '';
      li.textContent = c.displayName + ' ' + num;
      list.appendChild(li);
    });
  });

  WebToApk.listContacts(id, 100, 0);   // limit 100, start at 0
}
```

**Next page.** Pass the number you already have as `offset`: `WebToApk.listContacts(id, 100, 100)`.

**Notes:** `limit` is clamped to 1-2000. Errors: `not_enabled` (Contacts was off at build time), `permission_denied` (the user said No), `foreign_origin` (called from a page that is not your app's own). Phone `type` is `home`, `mobile`, `work`, `main`, `work_fax`, `home_fax` or `other`. `photoUri` is a `content://` address, not an image URL a page can load. To let the user choose just one person, use `pickContact` instead - it needs no permission. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

### `pickContact`

```js
window.WebToApk.pickContact(requestId: String)
```

System contact picker - needs NO permission, and is the right choice whenever the page just wants the user to choose one person. Picks from the PHONE table, not the contact table: the picker grants read access only to the row it returns, so picking a phone row is what makes the number readable without READ_CONTACTS. Picking a bare contact would return a row with a name and no number, and reading the number from it would need the permission this method exists to avoid.

**Example**

Opens Android's own contact picker so the user chooses one phone number. No permission is needed.

**Returns:** nothing now. The answer is the `appmint:contacts` event (and `window.onAppMintContacts`) with `detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }`. If the user closes the picker: `{ requestId, error: 'cancelled' }`. **Needs:** nothing - no build switch, no permission prompt.

**Fill a form with the chosen person.**

```js
function chooseContact() {
  if (!(window.WebToApk && window.WebToApk.pickContact)) {
    showMessage('Open this page in the app to pick a contact.');
    return;
  }
  var id = 'pick-' + Date.now();

  window.addEventListener('appmint:contacts', function handler(e) {
    if (!e.detail || e.detail.requestId !== id) return;
    window.removeEventListener('appmint:contacts', handler);

    if (e.detail.error === 'cancelled') return;              // user closed the picker
    if (e.detail.error) { showMessage('Could not open contacts'); return; }
    if (!e.detail.contact) { showMessage('Could not read that contact'); return; }

    var c = e.detail.contact;
    document.getElementById('name').value = c.displayName;
    document.getElementById('phone').value = c.phones.length ? c.phones[0].number : '';
  });

  WebToApk.pickContact(id);
}
```

**Notes:** You get the name and the ONE number the user tapped - never emails, never the rest of the book. `emails` is always empty. Errors: `cancelled`, `no_picker` (no contacts app on the phone). If the read fails after a pick, `contact` is `null`. Because nothing is read without the user's choice, this is the Play-friendly way to get a phone number; prefer it over `listContacts` when you only need one person. For AI-built apps and Google AI Studio imports the build scans your code; a `pickContact(` call does not tick **Contacts** - only `listContacts`, `searchContacts` and `getContact` do - so the picker adds no contacts-read permission.

### `searchContacts`

```js
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
```

All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way.    WebToApk.listContacts(id, limit, offset)   WebToApk.searchContacts(id, query, limit)   WebToApk.getContact(id, contactId)   WebToApk.pickContact(id)      <- NO permission needed (system picker)   WebToApk.addContact(id, json) <- needs the Contacts-write permission

_Described by its group, Contacts, rather than on its own._

**Example**

Finds contacts whose name matches a search word, without loading the whole address book.

**Returns:** nothing now. The answer is the `appmint:contacts` event (and `window.onAppMintContacts`) with `detail = { requestId, contacts: [...] }` - the same contact shape as `listContacts` - or `{ requestId, error }`. **Needs:** turn on **Contacts** in Step 4 (Access) when you build; the user is asked for permission on the first call.

**Search as the user types.** Only the answer to the latest search is shown.

```js
var lastSearchId = '';

function searchPeople(word) {
  if (!(window.WebToApk && window.WebToApk.searchContacts)) return;
  lastSearchId = 'search-' + Date.now();
  WebToApk.searchContacts(lastSearchId, word, 20);   // at most 20 results
}

window.addEventListener('appmint:contacts', function (e) {
  if (!e.detail || e.detail.requestId !== lastSearchId) return;   // old or other request
  if (e.detail.error) { showMessage('Search failed: ' + e.detail.error); return; }

  var names = e.detail.contacts.map(function (c) {
    return c.displayName + (c.phones.length ? ' - ' + c.phones[0].number : '');
  });
  document.getElementById('results').textContent = names.join('\n') || 'No match';
});

document.getElementById('q').addEventListener('input', function (e) {
  searchPeople(e.target.value);
});
```

**Notes:** An empty search word returns contacts from the start of the list (like `listContacts`). `limit` is clamped to 1-2000 and there is no offset. Errors: `not_enabled`, `permission_denied`, `foreign_origin`. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.

