Criar APK grátis

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 event

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

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

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

Observaçõ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 refund

Observaçõ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.

Gerado a partir do runtime do app e de seus arquivos de exemplo a cada build da documentação. Leia em Markdown · Todas as famílias.

Conferido com a ponte atual em 22/10/2026.