API del puente de JavaScript
Compras y derechos
Vender desde tu propia página y preguntar qué posee ya este comprador. Consulta la guía de compras dentro de la app para el código que rodea esto.
AppMint.revenuecat ayudante#
AppMint.revenuecat — RevenueCat subscriptions
Ejemplo
Suscripciones y compras únicas mediante tu proyecto de RevenueCat: ofertas, compras, restauraciones y derechos. Las compras se realizan con Google Play en la app; RevenueCat registra cada una antes de que la app la finalice.
Devuelve: cada método devuelve una Promise que siempre se resuelve con {ok: true, …} o {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}. Requiere: paso Integrar → RevenueCat con tu clave PÚBLICA de 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();Vincula las compras a tu propia cuenta:
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();
};
}Muestra un mensaje honesto para cada fallo:
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);
}Notas: customerInfo = {appUserId, originalAppUserId, requestDate, managementUrl, entitlements: {active, all}, activeSubscriptions, allPurchasedProductIds, nonSubscriptionTransactions}; cada derecho {identifier, isActive, productIdentifier, expirationDate, latestPurchaseDate, willRenew, periodType, store, isSandbox}. Pasa {consumable: true} para monedas/créditos para que puedan comprarse de nuevo; las suscripciones y los desbloqueos de pago único se confirman. Sin conexión, getCustomerInfo responde con la última información conocida con stale: true; una compra hecha mientras RevenueCat no está disponible se finaliza igualmente y su recibo se envía en el siguiente configure/restore (receiptQueued: true). Errores: not_configured, network, revenuecat_error (con status, code, message), billing_unavailable, billing_error, product_not_found, offer_not_found, cancelled, pending, already_anonymous, foreign_origin. Configura la conexión de cuenta de servicio de Google Play de RevenueCat como RevenueCat lo pide; nunca pongas una clave secreta sk_ en una app. Las compras existentes de AppMintIAP siguen funcionando sin cambios.
AppMintIAP.available ayudante#
window.AppMintIAP.available
Ejemplo
Una propiedad (no una función): true cuando esta página realmente puede comprar mediante Google Play.
Devuelve: una propiedad booleana. Requiere: 💰 Vender productos dentro de la app. Sin esto, window.AppMintIAP no existe en absoluto.
Compruébalo antes de mostrar nada a la venta y di claramente cuando aquí no es posible comprar:
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';
}Un botón de mejora para tu sitio web y tu app: Google Play dentro de la app, tu propio pago en la 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
}
});En React o Next.js, léelo dentro de un efecto (el renderizado del servidor no tiene window), para que no se muestre nada a la venta fuera de la 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;Notas: window.AppMintIAP se instala antes de que se ejecute el primer script de tu página; solo en WebViews anteriores a 2021 aproximadamente llega cuando la página ha terminado de cargar, así que el código que también deba ejecutarse allí debe esperarlo (consulta el patrón whenIap en AppMintIAP.sell). El mismo valor está en AppMintIAP.state().available. Envolver la oferta en data-iap-container la oculta fuera de la app sin código.
AppMintIAP.buy ayudante#
window.AppMintIAP.buy(id)
Ejemplo
Abre la hoja de compra de Google Play para un producto, desde tu propio botón. No concede nada por sí mismo.
Devuelve: nada. El resultado llega como appmint:purchase {productId, owned:true} (y mediante AppMintIAP.onChange) o appmint:purchase-failed {productId, reason}. Requiere: 💰 Vender productos dentro de la app y el producto activo en Play Console.
Úsalo cuando dibujes tú mismo el botón; el desbloqueo viene de la propiedad, no del 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 eventUn selector de planes con un solo botón "Continuar" que compra el plan seleccionado:
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.');
});Al tocar una herramienta solo Pro, se ejecuta si se posee y, si no, abre la hoja 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');
});Notas: buy() es el WebToApk.purchase(id) simple: sin etiqueta de ocupado, sin comprobación de grupo. Los botones conectados con sell(), start() o data-iap-buy hacen todo eso por ti, así que prefiérelos.
AppMintIAP.isOwned ayudante#
window.AppMintIAP.isOwned(id)
Ejemplo
Comprueba, al instante y sin conexión, si el usuario posee un producto (un producto de pago único o una suscripción que sigue activa).
Devuelve: true / false, de forma síncrona; false fuera de la app. Requiere: 💰 Vender productos dentro de la 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 mediante cualquiera de varios planes: cuentan un producto mensual, uno anual y uno de por vida:
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 refundUna guarda de ruta: las pantallas solo Pro envían a un usuario sin Pro a la pantalla de mejora:
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();Notas: Misma respuesta que WebToApk.isOwned(id). La propiedad se restaura desde la cuenta de Google del usuario en cada inicio y se guarda en el teléfono, así que sobrevive a las reinstalaciones y funciona sin conexión. Nunca guardes el derecho solo en localStorage.
AppMintIAP.onChange ayudante#
window.AppMintIAP.onChange(fn)
Ejemplo
Llama a tu función con el estado de compra de inmediato, y de nuevo cada vez que cambia (llegaron los precios, se completó una compra, la restauración al iniciar, un reembolso, una suscripción vencida).
Devuelve: una función para cancelar la suscripción. fn recibe {available, adFree, owned, ownedIds, products}; products es {id: {price, currency, type, period, owned}} para los productos que el asistente conoce (de sell, start o data-iap-buy). Requiere: 💰 Vender productos dentro de la app.
Un hook de React. Devuelve la función para cancelar la suscripción directamente desde el efecto:
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;Un muro de pago en JavaScript simple: sigue dos planes sin botones y luego dibuja sus precios y el plan actual en cada cambio:
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;
});
});
}Suscríbete mientras una pantalla de cuenta está abierta y cancela la suscripción cuando se cierre. adFree y ownedIds se rellenan una vez que el asistente está en marcha (la página usa sell(), start() o 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');
}Notas: Lee la propiedad por producto (ownedIds o products[id].owned). El owned de nivel superior solo significa "algo está poseído". adFree es el derecho de Quitar anuncios (WebToApk.isPremium()). Un producto está en products solo después de que Google respondió por él.
AppMintIAP.owned ayudante#
window.AppMintIAP.owned()
Ejemplo
Lista todos los ID de producto que el usuario posee.
Devuelve: un arreglo de ID de producto, de forma síncrona, por ejemplo ['pro_unlock', 'level_pack_2']; [] fuera de la app o cuando no se posee nada. Requiere: 💰 Vender productos dentro de la 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);Cuenta cuántos niveles están abiertos cuando cada paquete de niveles es su propio producto:
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);Una lista de "Mis compras" en tu pantalla de ajustes, con tu propio nombre para cada producto:
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);Notas: Analizado por ti desde WebToApk.getOwnedProducts(). Incluye suscripciones activas y el producto Quitar anuncios. Funciona sin conexión.
AppMintIAP.refresh ayudante#
window.AppMintIAP.refresh()
Ejemplo
Busca de nuevo botones data-iap-buy añadidos desde el último análisis y los conecta (o inicia el asistente si aún no había encontrado ninguno).
Devuelve: el objeto AppMintIAP. Requiere: 💰 Vender productos dentro de la app.
El asistente ya vigila la página en busca de botones data-iap-buy nuevos, así que rara vez necesitas esto. Llámalo después de insertar botones de una forma que el vigilante podría no detectar, por ejemplo al cambiar atributos de un 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();
}Tarjetas de tienda cuyos productos vienen de tu propio servidor: los botones ya están en la página, solo cambian sus atributos, así que conéctalos con 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();
});Un paquete de fiestas que sale a la venta en un momento fijado, en un botón que estaba en la página desde el principio:
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 itNotas: Cada botón nuevo recibe su precio de Google y su estado de propiedad. Un botón que ya está conectado no se conecta dos veces. Los botones cuyo elemento se eliminó de la página se descartan.
AppMintIAP.sell ayudante#
window.AppMintIAP.sell(id, button, opts)
Ejemplo
Conecta un producto a un botón en una sola línea. Llámalo una vez por producto.
Devuelve: el objeto AppMintIAP (para encadenar). Requiere: 💰 Vender productos dentro de la app en el asistente de compilación y el producto creado y activo en Play Console. window.AppMintIAP se instala antes de que se ejecute el primer script de tu página (en WebViews anteriores a 2021 aproximadamente, cuando la página termina de cargar; el patrón whenIap de abajo cubre ambos 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}' });
});Varios planes en los que comprar uno reemplaza a los demás comparten un group:
whenIap(function (iap) {
iap.sell('pass_monthly', '#monthly', { label: 'Monthly — {price}', group: 'pass' })
.sell('pass_yearly', '#yearly', { label: 'Yearly — {price}', group: 'pass' });
});Sigue un producto sin botón y dibuja tú mismo la oferta (null no conecta nada; tu botón llama a 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');
});
});Conecta un botón que solo existe cuando se abre tu diálogo de tienda (una llamada tardía se une al asistente en marcha):
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
});
});Notas: button es un elemento o un selector CSS; pasa null para seguir un producto sin botón (tu propia interfaz lee AppMintIAP.state()). Otras opts: labelEl (el elemento dentro del botón cuyo texto cambia). Funciona antes o después de que el asistente se inicie, incluso desde un efecto de React. Es lo mismo que los atributos data-iap-buy (consulta AppMintIAP.start). Desbloquea contenido de pago solo a partir de la propiedad (isOwned, onChange), nunca en tu propio manejador de clics.
AppMintIAP.start ayudante#
window.AppMintIAP.start(config) and the data-iap-* attributes
Ejemplo
Conecta tus botones de compra con Google Play en un solo paso: precios en vivo en los botones, la hoja de compra al tocar, etiquetas de "comprado", bloques de agradecimiento, restauración al iniciar y un mensaje visible para cada fallo.
Devuelve: el objeto AppMintIAP (para encadenar). Requiere: 💰 Vender productos dentro de la app en el asistente de compilación y los ID de producto creados y activos en Play Console. La app instala window.AppMintIAP antes de que se ejecute el primer script de tu página (en WebViews anteriores a 2021 aproximadamente, solo cuando la página ha terminado de cargar).
Sin nada de código: marca el HTML. El asistente lee estos atributos por sí solo en cuanto se inyecta y vigila los botones que React o Vue muestren más tarde:
<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 -->Con código: llama tú mismo a start() cuando quieras tu propia función de mensajes o ganchos de desbloqueo. Úsalo en páginas SIN botones data-iap-buy en el HTML (con ellos, el asistente ya se inició y un segundo inicio no hace 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
});
});En React: empieza con planes sin button (no se dibuja nada) y deja que React renderice a partir de onChange, que devuelve la función para cancelar la suscripción del efecto:
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>;
}Notas: Atributos: data-iap-buy="id", data-iap-label ({price} se convierte en el precio en vivo de Play), data-iap-owned (etiqueta una vez poseído; por defecto "✓ Purchased"), data-iap-group (planes que se reemplazan entre sí; cuando uno se posee, los demás se ocultan salvo que hideOtherPlans: false), data-iap-show / data-iap-hide (valor = id de producto o grupo; vacío = cualquier cosa poseída), data-iap-text (la parte de un botón cuyo texto cambia), data-iap-container. Un botón poseído recibe data-iap-state="owned" y la clase iap-unlocked. Cancelar en la hoja de Google no envía ningún evento; el botón se libera cuando la página vuelve a ser visible (o tras 30 s). Llamar a start(options) después de que el asistente ya se inició solo a partir de atributos data-iap-buy COMBINA las opciones (planes, mostrar/ocultar, contenedor, onUnlock/onLock, toast, busyLabel, hideOtherPlans) con el conjunto en marcha; llamarlo dos veces es inofensivo. Una página que incluye su propio appmint-iap.js lo conserva: la copia integrada se retira.
AppMintIAP.started ayudante#
window.AppMintIAP.started
Ejemplo
Una propiedad (no una función): true una vez que el asistente está en marcha, es decir, ha conectado al menos un producto desde botones start(), sell() o data-iap-buy.
Devuelve: una propiedad booleana. Requiere: 💰 Vender productos dentro de la app.
Úsalo para decidir entre start() (con tus propias opciones) y sell() (que también funciona sobre un asistente en marcha):
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);Avísate durante el desarrollo cuando nada en la página quedó conectado (un error de escritura en data-iap-buy, o los botones nunca se mostraron):
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);
});Distingue una pantalla de tienda de "aún cargando" y de "compras no disponibles":
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());Notas: Un segundo start() nunca lo inicia dos veces (React StrictMode ejecuta los efectos dos veces); sus opciones se combinan con el conjunto en marcha. Permanece false mientras no haya ningún producto conectado; entonces el asistente sigue vigilando la página en busca de botones data-iap-buy.
AppMintIAP.state ayudante#
window.AppMintIAP.state()
Ejemplo
Devuelve una vez el estado de compra actual, sin suscribirse.
Devuelve: {available, adFree, owned, ownedIds, products}, de forma síncrona. ownedIds es un arreglo de ID de producto; products es {id: {price, currency, type: 'inapp'|'subs', period, owned}} para los productos sobre los que el asistente ha preguntado a Google. Requiere: 💰 Vender productos dentro de la 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();Comprueba la propiedad en el momento en que el jugador toca un nivel bloqueado y, si no, envíalo a tu tienda:
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);
}Oculta tus propios avisos de "quita los anuncios" una vez que el derecho sin anuncios de la compilación esté activo, y lista los precios que Google ha enviado hasta ahora:
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);
});
}Notas: Es una instantánea. Para seguir los cambios usa AppMintIAP.onChange(fn), que da el mismo objeto cada vez que algo cambia. owned es true cuando se posee cualquier cosa; comprueba un producto con ownedIds o AppMintIAP.isOwned(id).
getOwnedProducts puente#
window.WebToApk.getOwnedProducts(): String
Arreglo JSON de todos los ID de producto poseídos, para bloquear pantallas enteras al iniciar.
Ejemplo
Lista todos los productos de Play que el usuario posee, para desbloquear pantallas enteras al iniciar.
Devuelve: una cadena JSON (arreglo de ID de producto), de forma síncrona, por ejemplo '["pro_unlock","level_pack_2"]'. Analízala con JSON.parse. '[]' cuando no se posee nada. Requiere: 💰 Vender productos dentro de la app en el asistente de compilación.
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', ...]}
});El mismo cambio también llama a window.onAppMintOwnedChanged(detail) si lo defines.
Una lista de "Mis compras" en tu pantalla de ajustes, con tu propio nombre para cada producto:
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);Calcula el mejor nivel que posee el usuario cuando cada nivel es su propio producto:
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()); });Notas: Incluye productos de pago único y suscripciones activas, y también el producto Quitar anuncios si el usuario lo compró. Funciona sin conexión; la lista se guarda en el teléfono en cada restauración al iniciar y en cada compra. appmint:owned-changed se dispara solo cuando la lista realmente cambió, así que puede que no se dispare en un inicio normal.
getProducts puente#
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
Pide a Play los detalles en vivo de los ID de producto indicados (arreglo JSON de cadenas). El resultado llega como 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. Los ID que Play no conoce no aparecen en el resultado: la página oculta esos elementos.
Ejemplo
Pide a Google Play los detalles en vivo de tus productos: título, descripción y precio localizado.
Devuelve: nada por ahora. La respuesta llega como el evento appmint:products: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type es 'inapp' o 'subs'; period es un período ISO-8601 como 'P1M' (solo suscripciones). Requiere: 💰 Vender productos dentro de la app en el asistente de compilación. El segundo argumento es una cadena JSON: usa 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' : '');
});
});La respuesta también se pasa a window.onAppMintProducts(detail) si lo defines.
Un selector de planes de suscripción, cada tarjeta con el precio de Play y su período de facturación:
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']));
}Mientras pruebas, registra los ID que Google Play no devolvió, mediante el 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));
}Notas: Muestra price exactamente como lo da Play y nunca lo guardes. Un ID que Play no conoce (no creado, no activo, o la app no se instaló desde Play) simplemente falta en products; mantén ese elemento oculto. Un requestId con aspecto numérico vuelve como número, así que usa un id de texto o compara con String().
isOwned puente#
window.WebToApk.isOwned(productId: String): Boolean
Comprobación instantánea de propiedad en el dispositivo (producto de pago único poseído O suscripción activa).
Ejemplo
Comprueba, al instante y sin conexión, si el usuario posee uno de tus productos de Play (un producto de pago único o una suscripción que sigue activa).
Devuelve: true / false, de forma síncrona. Requiere: 💰 Vender productos dentro de la app en el asistente de compilación. Con ese interruptor activado también responde por el producto Quitar anuncios (IAP); en una app que solo vende la eliminación de anuncios (con el interruptor desactivado) responde false para cualquier producto, así que allí comprueba la eliminación de anuncios con isPremium().
function hasPro() {
var b = window.WebToApk;
return !!(b && typeof b.isOwned === 'function' && b.isOwned('pro_unlock'));
}
function paint() {
document.getElementById('pro-features').hidden = !hasPro();
document.getElementById('buy-pro').hidden = hasPro();
}
paint();
// Ownership changes: a purchase landed, or the launch restore / a refund changed the list.
window.addEventListener('appmint:purchase', paint);
window.addEventListener('appmint:owned-changed', paint);Comprueba en el momento de uso: una exportación solo Pro se ejecuta si se posee y, si no, abre la hoja de compra y termina cuando la compra se completa:
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 mediante cualquiera de varios planes: cuentan un producto mensual, uno anual y uno de por vida:
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 refundNotas: La lista de productos poseídos se restaura desde la cuenta de Google del usuario en cada inicio y se guarda en el teléfono, así que funciona sin conexión y tras reinstalar, sin servidor. Siempre false en un navegador. Una suscripción deja de contar cuando Play ya no la informa como activa.
isPremium puente#
window.WebToApk.isPremium(): Boolean
Verdadero cuando el usuario posee "Quitar anuncios" (derecho de Play Billing, restaurado automáticamente en cada inicio). Coincide con el motivo 'ad_free' en 'appmint:ad-unavailable'.
Ejemplo
Te dice si el usuario posee tu producto "Quitar anuncios", para que la página pueda ocultar su botón "Quitar anuncios" y la interfaz relacionada con anuncios.
Devuelve: true / false, de forma síncrona. Requiere: Anuncios activados y Quitar anuncios (IAP) con tu ID de producto de Play Store. Sin eso siempre es 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}
});El mismo cambio también llama a un gancho de página, si lo defines:
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); }; // {premium}Muestra tu propia oferta de "Quitar anuncios" en la pantalla de fin de juego solo a los jugadores que aún ven anuncios:
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;
}Notas: La propiedad se restaura desde la cuenta de Google del usuario en cada inicio, así que léela una vez al comenzar y luego sigue appmint:premium. El campo ID de producto acepta varios ID separados por comas (por ejemplo un plan mensual y uno de por vida); poseer cualquiera cuenta. Mientras sea premium, los anuncios se eliminan y las llamadas de anuncios responden ad_free.
purchase puente#
window.WebToApk.purchase(productId: String)
Abre la hoja de compra de Google para [productId] (producto de pago único o suscripción; se resuelve automáticamente). Concede SOLO con 'appmint:purchase' {productId, owned:true}; los fallos disparan 'appmint:purchase-failed' {reason}. Si ya se posee, vuelve a disparar el evento de compra para que la página pueda desbloquear de forma idempotente.
Ejemplo
Abre la hoja de compra de Google Play para uno de tus productos (un producto de pago único o una suscripción; la app averigua cuál).
Devuelve: nada por ahora. El éxito dispara appmint:purchase {productId, owned: true}. Un fallo dispara appmint:purchase-failed {productId, reason}. Requiere: 💰 Vender productos dentro de la app en el asistente de compilación, y el producto creado y activo en Play Console.
Desbloquea SOLO en el manejador de appmint:purchase, nunca al tocar:
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; });Los mismos resultados también llaman a window.onAppMintPurchase(detail) y window.onAppMintPurchaseFailed(detail) si los defines.
Compra el plan que el usuario eligió en un selector de suscripciones y oculta un plan cuyo ID Play no conoce:
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;
});Desbloquea un paquete de niveles desde el nivel bloqueado que tocó el jugador y luego abre ese nivel:
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);
};Notas: Motivos: disabled (Vender productos dentro de la app está desactivado), not_found (el ID no está en Play Console), billing_error, pending (el usuario pagó con un método lento; el desbloqueo llega después mediante la restauración al iniciar y appmint:owned-changed). Si el producto ya se posee, appmint:purchase se dispara de nuevo al instante, así que tu desbloqueo debe poder repetirse sin problema. La app también muestra su propio mensaje breve para los errores.
revenuecat puente#
window.WebToApk.revenuecat(action: String, argsJson: String, callbackId: String)
Una [action] de RevenueCat con [argsJson]; resuelve [callbackId] con {ok, …} o {ok:false, error}.
Ejemplo
El método directo detrás de AppMint.revenuecat: usa el contenedor. Un solo punto de entrada: action es configure, getOfferings, purchase, restore, getCustomerInfo, logIn, logOut o getAppUserId.
Devuelve: nada directamente; resuelve window.__appmintReplies[callbackId] con la misma respuesta que da el contenedor. Requiere: paso Integrar → RevenueCat con tu clave pública goog_.
window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc1'] = (r) => console.log(r.ok ? r.customerInfo : r.error);
WebToApk.revenuecat('getCustomerInfo', '{}', 'rc1');Configura con un id de usuario y lee las 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');
}Compra y maneja los rechazos:
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');Notas: Consulta AppMint.revenuecat para los argumentos y respuestas de cada acción.
startRemoveAdsPurchase puente#
window.WebToApk.startRemoveAdsPurchase()
Inicia desde la página el flujo de compra "Quitar anuncios" de Google Play (iniciado por el usuario, p. ej. el propio botón "Quitar anuncios" de la app). Sin esto, el ÚNICO punto de entrada era el elemento nativo del menú lateral, inaccesible en apps compiladas sin menú lateral. El resultado llega a la página mediante 'appmint:premium' {premium:true}; si ya lo posee, vuelve a notificar de inmediato; los errores del flujo aparecen como un aviso nativo.
Ejemplo
Abre la hoja de compra de Google Play para tu producto "Quitar anuncios", desde el propio botón "Quitar anuncios" de tu página.
Devuelve: nada. El resultado llega como appmint:premium {premium: true} (y window.onAppMintPremiumChanged). Requiere: Anuncios activados y Quitar anuncios (IAP) con tu ID de producto de Play Store (el primer ID de la lista es el que se vende). Cuando el Menú lateral está activado, la app también tiene allí su propio elemento Quitar anuncios (oculto cuando el usuario ya lo posee); con el menú lateral desactivado, este botón de tu página es la ú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;
});Una fila de ajustes que muestra si los anuncios están eliminados, con el botón que lo compra:
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
}Ofrécelo después de cada tercera ronda terminada, en un diálogo que el jugador responde con un 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(); });Notas: Si el usuario ya lo posee, appmint:premium {premium:true} se dispara al instante. Los errores (Play no disponible, producto no encontrado en Play Console) los muestra la app como un mensaje nativo breve; la página no recibe ningún evento por ellos. Prueba con una app instalada desde un canal de pruebas de Play.