Создать APK бесплатно

JavaScript bridge API

Покупки и права доступа

Продажа с вашей собственной страницы и запрос о том, чем уже владеет этот покупатель. Код вокруг этих вызовов см. в руководстве по покупкам в приложении.

AppMint.revenuecat помощник#

AppMint.revenuecat — RevenueCat subscriptions

Пример

Подписки и разовые покупки через ваш проект RevenueCat: предложения, покупки, восстановления и права доступа. В приложении покупки проходят через Google Play; RevenueCat записывает каждую до того, как приложение её завершит.

Возвращает: каждый метод возвращает Promise, который всегда завершается {ok: true, …} или {ok: false, error, message?}. configure({appUserId?}), getCustomerInfo(), restore(), logIn(id), logOut() → {ok, customerInfo}; getOfferings() → {ok, current, all}; purchase(pkgOrProductId, {offeringId?, offerId?, consumable?}) → {ok, productId, transaction, customerInfo}; getAppUserId() → {ok, appUserId}. Требуется: шаг «Интеграция» → RevenueCat с вашим ПУБЛИЧНЫМ ключом Google Play (goog_…).

if (window.AppMint && AppMint.revenuecat) {
  await AppMint.revenuecat.configure({ appUserId: currentUser && currentUser.id });   // omit for an anonymous user
  var off = await AppMint.revenuecat.getOfferings();
  if (off.ok && off.current) {
    off.current.availablePackages.forEach((p) => addPlanButton(p.identifier, p.title, p.price, p.period));
  }
}

async function buy(pkg) {                        // a package object from getOfferings, or its identifier
  var r = await AppMint.revenuecat.purchase(pkg);
  if (r.ok) unlock(r.customerInfo.entitlements.active);
  else if (r.error === 'pending') alert('Payment pending — it will unlock when Google confirms it.');
  else if (!r.cancelled) alert('Purchase failed: ' + (r.message || r.error));
}

var info = await AppMint.revenuecat.getCustomerInfo();
var isPro = info.ok && !!info.customerInfo.entitlements.active.pro;

window.addEventListener('appmint:revenuecat', (e) => render(e.detail.customerInfo));   // entitlements changed
document.getElementById('restore').onclick = () => AppMint.revenuecat.restore();

Привяжите покупки к вашему собственному аккаунту:

if (window.AppMint && AppMint.revenuecat) {
  var li = await AppMint.revenuecat.logIn(user.id);       // after sign-in
  if (li.ok) refreshPlan(li.customerInfo.entitlements.active);
  document.getElementById('signout').onclick = async function () {
    await AppMint.revenuecat.logOut();
    showAnonymous();
  };
}

Показывайте понятное сообщение для каждого сбоя:

async function buyPackage(pkg) {
  var r = await AppMint.revenuecat.purchase(pkg);
  if (r.ok) return unlock(r.customerInfo.entitlements.active);
  if (r.cancelled || r.error === 'cancelled') return;
  var text = {
    not_configured: 'Purchases are not set up in this build',
    billing_unavailable: 'Google Play billing is not available on this phone',
    product_not_found: 'This plan is not available right now',
    network: 'No connection - try again online'
  }[r.error] || ('Purchase failed: ' + (r.message || r.error));
  alert(text);
}

Примечания: customerInfo = {appUserId, originalAppUserId, requestDate, managementUrl, entitlements: {active, all}, activeSubscriptions, allPurchasedProductIds, nonSubscriptionTransactions}; у каждого права {identifier, isActive, productIdentifier, expirationDate, latestPurchaseDate, willRenew, periodType, store, isSandbox}. Передайте {consumable: true} для монет/кредитов, чтобы их можно было покупать снова; подписки и разовые разблокировки подтверждаются. Офлайн getCustomerInfo отвечает последними известными данными с stale: true; покупка, совершённая, пока RevenueCat недоступен, всё равно завершается, и её чек отправляется при следующем configure/restore (receiptQueued: true). Ошибки: not_configured, network, revenuecat_error (с status, code, message), billing_unavailable, billing_error, product_not_found, offer_not_found, cancelled, pending, already_anonymous, foreign_origin. Настройте связь служебного аккаунта Google Play с RevenueCat так, как просит RevenueCat; никогда не вставляйте в приложение секретный ключ sk_. Существующие покупки AppMintIAP продолжают работать без изменений.

AppMintIAP.available помощник#

window.AppMintIAP.available

Пример

Свойство (не функция): true, когда эта страница действительно может покупать через Google Play.

Возвращает: булево свойство. Требуется: 💰 Продажа покупок в приложении. Без этого window.AppMintIAP не существует вовсе.

Проверьте это перед показом любого товара и прямо скажите, когда покупка здесь невозможна:

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';
}

Одна кнопка обновления для вашего сайта и приложения: Google Play внутри приложения, ваш собственный платёж в вебе:

document.getElementById('upgrade').addEventListener('click', function () {
  var iap = window.AppMintIAP;
  if (iap && iap.available) {
    iap.buy('pro_unlock');                                   // inside the app
  } else if (window.WebToApk) {
    showMessage('Upgrades are not available in this version of the app.');
  } else {
    location.href = '/checkout/pro';                         // your website's own checkout
  }
});

В React или Next.js читайте это внутри эффекта (у серверного рендера нет window), чтобы вне приложения ничего для покупки не отрисовывалось:

import { useEffect, useState } from 'react';

export function useCanBuy() {
  const [canBuy, setCanBuy] = useState(false);     // false during the server render
  useEffect(() => {
    setCanBuy(!!(window.AppMintIAP && window.AppMintIAP.available));
  }, []);
  return canBuy;
}

// const canBuy = useCanBuy();
// return canBuy ? <UpgradeButton /> : null;

Примечания: window.AppMintIAP устанавливается до запуска первого скрипта вашей страницы; только в WebView старше примерно 2021 года он появляется после полной загрузки страницы, поэтому код, который должен работать и там, должен его дождаться (см. шаблон whenIap в AppMintIAP.sell). То же значение есть в AppMintIAP.state().available. Если обернуть предложение в data-iap-container, оно скрывается вне приложения без кода.

AppMintIAP.buy помощник#

window.AppMintIAP.buy(id)

Пример

Открывает окно покупки Google Play для товара по вашей собственной кнопке. Само по себе ничего не выдаёт.

Возвращает: ничего. Результат приходит как appmint:purchase {productId, owned:true} (и через AppMintIAP.onChange) или appmint:purchase-failed {productId, reason}. Требуется: 💰 Продажа покупок в приложении и товар, активный в Play Console.

Используйте, когда вы рисуете кнопку сами; разблокировка приходит от владения, а не от нажатия:

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

Выбор плана с одной кнопкой «Продолжить», которая покупает выбранный план:

document.getElementById('continue').addEventListener('click', function () {
  var choice = document.querySelector('input[name="plan"]:checked');   // value = product ID
  var iap = window.AppMintIAP;
  if (!choice || !iap || !iap.available) return;
  if (iap.isOwned(choice.value)) { showMessage('You already have this plan.'); return; }
  iap.buy(choice.value);
});

window.addEventListener('appmint:purchase', function () {
  document.getElementById('plan-picker').hidden = true;
  showMessage('Thanks! Your plan is active.');
});

Нажатие на инструмент только для Pro запускает его, если куплено, а иначе открывает окно покупки:

document.querySelectorAll('[data-pro-tool]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var iap = window.AppMintIAP;
    if (iap && iap.isOwned('pro_unlock')) { runTool(btn.getAttribute('data-pro-tool')); return; }
    if (iap && iap.available) iap.buy('pro_unlock');
    else showMessage('Pro tools are available in the Android app.');
  });
});

window.addEventListener('appmint:purchase', function (e) {
  if (e.detail.productId === 'pro_unlock') document.body.classList.add('pro');
});

Примечания: buy() — это «голый» WebToApk.purchase(id): без надписи о занятости, без проверки группы. Кнопки, подключённые через sell(), start() или data-iap-buy, делают всё это за вас, так что предпочитайте их.

AppMintIAP.isOwned помощник#

window.AppMintIAP.isOwned(id)

Пример

Мгновенно и офлайн проверяет, владеет ли пользователь товаром (разовым товаром или ещё активной подпиской).

Возвращает: true / false синхронно; false вне приложения. Требуется: 💰 Продажа покупок в приложении.

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

Pro через любой из нескольких планов: месячный, годовой и пожизненный товары — все учитываются:

var PRO_PLANS = ['pro_monthly', 'pro_yearly', 'pro_lifetime'];

function isPro() {
  var iap = window.AppMintIAP;
  return !!iap && PRO_PLANS.some(function (id) { return iap.isOwned(id); });
}

function paintPlan() { document.body.classList.toggle('pro', isPro()); }
paintPlan();
window.addEventListener('appmint:purchase', paintPlan);
window.addEventListener('appmint:owned-changed', paintPlan);   // an expired subscription or a refund

Защита маршрута: экраны только для Pro отправляют пользователя без Pro на экран обновления:

function route() {
  var path = location.hash.slice(1) || '/';
  var proOnly = path.indexOf('/reports') === 0;
  if (proOnly && !(window.AppMintIAP && window.AppMintIAP.isOwned('pro_unlock'))) {
    location.replace('#/upgrade');
    return;
  }
  render(path);
}

window.addEventListener('hashchange', route);
route();

Примечания: Тот же ответ, что и у WebToApk.isOwned(id). Владение восстанавливается из аккаунта Google пользователя при каждом запуске и сохраняется на телефоне, поэтому переживает переустановку и работает офлайн. Никогда не храните право только в localStorage.

AppMintIAP.onChange помощник#

window.AppMintIAP.onChange(fn)

Пример

Сразу вызывает вашу функцию с состоянием покупок и снова при каждом его изменении (пришли цены, прошла покупка, восстановление при запуске, возврат, истечение подписки).

Возвращает: функцию отписки. fn получает {available, adFree, owned, ownedIds, products}; products — это {id: {price, currency, type, period, owned}} для товаров, которые знает помощник (из sell, start или data-iap-buy). Требуется: 💰 Продажа покупок в приложении.

Хук React. Он возвращает функцию отписки прямо из эффекта:

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;

Платный доступ на чистом JavaScript: отслеживайте два плана без кнопок, затем рисуйте их цены и текущий план при каждом изменении:

var iap = window.AppMintIAP;
var PLANS = ['pro_monthly', 'pro_yearly'];

if (iap) {
  iap.start({ plans: PLANS.map(function (id) { return { id: id }; }) });   // tracked, not drawn
  iap.onChange(function (s) {
    document.getElementById('paywall').hidden = !s.available;
    PLANS.forEach(function (id) {
      var el = document.querySelector('[data-plan="' + id + '"]');
      var p = s.products[id];
      el.hidden = !p;                                // Google has not answered for it (yet)
      if (p) el.querySelector('.price').textContent = p.owned ? 'Your plan' : p.price;
    });
  });
}

Подпишитесь, пока открыт экран аккаунта, и отпишитесь, когда он закрывается. adFree и ownedIds заполняются после запуска помощника (страница использует sell(), start() или data-iap-buy):

var stopAccount = null;

function openAccountScreen() {
  if (window.AppMintIAP) {
    stopAccount = window.AppMintIAP.onChange(function (s) {
      document.getElementById('ad-status').textContent = s.adFree ? 'Ad-free' : 'With ads';
      document.getElementById('owned-count').textContent = s.ownedIds.length + ' purchase(s)';
    });
  }
  showScreen('account');
}

function closeAccountScreen() {
  if (stopAccount) { stopAccount(); stopAccount = null; }   // no updates for a hidden screen
  showScreen('home');
}

Примечания: Читайте владение по каждому товару (ownedIds или products[id].owned). Верхнеуровневое owned означает лишь «чем-то владеют». adFree — право «Отключить рекламу» (WebToApk.isPremium()). Товар попадает в products только после того, как Google ответил по нему.

AppMintIAP.owned помощник#

window.AppMintIAP.owned()

Пример

Перечисляет все идентификаторы товаров, которыми владеет пользователь.

Возвращает: массив идентификаторов товаров синхронно, например ['pro_unlock', 'level_pack_2']; [] вне приложения или когда ничем не владеют. Требуется: 💰 Продажа покупок в приложении.

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

Посчитайте, сколько уровней открыто, когда каждый пакет уровней — отдельный товар:

function unlockedLevelCount() {
  var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
  var packs = ids.filter(function (id) { return /^level_pack_\d+$/.test(id); }).length;
  return 10 + packs * 10;                         // 10 free levels, 10 more per pack
}

function paintLevelCount() {
  document.getElementById('levels-label').textContent = unlockedLevelCount() + ' levels unlocked';
}
paintLevelCount();
window.addEventListener('appmint:owned-changed', paintLevelCount);

Список «Мои покупки» на вашем экране настроек с вашим собственным названием для каждого товара:

var LABELS = { pro_unlock: 'Pro features', remove_ads: 'No ads', level_pack_2: 'Levels 11-20' };

function renderPurchases() {
  var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
  var list = document.getElementById('purchases');
  list.textContent = '';
  ids.forEach(function (id) {
    var li = document.createElement('li');
    li.textContent = LABELS[id] || id;
    list.appendChild(li);
  });
  document.getElementById('no-purchases').hidden = ids.length > 0;
}
renderPurchases();
window.addEventListener('appmint:owned-changed', renderPurchases);

Примечания: Разбирается из WebToApk.getOwnedProducts() за вас. Включает активные подписки и товар «Отключить рекламу». Работает офлайн.

AppMintIAP.refresh помощник#

window.AppMintIAP.refresh()

Пример

Заново ищет кнопки data-iap-buy, добавленные с последнего сканирования, и подключает их (или запускает помощника, если он пока ничего не нашёл).

Возвращает: объект AppMintIAP. Требуется: 💰 Продажа покупок в приложении.

Помощник уже следит за появлением новых кнопок data-iap-buy на странице, так что это нужно редко. Вызывайте после вставки кнопок способом, который наблюдатель мог пропустить, например при изменении атрибутов существующего элемента:

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

Карточки магазина, товары которых приходят с вашего собственного сервера: кнопки уже есть на странице, меняются только их атрибуты, поэтому подключите их через refresh():

fetch('/shop.json').then(function (r) { return r.json(); }).then(function (shop) {
  shop.items.forEach(function (item) {           // [{slot, productId, name}]
    var btn = document.querySelector('.shop-card[data-slot="' + item.slot + '"] button');
    if (!btn) return;
    btn.setAttribute('data-iap-buy', item.productId);
    btn.setAttribute('data-iap-label', item.name + ' - {price}');
  });
  if (window.AppMintIAP) window.AppMintIAP.refresh();
});

Праздничный набор, который поступает в продажу в заданное время, на кнопке, которая была на странице с самого начала:

var saleStart = new Date('2026-12-01T00:00:00').getTime();

function openSale() {
  var btn = document.getElementById('sale-btn');
  btn.setAttribute('data-iap-buy', 'holiday_bundle');
  btn.setAttribute('data-iap-owned', 'Bundle unlocked');
  document.getElementById('sale-card').hidden = false;
  if (window.AppMintIAP) window.AppMintIAP.refresh();
}

var wait = saleStart - Date.now();
if (wait <= 0) openSale();
else if (wait < 86400000) setTimeout(openSale, wait);   // later than a day: the next launch opens it

Примечания: Каждая новая кнопка получает цену от Google и состояние «куплено». Уже подключённая кнопка повторно не подключается. Кнопки, элемент которых удалён со страницы, отбрасываются.

AppMintIAP.sell помощник#

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

Пример

Подключает один товар к одной кнопке одной строкой. Вызывайте один раз для каждого товара.

Возвращает: объект AppMintIAP (для цепочки вызовов). Требуется: 💰 Продажа покупок в приложении в мастере сборки и созданный и активный товар в Play Console. window.AppMintIAP устанавливается до запуска первого скрипта вашей страницы (в WebView старше примерно 2021 года — после полной загрузки страницы; шаблон whenIap ниже покрывает оба случая).

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

Несколько планов, где покупка одного заменяет остальные, используют общий group:

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

Отслеживайте товар без кнопки и рисуйте предложение сами (null ничего не подключает; ваша кнопка вызывает buy):

whenIap(function (iap) {
  iap.sell('remove_ads', null);                      // tracked, never drawn by the helper
  iap.onChange(function (s) {
    var p = s.products.remove_ads;                   // undefined until Google answers
    var btn = document.getElementById('remove-ads');
    if (!s.available || (p && p.owned)) { btn.hidden = true; return; }
    btn.hidden = false;
    btn.textContent = p && p.price ? 'Remove ads for ' + p.price : 'Remove ads';
  });
  document.getElementById('remove-ads').addEventListener('click', function () {
    iap.buy('remove_ads');
  });
});

Подключите кнопку, которая появляется, только когда открывается диалог вашего магазина (поздний вызов присоединяется к работающему помощнику):

document.getElementById('open-shop').addEventListener('click', function () {
  openShopDialog();                                  // your code: adds <button id="buy-levels">
  if (!window.AppMintIAP) return;                    // not in the app
  window.AppMintIAP.sell('level_pack_2', '#buy-levels', {
    label: 'Levels 11-20 — {price}',
    owned: '✓ Levels 11-20 unlocked',
    show: '#levels-2-link'                           // your menu entry, shown once owned
  });
});

Примечания: button — это элемент или селектор CSS; передайте null, чтобы отслеживать товар без кнопки (ваш собственный интерфейс читает AppMintIAP.state()). Другие opts: labelEl (элемент внутри кнопки, текст которого меняется). Работает до или после запуска помощника, в том числе из эффекта React. Так же, как атрибуты data-iap-buy (см. AppMintIAP.start). Разблокируйте платный контент только по владению (isOwned, onChange), а не в собственном обработчике нажатия.

AppMintIAP.start помощник#

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

Пример

Подключает ваши кнопки покупки к Google Play за один шаг: актуальные цены на кнопках, окно покупки по нажатию, метки «куплено», блоки с благодарностью, восстановление при запуске и понятное сообщение при каждом сбое.

Возвращает: объект AppMintIAP (для цепочки вызовов). Требуется: 💰 Продажа покупок в приложении в мастере сборки и созданные и активные идентификаторы товаров в Play Console. Приложение устанавливает window.AppMintIAP до запуска первого скрипта вашей страницы (в WebView старше примерно 2021 года — только после полной загрузки страницы).

Совсем без кода: разметьте HTML. Помощник сам читает эти атрибуты сразу после внедрения и следит за кнопками, которые React или Vue отрисуют позже:

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

С кодом: вызывайте start() сами, когда нужна своя функция сообщений или хуки разблокировки. Используйте на страницах БЕЗ кнопок data-iap-buy в HTML (если они есть, помощник уже запущен, и второй запуск ничего не делает):

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

В React: начните с планов без button (ничего не рисуется) и пусть React отрисовывает из onChange, который возвращает функцию отписки для эффекта:

function ProButton() {
  const [iap, setIap] = React.useState({ available: false, products: {} });

  React.useEffect(() => {
    if (typeof window === 'undefined' || !window.AppMintIAP) return;   // server render / browser
    window.AppMintIAP.start({ plans: [{ id: 'pro_unlock' }] });       // safe to run twice
    return window.AppMintIAP.onChange(setIap);
  }, []);

  const pro = iap.products?.pro_unlock;                                // read ownership per product
  if (!iap.available) return null;                                     // nothing to buy outside the app
  return pro?.owned
    ? <p>Thanks! Pro is unlocked.</p>
    : <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>Unlock Pro {pro?.price}</button>;
}

Примечания: Атрибуты: data-iap-buy="id", data-iap-label ({price} становится актуальной ценой Play), data-iap-owned (надпись после покупки; по умолчанию «✓ Куплено»), data-iap-group (планы, заменяющие друг друга; после покупки одного остальные скрываются, если не hideOtherPlans: false), data-iap-show / data-iap-hide (значение = идентификатор товара или группа; пусто = чем-либо куплено), data-iap-text (часть кнопки, текст которой меняется), data-iap-container. Купленная кнопка получает data-iap-state="owned" и класс iap-unlocked. Отмена в окне Google не посылает событие; кнопка освобождается, когда страница снова видна (или через 30 с). Вызов start(options) после того, как помощник уже сам запустился по атрибутам data-iap-buy, ОБЪЕДИНЯЕТ параметры (планы, показ/скрытие, контейнер, onUnlock/onLock, toast, busyLabel, hideOtherPlans) с работающими; двойной вызов безвреден. Страница со своим appmint-iap.js сохраняет его — встроенная копия уступает.

AppMintIAP.started помощник#

window.AppMintIAP.started

Пример

Свойство (не функция): true, когда помощник запущен, то есть подключил хотя бы один товар из кнопок start(), sell() или data-iap-buy.

Возвращает: булево свойство. Требуется: 💰 Продажа покупок в приложении.

Используйте, чтобы выбрать между start() (с вашими параметрами) и sell() (который работает и на уже запущенном помощнике):

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

Предупредите себя во время разработки, если на странице ничего не подключено (опечатка в data-iap-buy или кнопки не отрисовались):

window.addEventListener('load', function () {
  setTimeout(function () {
    var iap = window.AppMintIAP;
    if (iap && !iap.started) {
      console.warn('AppMintIAP is still waiting: no data-iap-buy button, sell() or start() on this page');
    }
  }, 3000);
});

Отличите экран магазина от «ещё загружается» и «покупки недоступны»:

function storeStatus() {
  var iap = window.AppMintIAP;
  if (!iap) return 'web';                            // opened in a normal browser
  if (!iap.started) return 'waiting';                // no product wired yet
  return iap.state().available ? 'ready' : 'unavailable';
}
document.getElementById('store').setAttribute('data-status', storeStatus());

Примечания: Второй вызов start() никогда не запускает его дважды (React StrictMode запускает эффекты дважды); его параметры объединяются с уже работающими. Пока ни один товар не подключён, остаётся false; тогда помощник продолжает следить за страницей в поисках кнопок data-iap-buy.

AppMintIAP.state помощник#

window.AppMintIAP.state()

Пример

Один раз возвращает текущее состояние покупок без подписки.

Возвращает: {available, adFree, owned, ownedIds, products} синхронно. ownedIds — массив идентификаторов товаров; products — это {id: {price, currency, type: 'inapp'|'subs', period, owned}} для товаров, о которых помощник спросил Google. Требуется: 💰 Продажа покупок в приложении.

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();

Проверьте владение в момент, когда игрок нажимает заблокированный уровень, а если не владеет — отправьте его в ваш магазин:

function openLevel(n) {
  var s = window.AppMintIAP ? window.AppMintIAP.state() : null;
  var needsPack = n > 10;
  if (needsPack && !(s && s.ownedIds.indexOf('level_pack_2') !== -1)) {
    showShop('level_pack_2');                        // your own screen
    return;
  }
  startLevel(n);
}

Скройте собственные призывы «Отключить рекламу», когда право на отсутствие рекламы в сборке активно, и покажите цены, которые Google уже прислал:

if (window.AppMintIAP) {
  var s = window.AppMintIAP.state();
  document.querySelectorAll('.go-ad-free').forEach(function (el) { el.hidden = s.adFree; });

  var list = document.getElementById('price-list');
  Object.keys(s.products).forEach(function (id) {
    var p = s.products[id];
    var li = document.createElement('li');
    li.textContent = id + ': ' + (p.owned ? 'owned' : (p.price || '…')) +
                     (p.type === 'subs' ? ' (subscription)' : '');
    list.appendChild(li);
  });
}

Примечания: Это снимок. Чтобы следить за изменениями, используйте AppMintIAP.onChange(fn), который каждый раз при изменении отдаёт тот же объект. owned равно true, когда чем-либо вообще владеют; проверяйте товар через ownedIds или AppMintIAP.isOwned(id).

getOwnedProducts мост#

window.WebToApk.getOwnedProducts(): String

Массив JSON со всеми купленными идентификаторами товаров — для закрытия целых экранов при запуске.

Пример

Перечисляет все товары Play, которыми владеет пользователь, чтобы разблокировать целые экраны при запуске.

Возвращает: строку JSON (массив идентификаторов товаров) синхронно, например '["pro_unlock","level_pack_2"]'. Разберите её через JSON.parse. '[]', когда ничем не владеют. Требуется: 💰 Продажа покупок в приложении в мастере сборки.

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', ...]}
});

То же изменение также вызывает window.onAppMintOwnedChanged(detail), если вы её определили.

Список «Мои покупки» на вашем экране настроек с вашим собственным названием для каждого товара:

var NAMES = { pro_unlock: 'Pro features', level_pack_2: 'Levels 11-20', remove_ads: 'No ads' };

function renderMyPurchases() {
  var b = window.WebToApk, ids = [];
  if (b && typeof b.getOwnedProducts === 'function') {
    try { ids = JSON.parse(b.getOwnedProducts()); } catch (e) {}
  }
  var list = document.getElementById('my-purchases');
  list.textContent = '';
  ids.forEach(function (id) {
    var li = document.createElement('li');
    li.textContent = NAMES[id] || id;
    list.appendChild(li);
  });
  document.getElementById('nothing-bought').hidden = ids.length > 0;
}
renderMyPurchases();
window.addEventListener('appmint:owned-changed', renderMyPurchases);

Определите лучший уровень, которым владеет пользователь, когда каждый уровень — отдельный товар:

var TIERS = ['premium_plus', 'premium', 'basic'];     // best first

function currentTier() {
  var b = window.WebToApk, owned = [];
  if (b && typeof b.getOwnedProducts === 'function') {
    try { owned = JSON.parse(b.getOwnedProducts()); } catch (e) {}
  }
  for (var i = 0; i < TIERS.length; i++) if (owned.indexOf(TIERS[i]) !== -1) return TIERS[i];
  return 'free';
}

applyTier(currentTier());
window.addEventListener('appmint:purchase', function () { applyTier(currentTier()); });
window.addEventListener('appmint:owned-changed', function () { applyTier(currentTier()); });

Примечания: Включает разовые товары и активные подписки, а также товар «Отключить рекламу», если пользователь его купил. Работает офлайн; список сохраняется на телефоне при каждом восстановлении при запуске и покупке. appmint:owned-changed срабатывает, только когда список действительно изменился, поэтому при обычном запуске может не сработать вовсе.

getProducts мост#

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

Запрашивает у Play актуальные данные по переданным идентификаторам товаров (массив строк JSON). Результат приходит как 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. Идентификаторы, которых Play не знает, в результате отсутствуют — страница скрывает такие позиции.

Пример

Запрашивает у Google Play актуальные данные о ваших товарах: название, описание и цену в местной валюте.

Возвращает: пока ничего. Ответ приходит как событие appmint:products: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type — это 'inapp' или 'subs'; period — период ISO-8601, например 'P1M' (только для подписок). Требуется: 💰 Продажа покупок в приложении в мастере сборки. Второй аргумент — строка JSON: используйте JSON.stringify.

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

Ответ также передаётся в window.onAppMintProducts(detail), если вы её определили.

Выбор плана подписки, где каждая карточка подписана ценой из Play и периодом оплаты:

var PERIODS = { P1W: 'week', P1M: 'month', P3M: '3 months', P6M: '6 months', P1Y: 'year' };

window.addEventListener('appmint:products', function (e) {
  if (e.detail.requestId !== 'plans') return;
  e.detail.products.forEach(function (p) {
    var card = document.querySelector('.plan[data-product="' + p.productId + '"]');
    if (!card || p.type !== 'subs') return;
    card.hidden = false;
    card.querySelector('.price').textContent = p.price + ' / ' + (PERIODS[p.period] || p.period);
    card.classList.toggle('current', p.owned);
  });
});

var b = window.WebToApk;
if (b && typeof b.getProducts === 'function') {
  b.getProducts('plans', JSON.stringify(['pro_weekly', 'pro_monthly', 'pro_yearly']));
}

При тестировании записывайте в журнал идентификаторы, которые Google Play не вернул, через хук страницы:

var WANTED = ['pro_unlock', 'coins_500', 'pro_monthly'];

window.onAppMintProducts = function (detail) {       // same payload as appmint:products
  if (detail.requestId !== 'store-check') return;
  var found = detail.products.map(function (p) { return p.productId; });
  var missing = WANTED.filter(function (id) { return found.indexOf(id) === -1; });
  if (missing.length) console.warn('Not returned by Google Play: ' + missing.join(', '));
  document.getElementById('store').hidden = found.length === 0;
};

if (window.WebToApk && typeof window.WebToApk.getProducts === 'function') {
  window.WebToApk.getProducts('store-check', JSON.stringify(WANTED));
}

Примечания: Показывайте price ровно так, как его даёт Play, и никогда не сохраняйте. Идентификатор, которого Play не знает (не создан, не активен или приложение установлено не из Play), просто отсутствует в products; держите такую позицию скрытой. requestId, похожий на число, возвращается как число, поэтому используйте текстовый идентификатор или сравнивайте с String().

isOwned мост#

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

Мгновенная проверка владения на устройстве (разовый товар куплен ИЛИ подписка активна).

Пример

Мгновенно и офлайн проверяет, владеет ли пользователь одним из ваших товаров Play (разовым товаром или ещё активной подпиской).

Возвращает: true / false синхронно. Требуется: 💰 Продажа покупок в приложении в мастере сборки. При включённом переключателе он отвечает и по товару Отключение рекламы (IAP); в приложении, которое продаёт только отключение рекламы (переключатель выключен), он отвечает false для любого товара, поэтому там проверяйте отключение рекламы через isPremium().

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

Проверяйте в момент использования: экспорт только для Pro выполняется, если куплено, а иначе открывает окно покупки и завершается, когда покупка пройдёт:

var exportWaiting = false;

document.getElementById('export-hd').addEventListener('click', function () {
  var b = window.WebToApk;
  if (!b || typeof b.isOwned !== 'function') { exportStandard(); return; }   // browser
  if (b.isOwned('pro_unlock')) { exportHd(); return; }
  exportWaiting = true;
  b.purchase('pro_unlock');
});

window.addEventListener('appmint:purchase', function (e) {
  if (e.detail.productId === 'pro_unlock' && exportWaiting) {
    exportWaiting = false;
    exportHd();                                  // finish what the user asked for
  }
});

Pro через любой из нескольких планов: месячный, годовой и пожизненный товары — все учитываются:

var PRO_PLANS = ['pro_monthly', 'pro_yearly', 'pro_lifetime'];

function isPro() {
  var b = window.WebToApk;
  if (!b || typeof b.isOwned !== 'function') return false;
  return PRO_PLANS.some(function (id) { return b.isOwned(id); });
}

function paintPlan() { document.body.classList.toggle('pro', isPro()); }
paintPlan();
window.addEventListener('appmint:purchase', paintPlan);
window.addEventListener('appmint:owned-changed', paintPlan);   // an expired subscription or a refund

Примечания: Список купленного восстанавливается из аккаунта Google пользователя при каждом запуске и сохраняется на телефоне, поэтому работает офлайн и после переустановки, без сервера. В браузере всегда false. Подписка перестаёт учитываться, когда Play больше не сообщает о ней как об активной.

isPremium мост#

window.WebToApk.isPremium(): Boolean

Истина, когда пользователь владеет «Отключить рекламу» (право Play Billing, автоматически восстанавливается при каждом запуске). Соответствует причине 'ad_free' в 'appmint:ad-unavailable'.

Пример

Сообщает, владеет ли пользователь вашим товаром «Отключить рекламу», чтобы страница могла скрыть свою кнопку «Отключить рекламу» и элементы интерфейса, связанные с рекламой.

Возвращает: true / false синхронно. Требуется: включённая реклама и Отключение рекламы (IAP) с вашим Идентификатором товара в Play Store. Без этого всегда false.

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

То же изменение также вызывает хук страницы, если вы его определили:

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

Показывайте своё предложение «Отключить рекламу» на экране окончания игры только тем игрокам, которые всё ещё видят рекламу:

function showGameOver(score) {
  document.getElementById('final-score').textContent = score;
  var b = window.WebToApk;
  var premium = !!(b && typeof b.isPremium === 'function' && b.isPremium());
  // Ad-free already, or a plain browser (no ads there): no offer.
  document.getElementById('remove-ads-offer').hidden = premium || !b;
  document.getElementById('game-over').hidden = false;
}

Примечания: Владение восстанавливается из аккаунта Google пользователя при каждом запуске, поэтому прочитайте его один раз при старте, а затем следите за appmint:premium. Поле «Идентификатор товара» принимает несколько идентификаторов через запятую (например, месячный план и пожизненный); владение любым из них засчитывается. Пока есть премиум, реклама отключена, а вызовы рекламы отвечают ad_free.

purchase мост#

window.WebToApk.purchase(productId: String)

Открывает окно покупки Google для [productId] (разовый товар или подписка — определяется автоматически). Выдавайте ТОЛЬКО по 'appmint:purchase' {productId, owned:true}; сбои вызывают 'appmint:purchase-failed' {reason}. Если товар уже куплен, событие покупки срабатывает снова, чтобы страница могла безопасно повторить разблокировку.

Пример

Открывает окно покупки Google Play для одного из ваших товаров (разового товара или подписки; приложение само определяет, какой именно).

Возвращает: пока ничего. Успех вызывает appmint:purchase {productId, owned: true}. Сбой вызывает appmint:purchase-failed {productId, reason}. Требуется: 💰 Продажа покупок в приложении в мастере сборки и созданный и активный товар в Play Console.

Разблокируйте ТОЛЬКО в обработчике appmint:purchase, никогда по нажатию:

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

Те же результаты также вызывают window.onAppMintPurchase(detail) и window.onAppMintPurchaseFailed(detail), если вы их определили.

Купите план, который пользователь выбрал в списке подписок, и скройте план, идентификатора которого Play не знает:

var picked = 'pro_yearly';
document.querySelectorAll('.plan').forEach(function (card) {
  card.addEventListener('click', function () { picked = card.getAttribute('data-product'); });
});

document.getElementById('subscribe').addEventListener('click', function () {
  var b = window.WebToApk;
  if (b && typeof b.purchase === 'function') b.purchase(picked);
});

window.addEventListener('appmint:purchase', function (e) {
  if (/^pro_/.test(e.detail.productId)) unlockPro();        // any plan unlocks Pro
});
window.addEventListener('appmint:purchase-failed', function (e) {
  if (e.detail.reason !== 'not_found') return;
  var card = document.querySelector('.plan[data-product="' + e.detail.productId + '"]');
  if (card) card.hidden = true;
});

Разблокируйте пакет уровней с заблокированного уровня, который нажал игрок, затем откройте этот уровень:

var wantedLevel = null;

function openLevel(n) {
  var b = window.WebToApk;
  var owned = !!(b && typeof b.isOwned === 'function' && b.isOwned('level_pack_2'));
  if (n <= 10 || owned) { startLevel(n); return; }
  if (!b || typeof b.purchase !== 'function') { showMessage('Levels 11-20 are in the Android app.'); return; }
  wantedLevel = n;
  b.purchase('level_pack_2');
}

window.onAppMintPurchase = function (detail) {       // {productId, owned: true}
  if (detail.productId !== 'level_pack_2' || !wantedLevel) return;
  var n = wantedLevel;
  wantedLevel = null;
  startLevel(n);
};

Примечания: Причины: disabled (Продажа покупок в приложении выключена), not_found (идентификатора нет в Play Console), billing_error, pending (пользователь заплатил медленным способом; разблокировка придёт позже через восстановление при запуске и appmint:owned-changed). Если товар уже куплен, appmint:purchase срабатывает снова сразу, так что ваша разблокировка должна быть безопасна при повторе. Приложение также показывает своё короткое сообщение об ошибках.

revenuecat мост#

window.WebToApk.revenuecat(action: String, argsJson: String, callbackId: String)

Одно действие RevenueCat [action] с [argsJson]; передаёт в [callbackId] {ok, …} или {ok:false, error}.

Пример

Низкоуровневый метод за AppMint.revenuecat — используйте обёртку. Одна точка входа: action — это configure, getOfferings, purchase, restore, getCustomerInfo, logIn, logOut или getAppUserId.

Возвращает: ничего напрямую; передаёт в window.__appmintReplies[callbackId] тот же ответ, что и обёртка. Требуется: шаг «Интеграция» → RevenueCat с вашим публичным ключом goog_.

window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc1'] = (r) => console.log(r.ok ? r.customerInfo : r.error);
WebToApk.revenuecat('getCustomerInfo', '{}', 'rc1');

Настройте с идентификатором пользователя и прочитайте предложения:

window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc-cfg'] = (r) => {
  if (!r.ok) return console.warn('RevenueCat:', r.error);
  window.__appmintReplies['rc-off'] = (o) => console.log(o.ok ? o.current : o.error);
  WebToApk.revenuecat('getOfferings', '{}', 'rc-off');
};
if (window.WebToApk && WebToApk.revenuecat) {
  WebToApk.revenuecat('configure', JSON.stringify({ appUserId: 'u_42' }), 'rc-cfg');
}

Покупайте и обрабатывайте отказы:

window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc-buy'] = (r) => {
  if (r.ok) return unlock(r.customerInfo.entitlements.active);
  if (r.error === 'foreign_origin') return showMessage('Purchases only work inside the app.');
  if (r.cancelled) return;
  showMessage('Purchase failed: ' + (r.message || r.error));
};
WebToApk.revenuecat('purchase', JSON.stringify({ package: '$rc_monthly' }), 'rc-buy');

Примечания: Аргументы и ответы каждого действия см. в AppMint.revenuecat.

startRemoveAdsPurchase мост#

window.WebToApk.startRemoveAdsPurchase()

Запускает со страницы покупку «Отключить рекламу» в Google Play (по действию пользователя, например по собственной кнопке приложения «Отключить рекламу»). Раньше единственной точкой входа был нативный пункт бокового меню — недоступный в приложениях, собранных без бокового меню. Результат приходит на страницу через 'appmint:premium' {premium:true}; если товар уже куплен, уведомление приходит сразу; ошибки потока показываются нативным уведомлением.

Пример

Открывает окно покупки Google Play для вашего товара «Отключить рекламу» по собственной кнопке «Отключить рекламу» на вашей странице.

Возвращает: ничего. Результат приходит как appmint:premium {premium: true} (и window.onAppMintPremiumChanged). Требуется: включённая реклама и Отключение рекламы (IAP) с вашим Идентификатором товара в Play Store (продаётся первый указанный идентификатор). Если включено Боковое меню, в приложении есть и собственный пункт Отключить рекламу (скрывается, когда пользователь уже владеет им); при выключенном боковом меню единственный способ купить — эта кнопка на вашей странице.

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

Строка настроек, показывающая, отключена ли реклама, с кнопкой для её покупки:

var row = document.getElementById('ads-setting');
var btn = row.querySelector('button');
var b = window.WebToApk;

function paintAdsRow(premium) {
  row.querySelector('.status').textContent = premium ? 'Ads removed. Thank you!' : 'Ads are on';
  btn.hidden = premium;
}

if (b && typeof b.startRemoveAdsPurchase === 'function') {
  paintAdsRow(b.isPremium());
  btn.addEventListener('click', function () { b.startRemoveAdsPurchase(); });
  window.onAppMintPremiumChanged = function (info) { paintAdsRow(info.premium); };   // {premium}
} else {
  row.hidden = true;                                  // browser: nothing to sell
}

Предлагайте после каждого третьего законченного раунда в диалоге, на который игрок отвечает нажатием:

var rounds = 0;
var offer = document.getElementById('remove-ads-offer');   // a <dialog>

function onRoundFinished() {
  var b = window.WebToApk;
  if (!b || typeof b.startRemoveAdsPurchase !== 'function' || b.isPremium()) return;
  rounds += 1;
  if (rounds % 3 === 0) offer.showModal();
}

offer.querySelector('.buy').addEventListener('click', function () {
  offer.close();
  window.WebToApk.startRemoveAdsPurchase();   // opened from the player's own tap
});
offer.querySelector('.later').addEventListener('click', function () { offer.close(); });

Примечания: Если пользователь уже владеет им, appmint:premium {premium:true} срабатывает сразу. Ошибки (Play недоступен, товар не найден в Play Console) приложение показывает коротким нативным сообщением; страница событий о них не получает. Тестируйте с приложением, установленным из тестового трека Play.