JavaScript bridge API
NFC
Reading a tag through window.AndroidNFC, and the write side behind the NDEFReader polyfill.
NDEFReader web standard#
new NDEFReader() — Web NFC
Example
Standard Web NFC: new NDEFReader() reads, writes and locks NFC tags in the installed app, with the same code Chrome on Android accepts.
Returns: Promises and reading / readingerror events, as in the web standard. Records are NDEFRecords: recordType ('text', 'url', 'absolute-url', 'mime', 'smart-poster', 'empty', 'unknown' or an external type such as 'example.com:thing'), mediaType (mime records), id, encoding and lang (text records) and data as a DataView. Needs: turn on Web NFC Support in Step 4 (Access) when you build. Without it NDEFReader does not exist. No runtime permission prompt; navigator.permissions.query({name:'nfc'}) answers granted.
Read tags. Decode data with TextDecoder, exactly as in 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
});Write text or a link. The Promise resolves after the user taps a tag:
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
}
}Lock a tag for ever. makeReadOnly() cannot be undone - ask first:
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); }
}Notes: Record types for write: text (string, or bytes with encoding), url and absolute-url (string), mime (bytes + mediaType), smart-poster ({records: [...]}), an external type 'domain:thing' (bytes or {records: [...]}), unknown (bytes) and empty. A plain string writes a text record; plain bytes write an application/octet-stream mime record. Only readers that called scan() get reading events; AbortController.abort() on the signal stops that reader (there is no stop() in the standard). readingerror fires for a tag that cannot hold NFC data. The phone waits for ONE tag operation at a time: a new write() or makeReadOnly() while one waits replaces it, and the replaced call rejects with AbortError (Chrome would queue it). A signal on write() / makeReadOnly() cancels the wait. Errors: NotSupportedError (no NFC, or the tag cannot hold NDEF, or it is too small), NotReadableError (NFC switched off), NotAllowedError (read-only tag, or overwrite: false on a tag with records), NetworkError (the tag moved away), TypeError (a record the standard does not allow). NDEFReader exists in the main page, not inside iframes.
__nfcCancelWrite bridge#
window.WebToApk.__nfcCancelWrite()
Cancels the waiting write/lock (the polyfill calls it when the operation's `signal` aborts).
Example
Drops a queued NFC write or lock so the next tag tap is read normally. Pages cancel through the web standard - an AbortController signal - and the NDEFReader polyfill calls this for them.
Returns: nothing. Needs: Web NFC Support turned on in Step 4 (Access) when you build.
The standard way, which the app honours - the waiting write() rejects with 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();
});Give up by itself when no tag arrives in time; a timer that aborts the signal cancels the same way:
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);
}
}Cancel a waiting lock (or write) when the user leaves the screen, so a later tap does not change a tag:
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; });
});Notes: Do not call WebToApk.__nfcCancelWrite() yourself: called directly it drops the native wait but cannot settle the page's Promise. The signal does both. Cancelling also drops a queued makeReadOnly().
__nfcMakeReadOnly bridge#
window.WebToApk.__nfcMakeReadOnly(): String
Queues a permanent lock (`NDEFReader.makeReadOnly()`) for the next tag tap; returns its op id, "" when NFC is off in this app.
Example
Internal transport behind NDEFReader.makeReadOnly(). Pages use new NDEFReader().makeReadOnly(); never call this directly.
Returns: (through makeReadOnly) a Promise that resolves after the user taps a tag and it is locked, or rejects with a DOMException. Needs: Web NFC Support turned on in Step 4 (Access) when you build.
Locking is permanent. Write the tag first, then lock it, and ask the user before both:
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);
}
});Lock a tag that is already written, and give up if no tag arrives within 20 seconds:
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);
}
}Read a tag first and only offer to lock it when it holds the expected link:
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); }
};
}Notes: Each step needs its own tap. The call returns an operation id and the result arrives through window.__appmintNfcDone. Rejections: NotSupportedError "Tag does not support NDEF", NotAllowedError "This tag cannot be made read-only" (many tags cannot be locked), NetworkError when the tag moves away. Starting a write() while a lock waits replaces the lock, which then rejects with AbortError. A signal passed to makeReadOnly({ signal }) cancels the wait.
__nfcState bridge#
window.WebToApk.__nfcState(): String
"on", "off" (NFC switched off in the phone's settings) or "none" (no NFC, or not enabled in this app) - what `NDEFReader.scan()` checks first.
Example
Internal check behind NDEFReader.scan(): whether this app can use NFC right now. Pages call scan() and read its rejection instead.
Returns: 'on', 'off' (NFC is switched off in the phone's settings) or 'none' (the phone has no NFC, or the app was built without Web NFC Support). Needs: Web NFC Support turned on in Step 4 (Access) when you build.
The standard way - scan() rejects with the same answer:
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);
}
}Check once when the screen opens, to choose between the NFC panel, a "turn on NFC" hint and hiding NFC altogether. Aborting the probe's scan right away leaves nothing running:
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
});Read tags only while one screen is open, and tell an empty or unreadable tag apart from NFC being off:
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; }
}Notes: Plumbing for the polyfill; use scan(). window.AndroidNFC.startScan() answers the same question as a boolean and shows a "Please enable NFC" message when NFC is off.
__nfcWrite bridge#
window.WebToApk.__nfcWrite(messageJson: String): String
Queues [messageJson] (`{records:[…], overwrite}`, built by `NDEFReader.write()`) for the next tag tap and returns its op id; "" when NFC is not enabled in this app.
Example
Internal transport behind NDEFReader.write(message). Pages use new NDEFReader().write(...); never call this directly.
Returns: (through write) a Promise that resolves after the user taps a tag and it is written, or rejects with a DOMException. Needs: Web NFC Support turned on in Step 4 (Access) when you build.
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);
}
});Write only to a blank tag, so a tag that already holds data is never replaced by mistake:
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);
}
}Write a smart poster, a link together with its title, as one record:
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);
}
});Notes: The polyfill checks the records, turns them into {records:[{recordType, id, data(base64), encoding, lang, mediaType, records?}], overwrite} and passes the JSON string here. The call returns an operation id; the result comes back to the page through window.__appmintNfcDone(id, errorName, message), so an answer for a replaced write never settles a newer one. Unformatted tags are formatted first. Rejections: NotAllowedError ("Tag is read-only", or a tag with records when overwrite is false), NotSupportedError ("Tag does not support NDEF", "Tag too small (N bytes available)"), NetworkError (the tag moved away), SyntaxError (a record the tag format cannot hold). The app also shows its own short "Ready to write" and "NFC tag written" messages.
startScan bridge#
window.AndroidNFC.startScan(): Boolean
Legacy availability probe, present only when the creator enabled NFC. Real NFC work goes through the standard NDEFReader polyfill (window.WebToApk.__nfc*), which is what a page should use; this only reports whether the adapter is on.
Described by its group, Web NFC, rather than on its own.
Example
Tells you whether the phone's NFC is switched on. It lives on window.AndroidNFC, not on window.WebToApk, and it does not read a tag - reading is done with NDEFReader.
Returns: true (synchronously) when the phone has NFC and it is on; false when the phone has no NFC, or when NFC is off (then the app also shows a short "Please enable NFC" message). Needs: Web NFC Support turned on in Step 4 (Access) when you build; without it window.AndroidNFC does not exist.
Check before showing an NFC button:
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.');
});Check right before a write: with NFC switched off the write would wait for a tap that never comes, while startScan() answers at once and shows the app's "Please enable NFC" message:
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);
}
});Re-check when the user comes back and taps "I've turned it on":
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.');
}
});Notes: window.AndroidNFC exists from the first script on the page. After the user turns NFC on in settings and comes back, tag reading starts by itself.