{"id":908,"date":"2026-07-01T20:45:07","date_gmt":"2026-07-01T18:45:07","guid":{"rendered":"https:\/\/www.linkingfields.com\/?p=908"},"modified":"2026-07-02T18:55:45","modified_gmt":"2026-07-02T16:55:45","slug":"billing-issues","status":"publish","type":"post","link":"https:\/\/www.linkingfields.com\/index.php\/billing-issues\/","title":{"rendered":"Fallos de pago en suscripciones (billing issues) con StoreKit"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"908\" class=\"elementor elementor-908\">\n\t\t\t\t<div class=\"elementor-element elementor-element-c8920db e-flex e-con-boxed e-con e-parent\" data-id=\"c8920db\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-3b47e95 elementor-widget elementor-widget-html\" data-id=\"3b47e95\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div class=\"lf-bf\">\n\n<style>\n.lf-bf{--lf-ink:#ffffff;--lf-mut:#cccccc;--lf-cap:#aaaaaa;--lf-line:rgba(255,255,255,.1);--lf-card:rgba(255,255,255,.04);--lf-accent:#4da6ff;--lf-pink:#f54ea2;--lf-green:#46d08a;--lf-red:#ff6b6b;--lf-amber:#f4b740;--lf-codebg:rgb(40,48,60);font-size:16px;line-height:1.7;color:var(--lf-ink);}\n.lf-bf a{color:var(--lf-accent);text-decoration:none;}\n.lf-bf a:hover{text-decoration:underline;}\n.lf-bf h2{font-size:28px;margin:2.5em 0 .4em;line-height:1.25;color:#fff;font-weight:700;}\n.lf-bf h3{font-size:22px;font-style:italic;margin:1.8em 0 .5em;color:#fff;}\n.lf-bf p{margin:0 0 1em;}\n.lf-bf strong{color:#fff;}\n\/* El color del codigo en linea lo aplica el tema del sitio (no se fuerza aqui, para que coincida con el resto de entradas). *\/\n.lf-bf pre{background:var(--lf-codebg);border:1px solid var(--lf-line);border-radius:8px;padding:1em;overflow:auto;margin:1.5em 0;}\n.lf-bf pre code{background:none;border:none;padding:0;white-space:pre;color:#e8edf3;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;}\n.lf-bf .card{border:1px solid var(--lf-line);border-radius:12px;padding:1.1rem 1.25rem;margin:1.5em 0;background:var(--lf-card);}\n.lf-bf .card-title{font-weight:700;font-size:1.05rem;margin:0 0 .9rem;color:#fff;}\n.lf-bf .grid2{display:flex;flex-wrap:wrap;gap:1rem;}\n.lf-bf .grid2 > *{flex:1 1 260px;}\n.lf-bf .panel{border-radius:10px;padding:1rem 1.1rem;}\n.lf-bf .panel h4{margin:.1rem 0 .5rem;font-size:1rem;color:#fff;display:flex;align-items:center;}\n.lf-bf .panel ul{margin:.3rem 0 0;padding-left:1.1rem;}\n.lf-bf .panel li{margin:.3rem 0;}\n.lf-bf .p-blue{background:rgba(77,166,255,.12);border:1px solid rgba(77,166,255,.3);}\n.lf-bf .p-green{background:rgba(70,208,138,.12);border:1px solid rgba(70,208,138,.3);}\n.lf-bf .p-red{background:rgba(255,107,107,.1);border:1px solid rgba(255,107,107,.3);}\n.lf-bf .p-amber{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.3);}\n.lf-bf .p-grey{background:rgba(255,255,255,.05);border:1px solid var(--lf-line);}\n.lf-bf .big{font-size:1.5rem;font-weight:800;line-height:1.1;margin:.3rem 0;color:#fff;}\n.lf-bf .sub{color:var(--lf-cap);font-size:.85rem;}\n.lf-bf table{width:100%;border-collapse:collapse;margin:.4rem 0;font-size:.93rem;}\n.lf-bf th,.lf-bf td{border:1px solid var(--lf-line);padding:.6rem .75rem;text-align:left;vertical-align:top;}\n.lf-bf td code,.lf-bf th code{white-space:normal;overflow-wrap:anywhere;word-break:break-word;}\n.lf-bf th{background:rgba(255,255,255,.05);font-weight:700;color:#fff;}\n.lf-bf .box{border-radius:8px;padding:1em 1.2em;margin:2em 0;background:rgba(255,255,255,.04);color:var(--lf-mut);font-size:.95em;}\n.lf-bf .box .box-label{margin:0 0 .5em;font-weight:600;display:flex;align-items:center;color:#fff;}\n.lf-bf .box .box-body{margin:0;color:var(--lf-mut);}\n.lf-bf .note{border-left:3px solid #888;}\n.lf-bf .warn{border-left:3px solid var(--lf-pink);}\n.lf-bf .warn .box-label{color:var(--lf-pink);}\n.lf-bf .danger{border-left:3px solid var(--lf-red);}\n.lf-bf .danger .box-label{color:var(--lf-red);}\n.lf-bf .ok{border-left:3px solid var(--lf-green);}\n.lf-bf .ok .box-label{color:var(--lf-green);}\n.lf-bf .focus{border-left:3px solid var(--lf-accent);}\n.lf-bf .focus .box-label{color:var(--lf-accent);}\n.lf-bf .flow{display:flex;flex-direction:column;gap:.6rem;margin:.6rem 0;}\n.lf-bf .step{display:flex;gap:.7rem;align-items:flex-start;border:1px solid var(--lf-line);border-radius:8px;padding:.7rem .9rem;background:rgba(255,255,255,.03);}\n.lf-bf .step.end{border-left:3px solid var(--lf-red);}\n.lf-bf .step.good{border-left:3px solid var(--lf-green);}\n.lf-bf .step .num{flex:0 0 auto;min-width:1.1rem;font-weight:700;color:#fff;font-size:.95rem;}\n.lf-bf .badge{display:inline-block;font-size:.78rem;font-weight:700;padding:.15em .65em;border-radius:999px;}\n.lf-bf .b-green{background:rgba(70,208,138,.16);color:var(--lf-green);}\n.lf-bf .b-red{background:rgba(255,107,107,.16);color:var(--lf-red);}\n.lf-bf .b-amber{background:rgba(244,183,64,.16);color:var(--lf-amber);}\n.lf-bf .b-blue{background:rgba(77,166,255,.16);color:var(--lf-accent);}\n.lf-bf .ic{flex:0 0 auto;margin-right:.4rem;vertical-align:-4px;}\n.lf-bf .statusrow{display:flex;gap:.7rem;align-items:flex-start;padding:.6rem .2rem;border-bottom:1px solid var(--lf-line);}\n.lf-bf .statusrow:last-child{border-bottom:none;}\n.lf-bf .statusrow .tag{flex:0 0 auto;font-family:ui-monospace,Menlo,monospace;font-size:.8rem;font-weight:700;padding:.2em .55em;border-radius:6px;background:rgba(255,255,255,.08);color:#fff;}\n<\/style>\n\n<!-- ====================== INTRO ====================== -->\n<p>Una de las causas m\u00e1s silenciosas de p\u00e9rdida de ingresos en una app de suscripci\u00f3n no es que el cliente cancele, sino que <strong>le falle el cobro<\/strong>: una tarjeta caducada, sin saldo o bloqueada por el banco. Es lo que se llama <em>churn involuntario<\/em>, y el cliente normalmente ni se entera de que ha dejado de pagar.<\/p>\n\n<p>La buena noticia: App Store tiene un sistema autom\u00e1tico para recuperar esos pagos. En esta entrada lo vemos de forma visual y resumida, y <strong>centr\u00e1ndonos en lo que tu app puede leer desde el propio dispositivo con StoreKit<\/strong>: c\u00f3mo detectar el fallo, qu\u00e9 es el <strong>periodo de gracia<\/strong> y, sobre todo, <strong>cu\u00e1ndo debes cortar el acceso y cu\u00e1ndo no<\/strong>.<\/p>\n\n<div class=\"box focus\">\n  <p class=\"box-label\"><svg class=\"ic\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"\/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"\/><\/svg>Enfoque de esta entrada<\/p>\n  <p class=\"box-body\">Toda esta informaci\u00f3n (estado de la suscripci\u00f3n, si est\u00e1 en reintento, cu\u00e1ndo acaba la gracia, etc.) se puede consultar de <strong>dos formas<\/strong>: a nivel de <strong>app<\/strong> (StoreKit, en el dispositivo) y a nivel de <strong>servidor<\/strong> (App Store Server API y notificaciones server-to-server). Aqu\u00ed nos quedamos a <strong>nivel de app<\/strong>, que es lo que necesitas si no tienes infraestructura de servidor. M\u00e1s adelante hay una tabla con la equivalencia entre ambos mundos.<\/p>\n<\/div>\n\n<!-- ====================== INFOGRAFIA 1: EL PROBLEMA ====================== -->\n<h2>1. El problema: churn involuntario<\/h2>\n\n<div class=\"card\">\n  <div class=\"card-title\">Dos formas de perder a un suscriptor<\/div>\n  <div class=\"grid2\">\n    <div class=\"panel p-grey\">\n      <h4>Churn voluntario<\/h4>\n      <div class=\"big\">El cliente decide irse<\/div>\n      <ul>\n        <li>Desactiva la renovaci\u00f3n a prop\u00f3sito.<\/li>\n        <li>En el dispositivo, <code>willAutoRenew<\/code> pasa a <code>false<\/code>.<\/li>\n        <li>Se combate con ofertas de retenci\u00f3n y win-back.<\/li>\n      <\/ul>\n    <\/div>\n    <div class=\"panel p-amber\">\n      <h4>Churn involuntario<\/h4>\n      <div class=\"big\">Falla el cobro<\/div>\n      <ul>\n        <li>Quiere seguir, pero el pago no pasa (tarjeta caducada, sin fondos).<\/li>\n        <li>El estado de la suscripci\u00f3n cambia a reintento o periodo de gracia.<\/li>\n        <li>Se combate con los <strong>reintentos<\/strong> y el <strong>periodo de gracia<\/strong> configurado en App Store Connect.<\/li>\n      <\/ul>\n    <\/div>\n  <\/div>\n  <p class=\"sub\" style=\"margin-top:.8rem\">El churn involuntario es, en buena medida, recuperable de forma autom\u00e1tica: el sistema de reintentos de App Store recupera m\u00e1s del 77% de las renovaciones fallidas a lo largo de 60 d\u00edas, y m\u00e1s del 80% de esas recuperaciones ocurren en los primeros 16 d\u00edas.<\/p>\n<\/div>\n\n<!-- ====================== INFOGRAFIA 2: LAS DOS DEFENSAS ====================== -->\n<h2>2. Las dos defensas de App Store<\/h2>\n\n<p>Cuando un cobro de renovaci\u00f3n falla, App Store <strong>no da por terminada la suscripci\u00f3n de inmediato<\/strong>: pone en marcha hasta dos mecanismos para recuperar el cobro.<\/p>\n\n<div class=\"card\">\n  <div class=\"grid2\">\n    <div class=\"panel p-blue\">\n      <h4><svg class=\"ic\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#4da6ff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-3-6.7\"\/><polyline points=\"21 3 21 9 15 9\"\/><\/svg>Billing Retry (reintento)<\/h4>\n      <p>Autom\u00e1tico y <strong>siempre activo<\/strong>. App Store reintenta el cobro durante un m\u00e1ximo de <strong>60 d\u00edas<\/strong>.<\/p>\n      <ul>\n        <li>No requiere configuraci\u00f3n.<\/li>\n        <li>Mientras dura, el estado es <code>.inBillingRetryPeriod<\/code>.<\/li>\n        <li>Si recupera, vuelve a <code>.subscribed<\/code>; si se agota, pasa a <code>.expired<\/code>.<\/li>\n      <\/ul>\n    <\/div>\n    <div class=\"panel p-green\">\n      <h4><svg class=\"ic\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#46d08a\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"\/><\/svg>Billing Grace Period (periodo de gracia)<\/h4>\n      <p><strong>Opcional<\/strong> (se activa en App Store Connect). Mantiene el acceso al servicio mientras se reintenta el cobro.<\/p>\n      <ul>\n        <li>Mientras dura, el estado es <code>.inGracePeriod<\/code>.<\/li>\n        <li>El cliente sigue usando la app durante el reintento.<\/li>\n        <li>Reduce la fricci\u00f3n y mejora la recuperaci\u00f3n.<\/li>\n      <\/ul>\n    <\/div>\n  <\/div>\n<\/div>\n\n<h3>Cu\u00e1nto dura el periodo de gracia<\/h3>\n<p>Se activa una sola vez en <strong>App Store Connect<\/strong> (Suscripciones, secci\u00f3n <em>Billing Grace Period<\/em>, bot\u00f3n <em>Set Up Billing Grace Period<\/em>) y se aplica a <strong>todas<\/strong> las suscripciones auto-renovables de la app: no se puede configurar por producto. Eliges una duraci\u00f3n de <strong>3, 16 o 28 d\u00edas<\/strong>, y la que se aplica de verdad depende de la duraci\u00f3n de la suscripci\u00f3n:<\/p>\n\n<div class=\"card\">\n  <table>\n    <thead><tr><th>Duraci\u00f3n elegida<\/th><th>Suscripci\u00f3n semanal<\/th><th>Mensual y anual<\/th><\/tr><\/thead>\n    <tbody>\n      <tr><td>3 d\u00edas<\/td><td>3 d\u00edas<\/td><td>3 d\u00edas<\/td><\/tr>\n      <tr><td>16 d\u00edas<\/td><td>6 d\u00edas<\/td><td>16 d\u00edas<\/td><\/tr>\n      <tr><td>28 d\u00edas<\/td><td>6 d\u00edas<\/td><td>28 d\u00edas<\/td><\/tr>\n    <\/tbody>\n  <\/table>\n  <p class=\"sub\" style=\"margin-top:.7rem\">Resumido: <strong>3 o 6 d\u00edas<\/strong> para suscripciones semanales, y <strong>3, 16 o 28 d\u00edas<\/strong> para mensuales o m\u00e1s largas. Las semanales se topan en 6 d\u00edas para que la gracia nunca sea m\u00e1s larga que la propia suscripci\u00f3n.<\/p>\n<\/div>\n\n<p >El periodo gracia <strong>se asigna en el momento del error de cobro<\/strong> y no se puede alterar una vez asignada a un usuario. Adem\u00e1s: tu elecci\u00f3n solo se aplica en producci\u00f3n; en sandbox la duraci\u00f3n la marca la cadencia de renovaci\u00f3n acelerada de la cuenta de prueba.<\/p>\n\n\n<!-- ====================== INFOGRAFIA 3: QUIEN CORTA EL ACCESO ====================== -->\n<h2>3. Una idea clave: App Store no corta tu servicio<\/h2>\n\n<p>Un malentendido habitual: App Store <strong>no desactiva las funciones de tu app<\/strong> cuando falla un pago. Lo que hace es <strong>gestionar el estado<\/strong> de la suscripci\u00f3n (reintentar el cobro, marcarla en gracia, en reintento o expirada) y expon\u00e9rtelo. <strong>Conceder o revocar el acceso es decisi\u00f3n de tu app<\/strong>, leyendo ese estado.<\/p>\n\n<div class=\"card\">\n  <div class=\"grid2\">\n    <div class=\"panel p-grey\">\n      <h4>Lo que hace App Store<\/h4>\n      <ul>\n        <li>Reintenta el cobro.<\/li>\n        <li>Marca el estado (gracia \/ reintento \/ expirada).<\/li>\n        <li>Mantiene o retira el entitlement seg\u00fan ese estado.<\/li>\n      <\/ul>\n    <\/div>\n    <div class=\"panel p-blue\">\n      <h4>Lo que hace tu app<\/h4>\n      <ul>\n        <li>Lee el estado con StoreKit.<\/li>\n        <li>Concede o corta el acceso a tu contenido.<\/li>\n        <li>Avisa al cliente si conviene.<\/li>\n      <\/ul>\n    <\/div>\n  <\/div>\n\n<\/div>\n\n<!-- ====================== INFOGRAFIA 4: LINEA TEMPORAL ====================== -->\n<h2>4. Qu\u00e9 ocurre, paso a paso<\/h2>\n\n<p>Recorrido t\u00edpico de un fallo de pago <strong>con periodo de gracia activado<\/strong>, visto desde el estado de la suscripci\u00f3n en el dispositivo (<code>RenewalState<\/code>):<\/p>\n\n<div class=\"card\">\n  <div class=\"card-title\">De fallo de cobro a recuperaci\u00f3n (o expiraci\u00f3n)<\/div>\n  <div class=\"flow\">\n    <div class=\"step\"><div class=\"num\">1.<\/div><div>Llega la fecha de renovaci\u00f3n y <strong>el cobro falla<\/strong>.<br><span class=\"sub\">Tarjeta caducada, sin saldo, rechazo del banco...<\/span><\/div><\/div>\n    <div class=\"step\"><div class=\"num\">2.<\/div><div>El estado pasa a <code>.inGracePeriod<\/code>.<br><span class=\"sub\">Mantienes el acceso hasta <code>gracePeriodExpirationDate<\/code>. El cliente no nota nada.<\/span><\/div><\/div>\n    <div class=\"step\"><div class=\"num\">3.<\/div><div>App Store <strong>reintenta el cobro<\/strong> en segundo plano durante d\u00edas.<\/div><\/div>\n    <div class=\"step good\"><div class=\"num\">4(a).<\/div><div><strong>\u00c9xito:<\/strong> el estado vuelve a <code>.subscribed<\/code>.<br><span class=\"sub\">Se conserva el ciclo de facturaci\u00f3n. Fin feliz, sin interrupci\u00f3n.<\/span><\/div><\/div>\n    <div class=\"step\"><div class=\"num\">4(b).<\/div><div>Si la gracia termina sin cobro: el estado pasa a <code>.inBillingRetryPeriod<\/code>.<br><span class=\"sub\">Ahora s\u00ed cortas el acceso; el reintento contin\u00faa sin servicio.<\/span><\/div><\/div>\n    <div class=\"step end\"><div class=\"num\">5.<\/div><div>Si se agotan los 60 d\u00edas de reintentos: el estado pasa a <code>.expired<\/code>.<br><span class=\"sub\">Con <code>expirationReason == .billingError<\/code>. Aqu\u00ed encaja una campa\u00f1a de win-back.<\/span><\/div><\/div>\n  <\/div>\n  <div class=\"box note\" style=\"margin:1rem 0 0\">\n    <p class=\"box-body\"><strong>Sin periodo de gracia<\/strong> la l\u00ednea es m\u00e1s corta: del fallo se pasa directamente a <code>.inBillingRetryPeriod<\/code> (cortando el acceso), y de ah\u00ed a <code>.subscribed<\/code> (recuperaci\u00f3n) o a <code>.expired<\/code> (fin).<\/p>\n  <\/div>\n<\/div>\n\n<!-- ====================== INFOGRAFIA 5: EL ESTADO EN EL DISPOSITIVO ====================== -->\n<h2>5. Leerlo desde la app<\/h2>\n\n<p>El punto de partida es <code>Product.SubscriptionInfo.Status<\/code>, que obtienes con <code>product.subscription?.status<\/code>. Cada estado trae tres cosas: el <code>state<\/code> (un <code>RenewalState<\/code>), la <code>transaction<\/code> y la <code>renewalInfo<\/code>.<\/p>\n\n<h3>RenewalState: los cinco estados<\/h3>\n<div class=\"card\">\n  <div class=\"statusrow\"><div class=\"tag\">.subscribed<\/div><div>Al d\u00eda, con acceso.<\/div><\/div>\n  <div class=\"statusrow\"><div class=\"tag\">.expired<\/div><div>Terminada, sin acceso. Mira <code>expirationReason<\/code> para saber por qu\u00e9.<\/div><\/div>\n  <div class=\"statusrow\"><div class=\"tag\" style=\"background:rgba(244,183,64,.22)\">.inBillingRetryPeriod<\/div><div>Reintentando el cobro <strong>sin<\/strong> periodo de gracia. Normalmente sin acceso.<\/div><\/div>\n  <div class=\"statusrow\"><div class=\"tag\" style=\"background:rgba(70,208,138,.22)\">.inGracePeriod<\/div><div>Reintentando el cobro pero <strong>con acceso mantenido<\/strong>.<\/div><\/div>\n  <div class=\"statusrow\"><div class=\"tag\" style=\"background:rgba(255,107,107,.22)\">.revoked<\/div><div>Reembolso o revocaci\u00f3n de Family Sharing.<\/div><\/div>\n<\/div>\n\n<h3>Campos de RenewalInfo a vigilar<\/h3>\n<div class=\"card\">\n  <table>\n    <thead><tr><th>Campo (en el dispositivo)<\/th><th>Para qu\u00e9<\/th><\/tr><\/thead>\n    <tbody>\n      <tr><td><code>isInBillingRetry<\/code><\/td><td>Indica si la suscripci\u00f3n est\u00e1 en reintento de cobro. \u00datil para detectar el riesgo y decidir si avisar al cliente.<\/td><\/tr>\n      <tr><td><code>gracePeriodExpirationDate<\/code><\/td><td>Fecha hasta la que se debe mantener el acceso durante la gracia. Si no es <code>nil<\/code>, est\u00e1s en periodo de gracia.<\/td><\/tr>\n      <tr><td><code>willAutoRenew<\/code><\/td><td>Si la renovaci\u00f3n sigue activada. Ayuda a distinguir churn voluntario (la desactiv\u00f3) de involuntario (fall\u00f3 el pago).<\/td><\/tr>\n      <tr><td><code>expirationReason<\/code><\/td><td>En una suscripci\u00f3n ya expirada, el motivo: <code>.billingError<\/code>, <code>.autoRenewDisabled<\/code>, <code>.didNotConsentToPriceIncrease<\/code>, <code>.productUnavailable<\/code>...<\/td><\/tr>\n    <\/tbody>\n  <\/table>\n<\/div>\n\n<h3>En c\u00f3digo<\/h3>\n<pre><code>guard let statuses = try await product.subscription?.status else { return }\n\nfor status in statuses {\n    guard case .verified(let renewalInfo) = status.renewalInfo else { continue }\n\n    switch status.state {\n    case .subscribed:\n        \/\/ al d\u00eda: concede acceso\n    case .inGracePeriod:\n        \/\/ fall\u00f3 el cobro pero mantienes acceso hasta gracePeriodExpirationDate\n    case .inBillingRetryPeriod:\n         \/\/ reintentando sin gracia: normalmente corta el acceso\n    case .expired:\n        \/\/ terminada; renewalInfo.expirationReason == .billingError, etc.\n    case .revoked:\n        \/\/ reembolso o Family Sharing\n    }\n}<\/code><\/pre>\n<p>Para el control de acceso en s\u00ed, recorre <code>Transaction.currentEntitlements<\/code>: durante <code>.inGracePeriod<\/code> la transacci\u00f3n sigue ah\u00ed, as\u00ed que el acceso contin\u00faa sin que tengas que hacer nada especial.<\/p>\n\n<!-- ====================== INFOGRAFIA 6: APP VS SERVIDOR ====================== -->\n<h2>6. Lo mismo, a nivel de servidor<\/h2>\n\n<p>Por si en el futuro montas infraestructura de servidor, esta es la equivalencia. <strong>No necesitas nada de esto para lo anterior<\/strong>: con la app basta. Solo cambian los nombres y el canal.<\/p>\n\n<div class=\"card\">\n  <table>\n    <thead><tr><th>Qu\u00e9 quieres saber<\/th><th>En el dispositivo (StoreKit)<\/th><th>En el servidor<\/th><\/tr><\/thead>\n    <tbody>\n      <tr><td>Estado de la suscripci\u00f3n<\/td><td><code>RenewalState<\/code> (<code>.inGracePeriod<\/code>...)<\/td><td>campo <code>status<\/code> (<code>4<\/code> = gracia, <code>3<\/code> = reintento)<\/td><\/tr>\n      <tr><td>Est\u00e1 reintentando el cobro<\/td><td><code>isInBillingRetry<\/code><\/td><td><code>isInBillingRetryPeriod<\/code><\/td><\/tr>\n      <tr><td>Hasta cu\u00e1ndo dura la gracia<\/td><td><code>gracePeriodExpirationDate<\/code><\/td><td><code>gracePeriodExpiresDate<\/code><\/td><\/tr>\n      <tr><td>Motivo de expiraci\u00f3n<\/td><td><code>expirationReason<\/code><\/td><td><code>expirationIntent<\/code><\/td><\/tr>\n      <tr><td>El cobro fall\u00f3 o se recuper\u00f3<\/td><td>cambio en <code>RenewalState<\/code><\/td><td>notificaciones <code>DID_FAIL_TO_RENEW<\/code> \/ <code>DID_RECOVER<\/code><\/td><\/tr>\n    <\/tbody>\n  <\/table>\n<\/div>\n\n<!-- ====================== INFOGRAFIA 7: AVISAR AL CLIENTE ====================== -->\n<h2>7. Avisar al cliente sin sacarlo de la app<\/h2>\n\n<p>StoreKit incluye una API de mensajes del sistema, <code>Message<\/code>, que se gestiona <strong>desde el cliente<\/strong>. Cuando hay un problema de facturaci\u00f3n, App Store prepara un mensaje con motivo <code>billingIssue<\/code>; t\u00fa decides cu\u00e1ndo presentar esa hoja del sistema, que invita al cliente a corregir su m\u00e9todo de pago sin abandonar tu app.<\/p>\n\n<pre><code>import StoreKit\n\n\/\/ Escucha los mensajes del App Store (configuralo al arrancar la app)\nfor await message in Message.messages {\n    \/\/ Solo nos interesa el problema de facturacion; el resto se pueden diferir\n    guard message.reason == .billingIssue else { continue }\n\n    \/\/ UIKit: indica que la app esta lista para mostrarlo\n    guard let windowScene = view.window?.windowScene else { continue }\n    try? message.display(in: windowScene)\n\n    \/\/ SwiftUI: presentalo con @Environment(\\.displayStoreKitMessage) (DisplayMessageAction)\n}<\/code><\/pre>\n\n<p><strong>Escuchar no basta:<\/strong> si no llamas a <code>display(in:)<\/code> (UIKit) o a <code>DisplayMessageAction<\/code> (SwiftUI) tras recibir el mensaje, App Store lo <strong>suprime<\/strong>. Ese es justo el mecanismo para diferirlo hasta un momento oportuno. Otros motivos de <code>Message.Reason<\/code> son <code>.priceIncreaseConsent<\/code> (subida de precio), <code>.generic<\/code> y <code>.winBackOffer<\/code>. Y presentarlo no garantiza que se muestre: si el cliente ya resolvi\u00f3 el problema, no aparecer\u00e1 nada.<\/p>\n\n\n<!-- ====================== INFOGRAFIA 8: SANDBOX ====================== -->\n<h2>8. Probarlo en sandbox<\/h2>\n\n<div class=\"box ok\">\n  <p class=\"box-label\"><svg class=\"ic\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"12\" y1=\"16\" x2=\"12\" y2=\"12\"\/><line x1=\"12\" y1=\"8\" x2=\"12.01\" y2=\"8\"\/><\/svg>C\u00f3mo simular un fallo de pago<\/p>\n  <p class=\"box-body\">En el dispositivo de prueba, dentro de los ajustes de la cuenta de Sandbox, desactiva <strong>\"Allow Purchases &amp; Renewals\"<\/strong> para forzar un fallo de renovaci\u00f3n y disparar el reintento. Ver\u00e1s c\u00f3mo el estado de la suscripci\u00f3n cambia a <code>.inGracePeriod<\/code> o <code>.inBillingRetryPeriod<\/code> sin tocar nada de servidor. Recuerda que la duraci\u00f3n del periodo de gracia en sandbox la marca la cadencia de renovaci\u00f3n acelerada de la cuenta, no las duraciones de producci\u00f3n.<\/p>\n<\/div>\n\n<!-- ====================== INFOGRAFIA 9: ERRORES ====================== -->\n<h2>9. Errores que conviene evitar<\/h2>\n\n<div class=\"card\">\n  <div class=\"grid2\">\n    <div class=\"panel p-red\">\n      <h4><svg class=\"ic\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ff6b6b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><line x1=\"15\" y1=\"9\" x2=\"9\" y2=\"15\"\/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"15\"\/><\/svg>No hagas<\/h4>\n      <ul>\n        <li>Cortar el acceso cuando el estado es <code>.inGracePeriod<\/code>.<\/li>\n        <li>Asumir que App Store deshabilita tus funciones por ti.<\/li>\n        <li>Decidir el acceso sin mirar el <code>RenewalState<\/code>.<\/li>\n        <li>Confundir los nombres del cliente (<code>isInBillingRetry<\/code>) con los del servidor (<code>isInBillingRetryPeriod<\/code>).<\/li>\n      <\/ul>\n    <\/div>\n    <div class=\"panel p-green\">\n      <h4><svg class=\"ic\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#46d08a\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"m8.5 12 2.5 2.5 4.5-5\"\/><\/svg>S\u00ed haz<\/h4>\n      <ul>\n        <li>Activar el periodo de gracia salvo que tengas una raz\u00f3n para no hacerlo.<\/li>\n        <li>Conceder acceso con <code>currentEntitlements<\/code> y afinar el mensaje con el <code>RenewalState<\/code>.<\/li>\n        <li>Avisar con la hoja <code>billingIssue<\/code> cuando detectes el problema.<\/li>\n        <li>Gestionar estados desconocidos sin romper (Apple a\u00f1ade nuevos cada a\u00f1o).<\/li>\n        <li>Reservar el win-back para <code>.expired<\/code>, no para el periodo de reintento.<\/li>\n      <\/ul>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- ====================== CIERRE ====================== -->\n<h2>10. Conclusi\u00f3n<\/h2>\n<p>El fallo de pago no es el final de la relaci\u00f3n con el suscriptor: es un bache que App Store intenta resolver por ti. Tu trabajo es no estorbar. Recuerda tres cosas: <strong>App Store gestiona el estado, pero el acceso lo concede tu app<\/strong> leyendo <code>RenewalState<\/code> y <code>currentEntitlements<\/code>; <strong>activa el periodo de gracia<\/strong> para no penalizar al cliente por algo que no decidi\u00f3; y <strong>distingue las rutas<\/strong>, mant\u00e9n durante <code>.inGracePeriod<\/code>, recupera al volver a <code>.subscribed<\/code> y corta solo en reintento sin gracia o en <code>.expired<\/code>. Pru\u00e9balo de principio a fin en el sandbox desactivando \"Allow Purchases &amp; Renewals\", y deja el win-back para cuando la suscripci\u00f3n haya expirado de verdad.<\/p>\n\n<!-- ====================== BIBLIOGRAFIA ====================== -->\n<h3 style=\"margin-top:2.5em; font-size:22px; color:white;\">Bibliograf\u00eda y fuentes consultadas<\/h3>\n<ul style=\"list-style-type: disc; padding-left:1.5em; color:#ccc; font-size:0.95em;\">\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/storekit\/reducing-involuntary-subscriber-churn\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Reducing involuntary subscriber churn<\/a>.\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/storekit\/product\/subscriptioninfo\/renewalstate\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Product.SubscriptionInfo.RenewalState<\/a>.\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/storekit\/product\/subscriptioninfo\/renewalinfo\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Product.SubscriptionInfo.RenewalInfo<\/a> (incluye <code>isInBillingRetry<\/code> y <code>gracePeriodExpirationDate<\/code>).\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/storekit\/message\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">StoreKit Message<\/a> (motivo <code>billingIssue<\/code>).\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer Help.\n    <a href=\"https:\/\/developer.apple.com\/help\/app-store-connect\/manage-subscriptions\/enable-billing-grace-period-for-auto-renewable-subscriptions\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Enable Billing Grace Period for auto-renewable subscriptions<\/a>.\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    WWDC19, sesi\u00f3n 302.\n    <a href=\"https:\/\/developer.apple.com\/videos\/play\/wwdc2019\/302\/\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Subscription Offers and the Billing Grace Period<\/a>.\n  <\/li>\n  <li style=\"margin:0.8em 0 0.3em;color:#aaa;font-size:0.92em;\">A nivel de servidor (no usado en esta entrada, como referencia):<\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/appstoreservernotifications\/app-store-server-notifications-v2\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">App Store Server Notifications V2<\/a> (<code>DID_FAIL_TO_RENEW<\/code>, <code>DID_RECOVER<\/code>, <code>GRACE_PERIOD_EXPIRED<\/code>, <code>EXPIRED<\/code>).\n  <\/li>\n  <li style=\"margin:0.5em 0;\">\n    Apple Developer.\n    <a href=\"https:\/\/developer.apple.com\/documentation\/appstoreserverapi\/get-all-subscription-statuses\" target=\"_blank\" rel=\"noopener noreferrer\" style=\"color:#4da6ff;\">Get All Subscription Statuses (App Store Server API)<\/a>.\n  <\/li>\n<\/ul>\n\n<div style=\"background: rgba(255,255,255,0.04); border-top: 1.8px solid rgba(255,255,255,0.1); padding: 1.2em; width: 100%; margin: 3em 0 0 0; color: #ccc; font-size: 0.95em; font-style: italic; text-align: left;\">\n  Reflexiones de alguien que disfruta investigando y conectando temas; no es asesor\u00eda legal.\n<\/div>\n\n<\/div>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Una de las causas m\u00e1s silenciosas de p\u00e9rdida de ingresos en una app de suscripci\u00f3n no es que el cliente cancele, sino que le falle el cobro: una tarjeta caducada, sin saldo o bloqueada por el banco. Es lo que se llama churn involuntario, y el cliente normalmente ni se entera de que ha dejado [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":920,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[54],"tags":[50,51,41,42],"class_list":["post-908","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-suscripciones-y-monetizacion","tag-apps-moviles","tag-desarrollo-ios","tag-ios","tag-swift"],"_links":{"self":[{"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/posts\/908","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/comments?post=908"}],"version-history":[{"count":18,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/posts\/908\/revisions"}],"predecessor-version":[{"id":927,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/posts\/908\/revisions\/927"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/media\/920"}],"wp:attachment":[{"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/media?parent=908"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/categories?post=908"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linkingfields.com\/index.php\/wp-json\/wp\/v2\/tags?post=908"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}