# Ads - JavaScript bridge API

> Showing an interstitial or a rewarded ad on your own cue, and telling the shell when a navigation starts and settles so it does not interrupt one.

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

### `AppMintAds.appOpen`

```js
window.AppMintAds.appOpen.setEnabled(on)
```

**Example**

Pauses or resumes the app-open ad, the full-screen ad shown at launch and when the user returns from another app.

**Returns:** nothing. **Needs:** **Ad Control → Developer-controlled** and an app-open ad unit ID.

Pause it while the user leaves the app on purpose for a short moment (a payment app, the camera, a share sheet), so coming back does not show an ad:

```js
var ads = window.AppMintAds;

async function payWithUpi(link) {
  if (ads) ads.appOpen.setEnabled(false);
  try {
    await openPaymentApp(link);           // your own code; the user switches apps and returns
  } finally {
    if (ads) ads.appOpen.setEnabled(true);
  }
}
```

Check whether the build has an app-open unit at all:

```js
var hasAppOpen = !!(window.AppMintAds && window.AppMintAds.config().formats.app_open);
```

**Notes:** It is on by default. At cold start the ad shows when the page is ready, or when it finishes loading within 15 seconds; a later load is kept for next time. An app-open ad older than 4 hours is never shown. You can also show one yourself with `AppMintAds.show('app_open')`.

### `AppMintAds.banner`

```js
window.AppMintAds.banner.attach(target, opts) / .detach(slotOrElement)
```

**Example**

Places a banner or a 300×250 medium-rectangle ad anywhere in your page, on top of an element you choose.

**Returns:** `attach` returns a Promise of `{ok:true, slot, width, height}` (CSS px) once the ad has loaded, or `{ok:false, slot?, code, message?}`. `detach` returns nothing. **Needs:** **Ad Control → Developer-controlled** and a banner (or medium rectangle) ad unit ID.

The element only reserves the space. The ad is drawn over it and follows it when the page scrolls or resizes:

```html
<div id="ad-slot"></div>

<script>
var ads = window.AppMintAds;
var slotEl = document.getElementById('ad-slot');

if (ads) {
  ads.banner.attach(slotEl, { size: 'banner' }).then(function (res) {
    // On success the helper sets slotEl.style.minHeight to res.height (pass keepHeight:true to stop that).
    if (!res.ok) {                         // no_fill, network, too_many_slots, ...
      if (res.slot) ads.banner.detach(res.slot);   // a failed slot still counts toward the limit of two
      slotEl.hidden = true;
    }
  });
} else {
  slotEl.hidden = true;                    // browser: no ads here
}
</script>
```

**Medium rectangle** (one at a time), and removing a banner again:

```js
var box = document.querySelector('.article-end-ad');
var mrecSlot = null;

if (window.AppMintAds) {
  window.AppMintAds.banner.attach(box, { size: 'medium_rectangle' }).then(function (res) {
    if (res.ok) mrecSlot = res.slot;
    else { if (res.slot) window.AppMintAds.banner.detach(res.slot); box.hidden = true; }
  });
}

function leaveArticle() {
  if (window.AppMintAds && mrecSlot) window.AppMintAds.banner.detach(mrecSlot);   // or detach(box)
  mrecSlot = null;
}
```

**Notes:** `target` is an element or a CSS selector; `size` is `'banner'` (default, adaptive width, at most 90 dp tall), `'medium_rectangle'` or `'mrec'`. At most two slots at once, and a slot whose ad failed to load still counts until you detach it. `detach` needs the element or the slot id from `attach`, not a selector. Removing the element from the DOM detaches it too. Codes: `no_element`, `duplicate_slot`, `too_many_slots`, `detached`, `no_fill`, `network`, `invalid_unit`, `ad_free`, `disabled`, `consent`, `automatic_mode`, `internal`.

### `AppMintAds.config`

```js
window.AppMintAds.config()
```

**Example**

Tells the page which ad control mode the build uses and which ad formats it can serve.

**Returns:** an object, synchronously: `{control: 'automatic' | 'developer', formats: {banner, mrec, interstitial, rewarded, rewarded_interstitial, app_open}}` (each format `true` / `false`). On an internal error it returns `{control:'automatic', formats:{}}`. **Needs:** nothing; `window.AppMintAds` exists only inside the installed app.

Show only the ad UI this build can really serve:

```js
var ads = window.AppMintAds;               // undefined in a browser
var cfg = ads ? ads.config() : null;

var developer = !!(cfg && cfg.control === 'developer');
document.getElementById('watch-ad').hidden = !(developer && cfg.formats.rewarded);
document.getElementById('ad-slot').hidden  = !(developer && cfg.formats.banner);
```

**Notes:** A format is `true` when ads are on and the build has an ad unit ID for it. In Automatic mode (`control: 'automatic'`) every `load` / `show` / `banner.attach` call answers `code: 'automatic_mode'`; use `WebToApk.showRewardedAd()` / `showInterstitialAd()` there. Choose **Ad Control → Developer-controlled** in the build wizard to drive ads from the page.

### `AppMintAds.isReady`

```js
window.AppMintAds.isReady(type)
```

**Example**

Tells you whether a full-screen ad of this type is loaded and can be shown right now.

**Returns:** `true` / `false`, synchronously. `type`: `'interstitial'`, `'rewarded'`, `'rewarded_interstitial'` or `'app_open'`. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for the type.

Keep a "Watch ad" button in step with the ad, using the `appmint:ad` status event:

```js
var ads = window.AppMintAds;
var btn = document.getElementById('watch-ad');

function refresh() { btn.disabled = !(ads && ads.isReady('rewarded')); }

if (ads) {
  refresh();
  window.addEventListener('appmint:ad', function (e) {
    if (e.detail.type === 'rewarded') refresh();    // phases: loaded, failed, closed, ...
  });
} else {
  btn.hidden = true;                                // browser: no ads here
}
```

**Notes:** `banner` and `mrec` always answer `false` (they are attached, not shown). An interstitial in its 60-second cooldown answers `false`; an app-open ad older than 4 hours answers `false`. In Automatic mode this is always `false`.

### `AppMintAds.load`

```js
window.AppMintAds.load(type)
```

**Example**

Loads a full-screen ad so that a later `show(type)` can play it at once.

**Returns:** a Promise that resolves (never rejects) with `{ok:true}`, `{ok:true, cached:true}` when one is already waiting, or `{ok:false, code, reason, message?, adCode?}`. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for the type (`interstitial`, `rewarded`, `rewarded_interstitial`, `app_open`).

```js
var ads = window.AppMintAds;
var btn = document.getElementById('watch-ad');
btn.disabled = true;

async function prepareRewarded() {
  if (!ads) { btn.hidden = true; return; }          // browser: no ads
  var res = await ads.load('rewarded');
  if (res.ok) { btn.disabled = false; return; }
  if (res.code === 'no_fill' || res.code === 'network') setTimeout(prepareRewarded, 30000);
  else console.log('rewarded ad unavailable:', res.code, res.message);
}
prepareRewarded();
```

The whole life of every request also streams on the `appmint:ad` event:

```js
window.addEventListener('appmint:ad', function (e) {
  var a = e.detail;   // {type, phase, slot?, code?, adCode?, message?, width?, height?, reward?, earned?}
  // phase: loading, loaded, failed, opened, impression, clicked, closed, rewarded, show_failed, opt_in, declined
  if (a.phase === 'failed') console.log(a.type, 'failed to load:', a.code, a.adCode, a.message);
});
// The same object also goes to window.onAppMintAd, if you define that function.
window.onAppMintAd = function (a) { console.log('ad', a.type, a.phase); };
```

**Notes:** Codes: `no_fill`, `network`, `invalid_unit`, `internal` (from AdMob), `automatic_mode`, `unknown_type`, `ad_free`, `disabled`, `consent`, and `use_slot` for `banner` / `mrec` (use `AppMintAds.banner.attach`). The app already preloads each full-screen type with a unit ID and loads the next one after every show, so `load` is mostly for knowing when the button may be enabled.

### `AppMintAds.show`

```js
window.AppMintAds.show(type)
```

**Example**

Shows a loaded full-screen ad: `interstitial`, `rewarded`, `rewarded_interstitial` or `app_open`.

**Returns:** a Promise that resolves (never rejects) when the ad has closed: `{ok:true, shown:true, earned, reward?}` (`reward` = `{type, amount}` for the rewarded formats), or `{ok:false, shown:false, code, reason, message?}` when nothing was shown. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for the type.

**Rewarded:** call it inside the user's own tap. That tap is the user's opt-in, so the ad plays at once. Grant only when `earned` is true:

```js
var ads = window.AppMintAds;

document.getElementById('watch-ad').addEventListener('click', async function () {
  if (!ads) return;                                  // browser: no ads here
  var res = await ads.show('rewarded');
  if (res.earned) addCoins(res.reward.amount);
  else if (res.reason === 'ad_free') addCoins(50);   // user paid to remove ads: reward anyway
  else if (res.shown) showMessage('Watch to the end to get the reward.');
  else if (res.reason !== 'declined') showMessage('No ad available right now.');
});
```

**Interstitial:** at a natural break. Continue whatever the answer is:

```js
async function levelFinished() {
  if (window.AppMintAds && window.AppMintAds.isReady('interstitial')) {
    await window.AppMintAds.show('interstitial');    // resolves when the ad closes
  }
  showNextLevel();
}
```

**Notes:** A rewarded or rewarded-interstitial `show()` that does NOT come from a user tap (a timer, page load, route change) first gets a "Watch an ad? / No thanks" screen, and "No thanks" answers `declined`. Never trigger rewarded formats that way. Other codes: `automatic_mode`, `unknown_type`, `ad_free`, `disabled`, `consent`, `not_full_screen`, `showing`, `cooldown` (60 s between interstitials), `not_loaded` (a new load starts for next time), `show_failed`, `internal`. In Automatic mode use `WebToApk.showRewardedAd()`.

### `adsAppOpenEnable`

```js
window.WebToApk.adsAppOpenEnable(enabled: Boolean)
```

Lets the page pause/resume the app-open ad on return from another app (on by default when a unit is set).

**Example**

The native half of `AppMintAds.appOpen.setEnabled(on)`: it pauses or resumes the app-open ad. Pages use `AppMintAds.appOpen.setEnabled`.

**Returns:** nothing. **Needs:** **Ad Control → Developer-controlled** and an app-open ad unit ID.

```js
function setAppOpenAds(on) {
  if (window.AppMintAds) window.AppMintAds.appOpen.setEnabled(on);
}

// Pause while the user is in the middle of something, resume afterwards.
function startCheckout() { setAppOpenAds(false); showCheckout(); }
function checkoutDone()  { setAppOpenAds(true); }
```

**Notes:** Raw signature `adsAppOpenEnable(enabled)`. On by default when an app-open unit is set. The app-open ad shows at cold start (if it loads within 15 seconds) and when the user comes back from another app; an ad older than 4 hours is never shown.

### `adsConfig`

```js
window.WebToApk.adsConfig(): String
```

`{control, formats:{banner, mrec, interstitial, rewarded, rewarded_interstitial, app_open}}` - the ad formats this build can serve.

**Example**

Reads which ad formats this build can serve and which ad control mode it uses. Pages normally call `AppMintAds.config()`, which parses this for you.

**Returns:** a JSON string: `{"control":"automatic"|"developer","formats":{"banner":bool,"mrec":bool,"interstitial":bool,"rewarded":bool,"rewarded_interstitial":bool,"app_open":bool}}`. A format is `true` when ads are on and the build has an ad unit ID for it. **Needs:** nothing.

The public API first:

```js
if (window.AppMintAds) {
  var cfg = window.AppMintAds.config();      // already parsed; {control:'automatic', formats:{}} on error
  if (cfg.control === 'developer' && cfg.formats.rewarded) {
    document.getElementById('watch-ad').hidden = false;
  }
}
```

The raw call, if you do not use `AppMintAds`:

```js
var b = window.WebToApk;
if (b && typeof b.adsConfig === 'function') {
  try {
    var cfg = JSON.parse(b.adsConfig());
    console.log(cfg.control, cfg.formats);
  } catch (e) { console.log('could not read ad config', e); }
}
```

**Notes:** In Automatic mode `formats` still tells you which units exist, but `AppMintAds.load/show` answer `code: 'automatic_mode'`; use `WebToApk.showRewardedAd()` / `showInterstitialAd()` there.

### `adsIsReady`

```js
window.WebToApk.adsIsReady(type: String): Boolean
```

True when a full-screen [type] is loaded and can be shown right now.

**Example**

Tells you whether a full-screen ad of one type is loaded and can show now (Developer-controlled ads). Pages normally call `AppMintAds.isReady(type)`.

**Returns:** `true` / `false`, synchronously. `type` is `'interstitial'`, `'rewarded'`, `'rewarded_interstitial'` or `'app_open'`; `'banner'` / `'mrec'` always answer `false`. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for that type.

The public API first:

```js
var watchBtn = document.getElementById('watch-ad');
function refresh() {
  watchBtn.disabled = !(window.AppMintAds && window.AppMintAds.isReady('rewarded'));
}
refresh();
window.addEventListener('appmint:ad', refresh);     // loaded / closed / failed all change readiness
```

The raw call:

```js
var b = window.WebToApk;
var ready = !!(b && typeof b.adsIsReady === 'function' && b.adsIsReady('interstitial'));
```

**Notes:** An interstitial inside its 60-second cooldown answers `false`. An app-open ad older than 4 hours answers `false` (Google says not to show it). In Automatic mode this is always `false`; use `WebToApk.isRewardedAdReady()` there.

### `adsLoad`

```js
window.WebToApk.adsLoad(type: String, callbackId: String)
```

Loads a full-screen [type]; the promise for [callbackId] resolves with the outcome.

**Example**

The native half of `AppMintAds.load(type)`: it starts loading a full-screen ad and answers the shim's callback. Pages use `AppMintAds.load(type)`, which returns a Promise; do not call this directly.

**Returns:** (through `AppMintAds.load`) a Promise of `{ok:true}`, `{ok:true, cached:true}` when one is already loaded, or `{ok:false, code, reason, message?, adCode?}`. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for the type.

```js
var watchBtn = document.getElementById('watch-ad');
watchBtn.disabled = true;

if (window.AppMintAds) {
  window.AppMintAds.load('rewarded').then(function (res) {
    if (res.ok) watchBtn.disabled = false;
    else console.log('rewarded ad not loaded:', res.code, res.message);
  });
} else {
  watchBtn.hidden = true;                       // browser: there are no ads here
}
```

**Notes:** Failure codes: `no_fill`, `network`, `invalid_unit`, `internal` (from AdMob, with `adCode`), and `automatic_mode`, `unknown_type`, `ad_free`, `disabled`, `consent`, and `use_slot` for `banner` / `mrec` (those load with `AppMintAds.banner.attach`). The app already preloads every full-screen type that has a unit ID, and reloads after each show. The raw signature is `adsLoad(type, callbackId)`; its answer goes to an internal resolver, so a direct call gives you no result.

### `adsShow`

```js
window.WebToApk.adsShow(type: String, callbackId: String, userGesture: Boolean)
```

Shows a loaded full-screen [type]; resolves when it closes, with earned/reward for rewarded formats. [userGesture] is the shim's `navigator.userActivation` reading - a rewarded format asked for without one gets AdMob's intro screen first (see DeveloperAds.offerRewarded).

**Example**

The native half of `AppMintAds.show(type)`: it shows a loaded full-screen ad and answers when it closes. Pages use `AppMintAds.show(type)`; do not call this directly.

**Returns:** (through `AppMintAds.show`) a Promise of `{ok:true, shown:true, earned, reward?}` after the ad closes (`reward` = `{type, amount}` for rewarded formats), or `{ok:false, shown:false, code, reason, message?}`. **Needs:** **Ad Control → Developer-controlled** and an ad unit ID for the type.

Call it from the user's tap. The shim passes `navigator.userActivation.isActive` as the third argument (`userGesture`):

```js
document.getElementById('watch-ad').addEventListener('click', function () {
  if (!window.AppMintAds) return;
  window.AppMintAds.show('rewarded').then(function (res) {
    if (res.earned) addCoins(res.reward.amount);
    else if (res.reason === 'ad_free') addCoins(50);      // paid to remove ads: reward anyway
    else if (res.reason !== 'declined') showMessage('No ad available right now.');
  });
});
```

**Notes:** Raw signature `adsShow(type, callbackId, userGesture)`. When `userGesture` is `false` for `rewarded` or `rewarded_interstitial`, the app first asks "Watch an ad? / No thanks" and a "no" answers `declined`. Other codes: `automatic_mode`, `unknown_type`, `ad_free`, `disabled`, `consent`, `not_full_screen`, `showing`, `cooldown` (interstitial, 60 s), `not_loaded` (a load starts for next time), `show_failed`, `internal`.

### `adsSlotAttach`

```js
window.WebToApk.adsSlotAttach(slotId: String, type: String, widthCss: Int, callbackId: String)
```

Creates a banner/mrec overlay for the page's slot [slotId], sized to [widthCss]; resolves with the ad's size once loaded.

**Example**

The native half of `AppMintAds.banner.attach(element, {size})`: it creates a banner or medium-rectangle ad over a page element. Pages use `AppMintAds.banner.attach`; do not call this directly.

**Returns:** (through `AppMintAds.banner.attach`) a Promise of `{ok:true, slot, width, height}` in CSS pixels once the ad has loaded, or `{ok:false, slot, code, message?}`. **Needs:** **Ad Control → Developer-controlled** and a banner (or medium rectangle) ad unit ID.

```html
<div id="ad-slot"></div>

<script>
if (window.AppMintAds) {
  window.AppMintAds.banner.attach('#ad-slot', { size: 'banner' }).then(function (res) {
    if (!res.ok) console.log('no banner:', res.code);   // e.g. no_fill, network, too_many_slots
  });
}
</script>
```

**Notes:** Raw signature `adsSlotAttach(slotId, type, widthCss, callbackId)`; `type` is `'banner'` or `'mrec'`. A WebView cannot hold a native view inside the page, so the ad is drawn over your element and follows it on scroll and resize (see `adsSlotRect`). At most two slots at once, and only one medium rectangle. Extra codes: `duplicate_slot`, `too_many_slots`, `detached`, `internal`, plus the load codes (`no_fill`, `network`, `invalid_unit`, `ad_free`, `disabled`, `consent`, `automatic_mode`).

### `adsSlotDetach`

```js
window.WebToApk.adsSlotDetach(slotId: String)
```

Removes a banner overlay.

**Example**

The native half of `AppMintAds.banner.detach(slotOrElement)`: it removes a banner overlay and frees its slot. Pages use `AppMintAds.banner.detach`.

**Returns:** nothing. **Needs:** **Ad Control → Developer-controlled**.

Remove the banner before a screen where an ad does not belong (a video player, a checkout):

```js
var slotEl = document.getElementById('ad-slot');

function openPlayer() {
  if (window.AppMintAds) window.AppMintAds.banner.detach(slotEl);   // the element, or the slot id from attach()
  showPlayer();
}

function closePlayer() {
  hidePlayer();
  if (window.AppMintAds) window.AppMintAds.banner.attach(slotEl, { size: 'banner' });
}
```

**Notes:** Raw signature `adsSlotDetach(slotId)`. `detach` takes the element or the `slot` string that `attach` returned, not a CSS selector. An `attach` Promise still waiting for its ad answers `{ok:false, code:'detached'}`. Removing the element from the DOM also detaches the slot.

### `adsSlotRect`

```js
window.WebToApk.adsSlotRect(slotId: String, x: Float, y: Float, w: Float, h: Float, visible: Boolean)
```

Internal: the slot element's current viewport rectangle (CSS px) - moves the overlay.

**Example**

Internal: moves a banner overlay to its element's current position. The `AppMintAds.banner` helper calls it for you on scroll, resize, visibility change and every 500 ms; pages use `AppMintAds.banner.attach` and never call this.

**Returns:** nothing. **Needs:** **Ad Control → Developer-controlled**, and a slot created by `AppMintAds.banner.attach`.

All a page does is give the slot element a place in the layout. The helper measures it and keeps the ad on top of it:

```html
<main>
  <article id="story">…</article>
  <div id="ad-slot" style="margin: 16px 0"></div>   <!-- the ad follows this box while scrolling -->
</main>

<script>
if (window.AppMintAds) window.AppMintAds.banner.attach('#ad-slot', { size: 'banner' });
</script>
```

**Notes:** Raw signature `adsSlotRect(slotId, x, y, w, h, visible)`, in CSS pixels relative to the viewport. The overlay is hidden while the element is off screen, has zero size, has `visibility: hidden`, or the page is in the background. If the element leaves the DOM, the helper detaches the slot by itself.

### `isInterstitialAdReady`

```js
window.WebToApk.isInterstitialAdReady(): Boolean
```

True when an interstitial is loaded AND past its cooldown (matches exactly what showInterstitialAd() will accept - no "ready" that then refuses to show).

**Example**

Tells you whether an interstitial ad is loaded AND past its 60-second cooldown, so `showInterstitialAd()` would show it now (Automatic ad control).

**Returns:** `true` / `false`, synchronously. **Needs:** Ads on and **Full Screen Ad → Interstitial Ad**.

```js
function onLevelDone() {
  var b = window.WebToApk;
  if (b && typeof b.isInterstitialAdReady === 'function' && b.isInterstitialAdReady()) {
    b.showInterstitialAd();               // wait for 'appmint:ad-closed' before continuing
  } else {
    startNextLevel();                     // nothing ready: never block the user
  }
}

window.addEventListener('appmint:ad-closed', function (e) {
  if (e.detail.adType === 'interstitial') startNextLevel();
});
```

**Notes:** `true` here means the show will not be refused for `not_loaded` or `cooldown`. Consent or a "Remove Ads" purchase can still stop it (`appmint:ad-unavailable`). In **Developer-controlled** builds use `AppMintAds.isReady('interstitial')`.

### `isRewardedAdReady`

```js
window.WebToApk.isRewardedAdReady(): Boolean
```

True when a rewarded ad is loaded and can be shown right now. Let the page enable/disable its "Watch ad" button off this (poll or call before showing).

**Example**

Tells you whether a rewarded ad is loaded and can be shown right now (Automatic ad control).

**Returns:** `true` / `false`, synchronously. **Needs:** Ads on and **Full Screen Ad → Rewarded Ad**.

Enable the "Watch an ad" button only when an ad is waiting:

```js
var btn = document.getElementById('earn');

function refreshWatchButton() {
  var b = window.WebToApk;
  var ready = !!(b && typeof b.isRewardedAdReady === 'function' && b.isRewardedAdReady());
  btn.disabled = !ready;
  btn.textContent = ready ? 'Watch an ad for 10 coins' : 'Loading ad…';
}
refreshWatchButton();
setInterval(refreshWatchButton, 2000);                 // an ad can finish loading at any time
window.addEventListener('appmint:ad-closed', refreshWatchButton);
```

**Notes:** Returns `false` in a browser, when ads were removed by purchase, and before the first ad has loaded. After an ad is shown the next one loads by itself. In **Developer-controlled** builds use `AppMintAds.isReady('rewarded')`.

### `navSettled`

```js
window.WebToApk.navSettled()
```

Companion to [navStarted]: the swapped-in view has painted.

**Example**

Tells the app that the new screen has painted, so it can end the top loading bar that `navStarted()` began.

**Returns:** nothing. **Needs:** nothing.

```js
function withLoadingBar(work) {
  var b = window.WebToApk;
  var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
  if (bar) b.navStarted();
  function end() { if (bar) b.navSettled(); }   // always, also when work() failed
  return Promise.resolve()
    .then(work)
    .then(function (v) { end(); return v; }, function (err) { end(); throw err; });
}

withLoadingBar(function () {
  return fetch('/api/feed').then(function (r) { return r.json(); }).then(renderFeed);
}).catch(function () { showError('Could not load the feed.'); });
```

**Notes:** Only ends a bar that `navStarted()` (or the app's own DOM observer) began; it never ends the bar of a real page load. The app's observer already calls both methods when your page swaps DOM, so you need them only for waits it cannot see.

### `navStarted`

```js
window.WebToApk.navStarted()
```

Internal (called by the injected navigation observer, not creator code): the page is changing view without changing its URL. This is the ONLY signal for it - a single-HTML app that swaps DOM nodes fires no onPageStarted, no onPageFinished, no doUpdateVisitedHistory, and leaves onProgressChanged pinned at 100, so from native the tap and the new screen are completely invisible. Ignored while a real document load is running: that load owns the bar, and its own callbacks decide when it ends.

**Example**

Tells the app that a screen change has started, so it can show its thin loading bar at the top. The app already calls this for you when your page swaps its DOM; call it yourself only for a change where nothing in the page moves until data arrives.

**Returns:** nothing. **Needs:** nothing.

Always pair it with `navSettled()`, in a `try/finally`:

```js
async function openOrder(id) {
  var b = window.WebToApk;
  var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
  if (bar) b.navStarted();
  try {
    var res = await fetch('/api/orders/' + id);
    renderOrder(await res.json());         // your own screen
  } catch (e) {
    showError('Could not load the order.');
  } finally {
    if (bar) b.navSettled();               // the new view has painted: end the bar
  }
}
```

**Notes:** Nothing is drawn unless the change takes longer than about 200 ms, so fast changes never flash the bar. A bar you forget to settle ends by itself after 2.5 seconds. The call is ignored while a real page load is running (that load owns the bar). Keep showing your own spinner or skeleton as usual; the bar is extra.

### `notifyUserAction`

```js
window.WebToApk.notifyUserAction()
```

Internal (called by the injected tap counter, not creator code): one meaningful user interaction happened in the page. Counts toward the "interstitial every N actions" cadence ONLY while no URL-based navigation has ever been observed - single-HTML apps that switch screens via DOM show/hide never change the URL, so without this their navigation counter stayed at 0 forever and configured ads never showed (TicTrek complaint class, 2026-07-26). URL/SPA apps are unaffected: their first real navigation permanently switches counting back to URL signals.

**Example**

Counts one meaningful user action toward the "full-screen ad every N actions" setting. The app calls this itself from its own tap counter; pages normally never call it.

**Returns:** nothing. **Needs:** Ads on, a **Full Screen Ad** with **Trigger Mode → After Navigations**, and Automatic ad control.

**How it works:** a page that switches screens by hiding and showing DOM elements never changes its URL, so the app cannot see a "navigation". The app therefore injects a tap counter (at most one count per 2 seconds) that calls this method. As soon as the app sees one real URL change (a page load, `pushState` or a `#hash`), it counts URL changes instead and this method stops counting.

Only if your screen changes do not come from a tap (for example a keyboard or gamepad move), report them yourself:

```js
function goToScreen(name) {
  showScreen(name);                                   // your own DOM show/hide
  var b = window.WebToApk;
  if (b && typeof b.notifyUserAction === 'function') b.notifyUserAction();
}

document.addEventListener('keydown', function (e) {
  if (e.key === 'Enter') goToScreen('next');
});
```

**Notes:** It does nothing when ads are off, removed by purchase, or when the build uses **Ad Control → Developer-controlled** (there you call `AppMintAds.show()` yourself). A rewarded ad reached this way is always offered with a "Watch an ad? / No thanks" question, never forced. Do not call it on every tap; that would show ads more often than you configured.

### `showInterstitialAd`

```js
window.WebToApk.showInterstitialAd()
```

Show an interstitial NOW (e.g. between levels / on a deliberate action). Not showable right now → skipped + 'appmint:ad-unavailable' with the reason.

**Example**

Shows a full-screen interstitial ad right now, for example between two game levels (Automatic ad control).

**Returns:** nothing. When the ad closes, `appmint:ad-closed` fires with `{adType:'interstitial', earned:false}`. When it cannot show, `appmint:ad-unavailable` fires with `{adType:'interstitial', reason}`. **Needs:** Ads on and **Full Screen Ad → Interstitial Ad**. Choose **Trigger Mode → On Demand** if only your page should decide when.

```js
var bridge = window.WebToApk;
var waitingForAd = false;

function levelFinished() {
  if (bridge && typeof bridge.isInterstitialAdReady === 'function' && bridge.isInterstitialAdReady()) {
    waitingForAd = true;
    bridge.showInterstitialAd();          // the next level starts when the ad closes
  } else {
    showNextLevel();                      // no ad ready: just continue
  }
}

window.addEventListener('appmint:ad-closed', function (e) {
  if (e.detail.adType === 'interstitial' && waitingForAd) { waitingForAd = false; showNextLevel(); }
});

window.addEventListener('appmint:ad-unavailable', function (e) {
  // reason: ad_free | disabled | consent | not_loaded | cooldown | show_failed
  if (e.detail.adType === 'interstitial' && waitingForAd) { waitingForAd = false; showNextLevel(); }
});
```

**Notes:** The app keeps at least 60 seconds between two interstitials; a call inside that gap answers `cooldown`. `isInterstitialAdReady()` already includes the cooldown, so checking it first avoids the event. Show interstitials at natural breaks, never on every tap. In **Developer-controlled** builds use `AppMintAds.show('interstitial')`.

### `showRewardedAd`

```js
window.WebToApk.showRewardedAd()
```

Show a rewarded ad NOW (user-initiated - the only policy-compliant way to show rewarded). Pair it with trigger mode "On Demand" in the wizard to get rewarded ads with NO automatic app-open/navigation ads at all. The reward is delivered back to the page via deliverRewardToPage(): window.addEventListener('appmint:reward', e => { /* e.detail = {type,amount} */ }) When the ad closes, 'appmint:ad-closed' fires with {adType:'rewarded', earned} - earned is false if the user backed out early, so grant nothing in that case. Not showable right now → skipped + 'appmint:ad-unavailable' fired with the reason.

**Example**

Shows a rewarded ad right now, from the user's own "Watch an ad" tap (Automatic ad control).

**Returns:** nothing. The outcome arrives later as events: `appmint:reward` `{type, amount}` when the ad was watched to the end, `appmint:ad-closed` `{adType:'rewarded', earned}` when it closes, or `appmint:ad-unavailable` `{adType:'rewarded', reason}` when nothing could show. **Needs:** Ads on, **Full Screen Ad → Rewarded Ad**, and **Trigger Mode → On Demand** so nothing shows by itself.

Grant the reward only in the reward handler, never on the tap:

```js
var btn = document.getElementById('earn');
var bridge = window.WebToApk;

function refresh() {
  btn.disabled = !(bridge && typeof bridge.isRewardedAdReady === 'function' && bridge.isRewardedAdReady());
}
refresh();
setInterval(refresh, 2000);

btn.addEventListener('click', function () {        // a real user tap: the only place to call it
  if (!bridge || typeof bridge.showRewardedAd !== 'function') return;
  btn.disabled = true;
  bridge.showRewardedAd();
});

// GRANT HERE ONLY: the ad was watched to the end.
window.addEventListener('appmint:reward', function (e) {
  addCoins(e.detail.amount);                        // e.detail = {type, amount}
});

// The ad closed. earned === false means the user left early: grant nothing.
window.addEventListener('appmint:ad-closed', function (e) {
  if (e.detail.adType === 'rewarded' && !e.detail.earned) showMessage('No reward: the ad was closed early.');
  refresh();
});

// Nothing could be shown.
window.addEventListener('appmint:ad-unavailable', function (e) {
  // e.detail = {adType, reason}; reason: ad_free | disabled | consent | not_loaded | show_failed
  if (e.detail.reason === 'ad_free') addCoins(1);   // the user paid to remove ads: reward anyway
  else showMessage('No ad right now. Try again in a moment.');
  refresh();
});
```

The same three moments also call page hooks, if you prefer one function each (each gets the same object as `e.detail`):

```js
window.onAppMintReward = function (reward) { addCoins(reward.amount); };      // {type, amount}
window.onAppMintAdClosed = function (info) { console.log(info.adType, info.earned); };
window.onAppMintAdUnavailable = function (info) { console.log(info.adType, info.reason); };
```

**Notes:** Call it only from a click handler. Never call it from a timer, page load or route change, and never put a core feature behind it. `not_loaded` also starts a new load, so the next tap usually works. In **Ad Control → Developer-controlled** builds use `AppMintAds.show('rewarded')` instead, which answers with a Promise.

