Build APK + AAB

JavaScript bridge API

QR and barcode scanner

Google’s scanner screen from one promise, AppMint.scanCode() - no camera permission, and the parsed URL, Wi-Fi, phone or contact for common QR codes.

AppMint.scanCode helper#

AppMint.scanCode(options)

Example

Opens Google's QR code and barcode scanner screen and gives you what was scanned. One call - no camera permission, no <video>, no UI of your own.

Returns: a Promise that always resolves: {ok: true, value, displayValue, format, type, …} or {ok: false, code, message}. Needs: nothing to switch on. The phone needs Google Play services.

Any code:

async function scan() {
  if (!(window.AppMint && AppMint.scanCode)) { alert('Open this page in the app to scan'); return; }
  var r = await AppMint.scanCode();
  if (!r.ok) {
    if (r.code === 'cancelled') return;                          // the user closed the scanner
    if (r.code === 'unavailable') alert('The scanner is not available on this phone');
    else if (r.code === 'busy') alert('A scan is already open');
    else alert('Scan failed: ' + r.message);                     // 'failed'
    return;
  }
  console.log(r.value, r.format, r.type);   // "TICKET-4821", "qr_code", "text"
}

Only some formats, and a "type it in" option for damaged labels:

var r = await AppMint.scanCode({
  formats: ['ean_13', 'ean_8', 'upc_a', 'upc_e'],   // shop barcodes
  manualInput: true,                                // lets the user type the code
  autoZoom: true                                    // default true
});
if (r.ok) lookUpProduct(r.value);

Ready-made parts - only the part that matches r.type is present:

var r = await AppMint.scanCode({ formats: ['qr_code'] });
if (r.ok) {
  if (r.type === 'url')     openLink(r.url.url);                      // url: { url, title }
  if (r.type === 'wifi')    showWifi(r.wifi.ssid, r.wifi.password, r.wifi.encryption); // 'wpa'|'wep'|'open'
  if (r.type === 'phone')   callNumber(r.phone);                      // a string
  if (r.type === 'email')   writeMail(r.email.address, r.email.subject, r.email.body);
  if (r.type === 'sms')     writeSms(r.sms.phoneNumber, r.sms.message);
  if (r.type === 'geo')     showMap(r.geo.lat, r.geo.lng);
  if (r.type === 'contact') addContact(r.contact.name, r.contact.organization, r.contact.phones, r.contact.emails);
}

Notes: Format names: qr_code, aztec, codabar, code_39, code_93, code_128, data_matrix, ean_8, ean_13, itf, pdf417, upc_a, upc_e (unknown names are ignored; none = all formats). type is one of url, wifi, text, phone, email, sms, geo, contact, calendar, isbn, product, driver_license, unknown. Error codes: cancelled (closed, including with Back), unavailable (no Play services, or the scanner could not be downloaded), busy (a scan is already open), failed. The first scan on a phone can take a few seconds while Play services downloads the scanner. AI-built apps use scanCode() from @/lib/appmintNative, which also scans on the web.

__ocrAvailable bridge#

window.WebToApk.__ocrAvailable(): Boolean

TextDetector / recognizeText(): Google Play services is here (the OCR module's home).

Example

Internal transport behind TextDetector: whether Google Play services (where the recognizer comes from) is on this phone. Pages use the standard API; never call this directly - the __ methods may change without notice.

Returns: true or false. When false, TextDetector is not installed at all. Needs: nothing.

Use the public API:

if ('TextDetector' in window) scanButton.hidden = false;

Choose between the scan flow and a typed form once, at start-up. When the phone has no Google Play services the detector is simply absent, so the page must offer another way in:

function setUpReceiptEntry() {
  var canScan = typeof window.TextDetector === 'function';
  document.getElementById('scan-receipt').hidden = !canScan;
  document.getElementById('type-receipt').hidden = canScan;
  if (!canScan) document.getElementById('entry-hint').textContent =
    'Text scanning is not available on this phone. Type the total below.';
}
document.addEventListener('DOMContentLoaded', setUpReceiptEntry);

A detector that exists can still answer that recognition is unavailable at the moment of the call. Tell the two failures apart by the error name:

async function readLabel(img) {
  if (typeof window.TextDetector !== 'function') return null;
  try {
    var lines = await new TextDetector().detect(img);
    return lines.map(function (l) { return l.rawValue; }).join('\n');
  } catch (e) {
    if (e.name === 'NotSupportedError') showMessage('Text recognition is not available right now.');
    else showMessage('Could not read the label: ' + e.message);
    return null;
  }
}

Notes: Implemented in the app shell (TextRecognitionBridge.kt).

__ocrRecognize bridge#

window.WebToApk.__ocrRecognize(requestId: String, base64: String)

Recognises the text in [base64] (PNG/JPEG); the answer arrives as `appmint:ocr {requestId, blocks|error}`.

Example

Internal transport behind TextDetector.detect(): recognises the text in a PNG/JPEG. Pages use the standard API; never call this directly - the __ methods may change without notice.

Returns: nothing; the answer arrives as the appmint:ocr event {requestId, blocks:[{text, box, corners, lines}]} or {requestId, error} (unavailable, bad-image, failed: …). Needs: nothing.

Use the public API:

const lines = await new TextDetector().detect(document.getElementById('receipt'));
console.log(lines.map((l) => l.rawValue));

Read the text of a photo the user picks. detect() accepts a Blob/File as well as an <img>, <canvas> or ImageData:

<input type="file" id="doc" accept="image/*" capture="environment">
<pre id="out"></pre>
<script>
document.getElementById('doc').addEventListener('change', async function (e) {
  var file = e.target.files[0];
  if (!file || typeof window.TextDetector !== 'function') return;
  try {
    var lines = await new TextDetector().detect(file);
    document.getElementById('out').textContent = lines.map(function (l) { return l.rawValue; }).join('\n');
  } catch (err) {
    document.getElementById('out').textContent = 'Could not read the photo (' + err.name + ')';
  }
});
</script>

Outline each recognised line on top of the image. boundingBox is in the image's own pixels, so scale it to the displayed size. The image must have finished loading, or detect() rejects with InvalidStateError:

async function highlightLines(img, overlay) {
  if (!img.complete) await new Promise(function (r) { img.onload = r; });
  var lines = await new TextDetector().detect(img);
  var sx = img.clientWidth / img.naturalWidth, sy = img.clientHeight / img.naturalHeight;
  overlay.innerHTML = '';
  lines.forEach(function (l) {
    var b = l.boundingBox, box = document.createElement('div');
    box.className = 'ocr-box';
    box.title = l.rawValue;
    box.style.cssText = 'position:absolute;left:' + b.x * sx + 'px;top:' + b.y * sy +
      'px;width:' + b.width * sx + 'px;height:' + b.height * sy + 'px';
    overlay.appendChild(box);
  });
}

Pull an amount out of a receipt. A cross-origin image that taints the canvas rejects with SecurityError, so serve the picture from the app's own origin:

async function findTotal(img) {
  try {
    var lines = await new TextDetector().detect(img);
    var hit = lines.find(function (l) { return /total/i.test(l.rawValue); });
    var m = hit && hit.rawValue.match(/(\d+[.,]\d{2})/);
    return m ? parseFloat(m[1].replace(',', '.')) : null;
  } catch (e) {
    if (e.name === 'SecurityError') console.warn('Image is from another origin; load it from the app instead.');
    else console.warn('OCR failed:', e.name, e.message);
    return null;
  }
}

Notes: Implemented in the app shell (TextRecognitionBridge.kt).

scanCode bridge#

window.WebToApk.scanCode(optionsJson: String, callbackId: String)

Opens Google's code scanner; [optionsJson] `{formats?, autoZoom?, manualInput?}`. Resolves the AppMint.scanCode promise [callbackId].

Example

Internal plumbing behind AppMint.scanCode() - pages use that Promise helper, which opens Google's QR/barcode scanner and resolves with the result.

Returns: nothing directly; the result resolves the AppMint.scanCode() promise. Needs: nothing to switch on (no camera permission). The phone needs Google Play services.

The public API:

async function scanTicket() {
  if (!(window.AppMint && AppMint.scanCode)) { alert('Open this page in the app to scan'); return; }
  var r = await AppMint.scanCode({ formats: ['qr_code'] });
  if (r.ok) checkTicket(r.value);                  // r.format, r.type, r.displayValue…
  else if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')');
}

Join a Wi-Fi network from a printed QR code. A Wi-Fi code comes back with type: 'wifi' and a wifi object:

async function scanWifi() {
  if (!(window.AppMint && AppMint.scanCode)) return;
  var r = await AppMint.scanCode({ formats: ['qr_code'] });
  if (!r.ok) { if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')'); return; }
  if (r.type !== 'wifi') { alert('That is not a Wi-Fi code.'); return; }
  showWifiCard(r.wifi.ssid, r.wifi.password, r.wifi.encryption);   // encryption: 'wpa' | 'wep' | 'open'
}

Look up a product by its shop barcode, letting the user type the digits when the label is damaged (manualInput). The first scan on a phone may download the scanner, which fails with unavailable when offline:

async function scanProduct() {
  if (!(window.AppMint && AppMint.scanCode)) { showManualEntry(); return; }
  var r = await AppMint.scanCode({ formats: ['ean_13', 'ean_8', 'upc_a', 'upc_e'], manualInput: true });
  if (r.ok) return lookUpProduct(r.value, r.format);            // e.g. '4006381333931', 'ean_13'
  if (r.code === 'busy') return;                                  // a scan is already open
  if (r.code === 'unavailable') alert('The scanner is not available: ' + r.message);
  else if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')');
}

Open whatever a QR code points at, by its decoded type:

async function scanAndOpen() {
  var r = await AppMint.scanCode({ formats: ['qr_code'], autoZoom: true });
  if (!r.ok) return;
  if (r.type === 'url') location.href = r.url.url;
  else if (r.type === 'phone') location.href = 'tel:' + r.phone;
  else if (r.type === 'email') location.href = 'mailto:' + r.email.address;
  else if (r.type === 'geo') showOnMap(r.geo.lat, r.geo.lng);
  else if (r.type === 'contact') saveContact(r.contact.name, r.contact.phones, r.contact.emails);
  else showText(r.displayValue);
}

Notes: The raw method takes optionsJson ({formats?, autoZoom?, manualInput?} as a JSON string) and a callbackId; the answer is delivered to the helper's internal reply table, so a page calling it directly gets no answer. Only one scan runs at a time - a second call answers code: 'busy'. See AppMint.scanCode for every result field and error code.

Generated from the app runtime and its example files on every docs build. Read it as Markdown · All families.

Checked against the shipped bridge on 2026-11-04.