Create Free APK

JavaScript bridge API

Launch, shortcuts and the back button

How the app was opened, long-press shortcuts on the icon, the in-app review prompt, and telling the shell the page wants the back press.

AppMint.requestReview helper#

AppMint.requestReview(options)

Example

Asks Google Play to show its own rating sheet inside the app (Play In-App Review) and tells you what happened.

Returns: a Promise that always resolves {result, openedStore, message?}. result is 'shown' (Play put its sheet up - it never says whether the user rated), 'not_from_play' (this install did not come from Google Play: a sideloaded APK or a test install, where Play never shows the sheet), 'quota' (Play chose not to show it now: its quota for this user, or they already reviewed) or 'error' (Play could not be asked: no Play Store, offline). With {openStore: true} the app's Play listing opens when the sheet did not show, and openedStore is true. Needs: nothing to switch on.

Any code:

// A "Rate us" button: the sheet when Play shows it, the Play listing otherwise.
document.getElementById('rate-btn').onclick = async function () {
  if (!(window.AppMint && AppMint.requestReview)) { location.href = 'https://play.google.com/store/apps/details?id=com.example.app'; return; }
  var r = await AppMint.requestReview({ openStore: true });
  if (r.result === 'shown' || r.openedStore) showToast('Thank you!');
};

Ask after a success moment instead of from a button, without ever leaving the app:

async function onOrderDelivered(count) {
  if (count !== 3 || !(window.AppMint && AppMint.requestReview)) return;
  var r = await AppMint.requestReview();      // no openStore: nothing opens if Play says no
  if (r.result === 'not_from_play') console.log('Test install: Play only shows the sheet for Play installs');
}

When the creator switched on Ask users to rate the app (Display step), the app asks by itself at a calm moment and tells the page what happened:

window.addEventListener('appmint:review', function (e) {
  // e.detail = { result, openedStore: false, source: 'auto' }
  analytics.track('rate_prompt', { result: e.detail.result });
});

Notes: Google Play rate-limits the sheet and decides by itself whether it appears; never reward a review or promise the sheet on a button. Test it with an internal-testing track install from Play - a sideloaded APK always answers not_from_play.

Without code: the appmint://review link. A button or link with this address asks for the same sheet (and opens the Play listing when Play will not show it, because the user asked to rate). In an in-app message, put it on a button in the send center (the Rate us template does); on a page, use it as a plain link:

<a href="appmint://review" class="rate-link">Rate us on Google Play</a>

From script, for example after the third finished order:

function maybeAskForRating(ordersDone) {
  if (ordersDone === 3) location.href = 'appmint://review';   // the app runs it; nothing is loaded
}

Hear what the user picked in a "Rate us" in-app message before the sheet opens:

window.addEventListener('appmint:inapp', function (e) {
  var d = e.detail;
  if (d.type === 'answered' && d.question === 'stars' && d.value <= 3) showFeedbackForm();
  if (d.type === 'clicked' && d.link === 'appmint://review') console.log('Opening the Play review sheet');
});

AppMint.setBackHandler helper#

AppMint.setBackHandler(handler)

Example

Gives your page the first say when the user presses the phone's Back button (or uses the back gesture). Needed by apps that change screens by showing and hiding elements, where Android's history is empty and Back would exit the app.

Returns: nothing. Your handler must return true (handled, stay here) or false (let the app go back / exit). Needs: nothing. Installed at document start, so you can register it in your first script.

if (window.AppMint && AppMint.setBackHandler) {
  AppMint.setBackHandler(function () {
    if (isMenuOpen)               { closeMenu();  return true; }   // consumed
    if (currentScreen !== 'home') { goTo('home'); return true; }
    return false;   // nothing to undo: normal Back (history back, exit dialog, exit)
  });
}

// Remove it again, for example when the screen that needed it closes:
// AppMint.setBackHandler(null);

The appmint:back event - cancelable. Call e.preventDefault() to consume the press. It runs before the handler, and any number of listeners can use it:

window.addEventListener('appmint:back', function (e) {
  var sheet = document.querySelector('.bottom-sheet.open');
  if (sheet) {
    sheet.classList.remove('open');
    e.preventDefault();   // consumed: the handler and the shell do nothing more
  }
});

Keep a half-written note from being lost: Back shows your own "Discard changes?" bar instead of leaving (a blocking confirm() would miss the 350 ms answer window, so the shell would go back anyway):

if (window.AppMint && AppMint.setBackHandler) {
  var noteDirty = false;
  document.getElementById('note').addEventListener('input', function () { noteDirty = true; });

  AppMint.setBackHandler(function () {
    if (!noteDirty) return false;        // nothing to lose: normal Back
    showDiscardBar();                    // your in-page "Discard changes? / Keep editing" bar
    return true;                         // stay here while the user decides
  });

  document.getElementById('discard-btn').onclick = function () {
    noteDirty = false;                   // the next Back press passes straight through
    hideDiscardBar();
    closeEditor();
  };
}

Notes: The answer must be synchronous: returning a Promise counts as "not handled". Answer quickly - if the page does not answer within about 350 ms, the shell goes back by itself. Order on each press: an open side menu (the app's own drawer) closes first and the page is not asked; then appmint:back listeners β†’ your handler β†’ the shell's built-in dialog closer (it closes an open <dialog>, ARIA dialog or full-screen modal) β†’ history back / exit confirmation / exit. Apps that use history.pushState for every screen (React Router and similar) do not need this. AI-built apps can use onBackGesture from @/lib/appmintNative.

__pullRefreshEligible bridge#

window.WebToApk.__pullRefreshEligible(eligible: Boolean)

See [pullPageEligible] / [pullProbeJs]. Called on every touchstart.

Example

Internal plumbing behind pull-to-refresh: the shell's own touch probe calls it on every touch to say whether a downward pull there should refresh the page. Pages never call it - they control pull-to-refresh with standard CSS and touch events.

Returns: nothing. Needs: Enable Pull-to-Refresh switched on in Step 3 (Integrate), under Display Options, when you build (without it the probe is not installed).

The public way to stop a pull from refreshing in part of your page - claim the gesture:

<style>
  /* A map, drawing canvas or slider: this element owns vertical drags. */
  #map, #canvas { touch-action: none; }
  /* A horizontal carousel: pan-x also tells the shell "not a refresh". */
  .carousel { touch-action: pan-x; }
</style>

Or claim it from a touch handler, the standard way:

document.getElementById('canvas').addEventListener('touchstart', function (e) {
  e.preventDefault();   // the page took this gesture: no pull-to-refresh
}, { passive: false });

Keep a pan-and-zoom surface from refreshing the page by claiming vertical drags with CSS only:

<div id="board" style="touch-action: none; height: 60vh"></div>
<script>
  // Dragging on #board moves the board; a pull from the top of the rest of the page still refreshes.
  document.getElementById('board').addEventListener('pointermove', function (e) { moveBoard(e); });
</script>

Notes: A pull only refreshes when the nearest scrollable box under the finger (or the page itself) is scrolled to the top, only one finger is down, and neither of the claims above applies. A touch inside a cross-origin iframe never refreshes. Do not call this method yourself.

getAppIcon bridge#

window.WebToApk.getAppIcon(): String

The launcher icon in use: "default" or one of the names from getAvailableIcons().

Example

The launcher icon in use: 'default' or one of the names from getAvailableIcons().

Returns: a string. 'default' in a build without alternate icons.

if (window.WebToApk && WebToApk.getAppIcon) {
  const current = WebToApk.getAppIcon();          // 'default', 'Dark', …
  document.querySelectorAll('[data-icon]').forEach(el => {
    el.classList.toggle('selected', el.dataset.icon === current);
  });
}

Switch to the next icon from a settings screen:

function nextIcon() {
  if (!(window.WebToApk && WebToApk.getAvailableIcons && WebToApk.setAppIcon)) return;
  const names = JSON.parse(WebToApk.getAvailableIcons());
  if (names.length < 2) return;                                  // no alternate icons in this build
  const next = names[(names.indexOf(WebToApk.getAppIcon()) + 1) % names.length];
  if (!WebToApk.setAppIcon(next)) alert('Could not change the icon.');
}

Show the icon name in a label, or hide it outside the app:

const label = document.getElementById('icon-label');
if (window.WebToApk && WebToApk.getAppIcon) {
  const name = WebToApk.getAppIcon();
  label.textContent = name === 'default' ? 'Classic icon' : name + ' icon';
} else {
  label.hidden = true;           // in a browser there is no launcher icon
}

getAvailableIcons bridge#

window.WebToApk.getAvailableIcons(): String

JSON array of the icon names setAppIcon accepts: "default" first, then the build's alternates.

Example

The icon names setAppIcon accepts, as a JSON array string: 'default' first, then the alternate icons from the build wizard in the order you added them.

Returns: '["default","Dark","Gold"]', or '["default"]' in a build without alternate icons.

Build an icon picker:

function iconPicker(container) {
  if (!(window.WebToApk && WebToApk.getAvailableIcons)) return;
  const names = JSON.parse(WebToApk.getAvailableIcons());
  if (names.length < 2) { container.hidden = true; return; }   // nothing to choose
  const current = WebToApk.getAppIcon();
  container.innerHTML = '';
  names.forEach(name => {
    const b = document.createElement('button');
    b.textContent = name === 'default' ? 'Classic' : name;
    b.className = name === current ? 'selected' : '';
    b.onclick = () => { if (WebToApk.setAppIcon(name)) iconPicker(container); };
    container.appendChild(b);
  });
}
iconPicker(document.querySelector('#iconPicker'));

Hide the picker in a build without alternate icons:

const picker = document.getElementById('iconPicker');
let names = ['default'];
if (window.WebToApk && WebToApk.getAvailableIcons) names = JSON.parse(WebToApk.getAvailableIcons());
picker.hidden = names.length < 2;      // only 'default' means there is nothing to choose

Switch the icon with the theme:

function matchIconToTheme(dark) {
  if (!(window.WebToApk && WebToApk.getAvailableIcons && WebToApk.setAppIcon)) return;
  const names = JSON.parse(WebToApk.getAvailableIcons());
  const wanted = dark ? 'Dark' : 'default';
  if (names.includes(wanted) && WebToApk.getAppIcon() !== wanted) WebToApk.setAppIcon(wanted);
}
matchIconToTheme(matchMedia('(prefers-color-scheme: dark)').matches);

getLaunchUrl bridge#

window.WebToApk.getLaunchUrl(): String

The deep link that launched or resumed this app, or "" if it was opened normally. Read it whenever your app mounts and route on it yourself: const u = window.WebToApk?.getLaunchUrl?.(); if (u) router.navigate(new URL(u).pathname); The value is KEPT, not consumed, so a late-mounting app still sees it; a later link arriving while the app runs replaces it and also fires 'appmint:deep-link'.

Example

The deep link that opened (or reopened) the app, as a string, or "" when the app was opened normally.

Returns: a string, synchronously. Needs: nothing. The value is kept, not consumed, so it is safe to read whenever your app mounts.

A link can arrive in three shapes: https://yoursite.com/path, the shell's own appmint-shortcut://<package>/<route> (launcher shortcuts, the home-screen widget), or your custom scheme myapp://segment/path. This helper turns all three into a route:

function routeOf(link) {
  var m = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]*)([^#]*)/i.exec(link || '');
  if (!m) return '/';
  var scheme = m[1].toLowerCase(), host = m[2], rest = m[3] || '';
  if (scheme === 'http' || scheme === 'https' || scheme === 'appmint-shortcut') return rest || '/';
  return '/' + host + rest;               // myapp://product/42 -> /product/42
}

// 1. On start: the link that launched the app.
if (window.WebToApk && typeof window.WebToApk.getLaunchUrl === 'function') {
  var link = window.WebToApk.getLaunchUrl();
  if (link) goTo(routeOf(link));
}

// 2. While running: a new link replaces the old one and fires 'appmint:deep-link'.
window.addEventListener('appmint:deep-link', function (e) {
  goTo(routeOf(e.detail.url));            // detail: { url }
});

The same announcement is also passed to a global hook, if you prefer one:

window.onAppMintDeepLink = function (detail) { goTo(routeOf(detail.url)); };

Read a referral code from a link such as https://yoursite.com/invite?code=AB12 or myapp://invite?code=AB12, once per link (it uses routeOf from above):

function applyInvite(link) {
  if (!link) return;
  var done = '';
  try { done = sessionStorage.getItem('handledLink') || ''; } catch (e) {}
  if (link === done) return;                      // the same url is announced again after a reload
  try { sessionStorage.setItem('handledLink', link); } catch (e) {}
  var code = null;
  try { code = new URL(link).searchParams.get('code'); } catch (e) { return; }
  if (code && routeOf(link).indexOf('/invite') === 0) showInviteBanner(code);
}

if (window.WebToApk && typeof window.WebToApk.getLaunchUrl === 'function') {
  applyInvite(window.WebToApk.getLaunchUrl());
  window.addEventListener('appmint:deep-link', function (e) { applyInvite(e.detail.url); });
}

Notes: appmint:deep-link is also fired after every page load while a link is kept, so the same url can arrive more than once. Ignore a link you already handled.

Website-mode apps are different: the shell itself loads an incoming https:// link (on a cold start and while running), and a custom-scheme link myapp://product/42 is first rewritten onto your site's address (https://yoursite.com/product/42), so getLaunchUrl() returns the https:// form and your page usually has nothing to route. A launcher shortcut or widget tap opens its route on your site (/today loads https://yoursite.com/today), and getLaunchUrl() still returns the appmint-shortcut:// form (see setAppShortcuts). In HTML, ZIP and AI-built apps (served from inside the app) the shell never navigates: your page gets the link and routes on it. A custom-scheme link stays as it arrived there, because there is no site address to map it onto. AI-built apps can use launchUrl() from @/lib/appmintNative.

rateApp bridge#

window.WebToApk.rateApp(): String

Opens the Play listing's rating flow. `rateApp()` already existed but only the native drawer entry could reach it, so an app that draws its own UI - every AI build, where the drawer is off - had no way to ask for a rating. It is the one store action a page genuinely cannot perform for itself: `market://` is an intent, not a navigable URL. Gated on the creator's own "Rate App Button" switch, exactly like the drawer entry and the bottom-nav Share item: turning Rate off must mean no rating surface anywhere, or the toggle looks broken.

Example

Opens the app's Google Play page so the user can rate it.

Returns: 'opened' when the listing was opened, 'off' when the creator switched Rate App off (then nothing happens). Needs: the Rate App Button switch in Step 5 (Menu) when you build (it is on by default; it also adds a "Rate App" item to the side menu). With it off, the call does nothing and returns 'off'. For Google Play's in-app rating sheet use AppMint.requestReview().

var rateBtn = document.getElementById('rate-btn');
if (window.WebToApk && typeof window.WebToApk.rateApp === 'function') {
  rateBtn.onclick = function () { window.WebToApk.rateApp(); };
} else {
  rateBtn.hidden = true;   // in a browser
}

Offer the rating once, after the user has finished five workouts, from a card they tap:

function maybeOfferRating(completedCount) {
  if (!window.WebToApk || typeof window.WebToApk.rateApp !== 'function') return;
  var asked = false;
  try { asked = localStorage.getItem('rateAsked') === '1'; } catch (e) {}
  if (asked || completedCount < 5) return;

  var card = document.getElementById('rate-card');   // "Enjoying the app? Rate it on Google Play"
  card.hidden = false;
  card.querySelector('.rate-yes').onclick = function () {
    window.WebToApk.rateApp();                         // opens the Play listing
    try { localStorage.setItem('rateAsked', '1'); } catch (e) {}
    card.hidden = true;
  };
  card.querySelector('.rate-later').onclick = function () { card.hidden = true; };
}

A "Rate us" row on your settings screen that says thank you when the user comes back from Google Play (the page's return to the foreground is the only signal that the user came back):

document.getElementById('settings-rate').onclick = function () {
  if (!window.WebToApk || typeof window.WebToApk.rateApp !== 'function') return;
  window.WebToApk.rateApp();
  document.addEventListener('visibilitychange', function onBack() {
    if (document.visibilityState !== 'visible') return;
    document.removeEventListener('visibilitychange', onBack);
    showToast('Thanks for taking a look!');           // your own toast; the user may not have rated
  });
};

Notes: It opens the Play Store listing (market://details?id=<package>), or the Play website when the Play Store app is missing. It is not the in-app review card. A page cannot open market:// itself - it is an Android intent, not a web address. Only ask after the user did something they liked, and from a tap.

requestReview bridge#

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

Asks Play for its review sheet; [optionsJson] `{openStore?}`. Resolves AppMint.requestReview [callbackId].

Example

Internal plumbing behind AppMint.requestReview() - pages use that Promise helper, which asks Google Play for its in-app review sheet and resolves with what happened.

Returns: nothing directly; the outcome resolves the AppMint.requestReview() promise. Needs: nothing to switch on. The sheet only ever appears for an app installed from Google Play.

The public API:

async function askForRating() {
  if (!(window.AppMint && AppMint.requestReview)) return;          // a browser: no store here
  var r = await AppMint.requestReview();
  console.log(r.result);   // 'shown' | 'not_from_play' | 'quota' | 'error'
}

Ask after a good moment, and ask for the store page when the sheet does not appear:

async function afterHappyMoment() {
  if (!(window.AppMint && AppMint.requestReview)) return;
  const r = await AppMint.requestReview({ openStore: true });   // {result, openedStore, message?}
  if (r.result === 'shown') thankUser();
  else if (r.openedStore) console.log('Opened the Play listing instead');
}

Listen for the automatic prompt and report an error:

window.addEventListener('appmint:review', (e) => {
  if (e.detail.source === 'auto') console.log('Rate prompt result:', e.detail.result);
});

AppMint.requestReview().then((r) => {
  if (r.result === 'error') console.warn(r.message);
  if (r.result === 'not_from_play') showMessage('Rating is available once the app is installed from Google Play.');
});

setAppIcon bridge#

window.WebToApk.setAppIcon(name: String): Boolean

Changes the app's launcher icon to one of the alternate icons added in the build wizard, by the name given there ("default" = the normal icon). Returns false for a name this build does not have. The launcher redraws in its own time (a few seconds on some phones), and some launchers close the app or move its home-screen icon when it changes - switch from a settings screen, not in the middle of a task.

Example

Changes the app's launcher icon to one of the alternate icons you added in the build wizard (Basic step, "Alternate app icons"), by the name you gave it there. 'default' is your normal icon.

Returns: true when the switch was made (or it is already that icon), false for a name this build does not have. Needs: alternate icons added in the wizard (up to 4). Some launchers take a few seconds to redraw the icon, and a few close the app or move its home-screen shortcut when it changes: switch from a settings screen, not in the middle of a task. Your launcher shortcuts (setAppShortcuts) are republished under the new icon automatically.

function useIcon(name) {
  if (!(window.WebToApk && window.WebToApk.setAppIcon)) return false;
  const ok = WebToApk.setAppIcon(name);
  if (!ok) console.warn('no icon called', name);
  return ok;
}

document.querySelector('#darkIcon').addEventListener('click', () => useIcon('Dark'));
document.querySelector('#normalIcon').addEventListener('click', () => useIcon('default'));

Build the picker from the icons this build has:

if (window.WebToApk && WebToApk.getAvailableIcons) {
  const names = JSON.parse(WebToApk.getAvailableIcons());   // 'default' first, then your alternates
  const current = WebToApk.getAppIcon();
  names.forEach((n) => addRadio('icon', n, n === current, () => WebToApk.setAppIcon(n)));
}

Check the answer for a name the build does not have:

function pickIcon(name) {
  if (!(window.WebToApk && WebToApk.setAppIcon)) return showMessage('Icons can be changed in the app only');
  if (!WebToApk.setAppIcon(name)) {
    showMessage('Unknown icon "' + name + '". Choose one of: ' + JSON.parse(WebToApk.getAvailableIcons()).join(', '));
  } else {
    showMessage('Done. Your launcher may take a few seconds to redraw the icon.');
  }
}

Notes: the first update that ADDS alternate icons moves the launcher entry to a new component, so a home-screen shortcut made before that update may have to be placed again.

setAppShortcuts bridge#

window.WebToApk.setAppShortcuts(itemsJson: String)

Launcher shortcuts (long-press the app icon). Up to four entries: `[{id, title, route}]`, where `route` is a path in the app ('/new-note'). A tap opens the app with that route as its launch deep link (`appmint-shortcut://<package><route>`), which the page reads through `getLaunchUrl()` and routes on exactly like any other deep link. In a Website-mode app the shell itself opens the route on the website (`/new-note` β†’ `https://<site>/new-note`). Pass `[]` to remove them. Android 7.1+; a no-op below that.

Example

Sets the launcher shortcuts that appear when the user touches and holds the app icon.

Returns: nothing. Needs: nothing. Android 7.1+ (a no-op below that).

itemsJson is a JSON string: an array of up to four {id, title, route} objects. route is a path inside your app. A tap opens the app with appmint-shortcut://<package><route> as its launch link:

if (window.WebToApk && typeof window.WebToApk.setAppShortcuts === 'function') {
  window.WebToApk.setAppShortcuts(JSON.stringify([
    { id: 'new-note', title: 'New note', route: '/new-note' },
    { id: 'search',   title: 'Search',   route: '/search' }
  ]));
}

Read the tap the same way as any other deep link, on start and while running:

function shortcutRoute(link) {
  var m = /^appmint-shortcut:\/\/[^\/?#]*([^#]*)/i.exec(link || '');
  return m ? (m[1] || '/') : null;
}
if (window.WebToApk && window.WebToApk.getLaunchUrl) {
  var r = shortcutRoute(window.WebToApk.getLaunchUrl());
  if (r) goTo(r);
}
window.addEventListener('appmint:deep-link', function (e) {
  var r = shortcutRoute(e.detail.url);
  if (r) goTo(r);
});

Remove all shortcuts:

if (window.WebToApk && window.WebToApk.setAppShortcuts) window.WebToApk.setAppShortcuts('[]');

Notes: Each call replaces the whole list. Entries without a title are skipped; the label is cut to 25 characters (50 for the long label). id defaults to appmint-<index>. A route without a leading / gets one. The icon is the app icon. A malformed list is ignored (logged), never thrown. AI-built apps can use shortcuts from @/lib/appmintNative. Website-mode apps: a tap opens the route on your website - /new-note loads https://<your site>/new-note - and getLaunchUrl() still returns the appmint-shortcut:// link, so a page that routes itself keeps working.

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.