API del bridge JavaScript
Acquisti e diritti
Vendere dalla tua pagina e chiedere che cosa possiede già questo acquirente. Vedi la guida agli acquisti in-app per il codice che li circonda.
AppMint.revenuecat helper#
AppMint.revenuecat — RevenueCat subscriptions
Esempio
Abbonamenti e acquisti una tantum tramite il tuo progetto RevenueCat: offerte, acquisti, ripristini e diritti. Gli acquisti passano da Google Play nell'app; RevenueCat registra ciascuno prima che l'app lo completi.
Restituisce: ogni metodo restituisce una Promise che si risolve sempre 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}. Richiede: passaggio Integrazione → RevenueCat con la tua chiave PUBBLICA di 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();Collega gli acquisti al tuo account:
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();
};
}Mostra un messaggio chiaro per ogni errore:
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);
}Note: customerInfo = {appUserId, originalAppUserId, requestDate, managementUrl, entitlements: {active, all}, activeSubscriptions, allPurchasedProductIds, nonSubscriptionTransactions}; ogni diritto {identifier, isActive, productIdentifier, expirationDate, latestPurchaseDate, willRenew, periodType, store, isSandbox}. Passa {consumable: true} per monete/crediti in modo che possano essere riacquistati; abbonamenti e sblocchi una tantum vengono confermati. Offline, getCustomerInfo risponde con le ultime informazioni note con stale: true; un acquisto fatto mentre RevenueCat non è raggiungibile viene comunque completato e la sua ricevuta viene inviata al successivo configure/restore (receiptQueued: true). Errori: 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 connessione del service account di Google Play di RevenueCat come richiesto da RevenueCat; non inserire mai una chiave segreta sk_ in un'app. Gli acquisti AppMintIAP esistenti continuano a funzionare senza modifiche.
AppMintIAP.available helper#
window.AppMintIAP.available
Esempio
Una proprietà (non una funzione): true quando questa pagina può davvero acquistare tramite Google Play.
Restituisce: una proprietà booleana. Richiede: 💰 Vendi prodotti in-app. Senza, window.AppMintIAP non esiste affatto.
Controllalo prima di mostrare qualcosa in vendita, e di' chiaramente quando qui non è possibile acquistare:
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 unico pulsante di upgrade per il tuo sito e la tua app: Google Play dentro l'app, il tuo checkout sul 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
}
});In React o Next.js, leggilo dentro un effetto (il rendering sul server non ha window), così fuori dall'app non viene mostrato nulla da acquistare:
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;Note: window.AppMintIAP viene installato prima che venga eseguito il primo script della tua pagina; solo nelle WebView precedenti al 2021 circa arriva quando la pagina ha finito di caricarsi, quindi il codice che deve funzionare anche lì dovrebbe aspettarlo (vedi lo schema whenIap in AppMintIAP.sell). Lo stesso valore è in AppMintIAP.state().available. Racchiudere l'offerta in data-iap-container la nasconde fuori dall'app senza codice.
AppMintIAP.buy helper#
window.AppMintIAP.buy(id)
Esempio
Apre la schermata di acquisto di Google Play per un prodotto, da un tuo pulsante. Da sola non concede nulla.
Restituisce: niente. Il risultato arriva come appmint:purchase {productId, owned:true} (e tramite AppMintIAP.onChange) oppure appmint:purchase-failed {productId, reason}. Richiede: 💰 Vendi prodotti in-app e il prodotto attivo in Play Console.
Usalo quando disegni tu il pulsante; lo sblocco deriva dal possesso, non dal tocco:
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 selettore di piani con un solo pulsante «Continua» che acquista il piano selezionato:
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.');
});Toccando uno strumento solo Pro, lo esegue se posseduto, altrimenti apre la schermata di acquisto:
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');
});Note: buy() è il semplice WebToApk.purchase(id): nessuna etichetta di attesa, nessun controllo del gruppo. I pulsanti collegati con sell(), start() o data-iap-buy fanno tutto questo per te, quindi preferisci quelli.
AppMintIAP.isOwned helper#
window.AppMintIAP.isOwned(id)
Esempio
Controlla, subito e offline, se l'utente possiede un prodotto (un prodotto una tantum, o un abbonamento ancora attivo).
Restituisce: true / false, in modo sincrono; false fuori dall'app. Richiede: 💰 Vendi prodotti in-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 tramite uno qualsiasi di più piani: un prodotto mensile, uno annuale e uno a vita contano tutti:
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 guardia di percorso: le schermate solo Pro mandano alla schermata di upgrade un utente senza Pro:
function route() {
var path = location.hash.slice(1) || '/';
var proOnly = path.indexOf('/reports') === 0;
if (proOnly && !(window.AppMintIAP && window.AppMintIAP.isOwned('pro_unlock'))) {
location.replace('#/upgrade');
return;
}
render(path);
}
window.addEventListener('hashchange', route);
route();Note: stessa risposta di WebToApk.isOwned(id). Il possesso viene ripristinato dall'account Google dell'utente a ogni avvio e salvato sul telefono, quindi sopravvive alle reinstallazioni e funziona offline. Non tenere mai il diritto solo in localStorage.
AppMintIAP.onChange helper#
window.AppMintIAP.onChange(fn)
Esempio
Chiama subito la tua funzione con lo stato degli acquisti, e di nuovo a ogni cambiamento (prezzi arrivati, acquisto completato, ripristino all'avvio, rimborso, abbonamento scaduto).
Restituisce: una funzione per annullare l'iscrizione. fn riceve {available, adFree, owned, ownedIds, products}; products è {id: {price, currency, type, period, owned}} per i prodotti che l'helper conosce (da sell, start o data-iap-buy). Richiede: 💰 Vendi prodotti in-app.
Un hook React. Restituisce la funzione di annullamento direttamente dall'effetto:
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 paywall in JavaScript puro: segui due piani senza pulsanti, poi disegna i loro prezzi e il piano attuale a ogni cambiamento:
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;
});
});
}Iscriviti mentre una schermata dell'account è aperta e annulla l'iscrizione quando si chiude. adFree e ownedIds vengono compilati quando l'helper è in esecuzione (la pagina 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');
}Note: leggi il possesso per singolo prodotto (ownedIds o products[id].owned). Il livello superiore owned significa solo «qualcosa è posseduto». adFree è il diritto Rimuovi annunci (WebToApk.isPremium()). Un prodotto è in products solo dopo che Google ha risposto per esso.
AppMintIAP.owned helper#
window.AppMintIAP.owned()
Esempio
Elenca ogni ID prodotto che l'utente possiede.
Restituisce: un array di ID prodotto, in modo sincrono, per esempio ['pro_unlock', 'level_pack_2']; [] fuori dall'app o quando nulla è posseduto. Richiede: 💰 Vendi prodotti in-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);Conta quanti livelli sono aperti quando ogni pacchetto di livelli è un prodotto a sé:
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);Un elenco «I miei acquisti» nella tua schermata delle impostazioni, con un nome tuo per ogni prodotto:
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);Note: analizzato da WebToApk.getOwnedProducts() per te. Include gli abbonamenti attivi e il prodotto Rimuovi annunci. Funziona offline.
AppMintIAP.refresh helper#
window.AppMintIAP.refresh()
Esempio
Cerca di nuovo i pulsanti data-iap-buy aggiunti dopo l'ultima scansione e li collega (o avvia l'helper se non ne aveva ancora trovati).
Restituisce: l'oggetto AppMintIAP. Richiede: 💰 Vendi prodotti in-app.
L'helper controlla già la pagina in cerca di nuovi pulsanti data-iap-buy, quindi raramente ti serve. Chiamala dopo aver inserito pulsanti in un modo che il controllo potrebbe non rilevare, per esempio cambiando gli attributi di un elemento esistente:
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();
}Schede negozio i cui prodotti arrivano dal tuo server: i pulsanti sono già nella pagina, cambiano solo i loro attributi, quindi collegali 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 pacchetto festivo che va in saldo a un'ora stabilita, su un pulsante presente nella pagina fin dall'inizio:
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 itNote: ogni nuovo pulsante riceve il suo prezzo da Google e il suo stato di possesso. Un pulsante già collegato non viene collegato due volte. I pulsanti il cui elemento è stato rimosso dalla pagina vengono scartati.
AppMintIAP.sell helper#
window.AppMintIAP.sell(id, button, opts)
Esempio
Collega un prodotto a un pulsante in una sola riga. Chiamala una volta per prodotto.
Restituisce: l'oggetto AppMintIAP (per il concatenamento). Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione e il prodotto creato e attivo in Play Console. window.AppMintIAP viene installato prima che venga eseguito il primo script della tua pagina (nelle WebView precedenti al 2021 circa, quando la pagina ha finito di caricarsi - lo schema whenIap qui sotto copre entrambi i casi).
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}' });
});Più piani in cui l'acquisto di uno sostituisce gli altri condividono un group:
whenIap(function (iap) {
iap.sell('pass_monthly', '#monthly', { label: 'Monthly — {price}', group: 'pass' })
.sell('pass_yearly', '#yearly', { label: 'Yearly — {price}', group: 'pass' });
});Segui un prodotto senza pulsante e disegna tu l'offerta (null non collega nulla; il tuo pulsante chiama 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');
});
});Collega un pulsante che esiste solo quando si apre la finestra del tuo negozio (una chiamata tardiva si unisce all'helper in esecuzione):
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
});
});Note: button è un elemento o un selettore CSS; passa null per seguire un prodotto senza pulsante (la tua interfaccia legge AppMintIAP.state()). Altre opts: labelEl (l'elemento dentro il pulsante il cui testo cambia). Funziona prima o dopo l'avvio dell'helper, anche da un effetto React. Equivale agli attributi data-iap-buy (vedi AppMintIAP.start). Sblocca i contenuti a pagamento solo in base al possesso (isOwned, onChange), mai nel tuo gestore del clic.
AppMintIAP.start helper#
window.AppMintIAP.start(config) and the data-iap-* attributes
Esempio
Collega i tuoi pulsanti di acquisto a Google Play in un solo passaggio: prezzi aggiornati sui pulsanti, la schermata di acquisto al tocco, etichette «acquistato», blocchi di ringraziamento, ripristino all'avvio e un messaggio visibile per ogni errore.
Restituisce: l'oggetto AppMintIAP (per il concatenamento). Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione e gli ID prodotto creati e attivi in Play Console. L'app installa window.AppMintIAP prima che venga eseguito il primo script della tua pagina (nelle WebView precedenti al 2021 circa, solo quando la pagina ha finito di caricarsi).
Nessun codice: contrassegna l'HTML. L'helper legge questi attributi da solo non appena viene iniettato e controlla i pulsanti che React o Vue mostrano più tardi:
<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 il codice: chiama tu stesso start() quando vuoi una tua funzione di messaggio o tuoi hook di sblocco. Usalo su pagine SENZA pulsanti data-iap-buy nell'HTML (con quelli, l'helper è già partito e un secondo avvio non fa nulla):
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
});
});In React: parti con piani senza button (non viene disegnato nulla) e lascia che React mostri a partire da onChange, che restituisce la funzione di annullamento per l'effetto:
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>;
}Note: attributi: data-iap-buy="id", data-iap-label ({price} diventa il prezzo aggiornato di Play), data-iap-owned (etichetta una volta posseduto; predefinita «✓ Acquistato»), data-iap-group (piani che si sostituiscono a vicenda; dopo che uno è posseduto gli altri si nascondono a meno che hideOtherPlans: false), data-iap-show / data-iap-hide (valore = id prodotto o gruppo; vuoto = qualsiasi cosa posseduta), data-iap-text (la parte di un pulsante il cui testo cambia), data-iap-container. Un pulsante posseduto riceve data-iap-state="owned" e la classe iap-unlocked. Un annullamento nella schermata di Google non invia alcun evento; il pulsante viene rilasciato quando la pagina torna visibile (o dopo 30 s). Chiamare start(options) dopo che l'helper è già partito da solo dagli attributi data-iap-buy UNISCE le opzioni (piani, mostra/nascondi, contenitore, onUnlock/onLock, toast, busyLabel, hideOtherPlans) a quelle in uso; chiamarla due volte è innocuo. Una pagina che include il proprio appmint-iap.js lo mantiene: la copia integrata si fa da parte.
AppMintIAP.started helper#
window.AppMintIAP.started
Esempio
Una proprietà (non una funzione): true quando l'helper è in esecuzione, cioè ha collegato almeno un prodotto dai pulsanti start(), sell() o data-iap-buy.
Restituisce: una proprietà booleana. Richiede: 💰 Vendi prodotti in-app.
Usala per scegliere tra start() (con le tue opzioni) e sell() (che funziona anche su un helper già in esecuzione):
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);Avvisati durante lo sviluppo quando nulla nella pagina è stato collegato (un errore di battitura in data-iap-buy, o i pulsanti non sono mai stati mostrati):
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);
});Distingui una schermata del negozio da «ancora in caricamento» e da «acquisti non disponibili»:
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());Note: un secondo start() non lo avvia mai due volte (React StrictMode esegue gli effetti due volte); le sue opzioni vengono unite a quelle in uso. Resta false finché nessun prodotto è collegato; l'helper continua poi a controllare la pagina in cerca di pulsanti data-iap-buy.
AppMintIAP.state helper#
window.AppMintIAP.state()
Esempio
Restituisce una volta lo stato attuale degli acquisti, senza iscriversi.
Restituisce: {available, adFree, owned, ownedIds, products}, in modo sincrono. ownedIds è un array di ID prodotto; products è {id: {price, currency, type: 'inapp'|'subs', period, owned}} per i prodotti su cui l'helper ha interrogato Google. Richiede: 💰 Vendi prodotti in-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();Controlla il possesso nel momento in cui il giocatore tocca un livello bloccato, altrimenti mandalo al tuo negozio:
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);
}Nascondi i tuoi inviti «passa a senza annunci» quando il diritto senza annunci della build è attivo, ed elenca i prezzi che Google ha inviato finora:
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);
});
}Note: è un'istantanea. Per seguire i cambiamenti usa AppMintIAP.onChange(fn), che dà lo stesso oggetto a ogni cambiamento. owned è true quando è posseduto qualcosa; controlla un prodotto con ownedIds o AppMintIAP.isOwned(id).
getOwnedProducts bridge#
window.WebToApk.getOwnedProducts(): String
Array JSON di ogni ID prodotto posseduto - per bloccare intere schermate all'avvio.
Esempio
Elenca tutti i prodotti Play che l'utente possiede, per sbloccare intere schermate all'avvio.
Restituisce: una stringa JSON (array di ID prodotto), in modo sincrono, per esempio '["pro_unlock","level_pack_2"]'. Analizzala con JSON.parse. '[]' quando nulla è posseduto. Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione.
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', ...]}
});Lo stesso cambiamento chiama anche window.onAppMintOwnedChanged(detail) se lo definisci.
Un elenco «I miei acquisti» nella tua schermata delle impostazioni, con un nome tuo per ogni prodotto:
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);Calcola il livello migliore posseduto dall'utente quando ogni livello è un prodotto a sé:
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()); });Note: include prodotti una tantum e abbonamenti attivi, e anche il prodotto Rimuovi annunci se l'utente lo ha acquistato. Funziona offline; l'elenco viene salvato sul telefono a ogni ripristino all'avvio e a ogni acquisto. appmint:owned-changed scatta solo quando l'elenco è davvero cambiato, quindi in un normale avvio potrebbe non scattare affatto.
getProducts bridge#
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
Chiede a Play i dettagli aggiornati degli ID prodotto indicati (array JSON di stringhe). Il risultato arriva come 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. Gli ID che Play non conosce sono assenti dal risultato: la pagina nasconde quegli elementi.
Esempio
Chiede a Google Play i dettagli aggiornati dei tuoi prodotti: titolo, descrizione e prezzo localizzato.
Restituisce: per ora niente. La risposta arriva come evento appmint:products: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type è 'inapp' o 'subs'; period è un periodo ISO-8601 come 'P1M' (solo abbonamenti). Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione. Il secondo argomento è una stringa 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 risposta viene passata anche a window.onAppMintProducts(detail) se la definisci.
Un selettore dei piani di abbonamento, con ogni scheda etichettata con il prezzo di Play e il suo periodo di fatturazione:
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 i test, registra gli ID che Google Play non ha restituito, tramite l'hook della pagina:
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));
}Note: mostra price esattamente come lo fornisce Play e non memorizzarlo mai. Un ID che Play non conosce (non creato, non attivo, o l'app non è stata installata da Play) semplicemente manca da products; tieni nascosto quell'elemento. Un requestId dall'aspetto numerico torna come numero, quindi usa un id testuale o confronta con String().
isOwned bridge#
window.WebToApk.isOwned(productId: String): Boolean
Controllo immediato del possesso sul dispositivo (prodotto una tantum posseduto O abbonamento attivo).
Esempio
Controlla, subito e offline, se l'utente possiede uno dei tuoi prodotti Play (un prodotto una tantum, o un abbonamento ancora attivo).
Restituisce: true / false, in modo sincrono. Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione. Con quell'interruttore attivo risponde anche per il prodotto Rimuovi annunci (IAP); in un'app che vende solo la rimozione degli annunci (interruttore disattivato) risponde false per ogni prodotto, quindi lì controlla la rimozione degli annunci 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);Controlla nel momento dell'uso: un'esportazione solo Pro viene eseguita se posseduta, altrimenti apre la schermata di acquisto e finisce quando l'acquisto è completato:
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 tramite uno qualsiasi di più piani: un prodotto mensile, uno annuale e uno a vita contano tutti:
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 refundNote: l'elenco dei prodotti posseduti viene ripristinato dall'account Google dell'utente a ogni avvio e salvato sul telefono, quindi funziona offline e dopo una reinstallazione, senza server. In un browser è sempre false. Un abbonamento smette di contare quando Play non lo segnala più come attivo.
isPremium bridge#
window.WebToApk.isPremium(): Boolean
Vero quando l'utente possiede «Rimuovi annunci» (diritto di Play Billing, ripristinato automaticamente a ogni avvio). Corrisponde al motivo 'ad_free' in 'appmint:ad-unavailable'.
Esempio
Ti dice se l'utente possiede il tuo prodotto «Rimuovi annunci», così la pagina può nascondere il suo pulsante «Rimuovi annunci» e l'interfaccia legata agli annunci.
Restituisce: true / false, in modo sincrono. Richiede: annunci attivi e Rimuovi annunci (IAP) con il tuo ID prodotto del Play Store. Senza è 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}
});Lo stesso cambiamento chiama anche un hook della pagina, se lo definisci:
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); }; // {premium}Mostra la tua offerta «Rimuovi annunci» nella schermata di fine partita solo ai giocatori che vedono ancora gli annunci:
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;
}Note: il possesso viene ripristinato dall'account Google dell'utente a ogni avvio, quindi leggilo una volta all'inizio e poi segui appmint:premium. Il campo ID prodotto accetta più ID separati da virgole (per esempio un piano mensile e uno a vita); possederne uno qualsiasi conta. Con il premium attivo, gli annunci vengono rimossi e le chiamate agli annunci rispondono ad_free.
purchase bridge#
window.WebToApk.purchase(productId: String)
Apre la schermata di acquisto di Google per [productId] (prodotto una tantum o abbonamento - risolto automaticamente). Concedi SOLO su 'appmint:purchase' {productId, owned:true}; gli errori fanno scattare 'appmint:purchase-failed' {reason}. Se già posseduto, l'evento di acquisto scatta di nuovo così la pagina può sbloccare in modo idempotente.
Esempio
Apre la schermata di acquisto di Google Play per uno dei tuoi prodotti (un prodotto una tantum o un abbonamento; l'app capisce quale).
Restituisce: per ora niente. Il successo fa scattare appmint:purchase {productId, owned: true}. Un errore fa scattare appmint:purchase-failed {productId, reason}. Richiede: 💰 Vendi prodotti in-app nella procedura guidata di creazione, e il prodotto creato e attivo in Play Console.
Sblocca SOLO nel gestore appmint:purchase, mai al tocco:
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; });Gli stessi risultati chiamano anche window.onAppMintPurchase(detail) e window.onAppMintPurchaseFailed(detail) se li definisci.
Acquista il piano scelto dall'utente in un selettore di abbonamenti, e nascondi un piano il cui ID Play non conosce:
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;
});Sblocca un pacchetto di livelli dal livello bloccato toccato dal giocatore, poi apri quel livello:
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);
};Note: motivi: disabled (Vendi prodotti in-app è disattivato), not_found (l'ID non è in Play Console), billing_error, pending (l'utente ha pagato con un metodo lento; lo sblocco arriva dopo tramite il ripristino all'avvio e appmint:owned-changed). Se il prodotto è già posseduto, appmint:purchase scatta di nuovo subito, quindi il tuo sblocco deve poter essere ripetuto senza problemi. L'app mostra anche un suo breve messaggio per gli errori.
revenuecat bridge#
window.WebToApk.revenuecat(action: String, argsJson: String, callbackId: String)
Una [action] di RevenueCat con [argsJson]; risponde a [callbackId] con {ok, …} o {ok:false, error}.
Esempio
Il metodo grezzo dietro AppMint.revenuecat - usa il wrapper. Un unico punto di ingresso: action è configure, getOfferings, purchase, restore, getCustomerInfo, logIn, logOut oppure getAppUserId.
Restituisce: niente direttamente; risponde a window.__appmintReplies[callbackId] con la stessa risposta data dal wrapper. Richiede: passaggio Integrazione → RevenueCat con la tua chiave pubblica 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 utente e leggi le offerte:
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');
}Acquista e gestisci i rifiuti:
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');Note: vedi AppMint.revenuecat per gli argomenti e le risposte di ogni azione.
startRemoveAdsPurchase bridge#
window.WebToApk.startRemoveAdsPurchase()
Avvia dalla pagina il flusso di acquisto «Rimuovi annunci» di Google Play (avviato dall'utente, per esempio dal pulsante «Rimuovi annunci» dell'app). Senza questo l'UNICO punto di ingresso era la voce del menu laterale nativo - irraggiungibile nelle app create senza menu laterale. L'esito raggiunge la pagina tramite 'appmint:premium' {premium:true}; se già posseduto notifica di nuovo subito; gli errori del flusso compaiono come toast nativo.
Esempio
Apre la schermata di acquisto di Google Play per il tuo prodotto «Rimuovi annunci», dal pulsante «Rimuovi annunci» della tua pagina.
Restituisce: niente. Il risultato arriva come appmint:premium {premium: true} (e window.onAppMintPremiumChanged). Richiede: annunci attivi e Rimuovi annunci (IAP) con il tuo ID prodotto del Play Store (il primo ID elencato è quello venduto). Quando il Menu laterale è attivo, l'app ha anche una propria voce Rimuovi annunci (nascosta quando l'utente lo possiede); con il menu laterale disattivato, questo pulsante nella tua pagina è l'unico modo per acquistare.
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 riga delle impostazioni che mostra se gli annunci sono stati rimossi, con il pulsante per acquistarlo:
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
}Proponilo dopo ogni terza partita finita, in una finestra a cui il giocatore risponde con un tocco:
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(); });Note: se l'utente lo possiede già, appmint:premium {premium:true} scatta subito. Gli errori (Play non raggiungibile, prodotto non trovato in Play Console) vengono mostrati dall'app come breve messaggio nativo; la pagina non riceve alcun evento. Prova con un'app installata da un canale di test di Play.