# Purchases and entitlements - JavaScript bridge API

> Selling from your own page, and asking what this buyer already owns. See the in-app purchases guide for the code around these.

- **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/iap

### `AppMintIAP.available`

```js
window.AppMintIAP.available
```

**Example**

A property (not a function): `true` when this page can really buy through Google Play.

**Returns:** a boolean property. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature). Without it, `window.AppMintIAP` does not exist at all.

Check it before you show anything for sale, and say plainly when buying is not possible here:

```js
var inApp = !!(window.AppMintIAP && window.AppMintIAP.available);
var buyBtn = document.getElementById('buy-pro');

if (!inApp) {
  buyBtn.disabled = true;
  buyBtn.textContent = 'Available in the Android app';
}
```

**Notes:** `window.AppMintIAP` is installed before your page's first script runs; only on WebViews older than about 2021 does it arrive once the page has finished loading, so code that must also run there should wait for it (see the `whenIap` pattern in `AppMintIAP.sell`). The same value is in `AppMintIAP.state().available`. Wrapping the offer in `data-iap-container` hides it outside the app with no code.

### `AppMintIAP.buy`

```js
window.AppMintIAP.buy(id)
```

**Example**

Opens Google Play's purchase sheet for a product, from your own button. It grants nothing by itself.

**Returns:** nothing. The result arrives as `appmint:purchase` `{productId, owned:true}` (and through `AppMintIAP.onChange`) or `appmint:purchase-failed` `{productId, reason}`. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature) and the product active in Play Console.

Use it when you draw the button yourself; the unlock comes from ownership, not from the tap:

```js
var btn = document.getElementById('buy-pro');

btn.addEventListener('click', function () {
  if (!window.AppMintIAP || !window.AppMintIAP.available) { showMessage('Available in the Android app.'); return; }
  btn.disabled = true;
  window.AppMintIAP.buy('pro_unlock');
});

window.addEventListener('appmint:purchase', function (e) {
  if (e.detail.productId === 'pro_unlock') unlockPro();     // must be safe to run twice
  btn.disabled = false;
});
window.addEventListener('appmint:purchase-failed', function (e) {
  btn.disabled = false;
  showMessage(e.detail.reason === 'pending' ? 'Payment pending. It unlocks once Google confirms.' : 'Purchase failed: ' + e.detail.reason);
});
document.addEventListener('visibilitychange', function () { if (!document.hidden) btn.disabled = false; });   // cancel: no event
```

**Notes:** `buy()` is the bare `WebToApk.purchase(id)`: no busy label, no group check. Buttons wired with `sell()`, `start()` or `data-iap-buy` do all of that for you, so prefer those.

### `AppMintIAP.isOwned`

```js
window.AppMintIAP.isOwned(id)
```

**Example**

Checks, instantly and offline, whether the user owns a product (a one-time product, or a subscription that is still active).

**Returns:** `true` / `false`, synchronously; `false` outside the app. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

```js
function paintPro() {
  var pro = !!(window.AppMintIAP && window.AppMintIAP.isOwned('pro_unlock'));
  document.getElementById('pro-tools').hidden = !pro;
}

// Re-check whenever ownership can change.
window.addEventListener('appmint:purchase', paintPro);
window.addEventListener('appmint:owned-changed', paintPro);

// AppMintIAP arrives after the page has loaded: paint once it is there.
var waitIap = setInterval(function () {
  if (window.AppMintIAP) { clearInterval(waitIap); paintPro(); }
}, 200);
setTimeout(function () { clearInterval(waitIap); }, 10000);   // not in the app, or selling is off
```

**Notes:** Same answer as `WebToApk.isOwned(id)`. Ownership is restored from the user's Google account at every launch and saved on the phone, so it survives reinstalls and works offline. Never keep the entitlement only in `localStorage`.

### `AppMintIAP.onChange`

```js
window.AppMintIAP.onChange(fn)
```

**Example**

Calls your function with the purchase state right away, and again every time it changes (prices arrived, a purchase landed, the launch restore, a refund, an expired subscription).

**Returns:** an unsubscribe function. `fn` gets `{available, adFree, owned, ownedIds, products}`; `products` is `{id: {price, currency, type, period, owned}}` for the products the helper knows (from `sell`, `start` or `data-iap-buy`). **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

A React hook. It returns the unsubscribe function straight from the effect:

```ts
import { useEffect, useState } from 'react';

type IapState = { available: boolean; adFree: boolean; owned: boolean; ownedIds: string[];
  products: Record<string, { price: string | null; currency: string | null; type: string | null; period: string | null; owned: boolean }> };

export function useIap(ids: string[]) {
  const [iap, setIap] = useState<IapState>({ available: false, adFree: false, owned: false, ownedIds: [], products: {} });
  useEffect(() => {
    let stop: (() => void) | undefined;
    const t = setInterval(() => {                   // AppMintIAP arrives after page load
      const api = (window as any).AppMintIAP;
      if (!api) return;
      clearInterval(t);
      ids.forEach((id) => api.sell(id, null));      // track them, so their prices are fetched
      stop = api.onChange(setIap);
    }, 200);
    return () => { clearInterval(t); if (stop) stop(); };
  }, []);
  return iap;
}

// const iap = useIap(['pro_unlock']);
// const isPro = iap.ownedIds.includes('pro_unlock');
// const price = iap.products.pro_unlock?.price;
```

**Notes:** Read ownership per product (`ownedIds` or `products[id].owned`). The top-level `owned` only means "anything is owned". `adFree` is the Remove Ads entitlement (`WebToApk.isPremium()`). A product is in `products` only after Google answered for it.

### `AppMintIAP.owned`

```js
window.AppMintIAP.owned()
```

**Example**

Lists every product ID the user owns.

**Returns:** an array of product IDs, synchronously, for example `['pro_unlock', 'level_pack_2']`; `[]` outside the app or when nothing is owned. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

```js
function unlockOwnedLevels() {
  var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
  document.querySelectorAll('[data-level-pack]').forEach(function (el) {
    el.classList.toggle('locked', ids.indexOf(el.getAttribute('data-level-pack')) === -1);
  });
}

window.addEventListener('appmint:owned-changed', unlockOwnedLevels);   // restore or refund changed the list
window.addEventListener('appmint:purchase', unlockOwnedLevels);
```

**Notes:** Parsed from `WebToApk.getOwnedProducts()` for you. Includes active subscriptions and the Remove Ads product. Works offline.

### `AppMintIAP.refresh`

```js
window.AppMintIAP.refresh()
```

**Example**

Looks again for `data-iap-buy` buttons that were added since the last scan, and wires them (or starts the helper if it had found none yet).

**Returns:** the `AppMintIAP` object. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

The helper already watches the page for new `data-iap-buy` buttons, so you rarely need this. Call it after you insert buttons in a way the watcher could miss, for example by changing attributes on an existing element:

```js
function showShop() {
  var btn = document.getElementById('buy-coins');
  btn.setAttribute('data-iap-buy', 'coins_500');          // attribute added to an EXISTING node
  btn.setAttribute('data-iap-label', '500 coins — {price}');
  if (window.AppMintIAP) window.AppMintIAP.refresh();
}
```

**Notes:** Each new button gets its price from Google and its owned state. A button that is already wired is not wired twice. Buttons whose element was removed from the page are dropped.

### `AppMintIAP.sell`

```js
window.AppMintIAP.sell(id, button, opts)
```

**Example**

Wires one product to one button in a single line. Call it once per product.

**Returns:** the `AppMintIAP` object (for chaining). **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature) and the product created and active in Play Console. `window.AppMintIAP` is installed before your page's first script runs (on WebViews older than about 2021, once the page has finished loading - the `whenIap` pattern below covers both).

```js
function whenIap(fn) {
  if (window.AppMintIAP) { fn(window.AppMintIAP); return; }
  var tries = 0;
  var t = setInterval(function () {
    if (window.AppMintIAP) { clearInterval(t); fn(window.AppMintIAP); }
    else if (++tries > 50) clearInterval(t);          // not in the app, or selling is off
  }, 200);
}

whenIap(function (iap) {
  iap.sell('pro_unlock', '#buy-pro', {
    label: 'Unlock Pro — {price}',     // {price} = Google's live, localized price
    owned: '✓ Unlocked',               // the button text once owned
    show: '#thanks',                   // shown once owned
    hide: '.upsell'                    // hidden once owned
  });
  iap.sell('coins_500', '#buy-coins', { label: '500 coins — {price}' });
});
```

Several plans where buying one replaces the others share a `group`:

```js
whenIap(function (iap) {
  iap.sell('pass_monthly', '#monthly', { label: 'Monthly — {price}', group: 'pass' })
     .sell('pass_yearly', '#yearly', { label: 'Yearly — {price}', group: 'pass' });
});
```

**Notes:** `button` is an element or a CSS selector; pass `null` to track a product without a button (your own UI reads `AppMintIAP.state()`). Other `opts`: `labelEl` (the element inside the button whose text changes). It works before or after the helper started, including from a React effect. The same as the `data-iap-buy` attributes (see `AppMintIAP.start`). Unlock paid content only from ownership (`isOwned`, `onChange`), never in your own click handler.

### `AppMintIAP.start`

```js
window.AppMintIAP.start(config) and the data-iap-* attributes
```

**Example**

Wires your buy buttons to Google Play in one step: live prices on the buttons, the purchase sheet on tap, "owned" labels, thank-you blocks, restore at launch, and a visible message for every failure.

**Returns:** the `AppMintIAP` object (for chaining). **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature) and the product IDs created and active in Play Console. The app installs `window.AppMintIAP` before your page's first script runs (on WebViews older than about 2021, only once the page has finished loading).

**No code at all:** mark the HTML. The helper reads these attributes by itself as soon as it is injected, and watches for buttons that React or Vue render later:

```html
<div data-iap-container>                              <!-- shown only inside the app -->
  <button data-iap-buy="pass_monthly" data-iap-group="pass"
          data-iap-label="Monthly — {price}" data-iap-owned="Active">Monthly</button>
  <button data-iap-buy="pass_lifetime" data-iap-group="pass"
          data-iap-label="Lifetime — {price}" data-iap-owned="Owned">
    <img src="star.svg" alt=""> <span data-iap-text>Lifetime</span>   <!-- only this text changes -->
  </button>
  <button data-iap-buy="coins_500" data-iap-label="500 coins — {price}">500 coins</button>
</div>

<p data-iap-show="pass" hidden>You are a member. Thank you!</p>   <!-- shown when any "pass" plan is owned -->
<p data-iap-hide="pass">Members get every level and no ads.</p>    <!-- hidden once it is owned -->
```

**With code:** call `start()` yourself when you want your own message function or unlock hooks. Use it on pages WITHOUT `data-iap-buy` buttons in the HTML (with them, the helper has already started and a second start does nothing):

```js
function whenIap(fn) {                               // AppMintIAP arrives after page load
  if (window.AppMintIAP) { fn(window.AppMintIAP); return; }
  var tries = 0;
  var t = setInterval(function () {
    if (window.AppMintIAP) { clearInterval(t); fn(window.AppMintIAP); }
    else if (++tries > 50) clearInterval(t);         // not in the app, or selling is off
  }, 200);
}

whenIap(function (iap) {
  iap.start({
    plans: [
      { id: 'pro_unlock', button: '#buy-pro', label: 'Unlock Pro — {price}', owned: '✓ Pro' },
      { id: 'coins_500', button: '#buy-coins', label: '500 coins — {price}' }
    ],
    show: '#pro-area',                 // shown once anything is owned
    hide: '.upsell',                   // hidden once anything is owned
    busyLabel: 'Opening Google Play…',
    onUnlock: function (id) { console.log('now owned:', id); },
    onLock: function (id) { console.log('no longer owned (refund or expiry):', id); },
    toast: function (msg) { showMessage(msg); }     // failure messages; default: window.showToast, else alert
  });
});
```

**Notes:** Attributes: `data-iap-buy="id"`, `data-iap-label` (`{price}` becomes Play's live price), `data-iap-owned` (label once owned; default "✓ Purchased"), `data-iap-group` (plans that replace each other; after one is owned the others hide unless `hideOtherPlans: false`), `data-iap-show` / `data-iap-hide` (value = product id or group; empty = anything owned), `data-iap-text` (the part of a button whose text changes), `data-iap-container`. An owned button gets `data-iap-state="owned"` and the class `iap-unlocked`. A cancel on Google's sheet sends no event; the button is released when the page is visible again (or after 30 s). Calling `start(options)` after the helper already started on its own from `data-iap-buy` attributes MERGES the options (plans, show/hide, container, `onUnlock`/`onLock`, `toast`, `busyLabel`, `hideOtherPlans`) into the running set; calling it twice is harmless. A page that ships its own `appmint-iap.js` keeps it - the built-in copy stands down.

### `AppMintIAP.started`

```js
window.AppMintIAP.started
```

**Example**

A property (not a function): `true` once the helper is running, i.e. it has wired at least one product from `start()`, `sell()` or `data-iap-buy` buttons.

**Returns:** a boolean property. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

Use it to decide between `start()` (with your own options) and `sell()` (which also works on a running helper):

```js
function setUpShop(iap) {
  if (!iap.started) {
    iap.start({
      plans: [{ id: 'pro_unlock', button: '#buy-pro', label: 'Unlock Pro — {price}' }],
      toast: function (msg) { showMessage(msg); }
    });
  } else {
    // Already running (the page had data-iap-buy buttons): start() would do nothing now.
    iap.sell('pro_unlock', '#buy-pro', { label: 'Unlock Pro — {price}' });
  }
}
if (window.AppMintIAP) setUpShop(window.AppMintIAP);
```

**Notes:** A second `start()` never starts it twice (React StrictMode runs effects twice); its options are merged into the running set. It stays `false` while no product is wired yet; the helper then keeps watching the page for `data-iap-buy` buttons.

### `AppMintIAP.state`

```js
window.AppMintIAP.state()
```

**Example**

Returns the current purchase state once, without subscribing.

**Returns:** `{available, adFree, owned, ownedIds, products}`, synchronously. `ownedIds` is an array of product IDs; `products` is `{id: {price, currency, type: 'inapp'|'subs', period, owned}}` for the products the helper has asked Google about. **Needs:** **💰 Sell In-App Products** (a ProMax-plan feature).

```js
function describePass() {
  if (!window.AppMintIAP) return 'Available in the Android app';
  var s = window.AppMintIAP.state();
  if (s.ownedIds.indexOf('season_pass') !== -1) return 'Your season pass is active';
  var p = s.products.season_pass;                       // undefined until Google answers
  if (!p || !p.price) return 'Season pass';
  var every = { P1W: 'week', P1M: 'month', P1Y: 'year' }[p.period] || 'period';
  return 'Season pass — ' + p.price + ' / ' + every;
}
document.getElementById('pass-label').textContent = describePass();
```

**Notes:** It is a snapshot. To follow changes use `AppMintIAP.onChange(fn)`, which gives the same object every time something changes. `owned` is `true` when anything at all is owned; check a product with `ownedIds` or `AppMintIAP.isOwned(id)`.

### `getOwnedProducts`

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

JSON array of every owned product ID - for gating whole screens at startup.

**Example**

Lists every Play product the user owns, to unlock whole screens at start.

**Returns:** a JSON string (array of product IDs), synchronously, for example `'["pro_unlock","level_pack_2"]'`. Parse it with `JSON.parse`. `'[]'` when nothing is owned. **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature).

```js
function ownedIds() {
  var b = window.WebToApk;
  if (!b || typeof b.getOwnedProducts !== 'function') return [];
  try { return JSON.parse(b.getOwnedProducts()); } catch (e) { return []; }
}

function unlockAll(ids) {
  ids.forEach(function (id) {
    var el = document.querySelector('[data-product="' + id + '"]');
    if (el) el.classList.add('unlocked');
  });
}
unlockAll(ownedIds());

// The owned list changed (launch restore, a new purchase, a refund): the full new list.
window.addEventListener('appmint:owned-changed', function (e) {
  document.querySelectorAll('.unlocked').forEach(function (el) { el.classList.remove('unlocked'); });
  unlockAll(e.detail.products);              // e.detail = {products: ['pro_unlock', ...]}
});
```

The same change also calls `window.onAppMintOwnedChanged(detail)` if you define it.

**Notes:** Includes one-time products and active subscriptions, and also the Remove Ads product if the user bought it. Works offline; the list is saved on the phone at every launch restore and purchase. `appmint:owned-changed` fires only when the list really changed, so it may not fire at all on a normal launch.

### `getProducts`

```js
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
```

Asks Play for live details of the given product IDs (JSON array of strings). Result arrives as 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. IDs Play doesn't know are absent from the result - the page hides those items.

**Example**

Asks Google Play for the live details of your products: title, description and the localized price.

**Returns:** nothing now. The answer arrives as the `appmint:products` event: `{requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}`. `type` is `'inapp'` or `'subs'`; `period` is an ISO-8601 period such as `'P1M'` (subscriptions only). **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature). The second argument is a JSON string: use `JSON.stringify`.

```js
function loadPrices(ids) {
  return new Promise(function (resolve) {
    var b = window.WebToApk;
    if (!b || typeof b.getProducts !== 'function') { resolve([]); return; }   // browser
    var requestId = 'prices-' + Date.now();
    window.addEventListener('appmint:products', function handler(e) {
      if (String(e.detail.requestId) !== requestId) return;   // another call's answer
      window.removeEventListener('appmint:products', handler);
      resolve(e.detail.products || []);
    });
    b.getProducts(requestId, JSON.stringify(ids));
  });
}

loadPrices(['pro_unlock', 'coins_500', 'pro_monthly']).then(function (products) {
  products.forEach(function (p) {
    var btn = document.querySelector('[data-product="' + p.productId + '"]');
    if (!btn) return;
    btn.hidden = false;
    btn.textContent = p.owned ? 'Owned' : p.title + ' · ' + p.price + (p.period === 'P1M' ? ' / month' : '');
  });
});
```

The answer is also passed to `window.onAppMintProducts(detail)` if you define it.

**Notes:** Show `price` exactly as Play gives it and never store it. An ID that Play does not know (not created, not active, or the app was not installed from Play) is simply missing from `products`; keep that item hidden. A numeric-looking `requestId` comes back as a number, so use a text id or compare with `String()`.

### `isOwned`

```js
window.WebToApk.isOwned(productId: String): Boolean
```

Instant on-device ownership check (one-time product owned OR sub active).

**Example**

Checks, instantly and offline, whether the user owns one of your Play products (a one-time product, or a subscription that is still active).

**Returns:** `true` / `false`, synchronously. **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature). It also answers for the **Remove Ads (IAP)** product.

```js
function hasPro() {
  var b = window.WebToApk;
  return !!(b && typeof b.isOwned === 'function' && b.isOwned('pro_unlock'));
}

function paint() {
  document.getElementById('pro-features').hidden = !hasPro();
  document.getElementById('buy-pro').hidden = hasPro();
}
paint();

// Ownership changes: a purchase landed, or the launch restore / a refund changed the list.
window.addEventListener('appmint:purchase', paint);
window.addEventListener('appmint:owned-changed', paint);
```

**Notes:** The owned list is restored from the user's Google account at every launch and saved on the phone, so it works offline and after a reinstall, with no server. Always `false` in a browser. A subscription stops counting once Play no longer reports it as active.

### `isPremium`

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

True when the user owns "Remove Ads" (Play Billing entitlement, auto-restored on every launch). Matches the reason 'ad_free' in 'appmint:ad-unavailable'.

**Example**

Tells you whether the user owns your "Remove Ads" product, so the page can hide its "Remove ads" button and ad-related UI.

**Returns:** `true` / `false`, synchronously. **Needs:** Ads on and **Remove Ads (IAP)** with your **Play Store Product ID**. Without that it is always `false`.

```js
var removeAdsBtn = document.getElementById('remove-ads');

function paintPremium(premium) {
  removeAdsBtn.hidden = premium;
  document.body.classList.toggle('premium', premium);
}

var b = window.WebToApk;
paintPremium(!!(b && typeof b.isPremium === 'function' && b.isPremium()));

// Changes later: purchase finished, restore at launch, refund or expired subscription.
window.addEventListener('appmint:premium', function (e) {
  paintPremium(e.detail.premium);            // e.detail = {premium: true | false}
});
```

The same change also calls a page hook, if you define it:

```js
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); };   // {premium}
```

**Notes:** Ownership is restored from the user's Google account at every launch, so read it once at start and then follow `appmint:premium`. The Product ID field accepts several IDs separated by commas (for example a monthly plan and a lifetime one); owning any of them counts. While premium, ads are removed and ad calls answer `ad_free`.

### `purchase`

```js
window.WebToApk.purchase(productId: String)
```

Opens Google's purchase sheet for [productId] (one-time product or subscription - resolved automatically). Grant ONLY on 'appmint:purchase' {productId, owned:true}; failures fire 'appmint:purchase-failed' {reason}. Already-owned re-fires the purchase event so the page can idempotently unlock.

**Example**

Opens Google Play's purchase sheet for one of your products (a one-time product or a subscription; the app finds out which).

**Returns:** nothing now. Success fires `appmint:purchase` `{productId, owned: true}`. A failure fires `appmint:purchase-failed` `{productId, reason}`. **Needs:** **💰 Sell In-App Products** in the build wizard (a ProMax-plan feature), and the product created and active in Play Console.

Unlock ONLY in the `appmint:purchase` handler, never on the tap:

```js
var b = window.WebToApk;
var buyBtn = document.getElementById('buy-pro');

if (!b || typeof b.purchase !== 'function') {
  buyBtn.hidden = true;                              // browser: nothing to buy here
} else {
  buyBtn.addEventListener('click', function () {
    buyBtn.disabled = true;
    b.purchase('pro_unlock');
  });
}

window.addEventListener('appmint:purchase', function (e) {
  if (e.detail.productId === 'pro_unlock') unlockPro();   // safe to run twice
  buyBtn.disabled = false;
});

window.addEventListener('appmint:purchase-failed', function (e) {
  buyBtn.disabled = false;
  if (e.detail.reason === 'pending') showMessage('Payment pending. It unlocks by itself once Google confirms.');
  else showMessage('Purchase is not available right now (' + e.detail.reason + ').');
});

// A cancel on Google's sheet sends no event: release the button when the page is visible again.
document.addEventListener('visibilitychange', function () { if (!document.hidden) buyBtn.disabled = false; });
```

The same results also call `window.onAppMintPurchase(detail)` and `window.onAppMintPurchaseFailed(detail)` if you define them.

**Notes:** Reasons: `disabled` (Sell In-App Products is off), `not_found` (the ID is not in Play Console), `billing_error`, `pending` (the user paid with a slow method; the unlock arrives later through the launch restore and `appmint:owned-changed`). If the product is already owned, `appmint:purchase` fires again at once, so your unlock must be safe to repeat. The app also shows its own short message for errors.

### `startRemoveAdsPurchase`

```js
window.WebToApk.startRemoveAdsPurchase()
```

Launch the Google Play "Remove Ads" purchase flow from the page (user-initiated, e.g. the app's own "Remove ads" button). Without this the ONLY entry point was the native side-menu item - unreachable in apps built without the side menu. Outcome reaches the page via 'appmint:premium' {premium:true}; already-owned re-notifies immediately; flow errors surface as a native toast.

**Example**

Opens Google Play's purchase sheet for your "Remove Ads" product, from your page's own "Remove ads" button.

**Returns:** nothing. The result arrives as `appmint:premium` `{premium: true}` (and `window.onAppMintPremiumChanged`). **Needs:** Ads on and **Remove Ads (IAP)** with your **Play Store Product ID** (the first ID listed is the one sold). The app has no menu item for this, so your page must have the button.

```js
var b = window.WebToApk;
var btn = document.getElementById('remove-ads');

if (!b || typeof b.startRemoveAdsPurchase !== 'function' || b.isPremium()) {
  btn.hidden = true;                          // browser, or already ad-free
} else {
  btn.addEventListener('click', function () {
    btn.disabled = true;
    b.startRemoveAdsPurchase();
  });
}

window.addEventListener('appmint:premium', function (e) {
  if (e.detail.premium) { btn.hidden = true; showMessage('Thank you! Ads are gone.'); }
});

// A cancel on Google's sheet sends no event: release the button when the page is visible again.
document.addEventListener('visibilitychange', function () {
  if (!document.hidden) btn.disabled = false;
});
```

**Notes:** If the user already owns it, `appmint:premium` `{premium:true}` fires at once. Errors (Play not reachable, product not found in Play Console) are shown by the app as a short native message; the page gets no event for them. Test with an app installed from a Play testing track.

