wandres.dev
MONETIZACIÓN · StoreKit 2

Suscripciones: pruebas, ofertas, renovación y el estado del suscriptor

Una suscripción no es una compra repetida: es una máquina de estados que vive en los servidores de Apple, avanza sin que tu app se entere y expone en cualquier instante una situación que puede ser prueba gratuita, activa, en gracia, en reintento de cobro, cancelada pero vigente o revocada. Esta lección modela ese autómata con precisión, explica la elegibilidad para ofertas introductorias y promocionales, y establece qué debe hacer una app en cada estado sin castigar a quien todavía está pagando.

⏱ 22 min

El error más caro que comete una app con suscripciones no es técnico sino conceptual: tratar la condición de suscriptor como un booleano. Ese booleano no existe. Lo que existe es un estado en una máquina con al menos seis situaciones distinguibles, transiciones que ocurren en servidores ajenos, y una dimensión temporal que convierte la pregunta esta persona paga en la pregunta mucho más matizada esta persona tiene derecho de acceso ahora, aunque su tarjeta haya fallado hace dos días y Apple siga intentando cobrar. Confundir ambas produce el peor resultado posible en la práctica: cortar el acceso a alguien que quiere seguir pagando y cuyo problema es un banco. Modelar bien ese autómata es, en términos de ingresos, más rentable que cualquier optimización de la pantalla de compra.

🎯 Al terminar esta lección sabrás
  • Modelar el estado de renovación completo y decidir qué acceso corresponde a cada situación.
  • Determinar la elegibilidad para ofertas introductorias y presentar el periodo de prueba con honestidad.
  • Distinguir oferta introductoria, promocional, de código y de recuperación, y saber qué exige cada una.
  • Gestionar cancelación, cambio de plan y reintento de cobro sin perder al usuario por el camino.

El autómata del suscriptor

Product.SubscriptionInfo cuelga de cada producto de suscripción y expone tanto la descripción del plan —periodo, precio, ofertas disponibles— como el estado del usuario en el grupo al que pertenece. Ese estado no es un valor suelto sino una colección, porque en un grupo familiar pueden coexistir varias situaciones, y cada elemento combina dos piezas: el estado de renovación y una información de renovación firmada con los detalles.

func situacion(de producto: Product) async -> Situacion {
    guard let suscripcion = producto.subscription,
          let estados = try? await suscripcion.status else { return .sinDatos }

    // En grupos familiares puede haber varias; se elige la mas favorable
    for estado in estados {
        guard case .verified(let renovacion) = estado.renewalInfo else { continue }
        switch estado.state {
        case .subscribed:
            return renovacion.willAutoRenew ? .activa : .activaPeroCancelada
        case .inGracePeriod:
            return .enGracia(hasta: renovacion.gracePeriodExpirationDate)
        case .inBillingRetryPeriod:
            return .reintentandoCobro
        case .expired:
            return .caducada(motivo: renovacion.expirationReason)
        case .revoked:
            return .revocada
        default:
            return .sinDatos
        }
    }
    return .sinDatos
}

Los dos estados que casi todas las integraciones tratan mal son inGracePeriod e inBillingRetryPeriod, y merecen desarrollarse. El periodo de gracia es una opción que se activa en App Store Connect y que concede al usuario acceso continuado mientras Apple resuelve un problema de cobro; durante esa ventana el usuario debe seguir usando la app con normalidad, y lo único correcto es mostrar un aviso discreto invitándole a revisar su método de pago. El reintento de cobro es la fase siguiente: la suscripción ya no está activa, pero Apple continuará intentando el cargo durante semanas y una fracción muy sustancial de esos intentos acaba teniendo éxito. Cortar el acceso ahí es lo correcto contractualmente; hacerlo con un muro agresivo y sin explicación es lo que convierte un fallo de tarjeta en una baja definitiva.

⚠️
Cancelada no significa fuera

Cuando alguien desactiva la renovación automática, el estado sigue siendo subscribed hasta la fecha de expiración: ha pagado el periodo en curso y tiene derecho a agotarlo. La señal está en willAutoRenew, que pasa a falso. Es, además, el mejor momento del ciclo de vida para intervenir: sabes quién se va, sabes cuándo, y todavía es tu usuario. Retirarle el acceso ese mismo día es ilegal en la práctica y suicida en lo comercial.

Pruebas y ofertas

Existen cuatro maneras de ofrecer un precio distinto del habitual y se confunden constantemente porque comparten vocabulario. La oferta introductoria se configura por grupo de suscripción y se aplica automáticamente a quien nunca haya tenido una suscripción en ese grupo; adopta tres formas —prueba gratuita, pago único reducido o varios periodos con descuento— y no requiere firma ni servidor. La oferta promocional se dirige a usuarios que ya son o fueron suscriptores, y su activación exige una firma criptográfica generada con una clave privada que solo debe vivir en tu servidor, lo que la convierte en la única de las cuatro que impone infraestructura. La oferta con código se canjea mediante una URL o un código de un solo uso, útil para prensa, soporte y campañas. Y la oferta de recuperación se dirige específicamente a quien canceló, y el sistema puede presentarla incluso fuera de la app.

func esElegibleParaPrueba(_ producto: Product) async -> Bool {
    guard let suscripcion = producto.subscription else { return false }
    return await suscripcion.isEligibleForIntroOffer
}

Esa consulta no es un adorno: mostrar un botón que promete quince días gratis a alguien que ya los consumió es una de las causas más frecuentes de valoraciones de una estrella, porque el usuario percibe engaño donde solo hubo descuido. La elegibilidad se evalúa por grupo de suscripción y por cuenta de Apple, no por producto ni por dispositivo, lo cual explica el caso que desconcierta a todo el mundo: alguien que probó el plan mensual el año pasado no es elegible para la prueba del plan anual si ambos comparten grupo.

🎁

Introductoria

Automática para quien nunca se suscribió en el grupo. Sin firma ni servidor. Comprueba la elegibilidad antes de prometerla en la interfaz.

🔐

Promocional

Para suscriptores actuales o pasados. Exige firmar la oferta en tu servidor con una clave privada. Es la palanca de retención más precisa que existe.

🎫

Con código

Canjeable por URL o código único. Ideal para prensa, compensaciones de soporte y campañas con socios externos.

🔄

De recuperación

Dirigida a quien canceló. El sistema puede mostrarla fuera de tu app, así que el mensaje debe sostenerse sin tu contexto.

Sobre el diseño de la prueba gratuita conviene decir algo que los datos del sector sostienen con bastante consistencia. Una prueba larga sin tarjeta maximiza las altas y hunde la conversión; una prueba corta con recordatorio antes del cargo convierte mucho mejor y genera menos disputas y reembolsos. Y hay una decisión que casi nadie toma conscientemente y que domina el resultado: qué se le enseña al usuario durante la prueba. Si el producto solo demuestra su valor tras varias sesiones, una prueba de tres días es una promesa que el producto no puede cumplir.

Cambios de plan, cancelación y el resto del ciclo

Subir o bajar de nivel dentro de un mismo grupo es una operación que el sistema resuelve solo, y sus reglas conviene conocerlas porque afectan a lo que tu app debe mostrar. Una mejora de plan es inmediata y se prorratea; una rebaja se aplica al final del periodo en curso, de modo que el usuario conserva el nivel superior hasta entonces y tu comprobación de derechos debe reflejarlo. El campo autoRenewPreference indica a qué producto se renovará la próxima vez, que puede no ser el actual: leerlo es la única forma de saber que alguien ya cambió de plan aunque todavía disfrute del anterior.

stateDiagram-v2
[*] --> Prueba
Prueba --> Activa: primer cobro correcto
Prueba --> Caducada: cancelada antes del cargo
Activa --> ActivaCancelada: desactiva renovacion
ActivaCancelada --> Caducada: llega la fecha de fin
Activa --> Gracia: fallo de cobro con gracia activada
Gracia --> Activa: cobro recuperado
Gracia --> Reintento: se agota la gracia
Reintento --> Activa: cobro recuperado
Reintento --> Caducada: se agotan los intentos
Activa --> Revocada: reembolso o retirada
Caducada --> Activa: vuelve con oferta de recuperacion

La cancelación merece un tratamiento explícito porque la ley y la plataforma coinciden en algo que muchas apps incumplen: cancelar debe ser fácil y el camino debe estar dentro de la app. StoreKit ofrece una hoja de gestión de suscripciones que se presenta desde la propia interfaz y lleva al usuario al panel del sistema sin expulsarlo a los ajustes. Esconder ese acceso no retiene a nadie, produce cancelaciones igualmente y añade una reseña furiosa por el camino.

struct BotonGestion: View {
    @State private var mostrandoGestion = false

    var body: some View {
        Button("Gestionar suscripción") { mostrandoGestion = true }
            .manageSubscriptionsSheet(isPresented: $mostrandoGestion)
    }
}

Queda un asunto que decide más ingresos que ningún otro y que es puramente de producto: qué ocurre con lo que el usuario creó mientras pagaba. Borrarlo al caducar es indefendible; dejarlo accesible en solo lectura es lo correcto y además convierte al ex suscriptor en un candidato natural a volver, porque su trabajo sigue ahí esperándole. Esa decisión debe estar escrita antes de lanzar, porque implementarla después de tener usuarios caducados es mucho más costoso.

Una suscripción es una relación con dimensión temporal, y el software que la modela como un booleano ya ha perdido

Vale la pena detenerse en la profundidad real de este cambio de perspectiva, porque separa a las apps que sostienen ingresos recurrentes de las que los ven evaporarse sin entender por qué. Un pago único es un evento: ocurre, se registra y el sistema puede olvidar el proceso quedándose solo con el resultado. Una suscripción no es un evento sino un proceso continuo con estado, y su propiedad más importante es que ese estado evoluciona en servidores que no controlas, en momentos en los que tu código no se está ejecutando, por causas que no dependen de la voluntad del usuario. Un cargo puede fallar porque una tarjeta caducó, porque el banco marcó la operación como sospechosa o porque el límite mensual se agotó tres días antes de la nómina, y en ninguno de esos casos la persona ha decidido dejar de ser cliente. Un sistema que colapsa toda esa riqueza en un booleano comete el mismo error epistemológico que confundir no he recibido respuesta con me han dicho que no, y las consecuencias son igual de asimétricas: el falso negativo —tratar como no suscriptor a alguien que sí quiere pagar— destruye una relación que estaba funcionando, mientras que el falso positivo apenas cuesta unos días de acceso. Por eso los estados de gracia y reintento existen: son la codificación explícita, por parte de la plataforma, de la incertidumbre inherente a cobrar dinero periódicamente en el mundo real, y una app que los ignora está tirando el trabajo que Apple ya hizo por ella. La lección se generaliza a todo el software que gestiona relaciones sostenidas —licencias corporativas, contratos, membresías, cuotas— y se resume en una regla de diseño que rara vez se enuncia: cuando el estado de una relación puede ser incierto, el modelo debe tener un nombre para esa incertidumbre. Si tu tipo de datos solo puede decir sí o no, tu producto tomará decisiones brutales precisamente en los momentos en los que hacía falta delicadeza.

📝
Lo esencial

El estado del suscriptor tiene al menos seis valores y inGracePeriod mantiene el acceso mientras inBillingRetryPeriod no. Cancelar no expulsa: willAutoRenew en falso con estado activo significa que el periodo pagado sigue vigente. La elegibilidad para la oferta introductoria es por grupo y por cuenta de Apple: consúltala antes de prometer nada. Las promocionales exigen firma en servidor. Y lo creado bajo suscripción debe seguir siendo legible cuando esta caduque.

⚔️ Modelar al suscriptor de verdad
  1. Sustituye tu booleano de acceso por un tipo con los seis estados y comprueba cuántas vistas cambian de comportamiento.
  2. Consulta la elegibilidad para la oferta introductoria y verifica que el texto del botón cambia cuando el usuario ya la consumió.
  3. Activa el periodo de gracia en App Store Connect y define qué mensaje exacto verá el usuario durante esa ventana sin perder acceso.
  4. Implementa la hoja de gestión de suscripciones y mídela: cuenta cuántos usuarios entran y cuántos acaban cancelando de verdad.
  5. Escribe la política de qué ocurre con los datos creados tras la caducidad y aplícala antes de tener un solo usuario caducado.