Create Free APK

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');

Feature-detect and report every refusal:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

List what is registered and remove one:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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(); });

Start tracking, then handle a refusal. The start call answers with a JSON string.

function startTracking() {
  if (!(window.WebToApk && WebToApk.startLocationUpdates)) return;
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({
    intervalMs: 15000, accuracy: 'balanced', background: true
  })));
  if (!r.ok) showMessage('Tracking not started: ' + r.reason);   // not_enabled, location_permission_required, ...
}

Live fixes while the page is open arrive as appmint:location; the buffer holds the rest.

window.addEventListener('appmint:location', function (e) {
  drawPoint(e.detail.lat, e.detail.lng);
});
document.getElementById('stop').addEventListener('click', function () {
  if (window.WebToApk && WebToApk.stopLocationUpdates) WebToApk.stopLocationUpdates();
  if (WebToApk.getBufferedLocations) {
    const left = JSON.parse(WebToApk.getBufferedLocations());
    console.log(left.length + ' fixes still buffered');
  }
});

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();
}

Add a geofence, then watch for transitions while the page is open.

if (window.WebToApk && WebToApk.addGeofence) {
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'shop-1', lat: 12.9716, lng: 77.5946, radiusM: 150, events: ['enter', 'exit']
  })));
  if (!r.ok) showMessage('Geofence refused: ' + r.reason);   // not_enabled, invalid_radius, ...
}
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

List what is registered and remove one when the user leaves a feature.

function dropGeofence(id) {
  if (!(window.WebToApk && WebToApk.removeGeofence)) return false;
  const known = JSON.parse(WebToApk.getGeofences()).length;
  const removed = WebToApk.removeGeofence(id);
  console.log(removed ? 'removed' : 'not found', 'of', known, 'registered');
  return removed;
}

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(); });

Start tracking, then handle a refusal. The start call answers with a JSON string.

function startTracking() {
  if (!(window.WebToApk && WebToApk.startLocationUpdates)) return;
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({
    intervalMs: 15000, accuracy: 'balanced', background: true
  })));
  if (!r.ok) showMessage('Tracking not started: ' + r.reason);   // not_enabled, location_permission_required, ...
}

Live fixes while the page is open arrive as appmint:location; the buffer holds the rest.

window.addEventListener('appmint:location', function (e) {
  drawPoint(e.detail.lat, e.detail.lng);
});
document.getElementById('stop').addEventListener('click', function () {
  if (window.WebToApk && WebToApk.stopLocationUpdates) WebToApk.stopLocationUpdates();
  if (WebToApk.getBufferedLocations) {
    const left = JSON.parse(WebToApk.getBufferedLocations());
    console.log(left.length + ' fixes still buffered');
  }
});

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();
}

Add a geofence, then watch for transitions while the page is open.

if (window.WebToApk && WebToApk.addGeofence) {
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'shop-1', lat: 12.9716, lng: 77.5946, radiusM: 150, events: ['enter', 'exit']
  })));
  if (!r.ok) showMessage('Geofence refused: ' + r.reason);   // not_enabled, invalid_radius, ...
}
window.addEventListener('appmint:geofence', function (e) {
  logVisit(e.detail.id, e.detail.event, e.detail.time);
});

List what is registered and remove one when the user leaves a feature.

function dropGeofence(id) {
  if (!(window.WebToApk && WebToApk.removeGeofence)) return false;
  const known = JSON.parse(WebToApk.getGeofences()).length;
  const removed = WebToApk.removeGeofence(id);
  console.log(removed ? 'removed' : 'not found', 'of', known, 'registered');
  return removed;
}

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');

Feature-detect and report every refusal:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

List what is registered and remove one:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

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.');
});

Check the state first and explain what the user must tap:

const b = window.WebToApk;
if (b && b.getLocationPermissionState) {
  const s = JSON.parse(b.getLocationPermissionState());
  if (s.foreground !== 'granted') showMessage('Location is off for this app.');
  else if (s.background !== 'granted') showMessage('Tap "Allow all the time" to enable arrival reminders.');
  else if (!s.precise) showMessage('Precise location is off; arrival alerts may be late.');
}

Request the permission, then add a geofence once it is granted:

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.error === 'not_enabled') return showMessage('This build has no background location.');
  if (e.detail.background !== 'granted') return;
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'home', lat: 51.5033, lng: -0.1196, radiusM: 120, events: ['enter']
  })));
  if (!r.ok) showMessage('Could not add the place: ' + r.reason);
}, { once: true });
WebToApk.requestBackgroundLocation();

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 }))
}

Feature-detect and tell the app run from the background run:

const b = window.WebToApk;
if (!(b && b.backgroundRegister)) {
  console.log('background tasks are only in the app');
} else if (b.isBackgroundRun()) {
  // the invisible copy: do the work, no UI
} else {
  const r = JSON.parse(b.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 5 })));
  if (!r.ok) console.warn(r.reason);                // 'interval_below_15_minutes'
}

Test a task and clean up:

const run = JSON.parse(WebToApk.backgroundRunNow('sync'));
if (!run.ok && run.reason === 'not_registered') WebToApk.backgroundRegister('sync', JSON.stringify({ intervalMinutes: 30 }));
JSON.parse(WebToApk.backgroundList()).forEach(function (t) { console.log(t.name, t.intervalMinutes, t.lastResult); });
WebToApk.backgroundUnregister('old-task');           // true if it existed

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 + ')');
});

Handle the answers that mean the build or the phone cannot do it:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine it with an upload loop: keep running only while the queue has work.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

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".');
});

Say what happened for each answer.

function askBatteryExemption() {
  const b = window.WebToApk;
  if (!(b && b.requestIgnoreBatteryOptimizations)) { showMessage('Only available in the app'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  const text = {
    already_ignoring: 'Already unrestricted',
    dialog_opened: 'Please tap Allow',
    settings_opened: 'Choose "Not optimised" for this app',
    unavailable: 'This phone has no such setting'
  };
  showMessage(text[how] || how);
}

Pair it with background tracking, since Doze would pause it.

if (window.WebToApk && WebToApk.startLocationUpdates) {
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({ intervalMs: 30000 })));
  if (r.ok && !WebToApk.isIgnoringBatteryOptimizations()) {
    showMessage('For steady tracking, turn off battery optimisation for this app.');
  }
}

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');

Feature-detect and report every refusal:

function addCircle(id, lat, lng, radiusM) {
  const b = window.WebToApk;
  if (!(b && b.addGeofence)) { showMessage('Place alerts need the app'); return false; }
  const r = JSON.parse(b.addGeofence(JSON.stringify({ id: id, lat: lat, lng: lng, radiusM: radiusM, events: ['enter'] })));
  if (r.ok) return true;
  if (r.reason === 'not_enabled') showMessage('Geofences are not switched on in this build');
  else if (r.reason === 'background_permission_required') b.requestBackgroundLocation();
  else if (r.reason === 'invalid_radius') showMessage('Pick a radius between 1 m and 100 km');
  else showMessage('Could not add the place: ' + r.reason);
  return false;
}

List what is registered and remove one:

const mine = JSON.parse(WebToApk.getGeofences());          // [] when none
mine.forEach(function (g) { addRow(g.id); });
document.getElementById('forget').onclick = function () {
  const existed = WebToApk.removeGeofence('office');       // true if it was there
  if (!existed) showMessage('That place was not registered');
};

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.');
});

Check the state first and explain what the user must tap:

const b = window.WebToApk;
if (b && b.getLocationPermissionState) {
  const s = JSON.parse(b.getLocationPermissionState());
  if (s.foreground !== 'granted') showMessage('Location is off for this app.');
  else if (s.background !== 'granted') showMessage('Tap "Allow all the time" to enable arrival reminders.');
  else if (!s.precise) showMessage('Precise location is off; arrival alerts may be late.');
}

Request the permission, then add a geofence once it is granted:

window.addEventListener('appmint:location-permission', function (e) {
  if (e.detail.error === 'not_enabled') return showMessage('This build has no background location.');
  if (e.detail.background !== 'granted') return;
  const r = JSON.parse(WebToApk.addGeofence(JSON.stringify({
    id: 'home', lat: 51.5033, lng: -0.1196, radiusM: 120, events: ['enter']
  })));
  if (!r.ok) showMessage('Could not add the place: ' + r.reason);
}, { once: true });
WebToApk.requestBackgroundLocation();

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".');
});

Say what happened for each answer.

function askBatteryExemption() {
  const b = window.WebToApk;
  if (!(b && b.requestIgnoreBatteryOptimizations)) { showMessage('Only available in the app'); return; }
  const how = b.requestIgnoreBatteryOptimizations();
  const text = {
    already_ignoring: 'Already unrestricted',
    dialog_opened: 'Please tap Allow',
    settings_opened: 'Choose "Not optimised" for this app',
    unavailable: 'This phone has no such setting'
  };
  showMessage(text[how] || how);
}

Pair it with background tracking, since Doze would pause it.

if (window.WebToApk && WebToApk.startLocationUpdates) {
  const r = JSON.parse(WebToApk.startLocationUpdates(JSON.stringify({ intervalMs: 30000 })));
  if (r.ok && !WebToApk.isIgnoringBatteryOptimizations()) {
    showMessage('For steady tracking, turn off battery optimisation for this app.');
  }
}

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 + ')');
});

Handle the answers that mean the build or the phone cannot do it:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine it with an upload loop: keep running only while the queue has work.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

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(); }

Report a bad option or a missing permission with the answer's reason:

function tryTrack() {
  const b = window.WebToApk;
  if (!(b && typeof b.startLocationUpdates === 'function')) {
    showMessage('Background tracking needs the installed app.');
    return;
  }
  const r = JSON.parse(b.startLocationUpdates(JSON.stringify({ intervalMs: 500 })));
  if (!r.ok && r.reason === 'invalid_interval') showMessage('Interval must be 1 second or more.');
  if (!r.ok && r.reason === 'location_permission_required') showMessage('Allow location first.');
}

A natural pair with the buffer: stop at the end of a shift and upload what was recorded while the page was away.

async function endShift() {
  if (!window.WebToApk) return;
  WebToApk.stopLocationUpdates();
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (points.length) {
    await fetch('/api/shift/points', { method: 'POST', body: JSON.stringify(points) });
    WebToApk.clearBufferedLocations();
  }
}

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 + ')');
});

Handle the answers that mean the build or the phone cannot do it:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine it with an upload loop: keep running only while the queue has work.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

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(); }

Report a bad option or a missing permission with the answer's reason:

function tryTrack() {
  const b = window.WebToApk;
  if (!(b && typeof b.startLocationUpdates === 'function')) {
    showMessage('Background tracking needs the installed app.');
    return;
  }
  const r = JSON.parse(b.startLocationUpdates(JSON.stringify({ intervalMs: 500 })));
  if (!r.ok && r.reason === 'invalid_interval') showMessage('Interval must be 1 second or more.');
  if (!r.ok && r.reason === 'location_permission_required') showMessage('Allow location first.');
}

A natural pair with the buffer: stop at the end of a shift and upload what was recorded while the page was away.

async function endShift() {
  if (!window.WebToApk) return;
  WebToApk.stopLocationUpdates();
  const points = JSON.parse(WebToApk.getBufferedLocations());
  if (points.length) {
    await fetch('/api/shift/points', { method: 'POST', body: JSON.stringify(points) });
    WebToApk.clearBufferedLocations();
  }
}

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 + ')');
});

Handle the answers that mean the build or the phone cannot do it:

function startKeepAlive() {
  const b = window.WebToApk;
  if (!(b && typeof b.startBackgroundService === 'function')) return;
  const r = JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Syncing', text: 'Uploading photos' })));
  if (r.ok) return;
  if (r.reason === 'not_enabled') showMessage('Keep running in background is not on in this build.');
  else if (r.reason === 'type_not_declared') showMessage('This build declares no service type.');
  else showMessage('Could not start: ' + r.reason);
}

Combine it with an upload loop: keep running only while the queue has work.

async function uploadAll(files) {
  const b = window.WebToApk;
  const keep = b && b.startBackgroundService &&
    JSON.parse(b.startBackgroundService(JSON.stringify({ title: 'Uploading', text: '0 of ' + files.length }))).ok;
  for (let i = 0; i < files.length; i++) {
    await upload(files[i]);
    if (keep) b.updateBackgroundService(JSON.stringify({ title: 'Uploading', text: (i + 1) + ' of ' + files.length }));
  }
  if (keep) b.stopBackgroundService();
}

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.

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.