API da ponte JavaScript
Compras e direitos de acesso
Vender a partir da sua própria página e perguntar o que este comprador já possui. Veja o guia de compras no app para o código ao redor disso.
AppMint.revenuecat helper#
AppMint.revenuecat — RevenueCat subscriptions
Exemplo
Assinaturas e compras avulsas pelo seu projeto RevenueCat: ofertas, compras, restaurações e direitos de acesso. As compras passam pelo Google Play no app; o RevenueCat registra cada uma antes de o app concluí-la.
Retorna: todo método retorna uma Promise que sempre resolve {ok: true, …} ou {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}. Requer: etapa Integrar → RevenueCat com sua chave PÚBLICA do 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();Vincule as compras à sua própria conta:
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();
};
}Mostre uma mensagem honesta para cada falha:
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);
}Observações: customerInfo = {appUserId, originalAppUserId, requestDate, managementUrl, entitlements: {active, all}, activeSubscriptions, allPurchasedProductIds, nonSubscriptionTransactions}; cada direito de acesso {identifier, isActive, productIdentifier, expirationDate, latestPurchaseDate, willRenew, periodType, store, isSandbox}. Passe {consumable: true} para moedas/créditos, para que possam ser comprados de novo; assinaturas e desbloqueios avulsos são confirmados. Offline, getCustomerInfo responde com as últimas informações conhecidas e stale: true; uma compra feita enquanto o RevenueCat está inacessível é concluída mesmo assim e seu recibo é enviado em configure/restore (receiptQueued: true). Erros: not_configured, network, revenuecat_error (com status, code, message), billing_unavailable, billing_error, product_not_found, offer_not_found, cancelled, pending, already_anonymous, foreign_origin. Configure a conexão de conta de serviço do Google Play no RevenueCat como ele pedir; nunca coloque uma chave secreta sk_ em um app. As compras AppMintIAP existentes continuam funcionando sem mudanças.
AppMintIAP.available helper#
window.AppMintIAP.available
Exemplo
Uma propriedade (não uma função): true quando esta página pode realmente comprar pelo Google Play.
Retorna: uma propriedade booleana. Requer: 💰 Vender produtos no app. Sem isso, window.AppMintIAP simplesmente não existe.
Verifique antes de mostrar qualquer coisa à venda e diga com clareza quando comprar não for possível aqui:
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';
}Um botão de upgrade para o seu site e o seu app: Google Play dentro do app, seu próprio checkout na web:
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
}
});No React ou Next.js, leia dentro de um efeito (a renderização no servidor não tem window), para que nada à venda seja renderizado fora do app:
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;Observações: window.AppMintIAP é instalado antes do primeiro script da sua página rodar; só em WebViews anteriores a cerca de 2021 ele chega depois que a página termina de carregar, então o código que também precisa rodar lá deve esperar por ele (veja o padrão whenIap em AppMintIAP.sell). O mesmo valor está em AppMintIAP.state().available. Envolver a oferta em data-iap-container a esconde fora do app sem código.
AppMintIAP.buy helper#
window.AppMintIAP.buy(id)
Exemplo
Abre a tela de compra do Google Play para um produto, a partir do seu próprio botão. Ela não concede nada por si só.
Retorna: nada. O resultado chega como appmint:purchase {productId, owned:true} (e por AppMintIAP.onChange) ou appmint:purchase-failed {productId, reason}. Requer: 💰 Vender produtos no app e o produto ativo no Play Console.
Use quando você mesmo desenha o botão; o desbloqueio vem da posse, não do toque:
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 eventUm seletor de plano com um botão "Continuar" que compra o plano selecionado:
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.');
});Tocar em uma ferramenta exclusiva do Pro a executa quando possuída e, caso contrário, abre a tela de compra:
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');
});Observações: buy() é o WebToApk.purchase(id) puro: sem rótulo de ocupado, sem verificação de grupo. Botões conectados com sell(), start() ou data-iap-buy fazem tudo isso por você, então prefira esses.
AppMintIAP.isOwned helper#
window.AppMintIAP.isOwned(id)
Exemplo
Verifica, na hora e offline, se o usuário possui um produto (um produto avulso ou uma assinatura ainda ativa).
Retorna: true / false, de forma síncrona; false fora do app. Requer: 💰 Vender produtos no app.
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 por qualquer um de vários planos: um produto mensal, um anual e um vitalício contam:
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 refundUm guarda de rota: telas exclusivas do Pro enviam um usuário sem Pro para a tela de upgrade:
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();Observações: Mesma resposta de WebToApk.isOwned(id). A posse é restaurada da conta Google do usuário a cada abertura e salva no celular, então sobrevive a reinstalações e funciona offline. Nunca guarde o direito de acesso apenas em localStorage.
AppMintIAP.onChange helper#
window.AppMintIAP.onChange(fn)
Exemplo
Chama sua função com o estado das compras imediatamente e de novo toda vez que ele muda (preços chegaram, uma compra concluída, a restauração na abertura, um reembolso, uma assinatura expirada).
Retorna: uma função para cancelar a inscrição. fn recebe {available, adFree, owned, ownedIds, products}; products é {id: {price, currency, type, period, owned}} para os produtos que o auxiliar conhece (de sell, start ou data-iap-buy). Requer: 💰 Vender produtos no app.
Um hook do React. Ele retorna a função de cancelar inscrição direto do efeito:
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;Um paywall em JavaScript puro: acompanhe dois planos sem botões e depois desenhe os preços e o plano atual a cada mudança:
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;
});
});
}Assine enquanto uma tela de conta estiver aberta e cancele a inscrição quando ela fechar. adFree e ownedIds são preenchidos quando o auxiliar está em execução (a página usa sell(), start() ou 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');
}Observações: Leia a posse por produto (ownedIds ou products[id].owned). O owned de nível superior significa apenas "algo é possuído". adFree é o direito de Remover anúncios (WebToApk.isPremium()). Um produto só está em products depois que o Google respondeu sobre ele.
AppMintIAP.owned helper#
window.AppMintIAP.owned()
Exemplo
Lista todos os IDs de produto que o usuário possui.
Retorna: uma lista de IDs de produto, de forma síncrona, por exemplo ['pro_unlock', 'level_pack_2']; [] fora do app ou quando nada é possuído. Requer: 💰 Vender produtos no app.
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);Conte quantas fases estão abertas quando cada pacote de fases é um produto próprio:
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);Uma lista "Minhas compras" na sua tela de configurações, com seu próprio nome para cada produto:
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);Observações: Interpretado de WebToApk.getOwnedProducts() para você. Inclui assinaturas ativas e o produto Remover anúncios. Funciona offline.
AppMintIAP.refresh helper#
window.AppMintIAP.refresh()
Exemplo
Procura de novo botões data-iap-buy adicionados desde a última varredura e os conecta (ou inicia o auxiliar se ele ainda não tinha encontrado nenhum).
Retorna: o objeto AppMintIAP. Requer: 💰 Vender produtos no app.
O auxiliar já observa a página em busca de novos botões data-iap-buy, então você raramente precisa disto. Chame depois de inserir botões de um jeito que o observador possa perder, por exemplo ao mudar atributos de um elemento existente:
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();
}Cartões de loja cujos produtos vêm do seu próprio servidor: os botões já estão na página, só os atributos mudam, então conecte-os com 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();
});Um pacote de fim de ano que entra em promoção em um horário definido, em um botão que estava na página desde o início:
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 itObservações: Cada novo botão recebe o preço do Google e o estado de possuído. Um botão já conectado não é conectado duas vezes. Botões cujo elemento foi removido da página são descartados.
AppMintIAP.sell helper#
window.AppMintIAP.sell(id, button, opts)
Exemplo
Conecta um produto a um botão em uma única linha. Chame uma vez por produto.
Retorna: o objeto AppMintIAP (para encadeamento). Requer: 💰 Vender produtos no app no assistente de build e o produto criado e ativo no Play Console. window.AppMintIAP é instalado antes do primeiro script da sua página rodar (em WebViews anteriores a cerca de 2021, só depois que a página termina de carregar - o padrão whenIap abaixo cobre os dois casos).
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}' });
});Vários planos em que comprar um substitui os outros compartilham um group:
whenIap(function (iap) {
iap.sell('pass_monthly', '#monthly', { label: 'Monthly — {price}', group: 'pass' })
.sell('pass_yearly', '#yearly', { label: 'Yearly — {price}', group: 'pass' });
});Acompanhe um produto sem botão e desenhe a oferta você mesmo (null não conecta nada; seu botão chama 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');
});
});Conecte um botão que só existe quando o diálogo da sua loja abre (uma chamada tardia se junta ao auxiliar em execução):
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
});
});Observações: button é um elemento ou um seletor CSS; passe null para acompanhar um produto sem botão (sua própria interface lê AppMintIAP.state()). Outras opts: labelEl (o elemento dentro do botão cujo texto muda). Funciona antes ou depois de o auxiliar iniciar, inclusive em um efeito do React. É o mesmo que os atributos data-iap-buy (veja AppMintIAP.start). Desbloqueie conteúdo pago apenas a partir da posse (isOwned, onChange), nunca no seu próprio handler de clique.
AppMintIAP.start helper#
window.AppMintIAP.start(config) and the data-iap-* attributes
Exemplo
Conecta seus botões de compra ao Google Play em um só passo: preços ao vivo nos botões, a tela de compra ao tocar, rótulos de "possuído", blocos de agradecimento, restauração na abertura e uma mensagem visível para toda falha.
Retorna: o objeto AppMintIAP (para encadeamento). Requer: 💰 Vender produtos no app no assistente de build e os IDs de produto criados e ativos no Play Console. O app instala window.AppMintIAP antes do primeiro script da sua página rodar (em WebViews anteriores a cerca de 2021, só depois que a página termina de carregar).
Sem nenhum código: marque o HTML. O auxiliar lê esses atributos sozinho assim que é injetado e observa botões que React ou Vue renderizam depois:
<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 -->Com código: chame start() você mesmo quando quiser sua própria função de mensagem ou ganchos de desbloqueio. Use em páginas SEM botões data-iap-buy no HTML (com eles, o auxiliar já iniciou e um segundo início não faz nada):
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
});
});No React: comece com planos sem button (nada é desenhado) e deixe o React renderizar a partir de onChange, que retorna a função de cancelar inscrição para o efeito:
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>;
}Observações: Atributos: data-iap-buy="id", data-iap-label ({price} vira o preço ao vivo do Play), data-iap-owned (rótulo depois de possuído; padrão "✓ Comprado"), data-iap-group (planos que se substituem; depois que um é possuído os outros somem, a menos que hideOtherPlans: false), data-iap-show / data-iap-hide (valor = id do produto ou grupo; vazio = qualquer coisa possuída), data-iap-text (a parte de um botão cujo texto muda), data-iap-container. Um botão possuído recebe data-iap-state="owned" e a classe iap-unlocked. Um cancelamento na tela do Google não envia evento; o botão é liberado quando a página volta a ficar visível (ou após 30 s). Chamar start(options) depois que o auxiliar já iniciou sozinho a partir de atributos data-iap-buy MESCLA as opções (planos, mostrar/ocultar, contêiner, onUnlock/onLock, toast, busyLabel, hideOtherPlans) no conjunto em execução; chamar duas vezes é inofensivo. Uma página que traz seu próprio appmint-iap.js o mantém - a cópia integrada se afasta.
AppMintIAP.started helper#
window.AppMintIAP.started
Exemplo
Uma propriedade (não uma função): true quando o auxiliar está em execução, ou seja, já conectou pelo menos um produto a partir de botões de start(), sell() ou data-iap-buy.
Retorna: uma propriedade booleana. Requer: 💰 Vender produtos no app.
Use para decidir entre start() (com suas próprias opções) e sell() (que também funciona em um auxiliar já em execução):
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);Avise a si mesmo durante o desenvolvimento quando nada na página foi conectado (um erro de digitação em data-iap-buy, ou os botões nunca foram exibidos):
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);
});Diferencie uma tela de loja de "ainda carregando" e de "compras indisponíveis":
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());Observações: Um segundo start() nunca o inicia duas vezes (o React StrictMode roda os efeitos duas vezes); as opções dele são mescladas ao conjunto em execução. Ele continua false enquanto nenhum produto estiver conectado; o auxiliar então continua observando a página em busca de botões data-iap-buy.
AppMintIAP.state helper#
window.AppMintIAP.state()
Exemplo
Retorna o estado atual das compras uma vez, sem se inscrever.
Retorna: {available, adFree, owned, ownedIds, products}, de forma síncrona. ownedIds é uma lista de IDs de produto; products é {id: {price, currency, type: 'inapp'|'subs', period, owned}} para os produtos sobre os quais o auxiliar perguntou ao Google. Requer: 💰 Vender produtos no app.
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();Verifique a posse no momento em que o jogador toca em uma fase bloqueada e, se não tiver, envie-o para a sua loja:
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);
}Esconda seus próprios convites para "ficar sem anúncios" quando o direito de sem anúncios do build estiver ativo e liste os preços que o Google enviou até agora:
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);
});
}Observações: É um instantâneo. Para acompanhar mudanças use AppMintIAP.onChange(fn), que dá o mesmo objeto toda vez que algo muda. owned é true quando qualquer coisa é possuída; verifique um produto com ownedIds ou AppMintIAP.isOwned(id).
getOwnedProducts ponte#
window.WebToApk.getOwnedProducts(): String
Lista JSON de todos os IDs de produtos possuídos - para bloquear telas inteiras na inicialização.
Exemplo
Lista todos os produtos do Play que o usuário possui, para desbloquear telas inteiras ao iniciar.
Retorna: uma string JSON (lista de IDs de produto), de forma síncrona, por exemplo '["pro_unlock","level_pack_2"]'. Interprete com JSON.parse. '[]' quando nada é possuído. Requer: 💰 Vender produtos no app no assistente de build.
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', ...]}
});A mesma mudança também chama window.onAppMintOwnedChanged(detail) se você o definir.
Uma lista "Minhas compras" na sua tela de configurações, com seu próprio nome para cada produto:
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);Descubra o melhor nível que o usuário possui quando cada nível é um produto próprio:
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()); });Observações: Inclui produtos avulsos e assinaturas ativas, e também o produto Remover anúncios se o usuário o comprou. Funciona offline; a lista é salva no celular a cada restauração na abertura e a cada compra. appmint:owned-changed só dispara quando a lista realmente mudou, então pode nem disparar em uma abertura normal.
getProducts ponte#
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
Pede ao Play os detalhes ao vivo dos IDs de produto informados (lista JSON de strings). O resultado chega como 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. IDs que o Play não conhece ficam ausentes do resultado - a página esconde esses itens.
Exemplo
Pede ao Google Play os detalhes ao vivo dos seus produtos: título, descrição e o preço localizado.
Retorna: nada agora. A resposta chega como o evento appmint:products: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type é 'inapp' ou 'subs'; period é um período ISO-8601 como 'P1M' (somente assinaturas). Requer: 💰 Vender produtos no app no assistente de build. O segundo argumento é uma string JSON: use 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' : '');
});
});A resposta também é passada para window.onAppMintProducts(detail) se você a definir.
Um seletor de plano de assinatura, cada cartão com o preço do Play e o período de cobrança:
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']));
}Durante os testes, registre os IDs que o Google Play não retornou, pelo gancho de página:
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));
}Observações: Mostre price exatamente como o Play informa e nunca o armazene. Um ID que o Play não conhece (não criado, não ativo, ou o app não foi instalado pelo Play) simplesmente não aparece em products; mantenha esse item oculto. Um requestId com aparência numérica volta como número, então use um id de texto ou compare com String().
isOwned ponte#
window.WebToApk.isOwned(productId: String): Boolean
Verificação instantânea de posse no dispositivo (produto avulso possuído OU assinatura ativa).
Exemplo
Verifica, na hora e offline, se o usuário possui um dos seus produtos do Play (um produto avulso ou uma assinatura ainda ativa).
Retorna: true / false, de forma síncrona. Requer: 💰 Vender produtos no app no assistente de build. Com essa chave ligada, também responde pelo produto Remover anúncios (IAP); em um app que só vende a remoção de anúncios (chave desligada), responde false para todo produto, então verifique a remoção de anúncios com isPremium() nesse caso.
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);Verifique no momento do uso: uma exportação exclusiva do Pro roda quando possuída e, caso contrário, abre a tela de compra e termina quando a compra é concluída:
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 por qualquer um de vários planos: um produto mensal, um anual e um vitalício contam:
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 refundObservações: A lista de itens possuídos é restaurada da conta Google do usuário a cada abertura e salva no celular, então funciona offline e após reinstalar, sem servidor. Sempre false em um navegador. Uma assinatura deixa de contar quando o Play não a informa mais como ativa.
isPremium ponte#
window.WebToApk.isPremium(): Boolean
Verdadeiro quando o usuário possui "Remover anúncios" (direito do Play Billing, restaurado automaticamente a cada abertura). Corresponde ao motivo 'ad_free' em 'appmint:ad-unavailable'.
Exemplo
Informa se o usuário possui seu produto "Remover anúncios", para a página poder esconder o botão "Remover anúncios" e a interface relacionada a anúncios.
Retorna: true / false, de forma síncrona. Requer: Anúncios ligados e Remover anúncios (IAP) com seu ID do produto na Play Store. Sem isso é sempre 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}
});A mesma mudança também chama um gancho de página, se você o definir:
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); }; // {premium}Mostre sua própria oferta "Remover anúncios" na tela de fim de jogo apenas para jogadores que ainda veem anúncios:
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;
}Observações: A posse é restaurada da conta Google do usuário a cada abertura, então leia uma vez no início e depois acompanhe appmint:premium. O campo ID do produto aceita vários IDs separados por vírgula (por exemplo um plano mensal e um vitalício); possuir qualquer um conta. Enquanto premium, os anúncios são removidos e as chamadas de anúncio respondem ad_free.
purchase ponte#
window.WebToApk.purchase(productId: String)
Abre a tela de compra do Google para [productId] (produto avulso ou assinatura - resolvido automaticamente). Conceda SOMENTE em 'appmint:purchase' {productId, owned:true}; falhas disparam 'appmint:purchase-failed' {reason}. Se já for possuído, o evento de compra dispara de novo para que a página possa desbloquear de forma idempotente.
Exemplo
Abre a tela de compra do Google Play para um dos seus produtos (um produto avulso ou uma assinatura; o app descobre qual).
Retorna: nada agora. O sucesso dispara appmint:purchase {productId, owned: true}. Uma falha dispara appmint:purchase-failed {productId, reason}. Requer: 💰 Vender produtos no app no assistente de build, e o produto criado e ativo no Play Console.
Desbloqueie SOMENTE no handler de appmint:purchase, nunca no toque:
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; });Os mesmos resultados também chamam window.onAppMintPurchase(detail) e window.onAppMintPurchaseFailed(detail) se você os definir.
Compre o plano que o usuário escolheu em um seletor de assinatura e esconda um plano cujo ID o Play não conhece:
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;
});Desbloqueie um pacote de fases a partir da fase bloqueada em que o jogador tocou e depois abra essa fase:
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);
};Observações: Motivos: disabled (Vender produtos no app está desligado), not_found (o ID não está no Play Console), billing_error, pending (o usuário pagou com um método lento; o desbloqueio chega depois pela restauração na abertura e por appmint:owned-changed). Se o produto já é possuído, appmint:purchase dispara de novo na hora, então seu desbloqueio deve ser seguro para repetir. O app também mostra uma mensagem curta própria para erros.
revenuecat ponte#
window.WebToApk.revenuecat(action: String, argsJson: String, callbackId: String)
Uma [action] do RevenueCat com [argsJson]; resolve [callbackId] com {ok, …} ou {ok:false, error}.
Exemplo
O método direto por trás de AppMint.revenuecat - use o wrapper. Um único ponto de entrada: action é configure, getOfferings, purchase, restore, getCustomerInfo, logIn, logOut ou getAppUserId.
Retorna: nada diretamente; resolve window.__appmintReplies[callbackId] com a mesma resposta que o wrapper dá. Requer: etapa Integrar → RevenueCat com sua chave pública goog_.
window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc1'] = (r) => console.log(r.ok ? r.customerInfo : r.error);
WebToApk.revenuecat('getCustomerInfo', '{}', 'rc1');Configure com um id de usuário e leia as ofertas:
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');
}Compre e trate as recusas:
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');Observações: Veja AppMint.revenuecat para os argumentos e as respostas de cada ação.
startRemoveAdsPurchase ponte#
window.WebToApk.startRemoveAdsPurchase()
Inicia o fluxo de compra "Remover anúncios" do Google Play a partir da página (iniciado pelo usuário, por exemplo o botão "Remover anúncios" do próprio app). Sem isto, o ÚNICO ponto de entrada era o item nativo do menu lateral - inacessível em apps compilados sem menu lateral. O resultado chega à página por 'appmint:premium' {premium:true}; se já for possuído, notifica de novo imediatamente; erros do fluxo aparecem como um aviso nativo.
Exemplo
Abre a tela de compra do Google Play para o seu produto "Remover anúncios", a partir do botão "Remover anúncios" da sua própria página.
Retorna: nada. O resultado chega como appmint:premium {premium: true} (e window.onAppMintPremiumChanged). Requer: Anúncios ligados e Remover anúncios (IAP) com seu ID do produto na Play Store (o primeiro ID da lista é o vendido). Quando o Menu lateral está ligado, o app também tem um item próprio Remover anúncios nele (oculto quando o usuário já possui); com o menu lateral desligado, este botão na sua página é a única forma de comprar.
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;
});Uma linha de configurações que mostra se os anúncios foram removidos, com o botão que compra isso:
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
}Ofereça depois de cada terceira rodada concluída, em uma caixa de diálogo que o jogador responde com um toque:
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(); });Observações: Se o usuário já possui, appmint:premium {premium:true} dispara na hora. Erros (Play inacessível, produto não encontrado no Play Console) são mostrados pelo app como uma mensagem nativa curta; a página não recebe evento para eles. Teste com um app instalado de uma faixa de teste do Play.