JavaScript bridge API
Running in the background
A foreground service the page starts and stops, location that keeps arriving with the app in the background, geofences that fire with the app closed, and your own code run periodically after a restart. Each needs its switch in the build and a Play Console declaration.
AppMint.background helper#
AppMint.background.register(name, options) / unregister / list / runNow / done / isBackgroundRun / currentRun / onRun
Example
Runs your page's OWN code periodically with the app closed - refresh data, check a price, post a notification. Each run loads your page invisibly (same origin, same localStorage/IndexedDB, no screen, no ads) with ?appmint_background=1&appmint_task=<name>&appmint_run=<runId>, fires appmint:background { name, runId } once it has loaded, and waits for done(runId, { ok }). A run is cut off after about 9 minutes.
Returns: every method returns a Promise. register → { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name, interval_below_15_minutes, invalid_interval, schedule_failed). unregister → true / false. list → [{ name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult }] (lastResult: ok, retry, failed, timeout, unsupported_mode, page_load_failed, …). runNow → { ok } (one run as soon as the conditions allow - for testing). done → true when the run was ended. isBackgroundRun() and currentRun() are synchronous. Needs: tick Run page code while closed (Access step → Background). Website and offline/ZIP apps; document and media apps have no page code to run.
One page, two roles:
function startApp() {
const bg = window.AppMint && AppMint.background;
if (bg && bg.isBackgroundRun()) return; // a background run: no UI work
renderUi();
if (bg) bg.register('prices', { intervalMinutes: 60, requiresNetwork: true }).then(function (r) {
if (!r.ok) console.warn('background task not scheduled:', r.reason);
});
}
// Runs only in the background copy of the page.
window.addEventListener('appmint:background', async function (e) {
const run = e.detail; // { name: 'prices', runId: '…' }
try {
const price = await fetch('https://api.example.com/price').then(function (r) { return r.json(); });
const last = Number(localStorage.getItem('lastPrice') || 0);
if (price.value < last) {
WebToApk.notify(JSON.stringify({ title: 'Price dropped', body: 'Now ' + price.value, tag: 'price' }));
}
localStorage.setItem('lastPrice', String(price.value));
AppMint.background.done(run.runId, { ok: true });
} catch (err) {
AppMint.background.done(run.runId, { ok: false, retry: true }); // retried with backoff
}
});
startApp();Late-mounting pages (React, a lazy route) use onRun, which also fires if the run already started, or the page hook:
if (window.AppMint && AppMint.background) {
AppMint.background.onRun(function (run) { syncInbox().then(function () { AppMint.background.done(run.runId, { ok: true }); }); });
}
window.onAppMintBackground = function (run) { console.log('background run', run.name, run.runId, AppMint.background.currentRun()); };Manage:
AppMint.background.list().then(function (tasks) { console.table(tasks); });
AppMint.background.runNow('prices');
AppMint.background.unregister('prices');Notes: Android decides the exact moment; 15 minutes is the shortest interval and battery saving can delay runs. With Start after phone restart every task also runs once after a restart. A run's WebToApk has only notify, showNotification, the location buffer, geofences, workEnqueueOnline, isBackgroundServiceRunning and these task methods; anything else is absent (feature-detect).
addGeofence bridge#
window.WebToApk.addGeofence(json: String): String
Adds a geofence {id, lat, lng, radiusM, events, dwellMs?, notify?} → JSON {ok, status:'adding'} or {ok:false, reason}; the result is appmint:geofence-added.
Example
A circle that fires enter, exit and/or dwell with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).
Returns: addGeofence → a JSON string { ok: true, status: 'adding', id } or { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. Play services' answer arrives as appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 per app, play_services_unavailable). A transition arrives as appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true when it happened while the app was closed and is handed over at the next start. removeGeofence → true if it existed; getGeofences → a JSON array of what you added. Needs: tick Geofences in the build, and "Allow all the time" (requestBackgroundLocation()).
function watchOffice() {
const b = window.WebToApk;
if (!(b && b.addGeofence)) return;
const r = JSON.parse(b.addGeofence(JSON.stringify({
id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
notify: {
enter: { title: 'At the office', body: 'Tap to check in' },
exit: { title: 'Left the office', body: 'Tap to check out' }
}
})));
if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}
window.addEventListener('appmint:geofence-added', function (e) {
if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
logVisit(e.detail.id, e.detail.event, e.detail.time);
});
console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');backgroundDone bridge#
window.WebToApk.backgroundDone(runId: String, resultJson: String): Boolean
AppMint.background.done(runId, result): ends a background run; always false in the app itself.
Example
Documented together with WebToApk.backgroundRegister - the example there shows this one too.
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}backgroundList bridge#
window.WebToApk.backgroundList(): String
AppMint.background.list(): JSON array {name, intervalMinutes, requiresNetwork, requiresCharging, registeredAt, lastRunAt, lastResult}.
Example
Documented together with WebToApk.backgroundRegister - the example there shows this one too.
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}backgroundRegister bridge#
window.WebToApk.backgroundRegister(name: String, optionsJson: String): String
AppMint.background.register(name, {intervalMinutes >= 15, requiresNetwork, requiresCharging}) → JSON {ok, name, intervalMinutes} or {ok:false, reason}.
Example
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}backgroundRunNow bridge#
window.WebToApk.backgroundRunNow(name: String): String
AppMint.background.runNow(name): one background run as soon as its conditions allow.
Example
Documented together with WebToApk.backgroundRegister - the example there shows this one too.
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}backgroundUnregister bridge#
window.WebToApk.backgroundUnregister(name: String): Boolean
AppMint.background.unregister(name); false when no task had that name.
Example
Documented together with WebToApk.backgroundRegister - the example there shows this one too.
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}clearBufferedLocations bridge#
window.WebToApk.clearBufferedLocations(): Boolean
Empties the buffer getBufferedLocations reads (call it after you saved the points).
Example
Documented together with WebToApk.getBufferedLocations - the example there shows this one too.
The fixes recorded while your page was away (app in the background or closed), oldest first, up to 10,000.
Returns: getBufferedLocations() → a JSON string array of { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' when Background location is off. clearBufferedLocations() → true when emptied. Needs: Background location in the build.
function syncTrack() {
if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
const points = JSON.parse(WebToApk.getBufferedLocations());
if (!points.length) return;
points.forEach(function (p) { drawPoint(p.lat, p.lng); });
saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });clearGeofenceEvents bridge#
window.WebToApk.clearGeofenceEvents(): Boolean
Empties the transition history getGeofenceEvents reads.
Example
Documented together with WebToApk.getGeofenceEvents - the example there shows this one too.
The last 100 geofence transitions, oldest first - including the ones that happened while the app was closed. Read it on start if your page mounts late and might miss the queued appmint:geofence events.
Returns: getGeofenceEvents() → a JSON string array of { id, event, time, lat, lng }; clearGeofenceEvents() → true. Needs: Geofences in the build.
if (window.WebToApk && WebToApk.getGeofenceEvents) {
JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
logVisit(ev.id, ev.event, ev.time);
});
WebToApk.clearGeofenceEvents();
}getBufferedLocations bridge#
window.WebToApk.getBufferedLocations(): String
JSON array of the fixes recorded while the page was away, oldest first (up to 10,000).
Example
The fixes recorded while your page was away (app in the background or closed), oldest first, up to 10,000.
Returns: getBufferedLocations() → a JSON string array of { type: 'fix', lat, lng, accuracy, time, …, background: true }; '[]' when Background location is off. clearBufferedLocations() → true when emptied. Needs: Background location in the build.
function syncTrack() {
if (!(window.WebToApk && WebToApk.getBufferedLocations)) return;
const points = JSON.parse(WebToApk.getBufferedLocations());
if (!points.length) return;
points.forEach(function (p) { drawPoint(p.lat, p.lng); });
saveTrack(points).then(function () { WebToApk.clearBufferedLocations(); });
}
document.addEventListener('visibilitychange', function () { if (!document.hidden) syncTrack(); });getGeofenceEvents bridge#
window.WebToApk.getGeofenceEvents(): String
JSON array of the last 100 geofence transitions {id, event, time, lat, lng}, oldest first.
Example
The last 100 geofence transitions, oldest first - including the ones that happened while the app was closed. Read it on start if your page mounts late and might miss the queued appmint:geofence events.
Returns: getGeofenceEvents() → a JSON string array of { id, event, time, lat, lng }; clearGeofenceEvents() → true. Needs: Geofences in the build.
if (window.WebToApk && WebToApk.getGeofenceEvents) {
JSON.parse(WebToApk.getGeofenceEvents()).forEach(function (ev) {
logVisit(ev.id, ev.event, ev.time);
});
WebToApk.clearGeofenceEvents();
}getGeofences bridge#
window.WebToApk.getGeofences(): String
JSON array of the geofences this app has added (they survive restarts).
Example
Documented together with WebToApk.addGeofence - the example there shows this one too.
A circle that fires enter, exit and/or dwell with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).
Returns: addGeofence → a JSON string { ok: true, status: 'adding', id } or { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. Play services' answer arrives as appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 per app, play_services_unavailable). A transition arrives as appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true when it happened while the app was closed and is handed over at the next start. removeGeofence → true if it existed; getGeofences → a JSON array of what you added. Needs: tick Geofences in the build, and "Allow all the time" (requestBackgroundLocation()).
function watchOffice() {
const b = window.WebToApk;
if (!(b && b.addGeofence)) return;
const r = JSON.parse(b.addGeofence(JSON.stringify({
id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
notify: {
enter: { title: 'At the office', body: 'Tap to check in' },
exit: { title: 'Left the office', body: 'Tap to check out' }
}
})));
if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}
window.addEventListener('appmint:geofence-added', function (e) {
if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
logVisit(e.detail.id, e.detail.event, e.detail.time);
});
console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');getLocationPermissionState bridge#
window.WebToApk.getLocationPermissionState(): String
JSON {foreground:'granted'|'denied', background:'granted'|'denied', precise: boolean}.
Example
Documented together with WebToApk.requestBackgroundLocation - the example there shows this one too.
Asks for location, then for "Allow all the time" (Android 10+ asks for them separately; on Android 11+ the second step opens the app's location settings page). Geofences need it; so does restarting location tracking after a phone restart.
Returns: nothing directly; the answer arrives as appmint:location-permission (and window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → the same object as a JSON string, without asking. Needs: Background location or Geofences in the build.
function askAlways() {
const b = window.WebToApk;
if (!(b && b.requestBackgroundLocation)) return;
const now = JSON.parse(b.getLocationPermissionState());
if (now.background === 'granted') { enableGeofences(); return; }
// Explain first: Play requires an in-app disclosure before this request.
if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
b.requestBackgroundLocation();
}
window.addEventListener('appmint:location-permission', function (e) {
if (e.detail.background === 'granted') enableGeofences();
else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});isBackgroundRun bridge#
window.WebToApk.isBackgroundRun(): Boolean
False here: the app's own page is never a background run (a headless run answers true).
Example
Documented together with WebToApk.backgroundRegister - the example there shows this one too.
The raw methods behind AppMint.background - use AppMint.background.register(...) (it returns promises and handles the run event). Listed here so every bridge method has an example.
Returns: backgroundRegister → JSON { ok: true, name, intervalMinutes } or { ok: false, reason } (not_enabled, invalid_name - letters, digits, _, -, up to 64 - interval_below_15_minutes, invalid_interval, schedule_failed). backgroundUnregister → true if it existed. backgroundList → JSON array. backgroundRunNow → JSON { ok, name } or { ok: false, reason: 'not_registered' }. backgroundDone(runId, resultJson) → true in a background run, always false in the app. isBackgroundRun() → false in the app, true in a background run. Needs: tick Run page code while closed.
const b = window.WebToApk;
if (b && b.backgroundRegister) {
const r = JSON.parse(b.backgroundRegister('refresh', JSON.stringify({ intervalMinutes: 60, requiresNetwork: true })));
if (!r.ok) console.warn('not scheduled:', r.reason);
console.log(JSON.parse(b.backgroundList()));
console.log(b.isBackgroundRun()); // false here: this is the app on screen
// b.backgroundRunNow('refresh'); b.backgroundUnregister('refresh');
// In a run: b.backgroundDone(runId, JSON.stringify({ ok: true }))
}isBackgroundServiceRunning bridge#
window.WebToApk.isBackgroundServiceRunning(): Boolean
Whether the app's foreground service is running (for either keep-running or background location).
Example
Documented together with WebToApk.startBackgroundService - the example there shows this one too.
Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, <audio> and the app's background music carry on (Android still slows the timers of a page nobody can see).
Returns: startBackgroundService → a JSON string { ok: true, status: 'starting', type, notificationShown } or { ok: false, reason } - reason not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } or { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. State changes arrive as appmint:background-service (and window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Needs: tick Keep running in background (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (notificationShown: false otherwise - the service still runs).
function startWorkout() {
const b = window.WebToApk;
if (!(b && b.startBackgroundService)) return; // browser
const r = JSON.parse(b.startBackgroundService(JSON.stringify({
title: 'Workout in progress',
text: '00:00 elapsed',
stopLabel: 'Stop', // a Stop button on the notification
progress: { value: 0, max: 100 }
})));
if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}
function tick(seconds) {
if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
WebToApk.updateBackgroundService(JSON.stringify({
title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
progress: { value: Math.min(100, seconds / 18), max: 100 }
}));
}
}
function finishWorkout() {
if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}
window.addEventListener('appmint:background-service', function (e) {
// reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
// 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
// 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});Notes: the type must be one the build declared (the one picked in the wizard, plus location when Background location is on). Swiping the app away stops a keep-running service unless you pass keepAfterSwipe: true (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.
isIgnoringBatteryOptimizations bridge#
window.WebToApk.isIgnoringBatteryOptimizations(): Boolean
Whether the user exempted this app from battery optimisation (Doze app standby).
Example
Checks and asks for the battery-optimisation exemption, so Doze does not defer your background work.
Returns: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (the system's Allow/Deny dialog - only when the build ticked Ask to skip battery optimisation directly), 'settings_opened' (the battery-optimisation list; no permission, no Play declaration) or 'unavailable'. Needs: nothing for the settings list. The direct dialog declares REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, which Play accepts only when it is the app's core function.
document.getElementById('reliable').addEventListener('click', function () {
const b = window.WebToApk;
if (!(b && b.isIgnoringBatteryOptimizations)) return;
if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
const how = b.requestIgnoreBatteryOptimizations();
if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});removeGeofence bridge#
window.WebToApk.removeGeofence(id: String): Boolean
Removes the geofence with this id; false when there was none.
Example
Documented together with WebToApk.addGeofence - the example there shows this one too.
A circle that fires enter, exit and/or dwell with the app closed, optionally posting a notification. Geofences survive a phone restart (they are added back automatically).
Returns: addGeofence → a JSON string { ok: true, status: 'adding', id } or { ok: false, reason } - not_enabled, invalid_id, invalid_position, invalid_radius (1 m … 100 km), invalid_events, location_permission_required, background_permission_required, foreign_origin. Play services' answer arrives as appmint:geofence-added (window.onAppMintGeofenceAdded) = { id, ok, reason? } (location_off, too_many_geofences - 100 per app, play_services_unavailable). A transition arrives as appmint:geofence (window.onAppMintGeofence) = { id, event: 'enter' | 'exit' | 'dwell', time, lat, lng, queued? } - queued: true when it happened while the app was closed and is handed over at the next start. removeGeofence → true if it existed; getGeofences → a JSON array of what you added. Needs: tick Geofences in the build, and "Allow all the time" (requestBackgroundLocation()).
function watchOffice() {
const b = window.WebToApk;
if (!(b && b.addGeofence)) return;
const r = JSON.parse(b.addGeofence(JSON.stringify({
id: 'office', lat: 51.5033, lng: -0.1196, radiusM: 150,
events: ['enter', 'exit', 'dwell'], dwellMs: 5 * 60000,
notify: {
enter: { title: 'At the office', body: 'Tap to check in' },
exit: { title: 'Left the office', body: 'Tap to check out' }
}
})));
if (!r.ok && r.reason === 'background_permission_required') b.requestBackgroundLocation();
}
window.addEventListener('appmint:geofence-added', function (e) {
if (!e.detail.ok) showMessage('Geofence ' + e.detail.id + ' not added: ' + e.detail.reason);
});
window.addEventListener('appmint:geofence', function (e) {
logVisit(e.detail.id, e.detail.event, e.detail.time);
});
console.log(JSON.parse(WebToApk.getGeofences()).map(function (g) { return g.id; }));
WebToApk.removeGeofence('office');requestBackgroundLocation bridge#
window.WebToApk.requestBackgroundLocation()
Asks for location, then "Allow all the time"; the result arrives as appmint:location-permission.
Example
Asks for location, then for "Allow all the time" (Android 10+ asks for them separately; on Android 11+ the second step opens the app's location settings page). Geofences need it; so does restarting location tracking after a phone restart.
Returns: nothing directly; the answer arrives as appmint:location-permission (and window.onAppMintLocationPermission(detail)) = { foreground: 'granted' | 'denied', background: 'granted' | 'denied', precise: boolean, error?: 'not_enabled' }. getLocationPermissionState() → the same object as a JSON string, without asking. Needs: Background location or Geofences in the build.
function askAlways() {
const b = window.WebToApk;
if (!(b && b.requestBackgroundLocation)) return;
const now = JSON.parse(b.getLocationPermissionState());
if (now.background === 'granted') { enableGeofences(); return; }
// Explain first: Play requires an in-app disclosure before this request.
if (!confirm('Allow location "all the time" so we can remind you when you arrive?')) return;
b.requestBackgroundLocation();
}
window.addEventListener('appmint:location-permission', function (e) {
if (e.detail.background === 'granted') enableGeofences();
else showMessage('Choose "Allow all the time" in the location settings to use arrival reminders.');
});requestIgnoreBatteryOptimizations bridge#
window.WebToApk.requestIgnoreBatteryOptimizations(): String
Asks for the battery exemption: 'already_ignoring' | 'dialog_opened' | 'settings_opened' | 'unavailable'.
Example
Documented together with WebToApk.isIgnoringBatteryOptimizations - the example there shows this one too.
Checks and asks for the battery-optimisation exemption, so Doze does not defer your background work.
Returns: isIgnoringBatteryOptimizations() → true / false. requestIgnoreBatteryOptimizations() → 'already_ignoring', 'dialog_opened' (the system's Allow/Deny dialog - only when the build ticked Ask to skip battery optimisation directly), 'settings_opened' (the battery-optimisation list; no permission, no Play declaration) or 'unavailable'. Needs: nothing for the settings list. The direct dialog declares REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, which Play accepts only when it is the app's core function.
document.getElementById('reliable').addEventListener('click', function () {
const b = window.WebToApk;
if (!(b && b.isIgnoringBatteryOptimizations)) return;
if (b.isIgnoringBatteryOptimizations()) { showMessage('Already unrestricted'); return; }
const how = b.requestIgnoreBatteryOptimizations();
if (how === 'settings_opened') showMessage('Find this app in the list and choose "Not optimised".');
});startBackgroundService bridge#
window.WebToApk.startBackgroundService(optionsJson: String): String
Starts the app's foreground service: {title, text, type?, progress?, stopLabel?, keepAfterSwipe?} → JSON {ok, status, type, notificationShown} or {ok:false, reason}.
Example
Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, <audio> and the app's background music carry on (Android still slows the timers of a page nobody can see).
Returns: startBackgroundService → a JSON string { ok: true, status: 'starting', type, notificationShown } or { ok: false, reason } - reason not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } or { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. State changes arrive as appmint:background-service (and window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Needs: tick Keep running in background (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (notificationShown: false otherwise - the service still runs).
function startWorkout() {
const b = window.WebToApk;
if (!(b && b.startBackgroundService)) return; // browser
const r = JSON.parse(b.startBackgroundService(JSON.stringify({
title: 'Workout in progress',
text: '00:00 elapsed',
stopLabel: 'Stop', // a Stop button on the notification
progress: { value: 0, max: 100 }
})));
if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}
function tick(seconds) {
if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
WebToApk.updateBackgroundService(JSON.stringify({
title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
progress: { value: Math.min(100, seconds / 18), max: 100 }
}));
}
}
function finishWorkout() {
if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}
window.addEventListener('appmint:background-service', function (e) {
// reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
// 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
// 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});Notes: the type must be one the build declared (the one picked in the wizard, plus location when Background location is on). Swiping the app away stops a keep-running service unless you pass keepAfterSwipe: true (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.
startLocationUpdates bridge#
window.WebToApk.startLocationUpdates(optionsJson: String): String
Location updates {intervalMs, minDistanceM, accuracy, background, uploadUrl?, headers?} → JSON {ok, background, backgroundPermission, precise} or {ok:false, reason}; fixes arrive as appmint:location.
Example
Location updates that keep coming while the app is in the background (a run tracker, a delivery app). With background: true (the default) they run inside the app's foreground service. Each fix goes to the page as appmint:location while the page is there, into a buffer (getBufferedLocations()) while it is not, and - when you give an uploadUrl - to your server as a JSON POST from native code, so tracking works with the page gone.
Returns: a JSON string { ok: true, background, backgroundPermission, precise } or { ok: false, reason } - not_enabled, invalid_options, invalid_interval (1 s … 24 h), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true if something was running. Fixes: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, or { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Needs: tick Background location (Access step → Background) and the user's location permission first.
function startTracking(token) {
const b = window.WebToApk;
if (!(b && b.startLocationUpdates)) return;
navigator.geolocation.getCurrentPosition(function () { // asks for location first
const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
uploadUrl: 'https://api.example.com/track',
headers: { Authorization: 'Bearer ' + token }
})));
if (!r.ok) showMessage('Tracking not started: ' + r.reason);
});
}
window.addEventListener('appmint:location', function (e) {
if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
else showMessage('Location stopped: ' + e.detail.reason);
});
function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }Notes: a location service started from the app keeps getting fixes in the background without "Allow all the time"; restarting it after a phone restart needs it (requestBackgroundLocation()). A POST that fails is handed to WorkManager and sent when the network is back. Phones without Google Play services answer play_services_unavailable. Play Console needs a background-location declaration.
stopBackgroundService bridge#
window.WebToApk.stopBackgroundService(): Boolean
Stops the service started by startBackgroundService (background location, if running, continues).
Example
Documented together with WebToApk.startBackgroundService - the example there shows this one too.
Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, <audio> and the app's background music carry on (Android still slows the timers of a page nobody can see).
Returns: startBackgroundService → a JSON string { ok: true, status: 'starting', type, notificationShown } or { ok: false, reason } - reason not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } or { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. State changes arrive as appmint:background-service (and window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Needs: tick Keep running in background (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (notificationShown: false otherwise - the service still runs).
function startWorkout() {
const b = window.WebToApk;
if (!(b && b.startBackgroundService)) return; // browser
const r = JSON.parse(b.startBackgroundService(JSON.stringify({
title: 'Workout in progress',
text: '00:00 elapsed',
stopLabel: 'Stop', // a Stop button on the notification
progress: { value: 0, max: 100 }
})));
if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}
function tick(seconds) {
if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
WebToApk.updateBackgroundService(JSON.stringify({
title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
progress: { value: Math.min(100, seconds / 18), max: 100 }
}));
}
}
function finishWorkout() {
if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}
window.addEventListener('appmint:background-service', function (e) {
// reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
// 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
// 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});Notes: the type must be one the build declared (the one picked in the wizard, plus location when Background location is on). Swiping the app away stops a keep-running service unless you pass keepAfterSwipe: true (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.
stopLocationUpdates bridge#
window.WebToApk.stopLocationUpdates(): Boolean
Stops the location updates started by startLocationUpdates; false when none were running.
Example
Documented together with WebToApk.startLocationUpdates - the example there shows this one too.
Location updates that keep coming while the app is in the background (a run tracker, a delivery app). With background: true (the default) they run inside the app's foreground service. Each fix goes to the page as appmint:location while the page is there, into a buffer (getBufferedLocations()) while it is not, and - when you give an uploadUrl - to your server as a JSON POST from native code, so tracking works with the page gone.
Returns: a JSON string { ok: true, background, backgroundPermission, precise } or { ok: false, reason } - not_enabled, invalid_options, invalid_interval (1 s … 24 h), invalid_distance, invalid_accuracy, invalid_upload_url, location_permission_required, start_not_allowed, foreign_origin. stopLocationUpdates() → true if something was running. Fixes: appmint:location / window.onAppMintLocation(detail) = { type: 'fix', lat, lng, accuracy, altitude?, speed?, bearing?, time, provider, background }, or { type: 'error', reason: 'play_services_unavailable' | 'location_unavailable' | 'location_permission_required' }. Needs: tick Background location (Access step → Background) and the user's location permission first.
function startTracking(token) {
const b = window.WebToApk;
if (!(b && b.startLocationUpdates)) return;
navigator.geolocation.getCurrentPosition(function () { // asks for location first
const r = JSON.parse(b.startLocationUpdates(JSON.stringify({
intervalMs: 10000, minDistanceM: 10, accuracy: 'high', background: true,
uploadUrl: 'https://api.example.com/track',
headers: { Authorization: 'Bearer ' + token }
})));
if (!r.ok) showMessage('Tracking not started: ' + r.reason);
});
}
window.addEventListener('appmint:location', function (e) {
if (e.detail.type === 'fix') drawPoint(e.detail.lat, e.detail.lng);
else showMessage('Location stopped: ' + e.detail.reason);
});
function stopTracking() { if (window.WebToApk) WebToApk.stopLocationUpdates(); }Notes: a location service started from the app keeps getting fixes in the background without "Allow all the time"; restarting it after a phone restart needs it (requestBackgroundLocation()). A POST that fails is handed to WorkManager and sent when the network is back. Phones without Google Play services answer play_services_unavailable. Play Console needs a background-location declaration.
updateBackgroundService bridge#
window.WebToApk.updateBackgroundService(optionsJson: String): String
Changes the running service's notification (same options as startBackgroundService) → JSON {ok} or {ok:false, reason:'not_running'}.
Example
Documented together with WebToApk.startBackgroundService - the example there shows this one too.
Keeps your app running while the user is away: a real Android foreground service with a notification. While it runs the page is not paused, so its JavaScript, <audio> and the app's background music carry on (Android still slows the timers of a page nobody can see).
Returns: startBackgroundService → a JSON string { ok: true, status: 'starting', type, notificationShown } or { ok: false, reason } - reason not_enabled, invalid_options, type_not_declared, invalid_progress, location_permission_required, start_not_allowed. updateBackgroundService → { ok: true } or { ok: false, reason: 'not_running' }. stopBackgroundService / isBackgroundServiceRunning → true / false. State changes arrive as appmint:background-service (and window.onAppMintBackgroundService(detail)): { state: 'running' | 'stopped' | 'failed' | 'not_restarted', reason?, types?, restored?, queued? }. Needs: tick Keep running in background (Access step → Background) and pick the service type. On Android 13+ the notification is only visible once the user allowed notifications (notificationShown: false otherwise - the service still runs).
function startWorkout() {
const b = window.WebToApk;
if (!(b && b.startBackgroundService)) return; // browser
const r = JSON.parse(b.startBackgroundService(JSON.stringify({
title: 'Workout in progress',
text: '00:00 elapsed',
stopLabel: 'Stop', // a Stop button on the notification
progress: { value: 0, max: 100 }
})));
if (!r.ok) showMessage('Could not keep running: ' + r.reason);
}
function tick(seconds) {
if (window.WebToApk && WebToApk.isBackgroundServiceRunning()) {
WebToApk.updateBackgroundService(JSON.stringify({
title: 'Workout in progress', text: seconds + ' s elapsed', stopLabel: 'Stop',
progress: { value: Math.min(100, seconds / 18), max: 100 }
}));
}
}
function finishWorkout() {
if (window.WebToApk && WebToApk.stopBackgroundService) WebToApk.stopBackgroundService();
}
window.addEventListener('appmint:background-service', function (e) {
// reason: 'user' (Stop button), 'page', 'timeout' (Android 15 dataSync 6-hour limit),
// 'task_removed' (app swiped away); after a phone restart, state 'not_restarted' with
// 'android_forbids_type_at_boot' | 'background_permission_required' | 'start_not_allowed'
if (e.detail.state === 'stopped') showMessage('Stopped (' + e.detail.reason + ')');
});Notes: the type must be one the build declared (the one picked in the wizard, plus location when Background location is on). Swiping the app away stops a keep-running service unless you pass keepAfterSwipe: true (the page itself is gone then). Play Console asks for a foreground-service declaration with a short video for every declared type.