Ganar dinero
Vender productos dentro de la app (IAP)
Compras de Google Play desde tu propio sitio web o app ZIP
1📥 Consigue la guía + la app de demostración funcional#
Todo lo de esta guía también es un PDF que puedes leer sin conexión, además de una app de demostración completa que puedes compilar y ejecutar hoy.
Toca abajo y ambos se guardan en tu teléfono en Downloads/AppMint/Guides:
- AppMint-InApp-Purchases-Guide.pdf - la guía completa, con los pasos de Play Console y los ejemplos de React / Next.js
- iap-demo-index.html - un pequeño juego que vende tres cosas: quitar anuncios (pago único), un paquete de niveles (pago único) y un pase de temporada mensual
Para probar la demo: comprime iap-demo-index.html por sí solo, súbelo en el asistente y sigue la guía. Es la forma más rápida de ver una compra real funcionar de principio a fin antes de tocar tu propia app.
En la app, el botón de este paso los guarda en tu teléfono. Aquí son descargas directas.
AppMint-InApp-Purchases-Guide.pdf iap-demo-index.html2¿Qué es esto? (en palabras sencillas)#
Tu app puede vender cosas (una mejora Pro, niveles extra, una suscripción) y Google cobra el dinero y te lo paga cada mes.
VAMOS A CONSTRUIR UN EJEMPLO JUNTOS, a lo largo de toda esta guía:
A game that sells two things
• 'Levels 11-20' ₹99 paid once, kept forever
• 'Remove ads' ₹49 every month
Al final añadirás cuatro líneas a tu página. Ese es todo el trabajo: no hay código de compra que escribir, y aunque nunca lo hayas hecho antes, te saldrá bien.
NO necesitas saber programación más allá de copiar y pegar, y NO necesitas un servidor, una base de datos ni una cuenta en una empresa de pagos. Google Play se encarga de lo difícil:
- Muestra la pantalla de pago (tarjeta / UPI / facturación del operador)
- Convierte tu precio a la moneda de cada país
- Recuerda quién compró qué, incluso después de reinstalar
- Envía el dinero a tu banco
AppMint se queda con el 0%: la venta va directamente a TU cuenta de Google Play.
Funciona igual para Website → APK, ZIP → APK y apps creadas con IA.
3Lo que necesitas antes de empezar#
Solo cuatro cosas:
- Tu sitio web o app HTML/ZIP (lo que conviertes en un APK).
- Algo que vender: decide qué reciben los compradores (p. ej. 'Versión Pro': sin límites, contenido extra).
- Una cuenta de desarrollador de Google Play: cuota única de $25 en play.google.com/console. La necesitas para publicar CUALQUIER app en Play Store, no solo las de pago.
- Aproximadamente 1 hora en total, repartida entre los pasos de abajo.
Importante saberlo desde ya: las compras dentro de la app solo funcionan cuando tu app está instalada DESDE Play Store. Es una regla de Google para todos, no un límite de AppMint.
4Paso 1 - Inventa tu ID de producto#
Un ID de producto es solo un nombre corto que te inventas para lo que vendes. Escribirás este mismo nombre en dos sitios más adelante (tu página + Play Console), así que mantenlo simple y anótalo.
Reglas: solo letras minúsculas, números y guiones bajos. Debe empezar por una letra o un número.
Copia uno de estos:
- pro_unlock - mejora a 'Versión Pro' (buena opción por defecto; el código de abajo ya la usa)
- remove_ads - mejora sin anuncios
- level_pack_2 - paquete de contenido extra
- coins_100 - paquete de moneda del juego
- premium_monthly - suscripción mensual
El precio NO forma parte del ID: fijarás el precio en el Paso 5 y puedes cambiarlo cuando quieras sin tocar tu código.
5Paso 2 - Marca tu botón (sin código que escribir)#
NO escribes ningún código de compra. Tu app ya lleva el motor de compras: solo etiquetas tu propio HTML para que sepa qué vender.
Copia el bloque de abajo en tu página y cambia solo:
(1) pro_unlock → tu ID de producto, en data-iap-buy
(2) las palabras del botón y dentro de la sección de pago
Eso es todo. Entonces el botón muestra el precio real de Google en la moneda del comprador, abre la pantalla de pago de Google al tocarlo, muestra la sección de pago tras pagar (y de nuevo en cada inicio para quienes ya compraron), la retira si una suscripción caduca o se reembolsa un pago, y muestra un mensaje claro si algo sale mal.
LAS CUATRO ETIQUETAS:
- data-iap-buy="id" - convierte esto en el botón de compra (obligatorio)
- data-iap-label="…{price}…" - texto del botón; {price} se convierte en el precio real de Google
- data-iap-owned="…" - texto del botón después de comprar
- data-iap-show - una sección que aparece solo después de comprar (empiézala 'hidden')
También disponibles: data-iap-hide (desaparece tras comprar), data-iap-container (un contenedor que solo aparece dentro de la app), data-iap-text (ponlo en el <span> que contiene el texto si tu botón también tiene iconos).
¿VENDES VARIAS COSAS DISTINTAS? Añade un botón para cada una. Son independientes: una app puede quitar anuncios Y vender paquetes de niveles Y ofrecer una suscripción, y comprar una nunca afecta a las demás. Apunta una sección a un producto con data-iap-show="level_pack_2".
Solo los IDs que escribes en 'Remove Ads (IAP) → Product ID' apagan los anuncios. Todo lo demás que vendes desbloquea lo que tu página haga con ello, y los anuncios siguen funcionando.
¿DOS PLANES PARA LO MISMO (mensual vs de por vida)? Da a ambos botones el mismo data-iap-group="adfree": así, comprar cualquiera oculta el otro y nadie paga dos veces. Sin grupo no se oculta nada, que es lo que necesitan los productos no relacionados.
<!-- (1) CHANGE pro_unlock to your product ID -->
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}"
data-iap-owned="✓ Unlocked">
Unlock Pro
</button>
<!-- (2) CHANGE the words. Appears only after buying. -->
<div data-iap-show hidden>
🎉 Pro content here — visible after buying.
</div>
<!-- Optional: disappears after buying -->
<div data-iap-hide>
Upgrade to remove the ads.
</div>
<!-- DIFFERENT THINGS: independent — buying one never affects the others -->
<button data-iap-buy="remove_ads" data-iap-label="Remove ads — {price}">Remove ads</button>
<button data-iap-buy="level_pack_2" data-iap-label="Levels 11-20 — {price}">Get levels</button>
<div data-iap-show="level_pack_2" hidden>Levels 11-20 unlocked!</div>
<!-- SAME THING, TWO PLANS: one group, so buying either hides the other -->
<button data-iap-buy="remove_ads_monthly" data-iap-group="adfree"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free (monthly)">Remove ads monthly</button>
<button data-iap-buy="remove_ads_lifetime" data-iap-group="adfree"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed forever">Remove ads forever</button>6Las 4 cosas que la gente suele vender#
Elige el que se parezca más a ti. La única diferencia entre ellos es dónde creas el producto en Play Console; el código de tu página tiene la misma forma siempre.
- QUITAR ANUNCIOS, PAGO ÚNICO (para siempre)
Créalo en: In-app products
Escribe también el id en: Remove Ads (IAP) → Product ID
- QUITAR ANUNCIOS, MENSUAL
Créalo en: Subscriptions
Escribe también el id en: Remove Ads (IAP) → Product ID
- DESBLOQUEAR NIVELES / CONTENIDO, PAGO ÚNICO
Créalo en: In-app products
NO lo escribas en la casilla Remove Ads: tus anuncios seguirían funcionando.
- DESBLOQUEAR NIVELES / CONTENIDO, MENSUAL
Créalo en: Subscriptions
NO lo escribas en la casilla Remove Ads: tus anuncios seguirían funcionando.
Puedes vender LOS CUATRO en la misma app, y tantos productos como quieras. Comprar uno nunca afecta a los demás.
<!-- 1. remove ads, paid once -->
<button data-iap-buy="remove_ads_lifetime"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed">Remove ads forever</button>
<!-- 2. remove ads, monthly -->
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free">Remove ads monthly</button>
<!-- 3. levels, paid once -->
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Get levels 11-20</button>
<!-- 4. all levels, monthly -->
<button data-iap-buy="all_levels_monthly"
data-iap-label="All levels — {price}/month"
data-iap-owned="✓ Subscribed">Subscribe</button>7¿Qué compras quitan los anuncios?#
Esto es lo único que la gente hace mal, así que vale la pena dedicarle 30 segundos.
Tus anuncios NO forman parte de tu página web. Son anuncios reales de Android colocados encima, así que nada en tu página puede apagarlos. Solo UNA cosa puede:
wizard Step 3 → Remove Ads (IAP) → Product ID
Cualquier id que escribas en esa casilla quita los anuncios cuando alguien lo compra. Cualquier otro id que vendas solo desbloquea lo que tu página haga con él, y los anuncios siguen.
¿Vendes un plan sin anuncios Y paquetes de niveles? Escribe solo el id del plan sin anuncios en esa casilla. En nuestro ejemplo:
Product ID box: remove_ads_monthly
NOT in the box: level_pack_2
¿Ofreces dos planes sin anuncios (mensual Y de por vida)? Pon ambos en esa misma casilla, separados por una coma: tener cualquiera de los dos quita los anuncios:
remove_ads_monthly,remove_ads_lifetime
¿No vendes la eliminación de anuncios en absoluto? Deja ese interruptor desactivado por completo y usa solo '💰 Sell In-App Products'.
Selling levels only:
Remove Ads (IAP) OFF
Sell In-App Products ON
Selling ad removal only:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly
Sell In-App Products ON
Selling both, with two ad-free plans:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly,remove_ads_lifetime
Sell In-App Products ON
(level_pack_2 stays out of the box)8Ejemplo completo - página HTML simple#
Aquí está toda nuestra app de ejemplo: vende 'Levels 11-20' una vez y 'Remove ads' mensualmente. Cópiala, cambia los dos ids y las palabras, y listo.
Lee qué hace cada etiqueta:
- data-iap-buy → lo que vende este botón
- data-iap-label → el texto; {price} se convierte en el precio real de Google
- data-iap-owned → el texto después de que hayan pagado
- data-iap-show="level_pack_2" → este cuadro aparece solo después de comprar ESE producto
- data-iap-hide → este cuadro desaparece en cuanto se compra cualquier cosa
Fíjate en que no hay <script> ni código de compra. Tu app ya lo contiene.
<h2>Get more levels</h2>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Levels 11-20 unlocked">
Levels 11-20
</button>
<div data-iap-show="level_pack_2" hidden>
🎉 Levels 11 to 20 are yours. Have fun!
</div>
<h2>Tired of ads?</h2>
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ No more ads">
Remove ads
</button>
<div data-iap-hide>
Ads help keep this game free.
</div>9Ejemplo completo - React o Next.js#
LA FORMA FÁCIL: las mismas etiquetas funcionan en React y Next.js, porque son solo marcado. Sin useState, sin useEffect, sin 'use client'. Los botones que React añade más tarde se detectan automáticamente.
Eso es todo lo que necesitan la mayoría de las apps: usa el primer bloque de abajo y no sigas.
LA FORMA CONTROLADA (segundo bloque): solo si quieres que React mismo dibuje el precio y cambie la pantalla. Entonces:
- En Next.js pon 'use client' en la línea 1 del archivo, o la compilación falla.
- Toca window.AppMintIAP solo dentro de useEffect: en el cuerpo del componente se ejecuta en el servidor, donde window no existe.
- onChange(fn) te entrega el estado de inmediato y otra vez en cada cambio, y devuelve la función para cancelar la suscripción que React quiere.
- En Next.js, compila como exportación estática: output: 'export' en next.config.js.
// ── EASY WAY — just JSX, nothing else ──────────────────────────
export default function Shop() {
return (
<>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Levels 11-20</button>
<div data-iap-show="level_pack_2" hidden>🎉 Levels 11-20 are yours!</div>
</>
);
}
// ── CONTROLLED WAY — React draws everything ───────────────────
'use client'; // Next.js: must be line 1
import { useEffect, useState } from 'react';
export default function Shop() {
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({
plans: [{ id: 'level_pack_2' }, { id: 'remove_ads_monthly' }]
});
return window.AppMintIAP.onChange(setIap);
}, []);
if (!iap.available) return null; // outside the app there is nothing to buy
const levels = iap.products?.level_pack_2; // always optional-chain
return levels?.owned
? <p>🎉 Levels 11-20 are yours!</p>
: <button onClick={() => window.AppMintIAP.buy('level_pack_2')}>
Levels 11-20 {levels?.price}
</button>;
}10Comprobación rápida en tu navegador#
Abre tu página en un navegador normal (Chrome en tu PC o teléfono).
Deberías ver el botón de compra ahí sin hacer nada, y tu sección de pago todavía oculta. Ambas cosas son correctas: fuera de la app no hay nada que comprar, así que no se puede comprar nada por accidente.
Dentro de la app es una página completamente distinta: el botón recibe el precio real, abre la pantalla de pago de Google y se oculta cuando el comprador ya lo tiene. Tu sitio web sigue funcionando con normalidad para los visitantes de siempre.
¿Quieres que la oferta desaparezca por completo en la web? Envuélvela en un <div data-iap-container hidden>: ese contenedor solo se revela dentro de la app.
11Paso 3 - ACTIVA las compras en el ASISTENTE#
⚠️ NO TE LO SALTES. Si lo haces, tus botones siguen apareciendo, pero cada toque responde 'In-app purchases are not enabled in this app.' y no cobras nada.
El ASISTENTE son las pantallas Paso 1 → Paso 2 → Paso 3 que usas para crear tu app. Las compras están en el PASO 3 DEL ASISTENTE.
HAZLO EN ESTE ORDEN:
- Abre el asistente y ve al PASO 3 DEL ASISTENTE (la pantalla con Anuncios, Notificaciones y Permisos).
STEPPER STEP 3 — what to set
Ads ......................... ON (needed to see the card below)
💰 Sell In-App Products ..... ON ← REQUIRED for selling anything
Selling ad removal too?
Remove Ads (IAP) ............ ON
Play Store Product ID ....... remove_ads_monthly
(two plans) .......... remove_ads_monthly,remove_ads_lifetime
License Key ................. paste from Play Console
Export ...................... AAB (not APK)12Paso 4 - Pon tu app en Google Play#
En tu Play Console (el sitio web donde los desarrolladores gestionan sus apps):
- 'Create app' → rellena el nombre y las preguntas básicas.
- En el menú izquierdo: 'Testing' → 'Internal testing' → 'Create new release' → sube tu archivo AAB → 'Save' y publícalo.
- Aún en Internal testing → pestaña 'Testers' → añade tu propio Gmail → copia el enlace 'join' para más tarde.
Internal testing es un área privada: nadie puede ver tu app todavía. Existe para que TÚ puedas probar las compras antes de publicar.
13Paso 5 - Crea tu producto y fija el precio#
Ahora dile a Google qué vendes. El menú que uses depende de si pagan una vez o cada mes.
PAGO ÚNICO (niveles, Pro, monedas, quitar anuncios para siempre)
- Play Console → tu app → 'Monetize' → 'Products' → 'In-app products'.
- 'Create product'.
- Product ID: escribe EXACTAMENTE el id de tu página: level_pack_2. Misma ortografía, mismas mayúsculas. Google nunca te deja cambiarlo ni reutilizarlo.
- Nombre: lo que ven los compradores, p. ej. 'Levels 11-20'. Descripción: una línea.
- Fija tu precio en tu propia moneda: Google lo convierte para cada país.
- Pulsa 'Activate'.
CADA MES (quitar anuncios mensual, suscripción a todos los niveles)
- Play Console → 'Monetize' → 'Products' → 'Subscriptions'.
- 'Create subscription'. Product ID: remove_ads_monthly.
- Guarda y añade un PLAN BASE: tipo 'Auto-renewing', periodo de facturación 'Monthly', tu precio.
- Pulsa 'Activate' en el plan base Y en la propia suscripción. Si olvidas cualquiera de los dos, tu app no puede verla.
Repite para cada id que usaste. El código de tu página no cambia: la app le pregunta a Google de qué tipo es cada id.
¿QUIERES MENSUAL *Y* ANUAL? Crea DOS suscripciones separadas (all_levels_monthly y all_levels_yearly) en lugar de dos planes base dentro de una suscripción. Una suscripción con varios planes base siempre ofrece el primero, sin forma de elegir.
Cambia cualquier precio más tarde en Play Console: tu app muestra el precio nuevo por sí sola, sin necesidad de recompilar.
14Paso 6 - Pruébalo (no te cobrarán)#
- Play Console → menú izquierdo 'Setup' → 'License testing' → añade tu Gmail a la lista de testers.
- En tu teléfono: abre el enlace de unión de internal testing del Paso 4, acepta e instala tu app DESDE Play Store.
- Abre la app. Aparece tu botón de compra con el precio real.
- Tócalo: se abre la hoja de pago de Google y dice 'Test card, always approves'. Cómpralo. Aparece tu contenido pro. No pagas nada.
- Comprobación extra: desinstala y reinstala la app: el contenido pro se desbloquea solo al iniciar. Es Google restaurando la compra.
⚠️ Si instalas el APK directamente (sideload) en lugar de desde Play Store, el botón de compra NO aparecerá. Es normal: prueba siempre a través del enlace de Play Store.
15Paso 7 - Publica y cobra#
- Cuando las pruebas se vean bien: Play Console → 'Production' → crea una versión con el mismo AAB → publícala. (Las apps nuevas pasan por la revisión de Google, normalmente unos días.)
- Los usuarios compran dentro de tu app → Google cobra el dinero.
- Google paga a tu cuenta bancaria cada mes (Play Console → 'Payments profile'). Google se queda con su comisión de servicio estándar (15% para la mayoría de los desarrolladores). AppMint no se queda con nada.
Para añadir más productos más tarde: añade el nuevo ID en el código de tu página, recompila una vez y crea el ID en Play Console. Los cambios de precio nunca necesitan recompilar.
16Si algo no funciona#
Tu app te dice qué salió mal en lugar de fallar en silencio. Busca el mensaje que ves:
'In-app purchases are not enabled in this app.'
→ '💰 Sell In-App Products' estaba DESACTIVADO en el PASO 3 DEL ASISTENTE al compilar. Actívalo y compila de nuevo.
'This item is not available on Google Play yet.'
→ Google no reconoce ese id. Compruébalo letra por letra con Play Console, confirma que el producto está ACTIVO (en una suscripción, el plan base Y la suscripción deben estar activados) y asegúrate de haber instalado la app desde el enlace de tester de Play.
'Google Play could not start the payment.'
→ La app no se instaló desde Play Store, o se firmó con una clave distinta de la que subiste.
'Payment pending - this unlocks automatically once Google confirms.'
→ No hay nada que hacer. Los métodos de pago lentos (vales en efectivo, algunos UPI) tardan unos minutos; el desbloqueo llega solo, incluso después de reiniciar la app.
NINGÚN MENSAJE, no pasa nada al tocar
→ Casi siempre es una errata en data-iap-buy, o la app no está instalada desde Play. Cualquier otra causa muestra uno de los mensajes de arriba.
EL PAGO FUNCIONÓ PERO LOS ANUNCIOS SIGUEN AHÍ
→ Ese id de producto no está en PASO 3 DEL ASISTENTE → 'Remove Ads (IAP)' → 'Play Store Product ID'. Añádelo (separa varios con comas) y compila de nuevo.
EL BOTÓN DE COMPRA NUNCA APARECE EN LA APP
→ '💰 Sell In-App Products' estaba desactivado al compilar. En un navegador web normal los botones se ocultan A PROPÓSITO: fuera de la app no hay nada que comprar.
NO PASÓ NADA DESPUÉS DE CERRAR LA HOJA DE PAGO DE GOOGLE
→ Es una cancelación normal. Toca de nuevo y completa el pago.
EL PRECIO PARECE ANTIGUO
→ Play guarda los precios en caché en el dispositivo durante unas horas. Una instalación nueva siempre muestra el precio nuevo.
17React, Next.js o JavaScript simple#
REACT Y NEXT.JS NO NECESITAN NADA ESPECIAL. Las etiquetas de arriba son solo marcado, así que la misma línea funciona también ahí: sin useState, sin useEffect, sin 'use client':
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}">Unlock Pro</button>
Los botones que React monta más tarde se encuentran automáticamente, y volver a montarlos es seguro. En Next.js, compila como exportación estática (output: 'export').
¿Prefieres código a etiquetas? Una línea hace el mismo trabajo:
AppMintIAP.sell('pro_unlock', '#buy-btn');
Tercer argumento opcional: {label:'Unlock Pro - {price}', owned:'✓ Unlocked', show:'#thanks', hide:'.upsell'}. Llámalo una vez por producto.
SOLO SI QUIERES QUE REACT DIBUJE EL PRECIO: si no, sáltatelo:
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({ plans: [{ id: 'pro_unlock' }] });
return window.AppMintIAP.onChange(setIap);
}, []);
const pro = iap.products?.pro_unlock; // always optional-chain
if (!iap.available) return null; // nothing to buy outside the app
if (pro?.owned) return <p>Thanks!</p>;
return <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>
Unlock Pro {pro?.price}
</button>;
DOS HÁBITOS EVITAN QUE ESTO SE ROMPA:
- Lee la propiedad POR PRODUCTO: iap.products?.<id>?.owned. El iap.owned de nivel superior significa 'algo es tuyo' y cambia de significado el día que vendes un segundo producto.
- Usa encadenamiento opcional en toda la ruta, para que un campo inesperado nunca lance un error mientras React renderiza.
onChange te entrega {available, adFree, owned, ownedIds, products} de inmediato y en cada cambio, y devuelve una función para cancelar la suscripción, así que un comprador que vuelve ya está desbloqueado en el primer renderizado. En Next.js añade 'use client' como línea 1 y compila con output: 'export'.
También: AppMintIAP.available, .isOwned(id), .owned(), .buy(id), .state(), .refresh(). Las etiquetas data-iap-buy también funcionan en JSX, y los botones que React monta más tarde se detectan automáticamente.
¿Prefieres manejar tú mismo el puente sin procesar? Todo lo anterior se basa en esto:
- iap.isOwned(id) - true/false al instante, funciona sin conexión
- iap.getOwnedProducts() - array JSON con todos los IDs que ya son tuyos
- iap.getProducts(reqId, idsJson) - precios en vivo; el resultado llega como el evento
'appmint:products'{requestId, products:[{productId, title, price, currency, type, owned}]} - iap.purchase(id) - productos de pago único Y suscripciones (detección automática)
- Eventos:
'appmint:purchase'{productId} (concede SOLO aquí: un toque sobre algo que ya es tuyo lo vuelve a disparar),'appmint:owned-changed'{products} (terminó la restauración al iniciar, o un reembolso quitó algo: vuelve a leer la propiedad),'appmint:purchase-failed'{reason: disabled | not_found | billing_error | pending}
¿Vendes solo una mejora única de 'quitar anuncios'? Usa en su lugar la pareja integrada más sencilla: activa 'Remove Ads (IAP)' en el Paso 3 del asistente:
var iap = window.WebToApk;
if (iap && !iap.isPremium()) {
showRemoveAdsButton(function onTap() {
iap.startRemoveAdsPurchase();
});
}
window.addEventListener('appmint:premium', function (e) {
if (e.detail.premium) hideRemoveAdsButton();
});Remove Ads (IAP)
Configuración de Google Play Billing
1Crea un producto gestionado (Managed Product)#
En el panel de tu Google Play Console:
- Ve a 'Monetization' > 'In-app products'
- Haz clic en 'Create product'
- Escribe un Product ID (p. ej., 'remove_ads_premium')
- Pon un Name (p. ej., 'Remove All Ads') y una Description
- Fija el precio (Price) y haz clic en 'Save'
- Haz clic en 'Activate' para que esté disponible para pruebas
¿Vendes la eliminación de anuncios como SUSCRIPCIÓN? Créala en 'Monetization' > 'Subscriptions' con un plan base de renovación automática, y activa TANTO el plan base como la suscripción. Sin anuncios dura entonces mientras la suscripción esté activa, y los anuncios vuelven automáticamente cuando caduca.
2Consigue tu clave de licencia (License Key)#
Google Play requiere una clave pública RSA para verificar las compras de forma segura.
En Play Console:
- Ve a 'Monetization' > 'Monetization setup'
- En 'Licensing', copia la 'Base64-encoded RSA public key'
- ⚠️ IMPORTANTE: Tendrás que pegar esto en el Generador más adelante.
Example Key Format:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...3Configuración en WebToApk Generator#
En la app del generador:
- Activa el interruptor 'Remove Ads (IAP)' en el Paso 3
- Escribe el MISMO 'Product ID' exacto que creaste en el Paso 1
- Pega tu 'Base64 License Key' del Paso 2
- Genera tu APK/AAB
¿Ofreces más de un plan sin anuncios (p. ej., una suscripción mensual Y un desbloqueo único de por vida)? Pon todos los Product ID en este único campo, separados por comas: tener CUALQUIERA de ellos quita los anuncios. El primer ID de la lista es el que compra el elemento de menú 'Remove Ads' integrado, así que pon primero tu plan por defecto.
Product ID: remove_ads_premium
Two plans:
remove_ads_monthly,remove_ads_lifetime4Configura los License Testers#
Para probar compras sin gastar dinero real:
- En Play Console, ve a 'Setup' > 'License testing'
- Añade tu dirección de Gmail a la lista 'License testers'
- Asegúrate de que 'License response' esté en 'RESPOND_NORMALLY'
5Prueba en Internal o Closed Track#
Las compras dentro de la app SOLO funcionan cuando la app está instalada desde Play Store (tracks de pruebas):
- Sube tu AAB generado al track 'Internal Testing' o 'Closed Testing'
- Comparte el enlace de unión de testers con tus cuentas de prueba
- Descarga la app desde Play Store
- ¡Toca el botón 'Remove Ads' en tu app para probar el flujo!
Note: Purchases will show a 'Test instrument, always approves' message for license testers.Recetas de código
Los pasos anteriores cubren lo básico: las cuatro etiquetas, una página simple completa, la línea de React. Estas recetas cubren lo que no: varios planes que se sustituyen entre sí, suscripciones que caducan, hooks de frameworks, el puente sin procesar completo y las tablas de referencia.
La regla única#
Una compra se concede solo cuando Google la confirma. No cuando se hace clic en el botón, no cuando se abre la hoja, no cuando el usuario vuelve a tu página. Todo lo de abajo son formas distintas de expresar esa única regla.
El runtime ya lo aplica: AppMintIAP vuelve a leer la propiedad desde el puente al iniciar, en cada compra, en cada reembolso y cada vez que se reabre la app. La propiedad sobrevive a reinstalaciones, a un teléfono nuevo y a estar sin conexión, porque vive en la cuenta de Google del comprador, no en tu página.
Nunca desbloquees en el manejador del clic, y nunca guardes el derecho solo en localStorage. Borrar el almacenamiento del navegador no debe costarle a un cliente lo que pagó, y un valor localStorage copiado no debe regalarlo.
Receta 1 - una tabla de precios con precios en vivo#
Tres planes donde comprar cualquiera sustituye a los demás. group es lo que los hace alternativas: sin él, son productos sin relación y comprar el plan de por vida dejaría el botón mensual ahí.
<div class="plans" data-iap-container>
<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_yearly" data-iap-group="pass"
data-iap-label="Yearly — {price}" data-iap-owned="Active">Yearly</button>
<button data-iap-buy="pass_lifetime" data-iap-group="pass"
data-iap-label="Lifetime — {price}" data-iap-owned="Owned">Lifetime</button>
</div>
<p data-iap-show="pass">You are a member. Thank you.</p>
<p data-iap-hide="pass">Members get every level and no ads.</p>Un id de producto que Google no conoce (aún no creado, no activado, o la app no se instaló desde Play Store) se queda simplemente sin precio. Su botón sigue siendo tocable a propósito: al tocarlo se informa de la razón real en lugar de no hacer nada.
Receta 2 - React#
onChange se dispara de inmediato con el estado actual y otra vez en cada cambio, y devuelve su propia función para cancelar la suscripción, así que es exactamente la forma que quiere un useEffect.
import { useEffect, useState } from 'react';
/** Purchase state for the whole app. Safe in a browser too: available is false there. */
export function useIap() {
const [iap, setIap] = useState({ available: false, ownedIds: [], products: {}, adFree: false });
useEffect(() => {
if (!window.AppMintIAP) return; // running in a browser, or IAP not enabled
return window.AppMintIAP.onChange(setIap); // returns the unsubscribe fn
}, []);
return {
...iap,
owns: (id) => iap.ownedIds.includes(id),
priceOf: (id) => iap.products[id]?.price ?? null,
buy: (id) => window.AppMintIAP?.buy(id)
};
}import { useIap } from './hooks/useIap';
export function ProGate({ children }) {
const { available, owns, priceOf, buy } = useIap();
if (owns('pro_unlock')) return children;
return (
<div className="upsell">
<h2>Pro</h2>
<p>Unlimited projects, no watermark.</p>
<button onClick={() => buy('pro_unlock')} disabled={!available}>
{available ? `Unlock Pro${priceOf('pro_unlock') ? ` — ${priceOf('pro_unlock')}` : ''}` : 'Not available here'}
</button>
</div>
);
}El botón llama a buy(); no desbloquea nada. El desbloqueo ocurre porque Google confirma, se dispara onChange, owns('pro_unlock') pasa a true y el componente se vuelve a renderizar. Es la misma regla, expresada en React.
Receta 3 - Vue#
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
const iap = ref({ available: false, ownedIds: [], products: {} });
let stop;
onMounted(() => { stop = window.AppMintIAP?.onChange(s => (iap.value = s)); });
onUnmounted(() => stop && stop());
const isPro = computed(() => iap.value.ownedIds.includes('pro_unlock'));
const price = computed(() => iap.value.products.pro_unlock?.price ?? '');
</script>
<template>
<slot v-if="isPro" />
<button v-else :disabled="!iap.available" @click="AppMintIAP.buy('pro_unlock')">
Unlock Pro {{ price }}
</button>
</template>Receta 4 - suscripciones#
Una suscripción se compra exactamente igual que un producto de pago único; la diferencia es que puede terminar. type te dice de qué tipo es un producto y period da el intervalo de facturación como duración ISO-8601: P1M mensual, P1Y anual, P1W semanal.
AppMintIAP.onChange(state => {
const pass = state.products.season_pass; // undefined until Google answers
const active = state.ownedIds.includes('season_pass');
document.body.classList.toggle('has-pass', active);
if (pass && !active) {
const every = { P1W: 'week', P1M: 'month', P1Y: 'year' }[pass.period] || 'period';
label.textContent = `Season pass — ${pass.price} / ${every}`;
}
});
// The pass lapsed (not renewed, or refunded): the same handler runs with it gone.
// Nothing to write — the class comes off and the paid content hides itself.No hace falta consultar periódicamente ni tener una fecha de caducidad. Play restaura el conjunto de productos en cada inicio, así que una suscripción caducada simplemente deja de aparecer en ownedIds.
Activa tanto la suscripción como su plan base en Play Console. Una suscripción con un plan base inactivo no devuelve ningún detalle, y el botón mostrará "not available on Google Play yet".
Receta 5 - el puente sin procesar, sin ayudante#
Todo lo anterior se basa en seis métodos y cinco eventos. Si quieres controlar la interfaz por completo, úsalos directamente.
const bridge = window.WebToApk;
const enabled = !!(bridge && bridge.purchase); // false in a browser
// --- what does Google charge for these, in this buyer's currency? ---
const reqId = 'prices-' + Date.now();
window.addEventListener('appmint:products', function onProducts(e) {
if (e.detail.requestId !== reqId) return; // not our answer
window.removeEventListener('appmint:products', onProducts);
for (const p of e.detail.products) {
// {productId, type:'inapp'|'subs', title, description, price, currency, period?, owned}
render(p);
}
});
bridge.getProducts(reqId, JSON.stringify(['pro_unlock', 'season_pass']));
// --- what does this buyer already own? instant, and works offline ---
const owned = JSON.parse(bridge.getOwnedProducts() || '[]');
const isPro = bridge.isOwned('pro_unlock');
// --- buy ---
buyButton.onclick = () => bridge.purchase('pro_unlock');
// --- the ONLY place a purchase is ever granted ---
window.addEventListener('appmint:purchase', e => {
if (e.detail.productId === 'pro_unlock' && e.detail.owned) unlockPro();
});
// --- and the only place failures show up ---
window.addEventListener('appmint:purchase-failed', e => {
toast({
not_found: 'This item is not available on Google Play yet.',
billing_error: 'Google Play could not start the payment. Please try again.',
pending: 'Payment pending — this unlocks automatically once Google confirms.',
disabled: 'In-app purchases are not enabled in this app.'
}[e.detail.reason] || 'Purchase could not be completed. Please try again.');
});
// --- the launch-time restore finished, or a refund took something away ---
window.addEventListener('appmint:owned-changed', () => refreshEverything());appmint:purchase se dispara de nuevo para algo que ya es tuyo, a propósito. Escribe unlockPro() de modo que ejecutarlo dos veces sea inofensivo y nunca tengas que llevar la cuenta de si ya lo ejecutaste.
Receta 6 - degradar con honestidad fuera de la app#
Normalmente la misma página también tiene que abrirse en un navegador normal: durante el desarrollo, o porque también la publicas en la web. Ahí window.WebToApk no existe y no se puede hacer ninguna compra. Dilo, en lugar de mostrar un botón que no hace nada.
const inApp = !!(window.AppMintIAP && window.AppMintIAP.available);
if (!inApp) {
buyButton.disabled = true;
buyButton.textContent = 'Available in the Android app';
// and let people get it:
storeLink.hidden = false;
}Referencia#
Todo lo que la página puede llamar, escuchar o marcar.
| Llamada | Qué hace |
|---|---|
AppMintIAP.available | False en un navegador, o cuando las compras dentro de la app no se activaron en esta compilación. Compruébalo antes de mostrar nada a la venta. |
AppMintIAP.sell(id, button, opts) | Conecta un producto a un botón. opts: label, owned, group, show, hide, labelEl. Llámalo una vez por producto. |
AppMintIAP.start(config) | Conecta varios a la vez: {plans, show, hide, container, toast, hideOtherPlans}. Úsalo cuando quieras tu propia función de aviso, o para mantener visibles los otros planes de un grupo después de comprar uno (hideOtherPlans: false). |
AppMintIAP.onChange(fn) | Se dispara de inmediato con el estado actual y luego en cada cambio. Devuelve una función para cancelar la suscripción. |
AppMintIAP.state() | El mismo objeto, una vez: {available, adFree, owned, ownedIds, products}. |
AppMintIAP.isOwned(id) | True/false, al instante, sin conexión. |
AppMintIAP.owned() | Lista de todos los ids de producto que ya son tuyos. |
AppMintIAP.buy(id) | Abre la hoja de compra de Google. Por sí solo no concede nada. |
AppMintIAP.refresh() | Vuelve a buscar botones data-iap-buy añadidos desde la última pasada. Rara vez hace falta: el runtime vigila el DOM. |
| Llamada | Devuelve |
|---|---|
purchase(productId) | Nada. Abre la hoja; el resultado llega como un evento. |
getProducts(requestId, idsJson) | Nada. La respuesta llega como appmint:products con el mismo requestId. |
getOwnedProducts() | Array JSON de ids de producto que ya son tuyos, como cadena de texto. |
isOwned(productId) | Booleano: un producto de pago único que ya es tuyo, o una suscripción actualmente activa. |
isPremium() | Booleano: específicamente el derecho a "quitar anuncios". |
startRemoveAdsPurchase() | Nada. Abre la hoja para el PRIMER id del campo "Remove Ads" del asistente; tener cualquier id de los listados allí quita los anuncios. El resultado llega como appmint:premium. |
| Evento | detalle | Cuándo |
|---|---|---|
appmint:purchase | {productId, owned} | Google lo confirmó. El único lugar para desbloquear. Se vuelve a disparar para algo que ya es tuyo. |
appmint:purchase-failed | {productId, reason} | La hoja nunca se abrió, el flujo dio error o el pago está pendiente. Nunca se dispara por una simple cancelación. |
appmint:products | {requestId, products[]} | Respuesta a una llamada getProducts(). Los ids que Play no conoce simplemente no aparecen. |
appmint:premium | {premium} | Cambió el derecho a no ver anuncios: compra, restauración al iniciar, reembolso o caducidad. |
appmint:owned-changed | {products[]} | Terminó la restauración al iniciar, o un reembolso quitó algo. Incluye todos los ids que ya son tuyos. |
| Atributo | Activado | Qué hace |
|---|---|---|
data-iap-buy="id" | botón | Hace que compre ese producto. |
data-iap-label="… {price}" | botón | Plantilla de etiqueta. {price} se convierte en el precio local actualizado de Google. |
data-iap-owned="…" | botón | Etiqueta cuando el producto ya es tuyo. |
data-iap-group="name" | botón | Marca los planes como alternativas entre sí. |
data-iap-show="id|group" | cualquier cosa | Se muestra solo cuando eso ya es tuyo. |
data-iap-hide="id|group" | cualquier cosa | Se oculta cuando eso ya es tuyo. |
data-iap-text | dentro de un botón | El elemento cuyo texto recibe la etiqueta, cuando el botón tiene dentro más cosas además de texto. |
data-iap-container | contenedor | Se muestra solo dentro de la app; se oculta en un navegador, donde no hay nada que comprar. Envuelve toda la oferta en él. |
data-iap-state="owned" | lo establece el runtime | En el botón de un producto ya adquirido, para que puedas darle estilo. |
| motivo | Qué pasó realmente |
|---|---|
not_found | Google no conoce este id de producto: no se creó, no se activó, o la app no se instaló desde Play Store. |
billing_error | Play no pudo iniciar el pago. Suele ser transitorio; el comprador debería intentarlo de nuevo. |
pending | Un método de pago lento (efectivo, transferencia bancaria). Se desbloquea solo cuando Google confirma; no lo trates como un fallo. |
disabled | Las compras dentro de la app no se activaron en esta compilación. |
unknown | Cualquier otra cosa. |
Antes de publicar#
- Todos los ids de producto de tu código existen en Play Console y están activados; en una suscripción, también el plan base.
- La compilación lleva los mismos ids y la clave de licencia RSA en Base64 de la configuración de monetización.
- Tu dirección de Gmail está en Setup → License testing, con la respuesta en RESPOND_NORMALLY.
- Subiste el AAB a Internal o Closed testing y lo instalaste desde el enlace de Play Store: un APK instalado por sideload nunca puede comprar nada.
- Compraste cada producto como licence tester y viste "Test instrument, always approves".
- Desinstalaste y reinstalaste, y la compra volvió sola.
- Apagaste la red y el contenido de pago sigue desbloqueado.
- Reembolsaste una compra de prueba en Play Console y viste cómo la app se bloqueaba de nuevo.
Errores que cuestan dinero#
- Desbloquear en el manejador del clic. Cualquiera que abra la hoja y se salga se lleva el producto gratis.
- Guardar el derecho solo en
localStorage. Borrar los datos del sitio quita algo que un cliente pagó; copiar un valor lo regala. - Poner el precio fijo en tu HTML. Será incorrecto en cualquier otro país y quedará desactualizado el día que lo cambies. Usa
{price}oproducts[id].price. - Vender bienes físicos o un servicio del mundo real mediante la facturación de Play. Google exige lo contrario: eso debe usar un procesador de pagos normal. Para eso es la guía de Stripe.
- Probar en un APK instalado por sideload. Las compras dentro de la app solo funcionan cuando la app se instaló desde Play Store. Nada de lo que escribas puede cambiar eso.
- Tratar
pendingcomo un fallo. El comprador pagó con un método lento; se desbloquea solo cuando Google lo confirma.