Start here
Writing HTML for your app
What the app runtime exposes to your page - contacts, SMS, notifications, device info, biometrics - and the rules for calling it correctly
AppMintAppwrightBoth builders - the steps below are the same in each.
1Rule 1 - always check first#
window.WebToApk only exists inside your built app. In a browser it is missing.
So always check before you call. If you do not, your page breaks everywhere except inside the app.
Some things need no check at all: navigator.vibrate(), speechSynthesis and Notification already work - the app connects them for you. Just use the normal web code.
// GOOD — safe everywhere
if (window.WebToApk) {
WebToApk.playClick();
}
// BAD — this breaks your page in a browser
WebToApk.playClick();2Rule 2 - some answers come back later#
Reading contacts, SMS or the call log takes time. So these do not answer straight away. If they did, your app would freeze.
You give the call a name (an id). The answer arrives later as an event. You listen for that event.
Event names: appmint:contacts, appmint:calllog, appmint:sms, appmint:sms-received, appmint:notification-action.
// 1. Listen for the answer
window.addEventListener('appmint:contacts', function (e) {
if (e.detail.error) {
alert('Problem: ' + e.detail.error);
return;
}
console.log(e.detail.contacts);
});
// 2. Ask the question ('my1' is any name you choose)
WebToApk.listContacts('my1', 50, 0);3Rule 3 - two switches must be ON#
A phone feature needs TWO things:
- You turned it on in Step 3 (Permissions) when you built the app. If not, you get error 'not_enabled' and the user sees nothing.
- The user said Yes on the phone. If they said No, you get error 'permission_denied'.
Always handle both errors so the user knows what happened.
Easier way: pickContact() and composeSms() need NO permission at all. Use them when they fit - they do the same job with less trouble.
window.addEventListener('appmint:contacts', function (e) {
if (e.detail.error === 'not_enabled') {
alert('Turn on Contacts when you build the app');
} else if (e.detail.error === 'permission_denied') {
alert('Please allow contacts access');
} else {
showContacts(e.detail.contacts);
}
});4Notifications with pictures and buttons#
notify() takes a list of options. You can add a big picture, an icon, buttons, and a progress bar.
channel can be: urgent, default, quiet, or ongoing.
Use ongoing:true for a notification the user cannot swipe away. Note: they can still remove it from Android settings. No app can make one that is impossible to remove.
Use tag to give it a name. Sending again with the same tag replaces the old one instead of adding a new one.
WebToApk.notify(JSON.stringify({
title: 'Order shipped',
body: 'Arriving Tuesday',
channel: 'urgent',
image: 'https://mysite.com/box.jpg',
actions: [{ id: 'track', label: 'Track' }],
tag: 'order-482'
}));
// Know which button the user pressed
window.addEventListener('appmint:notification-action', function (e) {
if (e.detail.actionId === 'track') showTracking();
});5Phone information and fingerprint#
getDeviceInfo() needs no permission. It gives you the Android version, the version ID (buildId), the phone model, screen size, battery level, and more.
About device ID: there is NO IMEI or serial number. Android blocked this for every app from Android 10. Use getInstallId() instead - a fixed ID for this install that stays the same after updates.
var info = JSON.parse(WebToApk.getDeviceInfo());
info.android.release; // "14"
info.android.buildId; // "TQ3A.230805.001" <- version ID
info.hardware.model; // "Pixel 7"
info.runtime.batteryLevel; // 82
// Fingerprint
window.__webToApkAuth = window.__webToApkAuth || {};
window.__webToApkAuth['unlock'] = function (json) {
var r = JSON.parse(json);
if (r.ok) showMyApp();
};
WebToApk.authenticateBiometricEx('unlock', JSON.stringify({
title: 'Unlock',
allowDeviceCredential: true
}));6Make your page fill the screen#
When you turn on Fullscreen, the app hides the Android bars for you. But add this CSS so your content is not hidden under the phone's notch.
Important: Fullscreen hides the ANDROID bars (clock, battery, back buttons). Your app's own coloured bar at the top is a different switch called 'Show top bar'. If you still see a bar after turning on Fullscreen, that is the one to turn off.
Also: test your app rotated. The notch moves to the side in landscape.
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
min-height: 100vh; /* old phones */
min-height: 100dvh; /* correct when bars are hidden */
}7Downloads keep the filename you chose#
Your normal download code already works. When your page saves a file - a PDF report, a CSV export, a backup - the app catches it and opens the Android 'Save as…' sheet with YOUR filename already filled in.
This covers every usual way of doing it: <a download="...">, a.click() from code, html2pdf / jsPDF, FileSaver.js saveAs(), and window.open() on a blob URL. A File object brings its own name with it.
If you would rather ask directly instead of building an <a> tag, use AppMint.downloadFile().
One thing to know: the name lives only in your page's JavaScript - Android never sees it on its own. So set download= (or pass a name) every time, or the file is saved as 'download'.
// The usual way — saves as Site_Report_2026-08.pdf
const blob = await html2pdf().from(el).outputPdf('blob');
const a = document.createElement('a');
a.download = 'Site_Report_2026-08.pdf';
a.href = URL.createObjectURL(blob);
a.click();
// Or ask directly
AppMint.downloadFile(base64String, 'Ledger_Q3.csv', 'text/csv');
AppMint.saveBlob(myBlob, 'Backup.json');8Make the Back button do what YOUR app expects#
If your app changes screens by showing and hiding elements (no URL changes, no history.pushState), Android's Back button cannot see those screens - its history is empty, so Back exits the app from anywhere.
Register a back handler and decide yourself: return true when you handled the press (closed a menu, went back a screen), return false to let the normal behaviour run - page history back, then the exit confirmation, then exit.
Apps that use history.pushState for every screen do not need this: Back already walks their history. And don't worry about freezing the app - if your handler ever hangs, the phone's Back keeps working natively after a short moment.
AppMint.setBackHandler(() => {
if (isMenuOpen) { closeMenu(); return true; } // consumed
if (screen !== 'home') { goTo('home'); return true; }
return false; // nothing open — normal exit behaviour
});
// Or listen instead: e.preventDefault() consumes the press
window.addEventListener('appmint:back', e => {
if (closeTopmost()) e.preventDefault();
});9Receive a file the user opened with your app#
Turn on 'Open with this app' in the build wizard. Your app then appears in Android's Open with / Share sheet for the file types it registers (JSON, CSV, TXT, XML, Markdown, GPX, TCX, FIT).
When someone taps a file, ask for it with AppMint.getOpenedFile(). Ask whenever you are ready - the file waits for you. This matters for React, Vue and Angular apps: they finish starting AFTER the page loads, so an app that only listened for the event used to miss the file completely and just show its home screen.
There is no size limit. f.file is always the whole file. f.text is filled in for text formats and f.base64 for small binaries, as a shortcut.
// Ask on startup — works however late your app starts
const f = await AppMint.getOpenedFile(); // null if opened normally
if (f) {
console.log(f.name, f.mimeType, f.size);
const text = f.text ?? await f.file.text();
importActivity(text);
}
// Or listen, if you prefer
window.addEventListener('appmint:fileopen', e => handle(e.detail));10Bluetooth sensors - standard Web Bluetooth#
Heart-rate straps, cadence and speed pods, power meters, smart trainers, scales - anything that speaks Bluetooth LE.
Use navigator.bluetooth, exactly the same code you would write for Chrome on a desktop. Appwright provides it inside the app; a plain Android WebView has none, which is why this code does nothing in other app builders.
The device chooser is drawn by the app, like the browser's - your page can only reach the device the user picked.
Turn on the Bluetooth permission in Step 3. If your code mentions navigator.bluetooth, Appwright turns it on for you when you pick your ZIP.
Not available: getDevices(), watchAdvertisements() and requestLEScan() - they reject with NotSupportedError, so check before using them.
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('heart_rate');
const chr = await service.getCharacteristic('heart_rate_measurement');
chr.addEventListener('characteristicvaluechanged', e => {
const v = e.target.value; // a DataView
const bpm = (v.getUint8(0) & 1) ? v.getUint16(1, true) : v.getUint8(1);
document.getElementById('bpm').textContent = bpm;
});
await chr.startNotifications();
device.addEventListener('gattserverdisconnected', () => showReconnect());11{exampleCount} ready-made examples - open them here#
Working code you can copy: send an SMS, read contacts, show a notification with a picture, fingerprint lock, phone information, saving files, Bluetooth sensors, and more. Each one is short and explained in simple words.
Opens inside Appwright. You can search it, and you can select the text to copy it into your page.
The same screen also has 'All methods' - the full list of all {methodCount} things your app can call. That list is built from the app runtime itself, so it always matches exactly what your app can do.
12Save the examples to your phone or computer#
Saves three files to Downloads/Appwright/Guides:
- Examples - the {exampleCount} examples above
- Method list - all {methodCount} methods
- index.html - a ready test page
The test page has buttons for device info, vibration, notifications and the contact picker. Build it as an app to see everything working, then change it into your own app. It also opens fine in a normal browser - nothing breaks, it just says it is not inside the app.
In the app, this step's button saves these to your phone. Here they are direct downloads.
Appwright-examples.md Appwright-all-methods.md index.html