JavaScript bridge API
Share, clipboard and print
The Android share sheet, the system clipboard, text another app shared into yours, and the print dialogue.
AppMint.pdf helper#
AppMint.pdf.fromPage(options) / fromHtml(html, options)
Example
Makes a real PDF (selectable text, vector graphics) with no print dialog: fromPage saves the page as it is on screen, laid out with your @media print CSS; fromHtml renders HTML you pass in an invisible page at the app's own address - so relative images, stylesheets and fonts load as in your page - waits for the fonts and every image, then saves it. window.print() still opens the normal print dialog.
Returns: a Promise of {ok: true, fileName, pages, bytes, saved, uri?, base64?} or {ok: false, code, message} - code is busy (one PDF at a time), bad-options, failed, too-large (base64 over 25 MB) or permission-denied (storage refused on Android 7-9). Needs: nothing - every build has it. Saving to Downloads needs no permission on Android 10+.
Options: fileName (".pdf" is added; default: the page title), pageSize 'A4' (default) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins in millimetres (one number, or {top, right, bottom, left}; left out = your own @page rule), save 'downloads' (default) | 'share' | 'base64'.
Save the current page to Downloads:
async function saveInvoice() {
if (!(window.AppMint && AppMint.pdf)) { window.print(); return; } // in a browser
var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
else alert('Could not make the PDF: ' + r.message);
}A receipt built from HTML, straight to the share sheet (images and fonts are loaded before the PDF is made):
async function shareReceipt(order) {
var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
'<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
'</table><p><b>Total ' + order.total + '</b></p>';
var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
if (!r.ok) console.warn(r.code, r.message);
}Get the bytes back to upload them yourself:
async function uploadReport() {
var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
if (!r.ok) return;
var bin = atob(r.base64), buf = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}The raw calls take the options as JSON text and a callback id; use the helper above instead:
if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
// AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
// and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}Notes: Hide buttons and menus from the PDF with @media print { .no-print { display: none } }. A downloaded PDF shows "Downloaded <name> · Open" like any download. fromHtml runs the HTML's scripts (charts drawn by script appear) but it has no access to the app's native features. Pages longer than one sheet are split as Chrome would; use break-before: page to control where.
window.print web standard#
window.print()
Example
The standard print call, made to work inside the app: it opens Android's print screen, where the user prints on paper or saves a PDF.
Returns: nothing. Needs: nothing.
document.getElementById('print-receipt').onclick = function () {
window.print();
};Hide the app's controls on paper with a print stylesheet:
<style>
@media print {
nav, .toolbar, button { display: none !important; }
body { background: #fff; color: #000; }
}
</style>Print a shipping label without the page around it - a same-origin srcdoc iframe prints only its own document:
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
});
}
function printLabel(name, address) {
var frame = document.createElement('iframe');
frame.style.cssText = 'position:fixed;width:0;height:0;border:0';
frame.srcdoc = '<title>Label</title><style>body{font:18px sans-serif;margin:24px}</style>' +
'<b>' + esc(name) + '</b><br>' + esc(address).replace(/\n/g, '<br>');
frame.onload = function () {
frame.contentWindow.print();
setTimeout(function () { frame.remove(); }, 1000);
};
document.body.appendChild(frame);
}Notes: window.print() prints the main page as it is drawn now. A same-origin <iframe> - the hidden iframe a print library fills with a receipt and calls iframe.contentWindow.print() on, or an iframe page calling its own window.print() - prints THAT iframe's document (see __printHtml). A cross-origin iframe cannot be printed from the page. The replacement exists before the page's first script. beforeprint cannot be cancelled (in any browser).
__clipboardRead bridge#
window.WebToApk.__clipboardRead(): String
navigator.clipboard.read(): `{parts:[{type, text|base64}]}`. Same reach as readClipboard() in this shell (which has no origin gate).
Example
Internal transport behind navigator.clipboard.read(). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: {parts:[{type, text|base64}]} - the same reach as readClipboard() in this app. Needs: nothing to switch on.
Use the public API:
canvas.toBlob(async (png) => {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
});
const [item] = await navigator.clipboard.read();
if (item && item.types.includes('image/png')) preview(await item.getType('image/png'));Paste formatted text into an editor, keeping the formatting when the clipboard has HTML:
async function pasteRich(editor) {
try {
var items = await navigator.clipboard.read();
if (!items.length) { showMessage('The clipboard is empty'); return; }
var item = items[0];
if (item.types.includes('text/html')) {
editor.insertHtml(sanitize(await (await item.getType('text/html')).text()));
} else if (item.types.includes('text/plain')) {
editor.insertText(await (await item.getType('text/plain')).text());
}
} catch (e) {
showMessage('Could not read the clipboard');
}
}Paste any copied picture (PNG, JPEG, WebP…) as a profile photo, and tell a refused read apart:
document.getElementById('paste-photo').onclick = async function () {
try {
var items = await navigator.clipboard.read();
var item = items[0];
var type = item && item.types.find(function (t) { return t.indexOf('image/') === 0; });
if (!type) { showMessage('Copy a picture first'); return; }
uploadAvatar(await item.getType(type)); // a Blob of that type
} catch (e) {
if (e.name === 'NotAllowedError') showMessage('This page cannot read the clipboard');
else showMessage('Could not paste the picture');
}
};Notes: Android 12+ shows its own "pasted from your clipboard" message when an app reads the clipboard.
__clipboardWrite bridge#
window.WebToApk.__clipboardWrite(partsJson: String): Boolean
navigator.clipboard.write(): [partsJson] is `[{type, text}|{type, base64}]` for one ClipboardItem - text/plain, text/html, or an image that is copied as a content URI.
Example
Internal transport behind navigator.clipboard.write([ClipboardItem]). Pages use the standard API; never call this directly - the __ methods may change without notice.
Returns: true when the clip was placed. Needs: nothing to switch on.
Use the public API:
canvas.toBlob(async (png) => {
await navigator.clipboard.write([new ClipboardItem({ 'image/png': png })]);
});
const [item] = await navigator.clipboard.read();
if (item && item.types.includes('image/png')) preview(await item.getType('image/png'));Copy formatted text and its plain version in one item, so rich editors get the table and plain fields get the text:
async function copyTable(table) {
if (!(navigator.clipboard && navigator.clipboard.write && window.ClipboardItem)) return;
try {
await navigator.clipboard.write([new ClipboardItem({
'text/html': new Blob([table.outerHTML], { type: 'text/html' }),
'text/plain': new Blob([table.innerText], { type: 'text/plain' })
})]);
showMessage('Table copied');
} catch (e) {
showMessage('Could not copy (' + e.name + ')'); // NotAllowedError when the write fails
}
}Copy an image the page shows, such as a QR code, so it can be pasted into a chat:
async function copyImage(img) {
try {
var blob = await (await fetch(img.src)).blob(); // an image/* type, up to 16 MB
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
showMessage('Image copied');
} catch (e) {
showMessage('Could not copy the image');
}
}Notes: Text, HTML and one image per item. The image is copied as a content URI through the app's FileProvider, so it pastes into WhatsApp, Gmail or Keep.
__printHtml bridge#
window.WebToApk.__printHtml(html: String, baseUrl: String, title: String)
Prints [html] - a same-origin iframe's serialised document, sent by the `window.print()` shim ([PrintShim]) - through an offscreen WebView at [baseUrl], because the main WebView's print adapter can only print the whole page. [title] names the print job ("<App name> Document" when blank).
Example
Internal transport behind printing an iframe: iframe.contentWindow.print() (and an iframe page's own window.print()) sends that iframe's document here, and the app prints it through Android's print screen. Pages use the standard calls; never call this directly.
Returns: nothing. Needs: nothing.
The receipt pattern this makes work - a hidden iframe filled with just the receipt:
function printReceipt(order) {
var frame = document.createElement('iframe');
frame.style.display = 'none';
document.body.appendChild(frame);
var doc = frame.contentWindow.document;
doc.open();
doc.write('<html><head><title>Receipt ' + order.id + '</title>' +
'<style>body{font:14px sans-serif} td{padding:2px 8px}</style></head><body>' +
'<h2>Receipt ' + order.id + '</h2><table>' +
order.lines.map(function (l) { return '<tr><td>' + l.name + '</td><td>' + l.price + '</td></tr>'; }).join('') +
'</table></body></html>');
doc.close();
frame.contentWindow.print(); // prints the RECEIPT, not the page behind it
setTimeout(function () { frame.remove(); }, 1000);
}A ticket page of your own, loaded into a hidden iframe from the same origin - its CSS and images load, and its <title> names the print job:
function printTicket(id) {
var frame = document.createElement('iframe');
frame.style.cssText = 'position:fixed;width:0;height:0;border:0';
frame.src = 'ticket.html?id=' + encodeURIComponent(id);
frame.onload = function () {
frame.contentWindow.print(); // prints ticket.html's DOM as it stands now
setTimeout(function () { frame.remove(); }, 1000);
};
document.body.appendChild(frame);
}Adjust the iframe's document just before the snapshot with beforeprint on the iframe's window:
function printWithTimestamp(frame) {
var w = frame.contentWindow;
w.addEventListener('beforeprint', function () {
var p = w.document.createElement('p');
p.textContent = 'Printed ' + new Date().toLocaleString();
w.document.body.appendChild(p);
}, { once: true });
w.print(); // the printed copy includes the line added above
}Notes: Arguments are (html, baseUrl, title): the iframe's serialised document, the address its relative links resolve against (the page's own address for an about:blank iframe), and the print job name (the iframe's <title>, else the page's). The snapshot is taken when print() is called - scripts inside it do not run again - and beforeprint / afterprint fire on the iframe's window around it. A cross-origin iframe cannot be printed from the page.
copyToClipboard bridge#
window.WebToApk.copyToClipboard(text: String): Boolean
Use the promise wrapper: const r = await AppMint.requestReview(); // {result, openedStore} const r = await AppMint.requestReview({ openStore: true }); // listing if no sheet r.result: 'shown' | 'not_from_play' | 'quota' | 'error'. See RatePrompt.
Described by its group, Google Play's in-app review sheet, rather than on its own.
Example
Copies plain text to the phone's clipboard. Pages normally use the standard navigator.clipboard.writeText(text), which the shell routes here.
Returns: true on success, false on failure (synchronously). Needs: nothing.
The standard API first:
document.getElementById('copy-btn').onclick = async function () {
try {
await navigator.clipboard.writeText('PROMO-2026');
showMessage('Copied');
} catch (e) {
showMessage('Could not copy');
}
};The raw call:
if (window.WebToApk && typeof window.WebToApk.copyToClipboard === 'function') {
var ok = window.WebToApk.copyToClipboard('PROMO-2026');
showMessage(ok ? 'Copied' : 'Could not copy');
}A "Copy" button on every code block, using the synchronous answer to update the label:
document.querySelectorAll('pre').forEach(function (pre) {
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.onclick = function () {
var ok = !!(window.WebToApk && typeof window.WebToApk.copyToClipboard === 'function' &&
window.WebToApk.copyToClipboard(pre.innerText));
btn.textContent = ok ? 'Copied' : 'Copy failed';
setTimeout(function () { btn.textContent = 'Copy'; }, 1500);
};
pre.parentNode.insertBefore(btn, pre);
});Notes: Plain text only (no images or HTML). Android 13+ shows its own small "Copied" preview, so you may not need your own message there.
pdfFromHtml bridge#
window.WebToApk.pdfFromHtml(html: String, optionsJson: String, callbackId: String)
Renders [html] offscreen at the app's origin and saves it as a PDF - use `AppMint.pdf.fromHtml(html, options)`.
Example
Documented together with AppMint.pdf - the example there shows this one too.
Makes a real PDF (selectable text, vector graphics) with no print dialog: fromPage saves the page as it is on screen, laid out with your @media print CSS; fromHtml renders HTML you pass in an invisible page at the app's own address - so relative images, stylesheets and fonts load as in your page - waits for the fonts and every image, then saves it. window.print() still opens the normal print dialog.
Returns: a Promise of {ok: true, fileName, pages, bytes, saved, uri?, base64?} or {ok: false, code, message} - code is busy (one PDF at a time), bad-options, failed, too-large (base64 over 25 MB) or permission-denied (storage refused on Android 7-9). Needs: nothing - every build has it. Saving to Downloads needs no permission on Android 10+.
Options: fileName (".pdf" is added; default: the page title), pageSize 'A4' (default) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins in millimetres (one number, or {top, right, bottom, left}; left out = your own @page rule), save 'downloads' (default) | 'share' | 'base64'.
Save the current page to Downloads:
async function saveInvoice() {
if (!(window.AppMint && AppMint.pdf)) { window.print(); return; } // in a browser
var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
else alert('Could not make the PDF: ' + r.message);
}A receipt built from HTML, straight to the share sheet (images and fonts are loaded before the PDF is made):
async function shareReceipt(order) {
var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
'<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
'</table><p><b>Total ' + order.total + '</b></p>';
var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
if (!r.ok) console.warn(r.code, r.message);
}Get the bytes back to upload them yourself:
async function uploadReport() {
var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
if (!r.ok) return;
var bin = atob(r.base64), buf = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}The raw calls take the options as JSON text and a callback id; use the helper above instead:
if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
// AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
// and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}Notes: Hide buttons and menus from the PDF with @media print { .no-print { display: none } }. A downloaded PDF shows "Downloaded <name> · Open" like any download. fromHtml runs the HTML's scripts (charts drawn by script appear) but it has no access to the app's native features. Pages longer than one sheet are split as Chrome would; use break-before: page to control where.
pdfFromPage bridge#
window.WebToApk.pdfFromPage(optionsJson: String, callbackId: String)
Saves the page as a PDF with no print dialog - use `AppMint.pdf.fromPage(options)`. [optionsJson] `{fileName, pageSize, landscape, margins, save}`; resolves [callbackId].
Example
Documented together with AppMint.pdf - the example there shows this one too.
Makes a real PDF (selectable text, vector graphics) with no print dialog: fromPage saves the page as it is on screen, laid out with your @media print CSS; fromHtml renders HTML you pass in an invisible page at the app's own address - so relative images, stylesheets and fonts load as in your page - waits for the fonts and every image, then saves it. window.print() still opens the normal print dialog.
Returns: a Promise of {ok: true, fileName, pages, bytes, saved, uri?, base64?} or {ok: false, code, message} - code is busy (one PDF at a time), bad-options, failed, too-large (base64 over 25 MB) or permission-denied (storage refused on Android 7-9). Needs: nothing - every build has it. Saving to Downloads needs no permission on Android 10+.
Options: fileName (".pdf" is added; default: the page title), pageSize 'A4' (default) | 'Letter' | 'Legal' | 'A3' | 'A5', landscape (true/false), margins in millimetres (one number, or {top, right, bottom, left}; left out = your own @page rule), save 'downloads' (default) | 'share' | 'base64'.
Save the current page to Downloads:
async function saveInvoice() {
if (!(window.AppMint && AppMint.pdf)) { window.print(); return; } // in a browser
var r = await AppMint.pdf.fromPage({ fileName: 'invoice-1042', pageSize: 'A4', margins: 12 });
if (r.ok) console.log('Saved ' + r.fileName + ' (' + r.pages + ' pages)');
else alert('Could not make the PDF: ' + r.message);
}A receipt built from HTML, straight to the share sheet (images and fonts are loaded before the PDF is made):
async function shareReceipt(order) {
var html = '<style>body{font-family:sans-serif} td{padding:4px 8px}</style>' +
'<img src="img/logo.png" width="120"><h1>Receipt #' + order.id + '</h1><table>' +
order.items.map(function (i) { return '<tr><td>' + i.name + '</td><td>' + i.price + '</td></tr>'; }).join('') +
'</table><p><b>Total ' + order.total + '</b></p>';
var r = await AppMint.pdf.fromHtml(html, { fileName: 'receipt-' + order.id, pageSize: 'Letter', save: 'share' });
if (!r.ok) console.warn(r.code, r.message);
}Get the bytes back to upload them yourself:
async function uploadReport() {
var r = await AppMint.pdf.fromPage({ landscape: true, margins: { top: 10, right: 8, bottom: 10, left: 8 }, save: 'base64' });
if (!r.ok) return;
var bin = atob(r.base64), buf = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
await fetch('/api/reports', { method: 'POST', headers: { 'Content-Type': 'application/pdf' }, body: buf });
}The raw calls take the options as JSON text and a callback id; use the helper above instead:
if (window.WebToApk && typeof window.WebToApk.pdfFromPage === 'function') {
// AppMint.pdf.fromPage wraps: WebToApk.pdfFromPage(JSON.stringify(options), callbackId)
// and AppMint.pdf.fromHtml wraps: WebToApk.pdfFromHtml(html, JSON.stringify(options), callbackId)
AppMint.pdf.fromPage({ save: 'downloads' }).then(function (r) { console.log(r); });
}Notes: Hide buttons and menus from the PDF with @media print { .no-print { display: none } }. A downloaded PDF shows "Downloaded <name> · Open" like any download. fromHtml runs the HTML's scripts (charts drawn by script appear) but it has no access to the app's native features. Pages longer than one sheet are split as Chrome would; use break-before: page to control where.
print bridge#
window.WebToApk.print()
Hands the page as it currently stands to Android's print dialogue, which can print on paper or save a PDF. Nothing to configure - the WebView renders the document.
Described by its group, Printing, rather than on its own.
Example
Opens Android's print screen for the page as it looks now - print on paper or "Save as PDF". Pages normally use the standard window.print(), which the shell routes here.
Returns: nothing. Needs: nothing.
The standard API first:
document.getElementById('print-btn').onclick = function () {
window.print(); // in the app: the Android print screen
};The raw call does the same:
if (window.WebToApk && typeof window.WebToApk.print === 'function') {
window.WebToApk.print();
}Print one invoice out of a list by marking it for a print stylesheet (leave the mark on - the print screen lays the page out after print() returns):
// CSS: @media print { .invoice:not(.print-me), nav, button { display: none !important; } }
function printInvoice(id) {
document.querySelectorAll('.invoice.print-me').forEach(function (el) {
el.classList.remove('print-me');
});
document.getElementById('invoice-' + id).classList.add('print-me');
if (window.WebToApk && typeof window.WebToApk.print === 'function') {
window.WebToApk.print();
} else {
window.print(); // a normal browser
}
}Notes: It prints the whole WebView - the main page. A same-origin iframe's own print() prints just that iframe (see window.print). Use a @media print stylesheet to hide buttons and menus. The job is named "<App name> Document". Errors are only logged.
readClipboard bridge#
window.WebToApk.readClipboard(): String
Use the promise wrapper: const r = await AppMint.requestReview(); // {result, openedStore} const r = await AppMint.requestReview({ openStore: true }); // listing if no sheet r.result: 'shown' | 'not_from_play' | 'quota' | 'error'. See RatePrompt.
Described by its group, Google Play's in-app review sheet, rather than on its own.
Example
Reads the text that is on the phone's clipboard now. Pages normally use the standard navigator.clipboard.readText(), which the shell routes here.
Returns: the text, or "" when the clipboard is empty or cannot be read (synchronously). Needs: nothing.
The standard API first - for example a "Paste code" button:
document.getElementById('paste-btn').onclick = async function () {
try {
var text = await navigator.clipboard.readText();
if (text) document.getElementById('code').value = text.trim();
else showMessage('The clipboard is empty');
} catch (e) {
showMessage('Could not read the clipboard');
}
};The raw call:
if (window.WebToApk && typeof window.WebToApk.readClipboard === 'function') {
var text = window.WebToApk.readClipboard();
if (text) document.getElementById('code').value = text;
}A "Paste link" button that takes the first web address out of whatever was copied:
document.getElementById('paste-link').onclick = function () {
if (!(window.WebToApk && typeof window.WebToApk.readClipboard === 'function')) return;
var text = window.WebToApk.readClipboard();
var m = /https?:\/\/\S+/.exec(text);
if (m) document.getElementById('link').value = m[0];
else showMessage('There is no link on the clipboard');
};Notes: Only your own app's pages can read it: a foreign website opened inside the app gets "". Android only lets the app read the clipboard while it is on screen, and Android 12+ shows a "pasted from clipboard" notice - read it on a user tap, not on page load.