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

  • El usuario pulsa «Continuar compra»
  • El botón pasa a «Procesando»
  • Unos segundos después 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 correctamente, pero al pulsar comprar:

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

A simple vista parecía que «la compra no se iniciaba», pero los logs mostraban otra realidad.

2. Primera ronda: verificar si el flujo de compra realmente se ejecuta

Primero añadimos logs en todos los puntos críticos del flujo:

  • Parámetros del clic 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 era correcta y también se ejecutaba transaction.finish()
  • Pero entitledIDs seguía vacío

Esto indica que: no era «la compra no arrancó», sino «tras la compra no se reconocieron los derechos».

3. Hallazgos clave al profundizar

Al ampliar los logs, aparecieron señales decisivas:

  1. El recuento de iteraciones de Transaction.currentEntitlements era 0
  2. Transaction.latest(for: monthly) tenía un registro válido, pero caducado
  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 antigua, 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 un fallo puntual, 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 caducado (aunque exista latest transaction, puede no ser un entitlement activo)

El comportamiento final fue: sin ventana, éxito aparente y derechos 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)
  • Recuento de iteraciones de currentEntitlements
  • Datos de transaction/expiration en latest(for:)

5.3 Doble vía para validar derechos (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á caducada o no)
  2. Registra el número de unfinished en rutas críticas
  3. Prueba tres escenarios: «primera compra / renovación tras caducar / restaurar compras»
  4. Revisa consistencia entre currentEntitlements y latest
  5. Si aparece «sin ventana», comprueba primero si cayó en una ruta de transacción antigua

7. Conclusión

«Pulsar 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.

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