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 offPro через любой из нескольких планов: месячный, годовой и пожизненный товары — все учитываются:
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.