JavaScript bridge API
NFC
Чтение метки через window.AndroidNFC и сторона записи за полифилом NDEFReader.
NDEFReader веб-стандарт#
new NDEFReader() — Web NFC
Пример
Стандартный Web NFC: new NDEFReader() читает, записывает и блокирует NFC-метки в установленном приложении тем же кодом, который принимает Chrome на Android.
Возвращает: Promise и события reading / readingerror, как в веб-стандарте. Записи — это NDEFRecord: recordType ('text', 'url', 'absolute-url', 'mime', 'smart-poster', 'empty', 'unknown' или внешний тип, например 'example.com:thing'), mediaType (mime-записи), id, encoding и lang (текстовые записи) и data как DataView. Требуется: включите Web NFC Support на шаге 4 (Доступ) при сборке. Без этого NDEFReader не существует. Запроса разрешения во время работы нет; navigator.permissions.query({name:'nfc'}) отвечает granted.
Чтение меток. Декодируйте data с помощью TextDecoder, точно как в Chrome:
function describe(record) {
if (record.recordType === 'text') return new TextDecoder(record.encoding).decode(record.data);
if (record.recordType === 'url' || record.recordType === 'absolute-url') return new TextDecoder().decode(record.data);
if (record.recordType === 'mime') return record.mediaType + ' (' + record.data.byteLength + ' bytes)';
return record.recordType;
}
var scanStop = null;
document.getElementById('read').addEventListener('click', async function () {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
var reader = new NDEFReader();
scanStop = new AbortController();
reader.onreading = function (e) {
showStatus('Tag ' + e.serialNumber + ': ' + e.message.records.map(describe).join(' | '));
};
reader.onreadingerror = function () { showStatus('That tag holds no NFC data this phone can read.'); };
try {
await reader.scan({ signal: scanStop.signal });
showStatus('Hold a tag to the back of the phone.');
} catch (err) {
showStatus('NFC error: ' + err.name + ' ' + err.message); // NotReadableError = NFC switched off
}
});
document.getElementById('stop').addEventListener('click', function () {
if (scanStop) scanStop.abort(); // the standard way to stop scanning
});Запись текста или ссылки. Promise завершается после того, как пользователь коснулся метки:
async function writeTag(kind, value) {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
var reader = new NDEFReader();
showStatus('Tap a tag to write it...');
try {
if (kind === 'text') await reader.write(value); // same as { records: [{ recordType: 'text', data: value }] }
else await reader.write({ records: [{ recordType: 'url', data: value }] }, { overwrite: false });
showStatus('Tag written.');
} catch (err) {
showStatus('Write failed: ' + err.name + ' ' + err.message); // NotAllowedError: read-only, or overwrite:false on a used tag
}
}Блокировка метки навсегда. makeReadOnly() нельзя отменить — сначала спросите пользователя:
async function lockTag() {
if (!('NDEFReader' in window)) return;
if (!confirm('Lock the next tag you tap? It can never be changed again.')) return;
try { await new NDEFReader().makeReadOnly(); showStatus('Tag locked.'); }
catch (err) { showStatus('Lock failed: ' + err.message); }
}Примечания: Типы записей для write: text (строка или байты с encoding), url и absolute-url (строка), mime (байты + mediaType), smart-poster ({records: [...]}), внешний тип 'domain:thing' (байты или {records: [...]}), unknown (байты) и empty. Обычная строка записывает текстовую запись; обычные байты — mime-запись application/octet-stream. События reading получают только читатели, вызвавшие scan(); AbortController.abort() на signal останавливает этого читателя (в стандарте нет stop()). readingerror срабатывает для метки, которая не может хранить данные NFC. Телефон ждёт ОДНУ операцию с меткой за раз: новый write() или makeReadOnly() во время ожидания заменяет предыдущий, а замещённый вызов отклоняется с AbortError (Chrome поставил бы его в очередь). signal на write() / makeReadOnly() отменяет ожидание. Ошибки: NotSupportedError (нет NFC, или метка не может хранить NDEF, или она слишком мала), NotReadableError (NFC выключен), NotAllowedError (метка только для чтения или overwrite: false на метке с записями), NetworkError (метку убрали), TypeError (запись, которую стандарт не допускает). NDEFReader существует на главной странице, но не внутри iframe.
__nfcCancelWrite мост#
window.WebToApk.__nfcCancelWrite()
Отменяет ожидающую запись/блокировку (полифил вызывает это, когда срабатывает `signal` операции).
Пример
Сбрасывает поставленную в очередь запись или блокировку NFC, чтобы следующее касание метки обрабатывалось как обычное чтение. Страницы отменяют через веб-стандарт — AbortController signal — и полифил NDEFReader вызывает это за них.
Возвращает: ничего. Требуется: включённая опция Web NFC Support на шаге 4 (Доступ) при сборке.
Стандартный способ, который приложение учитывает — ожидающий write() отклоняется с AbortError:
var controller = null;
async function writeWithCancel(text) {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
controller = new AbortController();
showStatus('Tap a tag to write it... (or press Cancel)');
try {
await new NDEFReader().write(text, { signal: controller.signal });
showStatus('Tag written.');
} catch (err) {
showStatus(err.name === 'AbortError' ? 'Cancelled.' : 'Write failed: ' + err.message);
}
}
document.getElementById('cancel').addEventListener('click', function () {
if (controller) controller.abort();
});Откажитесь сами, если метка не появилась вовремя; таймер, прерывающий signal, отменяет так же:
async function writeWithTimeout(url, ms) {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
var controller = new AbortController();
var timer = setTimeout(function () { controller.abort(); }, ms);
showStatus('Tap a tag within ' + Math.round(ms / 1000) + ' seconds...');
try {
await new NDEFReader().write({ records: [{ recordType: 'url', data: url }] }, { signal: controller.signal });
showStatus('Tag written.');
} catch (err) {
showStatus(err.name === 'AbortError' ? 'Timed out. Tap Write to try again.' : 'Write failed: ' + err.message);
} finally {
clearTimeout(timer);
}
}Отмените ожидающую блокировку (или запись), когда пользователь уходит с экрана, чтобы позднее касание не изменило метку:
var pendingTagOp = null;
window.addEventListener('hashchange', function () {
if (pendingTagOp) { pendingTagOp.abort(); pendingTagOp = null; }
});
document.getElementById('lock').addEventListener('click', function () {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
if (pendingTagOp) pendingTagOp.abort();
var controller = pendingTagOp = new AbortController();
showStatus('Tap the tag to lock it...');
new NDEFReader().makeReadOnly({ signal: controller.signal })
.then(function () { showStatus('Tag locked.'); })
.catch(function (err) { if (err.name !== 'AbortError') showStatus('Lock failed: ' + err.message); })
.finally(function () { if (pendingTagOp === controller) pendingTagOp = null; });
});Примечания: Не вызывайте WebToApk.__nfcCancelWrite() сами: при прямом вызове нативное ожидание сбрасывается, но Promise страницы не завершается. signal делает и то и другое. Отмена также сбрасывает поставленную в очередь makeReadOnly().
__nfcMakeReadOnly мост#
window.WebToApk.__nfcMakeReadOnly(): String
Ставит в очередь постоянную блокировку (`NDEFReader.makeReadOnly()`) для следующего касания метки; возвращает id операции, "" — если NFC в этом приложении отключён.
Пример
Внутренний транспорт за NDEFReader.makeReadOnly(). Страницы используют new NDEFReader().makeReadOnly(); никогда не вызывайте это напрямую.
Возвращает: (через makeReadOnly) Promise, который завершается после того, как пользователь коснулся метки и она заблокирована, либо отклоняется с DOMException. Требуется: включённая опция Web NFC Support на шаге 4 (Доступ) при сборке.
Блокировка необратима. Сначала запишите метку, затем заблокируйте её и спросите пользователя перед обоими действиями:
document.getElementById('publish-tag').addEventListener('click', async function () {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
if (!confirm('Write and lock this tag? A locked tag can never be changed.')) return;
var reader = new NDEFReader();
try {
showStatus('Tap the tag to write it...');
await reader.write({ records: [{ recordType: 'url', data: 'https://example.com/item/42' }] });
showStatus('Now tap the same tag again to lock it...');
await reader.makeReadOnly();
showStatus('Tag written and locked.');
} catch (err) {
showStatus('Failed: ' + err.message);
}
});Заблокируйте уже записанную метку и откажитесь, если метка не появилась в течение 20 секунд:
async function lockTag() {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
if (!confirm('Lock this tag for good? It can never be changed again.')) return;
var controller = new AbortController();
var timer = setTimeout(function () { controller.abort(); }, 20000);
showStatus('Tap the tag to lock it...');
try {
await new NDEFReader().makeReadOnly({ signal: controller.signal });
showStatus('Tag locked.');
} catch (err) {
if (err.name === 'AbortError') showStatus('No tag was tapped. Try again when ready.');
else if (err.name === 'NotAllowedError') showStatus('This kind of tag cannot be locked.');
else showStatus('Lock failed: ' + err.message);
} finally {
clearTimeout(timer);
}
}Сначала прочитайте метку и предлагайте заблокировать её, только если на ней нужная ссылка:
async function checkThenLock(expectedUrl) {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
var reader = new NDEFReader();
var scanning = new AbortController();
var lockBtn = document.getElementById('lock-btn');
reader.onreading = function (e) {
var rec = e.message.records.find(function (r) { return r.recordType === 'url'; });
var url = rec ? new TextDecoder().decode(rec.data) : null;
showStatus(url ? 'Tag points to ' + url : 'This tag holds no link.');
lockBtn.disabled = url !== expectedUrl;
};
await reader.scan({ signal: scanning.signal });
showStatus('Hold the tag to the phone to check it.');
lockBtn.onclick = async function () {
if (!confirm('Lock this tag? It can never be changed again.')) return;
scanning.abort();
showStatus('Tap the tag again to lock it...');
try { await reader.makeReadOnly(); showStatus('Tag locked.'); }
catch (err) { showStatus('Lock failed: ' + err.message); }
};
}Примечания: Для каждого шага нужно отдельное касание. Вызов возвращает id операции, а результат приходит через window.__appmintNfcDone. Отказы: NotSupportedError «Tag does not support NDEF», NotAllowedError «This tag cannot be made read-only» (многие метки заблокировать нельзя), NetworkError — когда метку убрали. Если запустить write(), пока ожидается блокировка, блокировка заменяется и отклоняется с AbortError. signal, переданный в makeReadOnly({ signal }), отменяет ожидание.
__nfcState мост#
window.WebToApk.__nfcState(): String
"on", "off" (NFC выключен в настройках телефона) или "none" (нет NFC или он не включён в этом приложении) — именно это сначала проверяет `NDEFReader.scan()`.
Пример
Внутренняя проверка за NDEFReader.scan(): может ли это приложение использовать NFC прямо сейчас. Страницы вызывают scan() и читают его отказ.
Возвращает: 'on', 'off' (NFC выключен в настройках телефона) или 'none' (в телефоне нет NFC или приложение собрано без Web NFC Support). Требуется: включённая опция Web NFC Support на шаге 4 (Доступ) при сборке.
Стандартный способ — scan() отклоняется с тем же ответом:
async function startReading() {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
try {
await new NDEFReader().scan();
showStatus('Hold a tag to the back of the phone.');
} catch (err) {
if (err.name === 'NotReadableError') showStatus('Turn on NFC in the phone settings, then try again.');
else if (err.name === 'NotSupportedError') showStatus('This phone has no NFC.');
else showStatus('NFC error: ' + err.message);
}
}Проверьте один раз при открытии экрана, чтобы выбрать между панелью NFC, подсказкой «включите NFC» и полным скрытием NFC. Если сразу прервать сканирование проверки, ничего не остаётся запущенным:
async function nfcStatus() {
if (!('NDEFReader' in window)) return 'none';
var probe = new AbortController();
try {
await new NDEFReader().scan({ signal: probe.signal });
return 'on';
} catch (err) {
return err.name === 'NotReadableError' ? 'off' : 'none';
} finally {
probe.abort();
}
}
async function refreshNfcUi() {
var state = await nfcStatus();
document.getElementById('nfc-panel').hidden = state !== 'on';
document.getElementById('nfc-off-hint').hidden = state !== 'off';
}
refreshNfcUi();
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') refreshNfcUi(); // back from the phone settings
});Читайте метки, только пока открыт один экран, и отличайте пустую или нечитаемую метку от выключенного NFC:
var tagScreen = null;
async function openTagScreen() {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
tagScreen = new AbortController();
var reader = new NDEFReader();
reader.onreading = function (e) {
var n = e.message.records.length;
showStatus(n ? 'Tag ' + e.serialNumber + ' holds ' + n + ' record(s).' : 'This tag is empty.');
};
reader.onreadingerror = function () { showStatus('This tag cannot be read. Try another one.'); };
try {
await reader.scan({ signal: tagScreen.signal });
showStatus('Hold a tag to the back of the phone.');
} catch (err) {
showStatus(err.name === 'NotReadableError' ? 'NFC is off. Turn it on in the phone settings.' : 'NFC error: ' + err.message);
}
}
function closeTagScreen() {
if (tagScreen) { tagScreen.abort(); tagScreen = null; }
}Примечания: Служебный механизм для полифила; используйте scan(). window.AndroidNFC.startScan() отвечает на тот же вопрос логическим значением и показывает сообщение «Включите NFC», если NFC выключен.
__nfcWrite мост#
window.WebToApk.__nfcWrite(messageJson: String): String
Ставит в очередь [messageJson] (`{records:[…], overwrite}`, созданный `NDEFReader.write()`) для следующего касания метки и возвращает id операции; "" — если NFC в этом приложении не включён.
Пример
Внутренний транспорт за NDEFReader.write(message). Страницы используют new NDEFReader().write(...); никогда не вызывайте это напрямую.
Возвращает: (через write) Promise, который завершается после того, как пользователь коснулся метки и запись выполнена, либо отклоняется с DOMException. Требуется: включённая опция Web NFC Support на шаге 4 (Доступ) при сборке.
document.getElementById('write-table-tag').addEventListener('click', async function () {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
showStatus('Tap a tag to write it...');
try {
await new NDEFReader().write({
records: [
{ recordType: 'url', data: 'https://example.com/menu' },
{ recordType: 'text', data: 'Table 12', lang: 'en' },
{ recordType: 'mime', mediaType: 'application/json',
data: new TextEncoder().encode(JSON.stringify({ table: 12 })) }
]
});
showStatus('Tag written.');
} catch (err) {
showStatus('Write failed: ' + err.message);
}
});Записывайте только на пустую метку, чтобы метка с данными случайно не была перезаписана:
async function writeToBlankTag(url) {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
showStatus('Tap a blank tag...');
try {
await new NDEFReader().write(
{ records: [{ recordType: 'url', data: url }] },
{ overwrite: false }
);
showStatus('Tag written.');
} catch (err) {
if (err.name === 'NotAllowedError') showStatus('That tag already holds data or is locked. Use a blank tag.');
else if (err.name === 'NetworkError') showStatus('Hold the tag still until it is written.');
else showStatus('Write failed: ' + err.message);
}
}Запишите smart poster — ссылку вместе с её названием — одной записью:
document.getElementById('write-poster').addEventListener('click', async function () {
if (!('NDEFReader' in window)) { showStatus('NFC is not available in this app.'); return; }
showStatus('Tap a tag to write the poster...');
try {
await new NDEFReader().write({
records: [{
recordType: 'smart-poster',
data: { records: [
{ recordType: 'url', data: 'https://example.com/events/spring-fair' },
{ recordType: 'text', data: 'Spring Fair tickets', lang: 'en' }
] }
}]
});
showStatus('Poster tag written.');
} catch (err) {
showStatus(err.name === 'NotSupportedError' ? 'This tag is too small or cannot hold NFC data.' : 'Write failed: ' + err.message);
}
});Примечания: Полифил проверяет записи, превращает их в {records:[{recordType, id, data(base64), encoding, lang, mediaType, records?}], overwrite} и передаёт сюда строку JSON. Вызов возвращает id операции; результат возвращается на страницу через window.__appmintNfcDone(id, errorName, message), поэтому ответ для замещённой записи никогда не завершит более новую. Неформатированные метки сначала форматируются. Отказы: NotAllowedError («Tag is read-only» или метка с записями, когда overwrite равно false), NotSupportedError («Tag does not support NDEF», «Tag too small (N bytes available)»), NetworkError (метку убрали), SyntaxError (запись, которую формат метки вместить не может). Приложение также показывает собственные короткие сообщения «Ready to write» и «NFC tag written».
startScan мост#
window.AndroidNFC.startScan(): Boolean
Устаревшая проверка доступности, присутствует только если автор включил NFC. Настоящая работа с NFC идёт через стандартный полифил NDEFReader (window.WebToApk.__nfc*), который и должна использовать страница; эта функция лишь сообщает, включён ли адаптер.
Описано в своей группе, Web NFC, а не отдельно.
Пример
Сообщает, включён ли NFC в телефоне. Он находится в window.AndroidNFC, а не в window.WebToApk, и не читает метку — чтение выполняется с помощью NDEFReader.
Возвращает: true (синхронно), когда в телефоне есть NFC и он включён; false — когда в телефоне нет NFC или NFC выключен (тогда приложение также показывает короткое сообщение «Включите NFC»). Требуется: включённая опция Web NFC Support на шаге 4 (Доступ) при сборке; без неё window.AndroidNFC не существует.
Проверьте перед показом кнопки NFC:
function nfcReady() {
if (!window.AndroidNFC || typeof window.AndroidNFC.startScan !== 'function') return false;
return window.AndroidNFC.startScan();
}
document.getElementById('scan-tag').addEventListener('click', async function () {
if (!nfcReady()) { showStatus('Turn on NFC in the phone settings, then try again.'); return; }
var reader = new NDEFReader();
reader.addEventListener('reading', function (e) { showStatus('Tag ' + e.serialNumber); });
await reader.scan();
showStatus('Hold a tag to the back of the phone.');
});Проверяйте прямо перед записью: при выключенном NFC запись будет ждать касания, которого не будет, тогда как startScan() отвечает сразу и показывает сообщение приложения «Включите NFC»:
document.getElementById('write-pass').addEventListener('click', async function () {
if (!window.AndroidNFC || typeof window.AndroidNFC.startScan !== 'function') {
showStatus('NFC is not available in this app.');
return;
}
if (!window.AndroidNFC.startScan()) { showStatus('NFC is off, or this phone has no NFC.'); return; }
showStatus('Tap a tag to write it...');
try {
await new NDEFReader().write('Visitor pass #1042');
showStatus('Tag written.');
} catch (err) {
showStatus('Write failed: ' + err.message);
}
});Проверьте снова, когда пользователь вернётся и нажмёт «Я включил»:
var retry = document.getElementById('nfc-retry');
retry.addEventListener('click', function () {
if (!window.AndroidNFC || typeof window.AndroidNFC.startScan !== 'function') {
showStatus('NFC is not available in this app.');
return;
}
if (window.AndroidNFC.startScan()) {
retry.hidden = true;
document.getElementById('nfc-panel').hidden = false;
startReading(); // your own NDEFReader.scan() code
} else {
showStatus('NFC is still off, or this phone has no NFC.');
}
});Примечания: window.AndroidNFC существует с первого скрипта на странице. Когда пользователь включит NFC в настройках и вернётся, чтение меток начнётся само.