Durante las pruebas de IAP antes del lanzamiento, nos topamos con un problema típico pero muy confuso:

  • El usuario toca «Continuar compra»
  • El botón cambia a «Procesando»
  • Después de unos segundos vuelve a «Continuar compra»
  • No aparece la ventana de confirmación de compra de App Store

Este artículo documenta todo el recorrido de diagnóstico, los logs clave, la causa raíz y una estrategia de corrección aplicable antes de publicar.

1. Síntoma

La lista de productos en la pantalla de compra cargaba bien, pero al tocar comprar:

  • Se llamaba a purchase()
  • No aparecía ningún diálogo del sistema
  • La UI terminaba regresando al estado de no comprado

A primera vista parecía que «la compra no arrancaba», pero los logs mostraban otra historia.

2. Primera ronda: confirmar si el flujo de compra realmente corre

Primero agregamos logs en todos los puntos críticos del flujo:

  • Parámetros del toque en el botón
  • Entrada de purchaseSelectedProduct() y ramas de guard
  • Antes/después de product.purchase()
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

Resultado:

  • purchase() devolvía .success
  • La verificación de la transacción fue correcta y también se ejecutó transaction.finish()
  • Pero entitledIDs seguía vacío

Esto indica que: no era «la compra no empezó», sino «después de comprar no se reconocieron los permisos».

3. Hallazgos clave al profundizar

Al ampliar los logs, aparecieron señales decisivas:

  1. El conteo de iteraciones de Transaction.currentEntitlements era 0
  2. Transaction.latest(for: monthly) tenía un registro válido, pero vencido
  3. Más importante: antes de comprar había transacciones antiguas en Transaction.unfinished
  4. El purchase() actual devolvía una transacción de una cadena anterior, no de una compra interactiva nueva

Esto explica por qué «no sale ventana»:

  • StoreKit devuelve primero una transacción antigua procesable (unfinished / cadena histórica)
  • Tu código la finaliza con finish()
  • La solicitud parece «exitosa», pero no pasa por el flujo de confirmación nuevo que espera el usuario

4. Resumen de la causa raíz

No fue una sola falla, sino la combinación de dos factores:

  • Transacciones no finalizadas (unfinished transaction) que interfieren en el flujo de compra
  • Cadena de suscripción sandbox en estado vencido (aunque exista latest transaction, puede no ser un entitlement activo)

El comportamiento final fue: sin ventana, éxito aparente y permisos vacíos.

5. Estrategia de corrección (validada en práctica)

5.1 Limpiar transacciones unfinished antes de comprar

Al iniciar la app y antes de comprar, recorre Transaction.unfinished y ejecuta finish() en las transacciones verificadas para evitar que las transacciones antiguas secuestren el flujo nuevo.

5.2 Mantener y reforzar logs de diagnóstico (recomendado antes de publicar)

Conviene mantener estos logs activos hasta el lanzamiento:

  • Snapshot de unfinished transaction
  • subscription status (state, renewal info)
  • Conteo de iteraciones de currentEntitlements
  • Datos de transaction/expiration en latest(for:)

5.3 Doble vía para validar permisos (mejora opcional)

Si en algunos entornos currentEntitlements queda vacío temporalmente, puedes usar latest(for:) como fallback (siempre validando que siga activo).

6. Checklist IAP antes del lanzamiento

  1. Antes de cada prueba, verifica el estado de suscripción de la cuenta Sandbox (si está vencida o no)
  2. Registra el número de unfinished en rutas críticas
  3. Prueba tres escenarios: «primera compra / renovación después de vencer / restaurar compras»
  4. Revisa consistencia entre currentEntitlements y latest
  5. Si aparece «sin ventana», revisa primero si cayó en una ruta de transacción antigua

7. Conclusión

«Tocar comprar sin ventana» no implica necesariamente un problema de UI o de timing de llamada.
En StoreKit 2, el estado de transacciones, la cola de unfinished y el ciclo de vida de la suscripción afectan directamente el resultado final.

Agregar estos logs y la limpieza previa antes de publicar reduce muchos problemas de IAP difíciles de reproducir en producción.