JavaScript bridge API
Purchases and entitlements
Selling from your own page, and asking what this buyer already owns. See the in-app purchases guide for the code around these.
AppMintIAP.available helper#
window.AppMintIAP.available
Example
A property (not a function): true when this page can really buy through Google Play.
Returns: a boolean property. Needs: π° Sell In-App Products (a ProMax-plan feature). Without it, window.AppMintIAP does not exist at all.
Check it before you show anything for sale, and say plainly when buying is not possible here:
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';
}Notes: window.AppMintIAP is installed before your page's first script runs; only on WebViews older than about 2021 does it arrive once the page has finished loading, so code that must also run there should wait for it (see the whenIap pattern in AppMintIAP.sell). The same value is in AppMintIAP.state().available. Wrapping the offer in data-iap-container hides it outside the app with no code.
AppMintIAP.buy helper#
window.AppMintIAP.buy(id)
Example
Opens Google Play's purchase sheet for a product, from your own button. It grants nothing by itself.
Returns: nothing. The result arrives as appmint:purchase {productId, owned:true} (and through AppMintIAP.onChange) or appmint:purchase-failed {productId, reason}. Needs: π° Sell In-App Products (a ProMax-plan feature) and the product active in Play Console.
Use it when you draw the button yourself; the unlock comes from ownership, not from the tap:
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 eventNotes: buy() is the bare WebToApk.purchase(id): no busy label, no group check. Buttons wired with sell(), start() or data-iap-buy do all of that for you, so prefer those.
AppMintIAP.isOwned helper#
window.AppMintIAP.isOwned(id)
Example
Checks, instantly and offline, whether the user owns a product (a one-time product, or a subscription that is still active).
Returns: true / false, synchronously; false outside the app. Needs: π° Sell In-App Products (a ProMax-plan feature).
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 offNotes: Same answer as WebToApk.isOwned(id). Ownership is restored from the user's Google account at every launch and saved on the phone, so it survives reinstalls and works offline. Never keep the entitlement only in localStorage.
AppMintIAP.onChange helper#
window.AppMintIAP.onChange(fn)
Example
Calls your function with the purchase state right away, and again every time it changes (prices arrived, a purchase landed, the launch restore, a refund, an expired subscription).
Returns: an unsubscribe function. fn gets {available, adFree, owned, ownedIds, products}; products is {id: {price, currency, type, period, owned}} for the products the helper knows (from sell, start or data-iap-buy). Needs: π° Sell In-App Products (a ProMax-plan feature).
A React hook. It returns the unsubscribe function straight from the effect:
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;Notes: Read ownership per product (ownedIds or products[id].owned). The top-level owned only means "anything is owned". adFree is the Remove Ads entitlement (WebToApk.isPremium()). A product is in products only after Google answered for it.
AppMintIAP.owned helper#
window.AppMintIAP.owned()
Example
Lists every product ID the user owns.
Returns: an array of product IDs, synchronously, for example ['pro_unlock', 'level_pack_2']; [] outside the app or when nothing is owned. Needs: π° Sell In-App Products (a ProMax-plan feature).
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);Notes: Parsed from WebToApk.getOwnedProducts() for you. Includes active subscriptions and the Remove Ads product. Works offline.
AppMintIAP.refresh helper#
window.AppMintIAP.refresh()
Example
Looks again for data-iap-buy buttons that were added since the last scan, and wires them (or starts the helper if it had found none yet).
Returns: the AppMintIAP object. Needs: π° Sell In-App Products (a ProMax-plan feature).
The helper already watches the page for new data-iap-buy buttons, so you rarely need this. Call it after you insert buttons in a way the watcher could miss, for example by changing attributes on an existing element:
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();
}Notes: Each new button gets its price from Google and its owned state. A button that is already wired is not wired twice. Buttons whose element was removed from the page are dropped.
AppMintIAP.sell helper#
window.AppMintIAP.sell(id, button, opts)
Example
Wires one product to one button in a single line. Call it once per product.
Returns: the AppMintIAP object (for chaining). Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature) and the product created and active in Play Console. window.AppMintIAP is installed before your page's first script runs (on WebViews older than about 2021, once the page has finished loading - the whenIap pattern below covers both).
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}' });
});Several plans where buying one replaces the others share a group:
whenIap(function (iap) {
iap.sell('pass_monthly', '#monthly', { label: 'Monthly β {price}', group: 'pass' })
.sell('pass_yearly', '#yearly', { label: 'Yearly β {price}', group: 'pass' });
});Notes: button is an element or a CSS selector; pass null to track a product without a button (your own UI reads AppMintIAP.state()). Other opts: labelEl (the element inside the button whose text changes). It works before or after the helper started, including from a React effect. The same as the data-iap-buy attributes (see AppMintIAP.start). Unlock paid content only from ownership (isOwned, onChange), never in your own click handler.
AppMintIAP.start helper#
window.AppMintIAP.start(config) and the data-iap-* attributes
Example
Wires your buy buttons to Google Play in one step: live prices on the buttons, the purchase sheet on tap, "owned" labels, thank-you blocks, restore at launch, and a visible message for every failure.
Returns: the AppMintIAP object (for chaining). Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature) and the product IDs created and active in Play Console. The app installs window.AppMintIAP before your page's first script runs (on WebViews older than about 2021, only once the page has finished loading).
No code at all: mark the HTML. The helper reads these attributes by itself as soon as it is injected, and watches for buttons that React or Vue render later:
<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 -->With code: call start() yourself when you want your own message function or unlock hooks. Use it on pages WITHOUT data-iap-buy buttons in the HTML (with them, the helper has already started and a second start does nothing):
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
});
});Notes: Attributes: data-iap-buy="id", data-iap-label ({price} becomes Play's live price), data-iap-owned (label once owned; default "β Purchased"), data-iap-group (plans that replace each other; after one is owned the others hide unless hideOtherPlans: false), data-iap-show / data-iap-hide (value = product id or group; empty = anything owned), data-iap-text (the part of a button whose text changes), data-iap-container. An owned button gets data-iap-state="owned" and the class iap-unlocked. A cancel on Google's sheet sends no event; the button is released when the page is visible again (or after 30 s). Calling start(options) after the helper already started on its own from data-iap-buy attributes MERGES the options (plans, show/hide, container, onUnlock/onLock, toast, busyLabel, hideOtherPlans) into the running set; calling it twice is harmless. A page that ships its own appmint-iap.js keeps it - the built-in copy stands down.
AppMintIAP.started helper#
window.AppMintIAP.started
Example
A property (not a function): true once the helper is running, i.e. it has wired at least one product from start(), sell() or data-iap-buy buttons.
Returns: a boolean property. Needs: π° Sell In-App Products (a ProMax-plan feature).
Use it to decide between start() (with your own options) and sell() (which also works on a running helper):
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);Notes: A second start() never starts it twice (React StrictMode runs effects twice); its options are merged into the running set. It stays false while no product is wired yet; the helper then keeps watching the page for data-iap-buy buttons.
AppMintIAP.state helper#
window.AppMintIAP.state()
Example
Returns the current purchase state once, without subscribing.
Returns: {available, adFree, owned, ownedIds, products}, synchronously. ownedIds is an array of product IDs; products is {id: {price, currency, type: 'inapp'|'subs', period, owned}} for the products the helper has asked Google about. Needs: π° Sell In-App Products (a ProMax-plan feature).
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();Notes: It is a snapshot. To follow changes use AppMintIAP.onChange(fn), which gives the same object every time something changes. owned is true when anything at all is owned; check a product with ownedIds or AppMintIAP.isOwned(id).
getOwnedProducts bridge#
window.WebToApk.getOwnedProducts(): String
JSON array of every owned product ID - for gating whole screens at startup.
Example
Lists every Play product the user owns, to unlock whole screens at start.
Returns: a JSON string (array of product IDs), synchronously, for example '["pro_unlock","level_pack_2"]'. Parse it with JSON.parse. '[]' when nothing is owned. Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature).
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', ...]}
});The same change also calls window.onAppMintOwnedChanged(detail) if you define it.
Notes: Includes one-time products and active subscriptions, and also the Remove Ads product if the user bought it. Works offline; the list is saved on the phone at every launch restore and purchase. appmint:owned-changed fires only when the list really changed, so it may not fire at all on a normal launch.
getProducts bridge#
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
Asks Play for live details of the given product IDs (JSON array of strings). Result arrives as 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. IDs Play doesn't know are absent from the result - the page hides those items.
Example
Asks Google Play for the live details of your products: title, description and the localized price.
Returns: nothing now. The answer arrives as the appmint:products event: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type is 'inapp' or 'subs'; period is an ISO-8601 period such as 'P1M' (subscriptions only). Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature). The second argument is a JSON string: use JSON.stringify.
function loadPrices(ids) {
return new Promise(function (resolve) {
var b = window.WebToApk;
if (!b || typeof b.getProducts !== 'function') { resolve([]); return; } // browser
var requestId = 'prices-' + Date.now();
window.addEventListener('appmint:products', function handler(e) {
if (String(e.detail.requestId) !== requestId) return; // another call's answer
window.removeEventListener('appmint:products', handler);
resolve(e.detail.products || []);
});
b.getProducts(requestId, JSON.stringify(ids));
});
}
loadPrices(['pro_unlock', 'coins_500', 'pro_monthly']).then(function (products) {
products.forEach(function (p) {
var btn = document.querySelector('[data-product="' + p.productId + '"]');
if (!btn) return;
btn.hidden = false;
btn.textContent = p.owned ? 'Owned' : p.title + ' Β· ' + p.price + (p.period === 'P1M' ? ' / month' : '');
});
});The answer is also passed to window.onAppMintProducts(detail) if you define it.
Notes: Show price exactly as Play gives it and never store it. An ID that Play does not know (not created, not active, or the app was not installed from Play) is simply missing from products; keep that item hidden. A numeric-looking requestId comes back as a number, so use a text id or compare with String().
isOwned bridge#
window.WebToApk.isOwned(productId: String): Boolean
Instant on-device ownership check (one-time product owned OR sub active).
Example
Checks, instantly and offline, whether the user owns one of your Play products (a one-time product, or a subscription that is still active).
Returns: true / false, synchronously. Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature). It also answers for the Remove Ads (IAP) product.
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);Notes: The owned list is restored from the user's Google account at every launch and saved on the phone, so it works offline and after a reinstall, with no server. Always false in a browser. A subscription stops counting once Play no longer reports it as active.
isPremium bridge#
window.WebToApk.isPremium(): Boolean
True when the user owns "Remove Ads" (Play Billing entitlement, auto-restored on every launch). Matches the reason 'ad_free' in 'appmint:ad-unavailable'.
Example
Tells you whether the user owns your "Remove Ads" product, so the page can hide its "Remove ads" button and ad-related UI.
Returns: true / false, synchronously. Needs: Ads on and Remove Ads (IAP) with your Play Store Product ID. Without that it is always 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}
});The same change also calls a page hook, if you define it:
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); }; // {premium}Notes: Ownership is restored from the user's Google account at every launch, so read it once at start and then follow appmint:premium. The Product ID field accepts several IDs separated by commas (for example a monthly plan and a lifetime one); owning any of them counts. While premium, ads are removed and ad calls answer ad_free.
purchase bridge#
window.WebToApk.purchase(productId: String)
Opens Google's purchase sheet for [productId] (one-time product or subscription - resolved automatically). Grant ONLY on 'appmint:purchase' {productId, owned:true}; failures fire 'appmint:purchase-failed' {reason}. Already-owned re-fires the purchase event so the page can idempotently unlock.
Example
Opens Google Play's purchase sheet for one of your products (a one-time product or a subscription; the app finds out which).
Returns: nothing now. Success fires appmint:purchase {productId, owned: true}. A failure fires appmint:purchase-failed {productId, reason}. Needs: π° Sell In-App Products in the build wizard (a ProMax-plan feature), and the product created and active in Play Console.
Unlock ONLY in the appmint:purchase handler, never on the tap:
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; });The same results also call window.onAppMintPurchase(detail) and window.onAppMintPurchaseFailed(detail) if you define them.
Notes: Reasons: disabled (Sell In-App Products is off), not_found (the ID is not in Play Console), billing_error, pending (the user paid with a slow method; the unlock arrives later through the launch restore and appmint:owned-changed). If the product is already owned, appmint:purchase fires again at once, so your unlock must be safe to repeat. The app also shows its own short message for errors.
startRemoveAdsPurchase bridge#
window.WebToApk.startRemoveAdsPurchase()
Launch the Google Play "Remove Ads" purchase flow from the page (user-initiated, e.g. the app's own "Remove ads" button). Without this the ONLY entry point was the native side-menu item - unreachable in apps built without the side menu. Outcome reaches the page via 'appmint:premium' {premium:true}; already-owned re-notifies immediately; flow errors surface as a native toast.
Example
Opens Google Play's purchase sheet for your "Remove Ads" product, from your page's own "Remove ads" button.
Returns: nothing. The result arrives as appmint:premium {premium: true} (and window.onAppMintPremiumChanged). Needs: Ads on and Remove Ads (IAP) with your Play Store Product ID (the first ID listed is the one sold). The app has no menu item for this, so your page must have the button.
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;
});Notes: If the user already owns it, appmint:premium {premium:true} fires at once. Errors (Play not reachable, product not found in Play Console) are shown by the app as a short native message; the page gets no event for them. Test with an app installed from a Play testing track.