# Launch, shortcuts and the back button - JavaScript bridge API

> 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.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/navigation

### `AppMint.requestReview`

```js
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:**

```js
// 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:

```js
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:

```js
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`.

### `AppMint.setBackHandler`

```js
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.

```js
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:

```js
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):

```js
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: `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`

```js
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 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:

```html
<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:

```js
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:

```html
<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.

### `getLaunchUrl`

```js
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:

```js
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:

```js
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):

```js
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.

```js
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. In a Website-mode app an `https://` link is loaded by the shell itself; offline and AI-built apps get the event and route on it themselves. AI-built apps can use `launchUrl()` from `@/lib/appmintNative`.

### `rateApp`

```js
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 on when you build (it is on by default). With it off, the call does nothing and returns `'off'`. For Google Play's in-app rating sheet use `AppMint.requestReview()`.

```js
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:

```js
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):

```js
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`

```js
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:

```js
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:**

```js
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:**

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

### `setAppShortcuts`

```js
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 `WebToApk.getLaunchUrl()` (and the `appmint:deep-link` event) 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:

```js
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:

```js
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:

```js
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`.

