# 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:** 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/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);
```

Pause it while the user picks a photo, since the system photo picker takes them out of the app for a moment:

```js
var ads = window.AppMintAds;
var input = document.getElementById('photo-input');   // <input type="file" accept="image/*" hidden>
var pausedForPicker = false;

document.getElementById('pick-photo').addEventListener('click', function () {
  if (ads) { ads.appOpen.setEnabled(false); pausedForPicker = true; }
  input.click();
});

input.addEventListener('change', function () {
  if (input.files[0]) uploadPhoto(input.files[0]);      // your own code
});

// Back in the app (a photo was picked or the picker was cancelled): turn it on again.
document.addEventListener('visibilitychange', function () {
  if (document.hidden || !pausedForPicker) return;
  pausedForPicker = false;
  setTimeout(function () { ads.appOpen.setEnabled(true); }, 1000);
});
```

**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;
}
```

Try a slot again later when no ad was available, freeing the failed slot first so it does not use up one of the two:

```js
function fillSlot(el, triesLeft) {
  var ads = window.AppMintAds;
  if (!ads) { el.hidden = true; return; }             // browser: no ads here
  ads.banner.attach(el, { size: 'banner' }).then(function (res) {
    if (res.ok) return;
    if (res.slot) ads.banner.detach(res.slot);
    if ((res.code === 'no_fill' || res.code === 'network') && triesLeft > 0) {
      setTimeout(function () { fillSlot(el, triesLeft - 1); }, 60000);
    } else {
      el.hidden = true;                                 // ad_free, disabled, consent, ...
    }
  });
}

fillSlot(document.getElementById('footer-ad'), 2);
```

**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);
```

Pick the rewarded format the build has for a "bonus" button, and hide the button when it has neither:

```js
var ads = window.AppMintAds;
var cfg = ads ? ads.config() : null;
var bonusBtn = document.getElementById('bonus');

var bonusType = null;
if (cfg && cfg.control === 'developer') {
  if (cfg.formats.rewarded) bonusType = 'rewarded';
  else if (cfg.formats.rewarded_interstitial) bonusType = 'rewarded_interstitial';
}
bonusBtn.hidden = !bonusType;

bonusBtn.addEventListener('click', async function () {
  var res = await ads.show(bonusType);                  // inside the tap: plays at once
  if (res.earned) addCoins(res.reward.amount);
});
```

Choose the ad size for the end of an article: a medium rectangle when the build has one, otherwise a banner:

```js
var ads = window.AppMintAds;
var cfg = ads ? ads.config() : null;
var box = document.getElementById('article-end-ad');

var size = null;
if (cfg && cfg.control === 'developer') {
  size = cfg.formats.mrec ? 'medium_rectangle' : (cfg.formats.banner ? 'banner' : null);
}

if (!size) box.hidden = true;
else ads.banner.attach(box, { size: size }).then(function (res) {
  if (!res.ok) { if (res.slot) ads.banner.detach(res.slot); box.hidden = true; }
});
```

**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
}
```

Offer "double your coins" on the results screen only when a rewarded ad is already waiting:

```js
function showResults(coins) {
  var ads = window.AppMintAds;
  var offer = document.getElementById('double-coins');
  renderResults(coins);                                 // your own screen
  offer.hidden = !(ads && ads.isReady('rewarded'));
  offer.onclick = async function () {
    offer.hidden = true;
    var res = await ads.show('rewarded');               // inside the tap: plays at once
    addCoins(res.earned ? coins : 0);                   // the bonus: the same amount again
  };
}
```

In a hint button, play whichever rewarded format is loaded right now:

```js
document.getElementById('hint').addEventListener('click', async function () {
  var ads = window.AppMintAds;
  if (!ads) return;                                     // browser: no ads here
  var type = ads.isReady('rewarded') ? 'rewarded'
           : ads.isReady('rewarded_interstitial') ? 'rewarded_interstitial'
           : null;
  if (!type) { showMessage('No hint video right now. Try again in a minute.'); return; }
  var res = await ads.show(type);
  if (res.earned) revealHint();
});
```

**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); };
```

Load the interstitial when a level starts, so it is there when the level ends, and stop asking once the user has removed ads:

```js
var ads = window.AppMintAds;
var interstitialsOff = false;

async function startLevel(n) {
  runLevel(n);                                          // your own game
  if (!ads || interstitialsOff) return;
  var res = await ads.load('interstitial');
  if (!res.ok && (res.code === 'ad_free' || res.code === 'disabled' || res.code === 'automatic_mode')) {
    interstitialsOff = true;                            // no interstitials in this app session
  }
}

async function levelEnded() {
  if (ads && !interstitialsOff && ads.isReady('interstitial')) await ads.show('interstitial');
  showLevelSummary();
}
```

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

**Rewarded interstitial** behind your own intro screen with a clear "No thanks". The tap on "Watch" is the opt-in:

```js
var ads = window.AppMintAds;
var intro = document.getElementById('free-life-intro');   // your own dialog: Watch / No thanks

function offerFreeLife() {
  if (ads && ads.isReady('rewarded_interstitial')) intro.hidden = false;
  else gameOver();
}

document.getElementById('free-life-watch').addEventListener('click', async function () {
  intro.hidden = true;
  var res = await ads.show('rewarded_interstitial');
  if (res.earned) continueWithExtraLife();
  else gameOver();
});

document.getElementById('free-life-skip').addEventListener('click', function () {
  intro.hidden = true;
  gameOver();
});
```

**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); }
```

Picking a photo leaves the app for the system picker, and coming back would count as a return. Pause the app-open ad around it:

```js
var input = document.getElementById('photo');

function appOpenAds(on) {
  if (window.AppMintAds) window.AppMintAds.appOpen.setEnabled(on);
}

input.addEventListener('click', function () { appOpenAds(false); });
function pickerDone() { setTimeout(function () { appOpenAds(true); }, 1000); }
input.addEventListener('change', pickerDone);
input.addEventListener('cancel', pickerDone);
```

Members of your own paid plan never see it. The switch is not saved, so set it early on every launch:

```js
// Runs from a <head> script: window.AppMintAds is already there at document start.
var plan = null;
try { plan = localStorage.getItem('plan'); } catch (e) {}

if (window.AppMintAds && plan === 'pro') {
  window.AppMintAds.appOpen.setEnabled(false);
}
```

**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); }
}
```

Reserve space only for the formats this build really has, and give the empty box back to the page otherwise:

```js
var box = document.getElementById('sidebar-ad');
var cfg = window.AppMintAds ? window.AppMintAds.config() : { control: 'automatic', formats: {} };

if (cfg.control === 'developer' && cfg.formats.mrec) {
  window.AppMintAds.banner.attach(box, { size: 'mrec' });
} else if (cfg.control === 'developer' && cfg.formats.banner) {
  window.AppMintAds.banner.attach(box, { size: 'banner' });
} else {
  box.remove();                                   // browser, Automatic mode, or no unit: no empty gap
}
```

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

Show an interstitial at the end of a level only when one is waiting, and never hold the player back when it is not:

```js
function onLevelComplete() {
  var ads = window.AppMintAds;
  if (ads && ads.isReady('interstitial')) {
    // resolves after the ad closes, or at once with {ok:false, code} if it could not show
    ads.show('interstitial').then(startNextLevel);
  } else {
    startNextLevel();                             // not loaded yet, or inside the 60-second cooldown
  }
}
```

**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
}
```

Retry a load that failed for a passing reason, and stop for the reasons a retry cannot fix:

```js
function loadWithRetry(type, attempt) {
  attempt = attempt || 0;
  return window.AppMintAds.load(type).then(function (res) {
    if (res.ok) return res;
    if ((res.code === 'no_fill' || res.code === 'network') && attempt < 3) {
      return new Promise(function (wait) { setTimeout(wait, 5000 * (attempt + 1)); })
        .then(function () { return loadWithRetry(type, attempt + 1); });
    }
    return res;                                   // invalid_unit, consent, ad_free, disabled ...
  });
}

if (window.AppMintAds) {
  loadWithRetry('rewarded').then(function (res) {
    document.getElementById('watch-ad').disabled = !res.ok;
  });
}
```

Warm two formats before a game session starts and hide every ad button when the user has no ads at all:

```js
if (window.AppMintAds) {
  Promise.all([
    window.AppMintAds.load('interstitial'),
    window.AppMintAds.load('rewarded_interstitial')
  ]).then(function (results) {
    var noAds = results.some(function (r) { return r.code === 'ad_free' || r.code === 'disabled'; });
    if (noAds) hideAllAdButtons();                // paid to remove ads, or ads are off in this build
    results.forEach(function (r) {
      if (!r.ok && r.adCode !== undefined) console.log('ad load failed', r.code, r.adCode, r.message);
    });
  });
}
```

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

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

Offer a rewarded interstitial from the user's own "Double my bonus" tap, and move on whatever the answer:

```js
document.getElementById('double-bonus').addEventListener('click', function () {
  if (!window.AppMintAds) return goToNextLevel();
  window.AppMintAds.show('rewarded_interstitial').then(function (res) {
    if (res.earned) doubleLevelBonus();
    else if (!res.shown && res.code !== 'declined') console.log('no ad:', res.code);
    goToNextLevel();                              // shown or not, the player keeps going
  });
});
```

Pause your game music while any full-screen ad is on screen, using the `appmint:ad` stream:

```js
window.addEventListener('appmint:ad', function (e) {
  var d = e.detail;                               // {type, phase, code?, adCode?, message?, earned?, reward?}
  if (d.type === 'banner' || d.type === 'mrec') return;
  if (d.phase === 'opened') music.pause();
  if (d.phase === 'closed' || d.phase === 'show_failed') music.play();
});

function onChapterSaved() {
  if (window.AppMintAds) window.AppMintAds.show('interstitial');   // a natural break
}
```

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

A medium rectangle on a results screen that gives its space back when no ad comes:

```js
var box = document.getElementById('result-ad');

if (window.AppMintAds) {
  window.AppMintAds.banner.attach(box, { size: 'mrec' }).then(function (res) {
    if (res.ok) return;                           // the box now has min-height = res.height
    window.AppMintAds.banner.detach(res.slot);    // free the slot for another banner
    box.hidden = true;
    if (res.code === 'too_many_slots') console.log('detach a banner first:', res.message);
  });
} else {
  box.hidden = true;
}
```

Count impressions and clicks per slot from the `appmint:ad` stream, keyed by the slot id `attach` returned:

```js
var slotNames = {};

if (window.AppMintAds) {
  window.AppMintAds.banner.attach('#footer-ad', { size: 'banner' }).then(function (res) {
    if (res.ok) slotNames[res.slot] = 'footer';
  });
}

window.addEventListener('appmint:ad', function (e) {
  var d = e.detail;                               // banner events carry {type, phase, slot}
  if (d.slot && (d.phase === 'impression' || d.phase === 'clicked')) {
    track('ad_' + d.phase, slotNames[d.slot] || d.slot);
  }
});
```

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

In a single-page app, free the banner when the route that holds it goes away. `attach` marks the element straight away, so detaching by element works even while the ad is still loading:

```js
function onRouteChange(route) {
  if (!window.AppMintAds) return renderRoute(route);
  var old = document.getElementById('article-ad');
  if (old) window.AppMintAds.banner.detach(old);
  renderRoute(route);
  if (route === '/articles') {
    window.AppMintAds.banner.attach('#article-ad', { size: 'banner' });
  }
}
```

After a "Remove Ads" purchase the overlays are gone; detach the slots too and drop the space they reserved:

```js
window.addEventListener('appmint:premium', function (e) {
  if (!e.detail.premium || !window.AppMintAds) return;
  document.querySelectorAll('[data-appmint-ad-slot]').forEach(function (el) {
    window.AppMintAds.banner.detach(el);
    el.remove();
  });
});
```

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

A banner inside a scrolling list, not the page: the helper listens to scroll in the capture phase, so the ad follows the inner container too:

```html
<div id="feed" style="height: 100vh; overflow-y: auto">
  <div class="card">…</div>
  <div class="card">…</div>
  <div id="feed-ad" style="margin: 12px 0"></div>
  <div class="card">…</div>
</div>

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

The ad is drawn above the page, so your own dialog would sit under it. Hide the slot while the dialog is open:

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

function openSettings() {
  slotEl.style.visibility = 'hidden';             // the helper hides the overlay on its next tick
  dialog.showModal();
}

dialog.addEventListener('close', function () {
  slotEl.style.visibility = '';                   // and puts it back over the box
});
```

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

Show an ad after every third finished quiz, and only when one would really show:

```js
var finished = 0;

function onQuizFinished() {
  finished++;
  var b = window.WebToApk;
  var ready = !!(b && typeof b.isInterstitialAdReady === 'function' && b.isInterstitialAdReady());
  if (finished % 3 === 0 && ready) b.showInterstitialAd();
  showResults();                                  // the results are there when the ad closes
}
```

Show a short "Ad in 3…" notice only when an ad is ready, then continue when it closes or cannot show:

```js
var afterAd = null;

function adBreakThen(next) {
  var b = window.WebToApk;
  if (!b || typeof b.isInterstitialAdReady !== 'function' || !b.isInterstitialAdReady()) return next();
  var note = document.getElementById('ad-break'), n = 3;
  note.hidden = false; note.textContent = 'Ad in 3…';
  var t = setInterval(function () {
    if (--n > 0) { note.textContent = 'Ad in ' + n + '…'; return; }
    clearInterval(t); note.hidden = true;
    afterAd = next;
    b.showInterstitialAd();
  }, 1000);
}

function runAfterAd(e) {
  if (e.detail.adType !== 'interstitial' || !afterAd) return;
  var next = afterAd; afterAd = null; next();
}
window.addEventListener('appmint:ad-closed', runAfterAd);
window.addEventListener('appmint:ad-unavailable', runAfterAd);
```

**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);
```

On game over, offer "Continue" only when an ad is waiting, so the button never leads nowhere:

```js
var continueBtn = document.getElementById('continue-with-ad');

function onGameOver() {
  var b = window.WebToApk;
  continueBtn.hidden = !(b && typeof b.isRewardedAdReady === 'function' && b.isRewardedAdReady());
  showGameOverPanel();                            // "Restart" is always there
}

continueBtn.addEventListener('click', function () {   // the user's own tap
  continueBtn.hidden = true;
  window.WebToApk.showRewardedAd();
});

window.addEventListener('appmint:reward', function () { revivePlayer(); });
```

Check at the moment of the tap and tell the user what is happening instead of firing a show that will be refused:

```js
document.getElementById('hint').addEventListener('click', function () {
  var b = window.WebToApk;
  if (!b || typeof b.isRewardedAdReady !== 'function') return showMessage('Hints need the app.');
  if (typeof b.isPremium === 'function' && b.isPremium()) return revealHint();   // paid to remove ads
  if (b.isRewardedAdReady()) b.showRewardedAd();
  else showMessage('The ad is still loading. Try again in a few seconds.');
});

window.addEventListener('appmint:reward', function () { revealHint(); });
```

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

End the bar only once the new screen's pictures are decoded, so the user never sees an empty gallery after the bar is gone:

```js
async function openAlbum(id) {
  var b = window.WebToApk;
  var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
  if (bar) b.navStarted();
  try {
    var photos = await fetch('/api/albums/' + id).then(function (r) { return r.json(); });
    var imgs = renderAlbum(photos);                   // your own code, returns the <img> elements
    await Promise.all(imgs.map(function (img) { return img.decode().catch(function () {}); }));
  } finally {
    if (bar) b.navSettled();
  }
}
```

A screen whose code is loaded on demand with a dynamic `import()`:

```js
async function openSettings() {
  var b = window.WebToApk;
  var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
  if (bar) b.navStarted();
  try {
    var mod = await import('./settings-screen.js');   // your own module
    mod.mount(document.getElementById('app'));
  } catch (e) {
    showError('Could not open settings.');
  } finally {
    if (bar) b.navSettled();
  }
}
```

**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
  }
}
```

A tab bar whose tabs need several requests before anything can be drawn:

```js
document.querySelectorAll('.tab').forEach(function (tab) {
  tab.addEventListener('click', async function () {
    var b = window.WebToApk;
    var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
    if (bar) b.navStarted();
    try {
      var urls = ['/api/stats?tab=' + tab.dataset.tab, '/api/items?tab=' + tab.dataset.tab];
      var data = await Promise.all(urls.map(function (u) { return fetch(u).then(function (r) { return r.json(); }); }));
      renderTab(tab.dataset.tab, data[0], data[1]);     // your own screen
    } catch (e) {
      showError('Could not load this tab.');
    } finally {
      if (bar) b.navSettled();
    }
  });
});
```

A search form that stays on the same page and swaps in the results:

```js
document.getElementById('search-form').addEventListener('submit', function (e) {
  e.preventDefault();
  var q = document.getElementById('search-input').value.trim();
  if (!q) return;
  var b = window.WebToApk;
  var bar = !!(b && typeof b.navStarted === 'function' && typeof b.navSettled === 'function');
  if (bar) b.navStarted();
  fetch('/api/search?q=' + encodeURIComponent(q))
    .then(function (r) { return r.json(); })
    .then(function (results) { renderResults(results); })
    .catch(function () { showError('Search failed. Try again.'); })
    .finally(function () { if (bar) b.navSettled(); });
});
```

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

A card-style app where the user swipes to the next screen (a swipe is not a tap on a button, so the tap counter never sees it):

```js
var startX = null;

document.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; }, { passive: true });

document.addEventListener('touchend', function (e) {
  if (startX === null) return;
  var dx = e.changedTouches[0].clientX - startX;
  startX = null;
  if (Math.abs(dx) < 80) return;                       // not a swipe
  showScreen(dx < 0 ? 'next' : 'previous');            // your own DOM show/hide
  var b = window.WebToApk;
  if (b && typeof b.notifyUserAction === 'function') b.notifyUserAction();
});
```

An HTML5 game played with a gamepad, counting one action per level the player starts with the A button:

```js
var aWasDown = false;

function pollPad() {
  var pad = navigator.getGamepads ? navigator.getGamepads()[0] : null;
  var aDown = !!(pad && pad.buttons[0] && pad.buttons[0].pressed);
  if (aDown && !aWasDown && isOnLevelSelect()) {       // your own game state
    startSelectedLevel();
    var b = window.WebToApk;
    if (b && typeof b.notifyUserAction === 'function') b.notifyUserAction();
  }
  aWasDown = aDown;
  requestAnimationFrame(pollPad);
}

window.addEventListener('gamepadconnected', function () { requestAnimationFrame(pollPad); }, { once: true });
```

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

Pause the game and its sound before the ad, and resume on either answer:

```js
var pausedForAd = false;

function adBetweenRounds() {
  var b = window.WebToApk;
  if (!b || typeof b.showInterstitialAd !== 'function') return;
  pausedForAd = true;
  pauseGame(); music.pause();
  b.showInterstitialAd();
}

function resumeAfterAd(e) {
  if (e.detail.adType !== 'interstitial' || !pausedForAd) return;
  pausedForAd = false;
  music.play(); resumeGame();
}
window.addEventListener('appmint:ad-closed', resumeAfterAd);
window.addEventListener('appmint:ad-unavailable', resumeAfterAd);
```

Open an article after an ad, as one Promise that resolves `true` when the ad was shown and `false` when it was skipped:

```js
function interstitialThen() {
  return new Promise(function (resolve) {
    var b = window.WebToApk;
    if (!b || typeof b.isInterstitialAdReady !== 'function' || !b.isInterstitialAdReady()) return resolve(false);
    function done(e) {
      if (e.detail.adType !== 'interstitial') return;
      window.removeEventListener('appmint:ad-closed', done);
      window.removeEventListener('appmint:ad-unavailable', done);
      resolve(e.type === 'appmint:ad-closed');
    }
    window.addEventListener('appmint:ad-closed', done);
    window.addEventListener('appmint:ad-unavailable', done);
    b.showInterstitialAd();
  });
}

interstitialThen().then(function () { openArticle(articleId); });
```

**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); };
```

Wrap the call and its events in one Promise, so each "Watch an ad" button reads like a normal async call:

```js
function watchRewardedAd() {                      // call it inside the user's tap
  return new Promise(function (resolve) {
    var b = window.WebToApk;
    if (!b || typeof b.showRewardedAd !== 'function') return resolve({ earned: false, reason: 'no_app' });
    function done(e) {
      if (e.detail.adType !== 'rewarded') return;
      window.removeEventListener('appmint:ad-closed', done);
      window.removeEventListener('appmint:ad-unavailable', done);
      resolve(e.type === 'appmint:ad-closed' ? { earned: e.detail.earned } : { earned: false, reason: e.detail.reason });
    }
    window.addEventListener('appmint:ad-closed', done);
    window.addEventListener('appmint:ad-unavailable', done);
    b.showRewardedAd();
  });
}

document.getElementById('revive').addEventListener('click', function () {
  watchRewardedAd().then(function (r) {
    if (r.earned || r.reason === 'ad_free') revivePlayer();
    else if (r.reason) showMessage('No ad right now.');
  });
});
```

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

