El ciclo de vida del consentimiento: lo que se envía después de la verificación de edad
El 4 de noviembre de 2025 Apple sumó a App Store Server Notifications un tipo de notificación que se dispara ante una decisión parental y no ante un pago: RESCIND_CONSENT, que indica que “el padre, la madre o el tutor ha retirado el consentimiento para que un menor use la app”.12
Es el único de los 23 tipos del servicio cuyo payload trae un objeto appData en lugar de información de transacción, y appData contiene una app transaction firmada que existe “incluso si un cliente no realiza ninguna compra dentro de la app”.319 Una app que nunca vendió nada adquiere así su primer motivo para levantar un endpoint de notificaciones.
La declaración de redes sociales cubre cómo leer el rango de edad de una persona: el entitlement, los umbrales de edad y qué significan los límites devueltos. Da eso por resuelto y arranca un paso más adelante. La verificación de edad responde qué edad tiene alguien. Las tres API de las que hablo aquí responden quién aprueba, si una aprobación ya concedida sobrevive a un cambio en tu app, y qué pasa cuando un tutor la retira.21012
En resumen
- Tres API se ubican aguas abajo de la verificación de edad, y Apple las entrega como un conjunto: la Significant Change API dentro de PermissionKit,
AppStore.ageRatingCodeen StoreKit y la notificación de servidorRESCIND_CONSENT.45 - Apple asocia su lista de cuatro herramientas a Texas, Utah y Luisiana, no solo a Texas; todas las fechas que dio para esos tres estados ya pasaron, y Apple acota el deber a “ciertas regiones, donde la ley lo exige”.5815
- Lo que decide si ejecutas algo de todo esto es
AgeRangeService.requiredRegulatoryFeatures, nuevo en 26.4, y el flujo que controla exige iOS 26.5 para escribirse completo.736 - Apple se niega a definir qué es un cambio significativo y lo dice cuatro veces, derivándote a tu asesoría legal. El único ejemplo concreto que publica se lo atribuye a la ley: Apple escribe que la ley de Texas considera como tal un cambio en la clasificación por edad de tu app.48
- Tu clasificación puede cambiar sin que envíes un build. Apple retiró el nivel 15+ de Australia y le dio a Vietnam un esquema nuevo de cuatro niveles el 18 de junio de 2026, en ambos casos sin pedirle nada a ningún desarrollador.937
- La revocación es la rama que nadie construye, y la documentación de Apple tampoco la construye demasiado:
RESCIND_CONSENTno aparece en ninguna de las tablas de ciclo de vida de la página que lee un equipo de backend.2
Conceder, volver a conceder, revocar. Apple documenta los dos primeros caminos con un proyecto de ejemplo y una matriz de casos en el entorno de pruebas. El tercero recibe un valor de enum y cuatro campos, que resultó ser más o menos la atención que también recibe en mi propio código.
El calendario que Apple publicó cuatro veces
Cada fecha de más abajo sale de las propias novedades para desarrolladores de Apple, y la secuencia importa más que cualquier entrada suelta.
Apple anunció la SB2420 de Texas el 8 de octubre de 2025, fechada “a partir del 1 de enero de 2026”, y describió su propia respuesta antes que el texto de la ley: las nuevas Apple Accounts de usuarios menores de 18 años pasarían a integrar un grupo de Family Sharing, y “los padres, madres o tutores deberán dar su consentimiento para todas las descargas del App Store, compras de apps y transacciones que el menor realice mediante el sistema de compras dentro de la app de Apple”.13 El 4 de noviembre Apple nombró las herramientas y las entregó en las betas 26.2.4 El 23 de diciembre el plan se frenó: “Una medida cautelar reciente dictada por un tribunal de distrito suspendió la aplicación de la ley estatal SB2420 de Texas … Apple pausará los planes de implementación anunciados previamente”.14 El 3 de junio de 2026 volvió a arrancar: “Debido a un fallo judicial reciente que levanta la medida cautelar sobre la ley SB 2420 de Texas … Estos cambios entrarán en vigor a partir del 4 de junio de 2026”.5
Ocho meses de vaivenes por una sola ley, y las API no se movieron ni una vez. Salieron en 26.2, siguieron disponibles para probarlas en el entorno de pruebas durante toda la medida cautelar y estaban esperando cuando se levantó.14 Quien leyó la pausa de diciembre como permiso para postergar perdió cinco meses de margen.
El resto del mapa llegó el 24 de febrero de 2026 y va más allá de Texas.
| Jurisdicción | Fecha que Apple indica | Qué indica Apple que aplica |
|---|---|---|
| Australia, Brasil, Singapur | 24 de febrero de 2026 | Apple bloquea la descarga de apps clasificadas 18+ “salvo que se haya confirmado que son adultos mediante métodos razonables”15 |
| Utah | 6 de mayo de 2026 | Las categorías de edad se comparten para nuevas Apple Accounts cuando se solicitan15 |
| Texas | 4 de junio de 2026 | Las nuevas Apple Accounts “ahora quedan sujetas a la ley”: consentimiento para descargas, compras dentro de la app y cambios significativos en nombre de menores de 18 años, y los tutores pueden revocarlo5 |
| Luisiana | 1 de julio de 2026 | Las categorías de edad se comparten para nuevas Apple Accounts cuando se solicitan15 |
Texas es el caso atípico por el umbral, no por las herramientas. Apple describe el caso de Texas como consentimiento “en nombre de menores de 18 años” y publica sus categorías como “menos de 13, 13-15, 16-17 o más de 18”, que es exactamente lo que produce la API: “Puedes especificar hasta tres umbrales de edad, que crean hasta cuatro rangos de edad posibles”.4529 Para Utah y Luisiana Apple menciona que las categorías de edad se comparten a pedido, y después afirma que las cuatro herramientas “se ampliaron para ayudar a los desarrolladores a cumplir con las obligaciones de Luisiana y Utah”, nombrando entre ellas a la Significant Change API.15 Las herramientas no son exclusivas de Texas. Si las obligaciones sí lo son, Apple prefiere no decirlo: acota el deber a “ciertas regiones, donde la ley lo exige” y manda la pregunta a la asesoría legal.8
No leas la tabla como una afirmación jurídica ni como una determinación de cumplimiento. Registra lo que Apple anunció y la fecha que le puso. Soy ingeniero, no abogado, y todo lo que sigue se detiene en el límite de la API.
El Q&A que deriva las preguntas de cumplimiento a la asesoría legal trae mejores noticias para la planificación de releases: ante la pregunta de si algo de esto cambia la revisión, Apple responde “No, no hay cambios en el proceso de App Review”.8 Las obligaciones caen en tiempo de ejecución en jurisdicciones concretas, no en el envío.
Así que la división es limpia. Si tu app debe pedir consentimiento en Texas, Utah o Luisiana es una pregunta para un abogado habilitado allí. Contra qué SDK compilar no lo es: Apple afirma que “debes compilar tu app contra los SDK de iOS 26.2 y iPadOS 26.2, o posteriores, con Xcode 26.2 (17C52) o posterior” para siquiera alcanzar los frameworks, y que las cuentas existentes en iOS 18 o anteriores “no se verán afectadas”.8
Apple no te va a decir qué significa “significativo”
El hueco de definición está en el centro de la función, y cuatro superficies de Apple te lo devuelven en vez de llenarlo: la página del símbolo (“Tú determinas qué constituye una actualización significativa según la normativa aplicable”),12 ambas notas de novedades (“es responsabilidad del desarrollador determinar cuándo hay un cambio significativo en su app”),45 y el Q&A, consultado directamente sobre si un cambio de términos o de privacidad cuenta (“Depende. Tú determinas qué constituye una actualización significativa de la app según las leyes aplicables”).8
Apple aporta exactamente un ejemplo trabajado, y viene de la ley antes que de Apple: “La ley estatal de Texas considera que un cambio en la clasificación por edad de una app es un cambio significativo, y los desarrolladores deben mantener actualizadas sus selecciones de clasificación por edad en App Store Connect”.4
Lo que Apple sí define es la cadena de texto que tú escribes. SignificantAppUpdateTopic presenta un buen ejemplo y uno malo lado a lado, franqueza inusual para la página de un símbolo:
// Specific
let topic = SignificantAppUpdateTopic(
description: "This update adds video calling and location sharing features."
)
// Vague
let topic = SignificantAppUpdateTopic(
description: "We've made improvements to the app."
)
La instrucción de Apple alrededor de ese listado es tajante: “Usa un lenguaje conciso y comprensible que explique con claridad qué cambió en tu app. Los padres, madres y tutores ven esta descripción cuando deciden si otorgan el permiso”.12 El texto de relleno de las notas de la versión ahora se le presenta a un padre que decide si su hijo conserva el acceso, lo que lo convierte en la cadena de changelog de mayor riesgo que la mayoría de las apps va a enviar en su vida.
La obligación asociada a esa respuesta no es nada vaga, aunque Apple matice su disparador. Apple te hace “responsable de impedir el acceso a tu app o a sus funciones cuando corresponda”, y después afirma sin rodeos que “hasta que el padre otorgue el consentimiento, debe impedirse que el menor acceda a la actualización significativa, lo que puede incluir todos los datos de la app y de la cuenta o funciones específicas”.8 Ahí “todos los datos de la app y de la cuenta” carga con mucho peso. Apple te deja a ti el radio de impacto y nombra la cuenta entera como cota superior.
La máquina de estados, y dónde hace agua el propio ejemplo de Apple
Apple publicó un proyecto de ejemplo, “Implementing age assurance and permissions”, que es la única descripción de punta a punta del flujo.6 Leerlo como una máquina de estados deja a la vista cuatro ramas y un listado que conviene reescribir.
La primera rama es la barata. AgeRangeService.requiredRegulatoryFeatures devuelve Set<AgeRangeService.RegulatoryFeature>, con tres miembros: declaredAgeRangeRequired, significantAppChangeRequiresParentalConsent y significantAppChangeRequiresAdultNotification.7 El ejemplo de Apple lo consulta primero, y “cuando ninguna de las dos funciones está presente, la app omite el flujo por completo”.6 Apple plantea la propiedad como un reflejo de la “región y la configuración de la cuenta” de la persona, lo que para mí se lee como la promesa de que los usuarios fuera de las jurisdicciones nombradas vuelven sin nada, aunque Apple no garantiza nada de eso.7
Las ramas restantes se separan por edad y por cómo se estableció esa edad:
| Persona | Función regulatoria requerida | Qué hace el ejemplo de Apple |
|---|---|---|
| Menor | significantAppChangeRequiresParentalConsent |
Envía una PermissionQuestion al tutor6 |
| Adulto, método confirmado | significantAppChangeRequiresAdultNotification |
Presenta la hoja de reconocimiento del sistema6 |
| Adulto, sin método confirmado | cualquiera | Bloquea el acceso “hasta que la persona verifique su cuenta en Configuración”6 |
| Compartido rechazado | cualquiera | Interpreta el caso y después no muestra ninguna rama para él6 |
La tercera fila es la que hay que mirar con calma. Un adulto que nunca asoció un método de pago a su Apple Account queda bloqueado por la implementación de referencia de Apple, dentro de un flujo pensado para proteger a menores. Apple no ofrece ninguna rama alternativa ni forma de saltarla.
Al contrastarlo con las declaraciones publicadas, el ruteo queda así. La división entre adulto y menor sigue el ejemplo de Apple más la matriz de casos del entorno de pruebas, donde una cuenta de 18 o más devuelve un lowerBound de 18 sin límite superior y un ageRangeDeclaration de confirmed o selfDeclared. Nota el piso de disponibilidad: leer .confirmed te cuesta iOS 26.5, un release posterior a todo lo demás en el flujo.61136
import DeclaredAgeRange
import PermissionKit
import SwiftUI
@available(iOS 26.5, *)
struct SignificantChangeGate: View {
enum Phase {
case checking
case clear
case awaitingGuardian(PermissionQuestion<SignificantAppUpdateTopic>)
case blocked
}
let changeDescription: String
@Environment(\.requestAgeRange) private var requestAgeRange
@Environment(\.showSignificantUpdateAcknowledgment) private var acknowledge
@State private var phase: Phase = .checking
var body: some View {
switch phase {
case .checking:
ProgressView().task { await resolve() }
case .clear:
ChangedFeatureView()
case .awaitingGuardian(let question):
PermissionButton(question: question) { Text("Ask a parent to approve") }
case .blocked:
AccountVerificationPrompt()
}
}
private func resolve() async {
let features = (try? await AgeRangeService.shared.requiredRegulatoryFeatures) ?? []
guard !features.isEmpty else {
phase = .clear // nothing applies to this person
return
}
guard let response = try? await requestAgeRange(ageGates: 18),
case let .sharing(range) = response else {
phase = .blocked // declined or unavailable: Apple documents no branch
return
}
let isAdult = range.lowerBound == 18
let isConfirmed = range.ageRangeDeclaration == .confirmed
switch (isAdult, isConfirmed) {
case (true, true) where features.contains(.significantAppChangeRequiresAdultNotification):
try? await acknowledge(updateDescription: changeDescription)
phase = .clear
case (true, false):
phase = .blocked // adult with no confirmed method
case (true, true):
phase = .clear
case (false, _):
guard features.contains(.significantAppChangeRequiresParentalConsent) else {
phase = .clear
return
}
let topic = SignificantAppUpdateTopic(description: changeDescription)
phase = .awaitingGuardian(PermissionQuestion(significantAppUpdateTopic: topic))
}
}
}
Enviar la pregunta es la mitad fácil. Recibir la respuesta es donde hace agua el ejemplo de Apple, y el listado es lo bastante corto como para leerlo con lupa:6
for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) {
guard response.choice.answer == .approval else {
return
}
versionManager.handleAllChanges()
}
Un return dentro de for await abandona la secuencia. Basta un rechazo para que la app deje de observar ese topic hasta el siguiente arranque, así que un tutor que toca Rechazar y se arrepiente un minuto después manda una aprobación a un flujo que nadie está leyendo. Escribe continue y registra el rechazo. Leer el listado como un defecto y no como una decisión deliberada es una inferencia mía; de cualquier modo, la corrección cuesta una palabra clave. Diseña el listener como si el arranque en segundo plano no existiera: solo la secuencia obsoleta a la que reemplaza llegó a prometerlo.2021
Pendiente es un estado, y no tiene ningún timeout que tú controles
La parte media del ciclo de vida es donde vive de verdad el trabajo de diseño, y cinco comportamientos documentados le dan forma. Empieza por el que Apple responde a medias: PermissionQuestion.expirationDate es un Optional<Date> tras el cual “la persona que recibe la pregunta ya no puede responder”, y Apple no publica ningún valor por defecto.16
Un menor puede cancelar antes de que el tutor llegue a ver la pregunta: “En cualquier momento del flujo de envío de la solicitud, el menor tiene la opción de cancelarla … En ese caso, el sistema no entrega ninguna respuesta a la app que llamó para esa pregunta en particular”.17 Sin respuesta, sin error, sin callback. Tu estado pendiente queda pendiente para siempre salvo que tú mismo le pongas un timeout.
Preguntarle a un adulto lanza una excepción. El artículo sobre el entorno de pruebas documenta un caso que la página del símbolo deja en blanco: “Llamar a AskCenter.ask(_:) para un usuario adulto lanza este error porque no cumple los requisitos de las solicitudes de permiso parental”.11 AskError.notAvailable es uno de los dos casos del enum sin resumen en su propia página, y es el que dispara un adulto.18 Rutea por edad antes de preguntar, en lugar de atrapar la excepción.
El estado de aprobación va en iCloud, no en el contenedor de la app. El ejemplo de Apple escribe cada cambio reconocido en NSUbiquitousKeyValueStore y observa didChangeExternallyNotification “para que otros dispositivos no vuelvan a presentar el mismo flujo”.6 Un padre que aprueba en el iPhone no debería generar una segunda solicitud en el iPad, y PermissionKit no hace nada de eso por ti.
El mejor detalle del ejemplo resuelve un problema que la mayoría de los equipos enviaría mal. Las instalaciones nuevas no deberían consentir un cambio que nunca vivieron, así que Apple lee AppTransaction.originalAppVersion y “marca automáticamente como atendidos todos los cambios que la app introdujo en esa versión o antes”.619 El seguimiento del consentimiento es, entonces, por cambio y por persona: identificadores de cambio con versiones de introducción, no un booleano.
Tu clasificación puede cambiar sin un build
AppStore.ageRatingCode es una static var que devuelve Int? de forma asíncrona, disponible desde 26.2 en iOS, iPadOS, macOS, tvOS, visionOS y watchOS.10 El uso que Apple declara es la comparación antes que la interpretación: “Usa esta propiedad para obtener la clasificación por edad de tu app y compararla con la última clasificación conocida para ver si cambió”.10 Comparar es todo lo que obtienes, porque Apple no publica en ninguna parte de la documentación una correspondencia entre el entero y el nivel de clasificación. No puedes preguntar si eres 13+, solo si eres lo que eras la vez pasada, así que el contrato real es que tú persistes el valor anterior y te haces cargo de la primera ejecución.
Por qué una app querría vigilar su propia clasificación se vuelve evidente en cuanto notas que las clasificaciones se mueven sin releases. El 21 de mayo de 2026 Apple anunció que a partir del 18 de junio “la clasificación por edad 15+ dejará de estar disponible en el App Store en Australia”, y le dio a Vietnam un esquema regional de cuatro niveles derivado de las respuestas ya existentes del cuestionario.9 Ambos cambios aterrizaron: la tabla de Australia en la Ayuda de App Store Connect ahora publica 16+ y R 18+ sin fila 15+, y Vietnam tiene tabla propia.37 Ninguno de los dos pidió un envío, un build ni acción alguna del desarrollador, y Apple escribe que la ley de Texas considera un cambio de clasificación como un cambio significativo.4
Contrasta con un cambio de clasificación que inicias tú: “Cuando un desarrollador actualiza la clasificación por edad de su app, la clasificación se actualiza en todos los dispositivos de los usuarios una vez que la versión está publicada”.4 Tus propios cambios viajan en un release que puedes instrumentar. Los de Apple no, y ese es justamente el hueco que llena la propiedad.
El problema de las pruebas es peor que un hueco. Una respuesta del personal de Apple en los Developer Forums lo dice sin rodeos: “Un valor de 0 es lo esperado durante el desarrollo de tu app cuando se compila y ejecuta desde Xcode y desde entornos de pruebas (incluido TestFlight)”.22 Cero no es nil, así que el propio ejemplo documentado de Apple pasa su guard let y devuelve 0 como clasificación válida, que es exactamente lo que vio en un dispositivo físico quien abrió ese hilo.1022
Sigue el hilo hasta el final. Persiste 0 como tu línea base y el primer arranque desde el App Store lee un código real, detecta un cambio y le pide a un padre que vuelva a consentir absolutamente nada. Tratar el 0 como un valor centinela que jamás escribes en la línea base es una inferencia mía a partir de esas dos afirmaciones de Apple, y es la única línea de código defensivo que no enviaría sin escribir.
Los metadatos de disponibilidad de Apple traen otro hallazgo que ninguna página en prosa menciona. Las filas de plataformas de estas cuatro capacidades dejan agujeros en lugares distintos:
| Capacidad | iOS / iPadOS | macOS | Mac Catalyst | visionOS | tvOS / watchOS |
|---|---|---|---|---|---|
AppStore.ageRatingCode10 |
26.2 | 26.2 | sin fila | 26.2 | 26.2 |
requiredRegulatoryFeatures7 |
26.4 | 26.4 | 26.4 | sin fila | sin fila |
SignificantAppUpdateTopic, PermissionButton1226 |
26.2 | 26.2 | 26.2 | 26.2 | sin fila |
showSignificantUpdateAcknowledgment27 |
26.4 | sin fila | 26.4 | sin fila | sin fila |
De ahí caen dos asimetrías. Una app nativa de macOS puede enterarse de que significantAppChangeRequiresAdultNotification aplica a una persona y no tener ninguna API con la cual satisfacerlo: showSignificantUpdateAcknowledgment(in:updateDescription:) recibe un UIWindowScene y no publica fila de macOS, y el SignificantUpdateAction de SwiftUI se detiene en las mismas tres plataformas.27 Apple sí proveyó una variante con NSWindow para la llamada vecina requestAgeRange, así que la omisión se lee como un hueco y no como una política; esa lectura es mía.28
Segundo, la detección llega más lejos que la solución. ageRatingCode publica filas de tvOS y watchOS; nada de lo que pide consentimiento lo hace.10 Esos destinos pueden observar un cambio de clasificación sin ninguna forma documentada de responder, y Return se distribuye en ambos: su versión 1.0.1 de tvOS está en READY_FOR_DISTRIBUTION en App Store Connect, y la app de iOS distribuida incorpora una app para el reloj.30 Ambas lecturas suponen que los metadatos de Apple están completos, algo que esos mismos metadatos socavan: ageRatingCode no publica fila de Mac Catalyst mientras que todos los símbolos vecinos sí lo hacen.10
Para qué sirve un servidor una vez que Apple bloquea el arranque
Apple enuncia el comportamiento de la revocación en una frase: “Cuando un padre, madre o tutor revoca el consentimiento para que su hijo acceda a una app, Apple impedirá que la app se abra. Para gestionar las revocaciones de consentimiento, usa el valor RESCIND_CONSENT de notificationType”.8
Fíjate en el orden de esas cláusulas. El sistema ya bloquea el acceso, así que RESCIND_CONSENT no es un punto de enganche para aplicar la restricción, y construirlo como tal lo desperdicia. Es la única señal que recibes sobre un usuario en cuyo dispositivo tu código ya no puede ejecutarse. Apple no dice nada sobre cómo archivar ese evento, así que el análogo más cercano que encontré es una solicitud de eliminación de cuenta: suscripciones por conciliar, estado por congelar y un hogar que puede reaparecer.
El payload es inusualmente delgado. appData trae cuatro campos: appAppleId, bundleId, environment y signedAppTransactionInfo.3 Ni transacción, ni suscripción, ni información de renovación, ni ningún identificador de cuenta tuyo. El vínculo con una persona pasa por la app transaction firmada, cuyo appTransactionID genera el App Store “para cada Apple Account que descarga tu app y para cada miembro del grupo familiar en apps compatibles con Family Sharing”.19 O sea que una app gratuita sí tiene una clave duradera por cuenta para cruzar, y una app que nunca guardó ninguna recibe una notificación que no puede atribuir a nadie.
El transporte no depara sorpresas: una URL de versión 2 por entorno en App Store Connect sobre TLS 1.2 o posterior, 17.0.0.0/8 en la lista de permitidos, 200 a 206 para éxito, y 40x o 50x para comprar cinco reintentos a 1, 12, 24, 48 y 72 horas, solo en producción.2324
RESCIND_CONSENT tampoco trae subtipo. Cada uno de los 19 valores de subtipo publicados se acota a un tipo de notificación con nombre, y la cadena RESCIND_CONSENT no aparece en ningún lado de esa página.25 Más revelador todavía: la página notificationType de Apple mapea 40 eventos en ocho tablas bajo “Handle use cases for In-App Purchase life-cycle events”, y RESCIND_CONSENT no figura en ninguna; el valor existe en la lista de valores posibles y en ningún otro lugar de la página.2 Apple documentó la notificación dentro del material de verificación de edad y nunca la conectó a la referencia que un equipo de backend lee de verdad.
Lo que mis propios proyectos ya hacen mal
Busqué en mi propio código antes de escribir sobre el de otros. La regla de selección era mecánica, y la regla es justamente donde estaba el error: cada fila de la tabla de Proyectos Activos en mi propio CLAUDE.md que tiene un proyecto de Xcode detrás, lo que da siete proyectos y 508 archivos entre todos los archivos Swift, entitlements, property lists y project.pbxproj.31 Cero coincidencias para PermissionKit, AskCenter, SignificantAppUpdateTopic, FamilyControls, ManagedSettings, DeviceActivity, CKShare, sharedCloudDatabase o ageRatingCode. Cinco de los siete tienen ficha en App Store Connect. Water y Yawara no tienen ninguna y nunca se distribuyeron, así que lee esos dos como código y no como apps en manos de nadie.31 Ace Citizenship parecía el hogar más probable para usuarios menores y es el menos probable: su sitio complementario declara la elegibilidad para la N-400 como “18 años o más”, y la app no pide fecha de nacimiento en ninguna parte.31
Después corrí los mismos patrones sobre todos los repositorios bajo ~/Projects, que es el escaneo que debí haber hecho primero. Los mismos patrones coinciden con 21 archivos, siete de ellos código, y seis de esos siete están en un solo proyecto de iOS que nunca llegó a tener fila en el registro.32
Randori es una bitácora de entrenamiento de jiu-jitsu, y tiene exactamente la superficie de consentimiento que la lista de patrones existe para atrapar: 20 coincidencias de CKShare y sharedCloudDatabase repartidas entre ConnectionStore.swift, RandoriApp.swift, CKPostTransport.swift, ProfileCardView.swift, CloudShareSheet.swift y SocialContracts.swift, donde la aceptación pasa por userDidAcceptCloudKitShareWith cuando la app ya está corriendo y por connectionOptions.cloudKitShareMetadata en arranque en frío.32 Randori además trae su propia primitiva de consentimiento para llevar el nombre de otra persona: TagConsentStore falla en modo cerrado, así que un atleta cuyo cliente no publicó una política de etiquetado no puede ser etiquetado en absoluto.32
O sea que el único proyecto con una superficie de consentimiento real escribió su propio registro y no adoptó ninguno de los de Apple. Debe tres cosas que no construí. Activar las superficies persona a persona que sus contratos mantienen dormidas es el caso de manual para SignificantAppUpdateTopic. ageRatingCode no tiene línea base almacenada, y una 1.0 sin publicar solo ha corrido desde Xcode, el entorno de pruebas o TestFlight, que es precisamente donde Apple dice que la propiedad devuelve 0.22 La revocación no tiene dónde aterrizar: Randori no vende nada y no corre ningún servidor propio.32
El hallazgo más interesante es que el consentimiento del tutor ya se distribuye en tres de los siete proyectos originales, bajo otro nombre más viejo.
Ask to Buy es el mismo mecanismo con la misma forma, y Apple lo describe casi con las mismas palabras: “con Ask to Buy, cuando un menor quiere hacer una compra o descarga elegible, el sistema envía la solicitud de compra al padre, madre o tutor”.34 Aparece como Product.PurchaseResult.pending, y la aprobación llega por Transaction.updates y no en el punto de llamada, porque esa secuencia transporta “transacciones que ocurren fuera de la app, como las transacciones de Ask to Buy”.35 Un rechazo no entrega nada: “Tu app no recibe ninguna transacción porque rechazaste Ask to Buy”.34
Tres de los siete venden algo, y cada uno maneja el estado pendiente de forma distinta:33
| App | Producto | Manejo de .pending |
|---|---|---|
| ResumeGeni | Suscripción mensual | Un resultado .pending con nombre propio y una vía de conciliación documentada |
| Reps | Suscripción, dos niveles | case .userCancelled, .pending: colapsados en una sola rama |
| Ace Citizenship | No consumible | Un case .pending: aparte que devuelve false, idéntico a cancelar |
Dos de tres reportan a un tutor deliberando como si fuera un usuario rechazando, y lo que ve el usuario es peor que una etiqueta equivocada. Reps cierra su paywall solo cuando purchase devuelve true; Ace avanza más allá de su paywall solo cuando su propia llamada devuelve éxito. Con .pending ninguna de las dos llamadas devuelve algo accionable, así que ambos paywalls quedan abiertos sin error, sin toast y sin spinner: un toque muerto.33 El padre aprueba minutos después, y la transacción aterriza en un listener cuya interfaz nunca reconoció que hubo una solicitud.
La rama colapsada es exactamente la falla que PermissionKit invita a cometer con más en juego, y yo la envié dos veces antes de que Apple le diera al patrón una segunda API. Pendiente no es una rama exótica. Pendiente es lo que se ve de un flujo de consentimiento desde adentro de una app, y el valor por defecto correcto es un estado que renderizas, no un valor que devuelves.
Una app ya está aguas abajo de la mitad de servidor, lo que vuelve concreta la rama faltante. El endpoint de versión 2 de ResumeGeni registró al menos 56 notificaciones desde el 26 de junio de 2026, 55 del entorno de pruebas y una de producción, que es como sé que ambas URLs de entorno están registradas y no solo una. Su manejador nombra 13 tipos de notificación y despacha ocho; los otros cinco aparecen solo en comentarios. RESCIND_CONSENT no está en ninguno de los dos grupos, ni en ningún otro lugar del repositorio.33
Preguntas frecuentes
¿Cuál API le pide consentimiento a un tutor y cuál se lo pide a un adulto?
Son frameworks distintos, y la división es fácil de invertir. El consentimiento parental pasa por PermissionKit: un SignificantAppUpdateTopic envuelto en PermissionQuestion(significantAppUpdateTopic:), enviado con PermissionButton y respondido a través de AskCenter.shared.responses(for:).12162126 El reconocimiento del adulto pasa en cambio por Declared Age Range, como showSignificantUpdateAcknowledgment(in:updateDescription:).27 Consulta requiredRegulatoryFeatures primero, porque preguntarle a un adulto vía PermissionKit lanza AskError.notAvailable.711
¿Cómo pruebo la revocación de consentimiento sin una cuenta familiar real?
Activa el modo de desarrollador, luego abre Configuración, Desarrollador, Cuenta de Apple del entorno de pruebas, inicia sesión, selecciona la cuenta, toca Gestionar y elige Revocar consentimiento de la app. Ingresa tu identificador de bundle y toca Revocar consentimiento; el sistema muestra “Notification Triggered”.11 Con una URL de versión 2 configurada, tu servidor recibe RESCIND_CONSENT con un objeto appData cuyos campos bundleId y environment confirman que la notificación corresponde a la app correcta.311 El entorno de pruebas envía cada notificación una sola vez y sin reintentos, así que un endpoint que devuelve un 50x a mitad de la prueba no tiene segunda oportunidad.24
¿Por qué una app vigilaría su propia clasificación por edad en tiempo de ejecución?
Porque Apple cambia clasificaciones en la tienda sin ningún envío de tu parte, y Apple escribe que la ley de Texas considera un cambio de clasificación como un cambio significativo, y después te deriva a la Significant Change API para solicitar el consentimiento parental.4 El 18 de junio de 2026 es el ejemplo trabajado: Australia perdió su nivel 15+ y Vietnam ganó un esquema de cuatro niveles, ambos aplicados a apps existentes a partir de respuestas de cuestionario ya existentes.937 Apple no publica ninguna correspondencia entre el entero de la propiedad y un nivel de clasificación, así que comparar contra un valor que guardaste es la única operación que admite.10
¿RESCIND_CONSENT bloquea al usuario por mí?
El sistema ya lo hace, que es lo que la mayoría de las implementaciones pasa por alto. Apple afirma que cuando un tutor revoca el consentimiento, “Apple impedirá que la app se abra”, y después te dirige a la notificación para gestionar el evento, no para aplicarlo.8 Así que el trabajo que queda tiene forma de servidor: congelar el estado de la cuenta, conciliar cualquier suscripción y dejar de enviar notificaciones push a un dispositivo que no puede abrir tu app. Trátalo como tratas una eliminación de cuenta, no como una verificación de autorización fallida.
Conclusiones clave
Para desarrolladores de iOS:
- Audita hoy mismo tu switch de Product.PurchaseResult. Ask to Buy es consentimiento del tutor que ya está en producción, .pending es cómo aparece, y colapsar ese caso dentro de .userCancelled es el defecto que PermissionKit va a invitar a repetir con más en juego.3435
- Presupuesta iOS 26.5, no 26.4, si tu flujo distingue un adulto confirmado de uno autodeclarado.36
- Corrige el return del listener de ejemplo de Apple antes de copiarlo, y nunca dejes que un 0 llegue a tu línea base de ageRatingCode.622
Para equipos que publican en varias plataformas de Apple:
- Revisa las filas de disponibilidad antes de planificar el trabajo. Una app nativa de macOS puede detectar que corresponde el reconocimiento de adulto y no tener ninguna API para presentarlo; tvOS y watchOS pueden leer ageRatingCode sin ninguna API de consentimiento detrás.71027
- Guarda el reconocimiento en NSUbiquitousKeyValueStore con clave por cambio, y usa AppTransaction.originalAppVersion para eximir a las instalaciones nuevas de cambios anteriores a ellas.619
Para responsables de backend y de releases:
- Registra el appTransactionID por cuenta antes de necesitarlo, y después levanta el endpoint de versión 2 incluso para una app gratuita. Un endpoint construido después de los hechos recibe eventos de revocación que no puede atribuir a nadie.319
- Haz pasar la descripción del cambio significativo por quien sea dueño del texto de producto. Es la única cadena que lee un tutor mientras decide si tu app conserva a un usuario.12
Tres de los cuatro puntos de aplicación de este ciclo se disparan por algo que tú controlas: la clave de pantalla de inicio en un SDK, la macro @State en una cadena de herramientas y la declaración de redes sociales en un envío. El consentimiento del tutor se dispara por un expediente judicial y una tabla de clasificaciones de la tienda. El hub completo de la serie es la Serie del ecosistema Apple.
Referencias
-
Apple, App Store Server Notifications changelog. Bajo el encabezado del 4 de noviembre de 2025, New features: “Updated the
responseBodyV2DecodedPayloadto include the new payload object,appData” y “Added the notification typeRESCIND_CONSENTtonotificationType”. Las dos entradas siguientes del changelog son del 10 de diciembre de 2025 y del 27 de abril de 2026, y ninguna toca el consentimiento. Leído desde el JSON de documentación de Apple el 26 de julio de 2026, dado que la página de HTML se renderiza mediante JavaScript. ↩ -
Apple, notificationType, App Store Server Notifications. Fuente de la definición de
RESCIND_CONSENT(“A notification type that indicates the parent or guardian has withdrawn consent for a child’s app usage”) y del recuento usado aquí: la página publica 23 valores posibles (CONSUMPTION_REQUEST, DID_CHANGE_RENEWAL_PREF, DID_CHANGE_RENEWAL_STATUS, DID_FAIL_TO_RENEW, DID_RENEW, EXPIRED, EXTERNAL_PURCHASE_TOKEN, GRACE_PERIOD_EXPIRED, METADATA_UPDATE, MIGRATION, OFFER_REDEEMED, ONE_TIME_CHARGE, PRICE_CHANGE, PRICE_INCREASE, REFUND, REFUND_DECLINED, REFUND_REVERSED, RENEWAL_EXTENDED, RENEWAL_EXTENSION, RESCIND_CONSENT, REVOKE, SUBSCRIBED, TEST). La cadenaRESCIND_CONSENTaparece exactamente una vez en el contenido de la página, dentro de la lista de valores posibles; las ocho tablas bajo “Handle use cases for In-App Purchase life-cycle events” suman 40 filas de eventos entre todas (4, 6, 7, 7, 8, 6, 6 y 4 filas contando cada encabezado) y ninguna la nombra. Nota queREVOKEes una pérdida de derechos por Family Sharing, no un retiro de consentimiento: “an In-App Purchase the customer was entitled to through Family Sharing is no longer available through sharing”. Leído desde el JSON de documentación de Apple el 26 de julio de 2026. ↩↩↩↩ -
Apple, appData, App Store Server Notifications, introducido en la versión 2.19. Fuente de “The
appDataobject is part of theresponseBodyV2DecodedPayload. This object is present in the payload when thenotificationTypeisRESCIND_CONSENT” y de las cuatro propiedades:appAppleId(“available for apps that users download from the App Store. It isn’t present in the sandbox environment”),bundleId,environmentysignedAppTransactionInfo(unJWSAppTransaction). La página responseBodyV2DecodedPayload enuncia la exclusividad de forma independiente, describiendoappDatacomo un campo que “appears when thenotificationTypeisRESCIND_CONSENT” y agregando que “Thedata,appData,summary, andexternalPurchaseTokenfields are mutually exclusive. The payload contains only one of these fields”. Esas dos páginas son la base para llamar aRESCIND_CONSENTel único tipo de notificación cuyo payload traeappData; la cadenaappDatano aparece en ninguna parte de la páginanotificationType. ↩↩↩↩ -
Apple, Next steps for apps distributed in Texas, Apple Developer News, 4 de noviembre de 2025. Fuente de las categorías de edad de Texas (“under 13, 13-15, 16-17, or over 18”), del nombre del framework que usa Apple (“the Significant Change API under the PermissionKit framework”), de la frase sobre responsabilidad del desarrollador (“It’s the developer’s responsibility to determine when there’s a significant change to their app”), del ejemplo de la clasificación por edad (“Texas state law considers a change in the age rating of an app to be a significant change, and developers should keep their age rating selections current in App Store Connect. When a developer updates their app’s age rating, the rating is updated on all user devices once the version is live”), del planteo sobre StoreKit (“Developers can use a new property type in StoreKit to automatically check when their app’s age rating has changed on a user’s device and then use the Significant Change API to request parental consent”), del comportamiento de revocación (“A parent or guardian in Texas can withdraw consent for any app, which will block launching of the app on the child or teen’s device”) y de la lista de cuatro puntos de “Next steps”. También es la fuente para fechar la disponibilidad en beta en iOS 26.2 y iPadOS 26.2. Consultado el 26 de julio de 2026. ↩↩↩↩↩↩↩↩↩
-
Apple, Update for Apps Distributed in Texas, Apple Developer News, 3 de junio de 2026. Fuente del levantamiento de la medida cautelar (“Due to a recent court ruling lifting an injunction on Texas law SB 2420, new Apple Accounts in Texas are now subject to the law”), del alcance (“age assurance and parent or guardian consent on behalf of minors under the age of 18 for downloads, Apple In-App Purchases, and significant changes associated with an app. Parents or guardians will also be able to revoke their consent for any app they previously approved for their child”), de la fecha de vigencia (“These changes will go into effect starting June 4, 2026”), del recordatorio repetido sobre la responsabilidad del desarrollador y de la misma lista de cuatro puntos de implementación. Consultado el 26 de julio de 2026. ↩↩↩↩↩↩
-
Apple, Implementing age assurance and permissions, código de ejemplo de Declared Age Range. Disponibilidad: iOS 26.5, iPadOS 26.5, Mac Catalyst 26.5, Xcode 27.0 beta; el artículo indica iniciar sesión en iCloud “on a device with iOS 26.4 or later” antes de ejecutarlo. Fuente del comportamiento con conjunto vacío (“When neither feature is present, the app skips the flow entirely”), del umbral de edad en 18, de las cuatro categorías interpretadas (
.minor,.verifiedAdultdescrita como “an adult with a confirmed payment method”,.unverifiedAdultcomo “an adult without account verification”,.declinedSharing), del desenlace para el adulto sin verificar (“the app sets the phase to.blockedand prevents access until the person verifies their account in Settings”), del camino del menor que construye unSignificantAppUpdateTopicy unaPermissionQuestion, del envío conPermissionButton, del listado del listenerAskCenter.shared.responses(for:)citado textualmente aquí, del comportamiento ante el rechazo (“When the parent denies the request, the app prevents the minor from using it”), del seguimiento de reconocimientos conNSUbiquitousKeyValueStoreydidChangeExternallyNotification(“so other devices don’t present the same flow again”), y de la exención pororiginalAppVersion(“People who install the app when a significant change is already present don’t need to acknowledge it”). El proyecto además requiere la capacidad Declared Age Range y el servicio de almacenamiento clave-valor de iCloud. Leído desde el JSON de documentación de Apple el 26 de julio de 2026. ↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, AgeRangeService.requiredRegulatoryFeatures y AgeRangeService.RegulatoryFeature, Declared Age Range. Ambos disponibles desde iOS 26.4, iPadOS 26.4, Mac Catalyst 26.4 y macOS 26.4, sin fila de visionOS, tvOS ni watchOS. Declaración:
var requiredRegulatoryFeatures: Set<AgeRangeService.RegulatoryFeature> { get async throws }, que lanzanotAvailable“if the regulatory feature’s service is unavailable”. El enum publica exactamente tres casos:declaredAgeRangeRequired(“Indicates the person is required to share their age range with your app”),significantAppChangeRequiresAdultNotification(“Indicates that adult users must acknowledge your app’s significant change”) ysignificantAppChangeRequiresParentalConsent(“Indicates a parent or guardian is required to acknowledge and consent to a significant app change”). ↩↩↩↩↩↩ -
Apple, Age assurance frameworks Q&A, Apple Developer Support. Fuente del piso de SDK (“you must build your app against the iOS 26.2 and iPadOS 26.2 SDKs, or later, with Xcode 26.2 (17C52) or later”), de la excepción para cuentas heredadas (“Existing Apple Accounts running iOS 18 and iPadOS 18, or earlier … won’t be affected”, frase cuya cláusula omitida especifica “including adult and child accounts for kids and teens”), de la respuesta sobre responsabilidad (“Yes, developers are responsible for their own age restrictions” y “For questions about your compliance obligations, consult your legal counsel”), del alcance regional citado dos veces en este artículo (“In certain regions, where legally required, Apple uses age assurance methods to confirm an Apple Account holder’s age and shares age categories with you through the Declared Age Range API. In those regions, you must check the age of the people using your app”, reformulado más adelante como “In regions where legally required, you need to check the age of the people using your app with the Declared Age Range API”), de la obligación de acceso (“For significant app updates, you’re responsible for preventing access to your app or features when required, and for handling the response from the parent or guardian. Until the parent provides consent, the child must be prevented from accessing the significant update, which may include all app and account data or specific features”), del comportamiento de revocación (“When a parent or guardian revokes consent for their child to access an app, Apple will prevent the app from launching. To handle consent revocations, use the
RESCIND_CONSENTvalue fromnotificationType”), de la respuesta sobre App Review (“No, there are no changes to the App Review process”) y de la respuesta sobre términos y privacidad (“It depends. You determine what constitutes a significant app update based on applicable laws”). Leído el 26 de julio de 2026. ↩↩↩↩↩↩↩↩↩ -
Apple, Upcoming changes to age ratings in Australia and Vietnam, Apple Developer News, 21 de mayo de 2026. Fuente de “Starting June 18, 2026, age ratings on the App Store will be updated in Australia and Vietnam”, del cambio australiano (“The 15+ age rating will no longer be available on the App Store in Australia. Apps currently rated 15+ with the following content descriptors will be updated to 16+”) y sus tres descriptores (acceso web sin restricciones; información médica o de tratamientos frecuente; loot boxes), y del cambio vietnamita (“To align with Article 38 of Vietnam Decree 147, apps available on the App Store in Vietnam will require a region-specific age rating. Based on your age rating questionnaire responses in App Store Connect, your app will receive one of four ratings (00+(all ages), 12+, 16+, or 18+)”). Ninguno de los dos cambios le pide al desarrollador que envíe un build. Consultado el 26 de julio de 2026. ↩↩↩
-
Apple, AppStore.ageRatingCode, StoreKit. Declaración
static var ageRatingCode: Int? { get async }, disponible desde iOS 26.2, iPadOS 26.2, macOS 26.2, tvOS 26.2, visionOS 26.2 y watchOS 26.2, sin fila de Mac Catalyst. Valor de retorno: “An integer representing the current age rating code, ornilif the age rating is unavailable”. Fuente del planteo de comparación (“Use this property to fetch the age rating for your app and compare it with the last known age rating to check if it has changed”) y del encadenamiento con PermissionKit (“If your app’s age rating has changed, consider informing parents or guardians by using the Significant Change API”), frase en la que “Significant Change API” es el texto del enlace y la página deSignificantAppUpdateTopicde Apple es su destino. El propio ejemplo de la página es unguard letalrededor de la propiedad que imprime un mensaje cuando el valor no está disponible. Busqué en la documentación de Apple una correspondencia entre estos enteros y los niveles de clasificación (4+, 9+, 13+, 16+, 18+) el 26 de julio de 2026 y no encontré ninguna, ni en esta página, ni en la página del tipoAppStore, ni en la referencia de clasificaciones por edad de la Ayuda de App Store Connect. ↩↩↩↩↩↩↩↩↩ -
Apple, Testing age assurance in sandbox, StoreKit. Fuente del camino en el dispositivo (Configuración, Desarrollador, Cuenta de Apple del entorno de pruebas, Gestionar, y después “Age Assurance or Revoke App Consent”), de la matriz de seis filas de prueba cuyas filas de 18 o más devuelven un límite inferior de 18 sin límite superior y una declaración de edad
selfDeclaredoconfirmed, del comportamiento al preguntarle a un adulto (“For 18+ test cases, PermissionKit throwsAskError.notAvailablerather than returning aPermissionChoice. CallingAskCenter.ask(_:)for an adult user throws this error because they don’t meet the requirements for parental permission requests”), de los pasos de revocación que terminan en la confirmación “Notification Triggered” con “A notification will be sent to the developer server soon”, y de la nota sobre el payload (“your server receives aRESCIND_CONSENTnotificationType. The notification payload includes anappDataobject with app metadata, including thebundleIdandenvironmentfields”). Las tres filas de menores de la matriz son menos de 13 aprobado, 13 a 15 aprobado y 16 a 17 rechazado, todas con declaración de edadguardianDeclared. ↩↩↩↩↩ -
Apple, SignificantAppUpdateTopic, PermissionKit. Disponible desde iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2 y visionOS 26.2; declarado
struct SignificantAppUpdateTopic, conforme aQuestionTopic, coninit(description: String). Fuente del traspaso de la definición (“You determine what constitutes a significant update based on applicable regulations”), de la guía sobre la descripción (“Use concise, understandable language that clearly explains what changed in your app. Parents and guardians see this description when deciding whether to grant permission”) y de ambos comentarios de código del listado Specific/Vague reproducido textualmente en este artículo. ↩↩↩↩↩↩ -
Apple, New requirements for apps available in Texas, Apple Developer News, 8 de octubre de 2025. Fuente del anuncio original (“Beginning January 1, 2026, a new state law in Texas … introduces age assurance requirements for app marketplaces and developers”, donde el texto elidido nombra la SB2420), del requisito de Family Sharing (“All new Apple Accounts for users under the age of 18 will be required to join a Family Sharing group, and parents or guardians will need to provide consent for all App Store downloads, app purchases, and transactions using Apple’s In-App Purchase system by the minor”) y del aviso anticipado sobre Utah y Luisiana (“Similar requirements will come into effect later next year”). Consultado el 26 de julio de 2026. ↩
-
Apple, Update on age requirements for apps distributed in Texas, Apple Developer News, 23 de diciembre de 2025. Fuente de la medida cautelar (“A recent injunction issued by a district court suspended enforcement of Texas state law SB2420 … In light of this ruling, Apple will pause previously announced implementation plans and monitor the ongoing legal process”), de la disponibilidad continuada de las cuatro herramientas en el entorno de pruebas y de su extensión a Utah y Luisiana. Consultado el 26 de julio de 2026. ↩↩
-
Apple, Age requirements for apps distributed in Brazil, Australia, Singapore, Utah, and Louisiana, Apple Developer News, 24 de febrero de 2026. Fuente de la restricción de descarga para 18+ (“Starting February 24, 2026, Apple will block users in Australia, Brazil, and Singapore from downloading apps rated 18+ unless they have been confirmed to be adults through reasonable methods. The App Store will perform this confirmation automatically. However, developers may have separate obligations to independently confirm that their users are adults”), de las fechas de Utah y Luisiana (“For users with new Apple Accounts in Utah as of May 6, 2026, and in Louisiana as of July 1, 2026, age categories will be shared with the developer’s app when requested through the Declared Age Range API”), de la frase sobre la ampliación citada en este artículo y de los cuatro enlaces que la siguen (“The tools we previously announced have been expanded to help developers meet compliance obligations for Louisiana and Utah, including:” Declared Age Range API, Significant Change API dentro de PermissionKit, nuevo tipo de propiedad de clasificación por edad en StoreKit, App Store Server Notifications), de la consecuencia sobre loot boxes en Brasil y de la primera mención pública de la Significant Update Action (“Developers can use the Declared Age Range API to present significant update notifications to adults in these states through the Significant Update Action, now in beta”). Consultado el 26 de julio de 2026. ↩↩↩↩↩
-
Apple, PermissionQuestion y expirationDate, PermissionKit.
final class PermissionQuestion<Topic> where Topic : QuestionTopic, disponible desde iOS 26.0 con cuatro inicializadores:init(handle:),init(handles:),init(communicationTopic:)einit(significantAppUpdateTopic:), el último introducido en iOS 26.2 y descrito como creador de “a permission question that asks parents or guardians for permission to continue using your app after a significant update”.expirationDateestá declaradofinal var expirationDate: Date?con la discusión “Once the date passes, the person that receives the question can no longer respond”. Apple no publica ningún valor por defecto para la propiedad ni orientación sobre cómo asignarla para el topic de actualización significativa. ↩↩ -
Apple, Creating a communication experience, PermissionKit. Fuente del comportamiento de cancelación citado aquí: “At any point during the send request flow, the child has the option to cancel the request, and decide not to send the question to their parent or guardian. In this scenario, the system doesn’t deliver a response to the calling app for that specific question”. También es la fuente de la restricción del framework a iMessage, que la página principal de PermissionKit enuncia como un aparte de tipo Important: “Communication experiences using the
PermissionKitframework are only available using iMessage”. Nota que el artículo documenta únicamente el flujo deCommunicationTopic; Apple no publica un artículo equivalente para el flujo de actualización significativa, y los ejemplos de código del artículo llaman aCommunicationLimits.current.permissionResponses, un símbolo que devuelve 404 en la documentación de Apple al 26 de julio de 2026. ↩ -
Apple, AskError, PermissionKit.
enum AskError, conforme aLocalizedError, con seis casos. Cuatro traen resumen y llegan en iOS 26.1:unknown,communicationLimitsNotEnabled(“Indicates communication limits isn’t enabled to send permission requests”),contactSyncNotSetupeinvalidQuestion. Dos no publican resumen en sus propias páginas:systemError(underlyingError:)en iOS 26.1 y notAvailable en iOS 26.2, que llega junto conSignificantAppUpdateTopic. El significado denotAvailableaparece únicamente en el artículo sobre el entorno de pruebas citado en la nota 11;systemErroral menos nombra su causa en la firma. SicommunicationLimitsNotEnabledocontactSyncNotSetuptambién pueden surgir en una consulta de actualización significativa es algo que no se declara. ↩ -
Apple, appTransactionID y originalAppVersion en
AppTransaction, StoreKit. Fuente de la semántica del identificador (“The App Store generates a single, globally uniqueappTransactionIDfor each Apple Account that downloads your app and for each family group member for apps that support Family Sharing”), de su estabilidad ante nueva descarga, reembolso, recompra y cambio de tienda, de su presencia en los payloads de la versión 2 de App Store Server Notifications, y de la línea crucial para apps gratuitas: “TheappTransactionIDis available even if a customer makes no in-app purchases”.originalAppVersiones “The app version that the customer originally purchased from the App Store”, que llevaCFBundleShortVersionStringen macOS yCFBundleVersionen el resto, y siempre1.0en el entorno de pruebas. El tipo correspondiente del lado del servidor es appTransactionId en la App Store Server API, introducido en la versión 1.15. ↩↩↩↩↩ -
Apple, CommunicationLimits y updates, PermissionKit.
updatesestá declaradofinal var updates: some AsyncSequence<PermissionResponse<CommunicationTopic>, Never> { get }y resumido como “Registers the communication topic with the system, so your app can be launched on-demand in the background to receive permission updates”. La documentación de Apple agrupaupdatesy ambas sobrecargas deCommunicationLimits.ask(_:in:)bajo un encabezado de API obsoletas; la clase en sí sigue vigente paraisKnownHandle(_:)yknownHandles(in:). La secuencia de reemplazo abandona la promesa: ni el resumen ni la discusión deAskCenter.responses(for:)mencionan el arranque en segundo plano, y ese es todo el hueco de documentación que señala el cuerpo del artículo. ↩ -
Apple, AskCenter y responses(for:), PermissionKit, ambos disponibles desde iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2 y visionOS 26.2.
AskCenteres unafinal classa la que se llega mediantestatic let shared, descrita como la que rutea “your questions through the appropriate family sharing channels” y entrega “responses back to your app when parents make their decisions”.responses(for:)está declaradofinal func responses<Topic>(for topicType: Topic.Type) -> some AsyncSequence<PermissionResponse<Topic>, Never> where Topic : QuestionTopicy resumido como “Registers the topic type with the system and returns an asynchronous sequence of responses”, sin ninguna mención al arranque en segundo plano. Existen cuatro sobrecargas deask(_:in:): dos que recibenUIViewControlleren iOS, iPadOS y visionOS, y dos que recibenNSWindowen macOS, una de cada una por tipo de topic.PermissionResponseexponechoiceyquestion;PermissionChoice.Answerpublica exactamente dos casos,approvalydenial. ↩↩ -
Respuesta del personal de Apple en AppStore.ageRatingCode always returns 0 on real device, Apple Developer Forums. Publicación original de abril de 2026 que reporta
0en un dispositivo físico con iOS 26.4, una cuenta del entorno de pruebas iniciada y la clasificación por edad configurada en App Store Connect. La respuesta, de un autor etiquetado como Apple Staff, afirma: “TheageRatingCodeAPI should be used to observe changes to your app’s age rating over time by comparing to its last known value. If your app’s age rating code has changed, consider informing parents or guardians by using the Significant Change API”, y “A value of0is expected during development of your app when it is built and run from Xcode and Sandbox environments (including TestFlight)”. Consultado dos veces el 26 de julio de 2026 con idéntica redacción; el foro se renderiza mediante JavaScript y muestra la antigüedad de la respuesta como la marca temporal relativa “1w” en lugar de una fecha, así que aquí no se informa fecha de publicación. La respuesta de un foro de desarrolladores es evidencia más débil que una página de documentación, y la documentación de Apple paraageRatingCodeno menciona el valor0en absoluto. La consecuencia que extraigo en este artículo, que persistir0como línea base fabrica un cambio falso en el primer build del App Store, es una inferencia mía a partir de esa respuesta y del patrón de comparación documentado por Apple. ↩↩↩↩ -
Apple, Enabling App Store Server Notifications. Fuente del piso de TLS (“your server must support the Transport Layer Security (TLS) 1.2 protocol or later”), de la configuración de URL por entorno en App Store Connect, de la restricción de puerto (443, o 1024 en adelante) y de la subred de la lista de permitidos (“add the IP address subnet
17.0.0.0/8”, que “applies to both the sandbox and the production environments”). El artículo complementario Receiving App Store Server Notifications describe elsignedPayloadfirmado con JWS y, al 26 de julio de 2026, menciona únicamente el objetodatasin referirse aappData. ↩ -
Apple, Responding to App Store Server Notifications. Fuente de los códigos de éxito (“Send HTTP
200, or any HTTP code between200and206”), del disparador de reintentos (“Send HTTP50xor40xto have the App Store retry the notification”), del calendario de reintentos de la versión 2 (“it retries five times, at 1, 12, 24, 48, and 72 hours after the previous attempt”), de la limitación del entorno de pruebas (“Retry notifications are available only in the production environment. In the sandbox environment, the App Store server attempts to send the notification one time”) y de la vía de recuperación medianteGet-Notification-History. ↩↩ -
Apple, subtype, App Store Server Notifications. La página publica 19 valores posibles (ACCEPTED, ACTIVE_TOKEN_REMINDER, AUTO_RENEW_DISABLED, AUTO_RENEW_ENABLED, BILLING_RECOVERY, BILLING_RETRY, CREATED, DOWNGRADE, FAILURE, GRACE_PERIOD, INITIAL_BUY, PENDING, PRICE_INCREASE, PRODUCT_NOT_FOR_SALE, RESUBSCRIBE, SUMMARY, UPGRADE, UNREPORTED, VOLUNTARY), cada uno acotado a un tipo de notificación con nombre. La cadena
RESCIND_CONSENTno aparece en ninguna parte del contenido de la página, leída el 26 de julio de 2026. ↩ -
Apple, PermissionButton, PermissionKit.
@MainActor @preconcurrency struct PermissionButton<Topic, Label> where Topic : QuestionTopic, Label : View, disponible desde iOS 26.2, iPadOS 26.2, Mac Catalyst 26.2, macOS 26.2 y visionOS 26.2, con dos sobrecargas deinit(question:label:)restringidas aCommunicationTopicySignificantAppUpdateTopicrespectivamente. Reemplaza aCommunicationLimitsButton, que Apple lista bajo API obsoletas en la página del framework. ↩↩↩ -
Apple, AgeRangeService.showSignificantUpdateAcknowledgment(in:updateDescription:), SignificantUpdateAction y el valor de entorno de SwiftUI showSignificantUpdateAcknowledgment. El método está declarado
@MainActor func showSignificantUpdateAcknowledgment(in windowScene: UIWindowScene, updateDescription: String) async throwsy publica iOS 26.4, iPadOS 26.4 y Mac Catalyst 26.4, sin fila de macOS;AgeRangeServicelista únicamente esta sobrecarga bajo “Displaying update acknowledgments”.SignificantUpdateActiony el valor de entorno publican las mismas tres plataformas. El aparte de tipo Important de Apple sobre el método: “Before calling this function, checkRegulatoryFeatureto determine if a person must acknowledge your significant app change”. La discusión del valor de entorno agrega que hay que invocar “this action from aButtonoronAppear(perform:)”. ↩↩↩↩ -
Apple, requestAgeRange(ageGates:::in:), Declared Age Range. Apple publica dos sobrecargas: una que recibe
in viewController: UIViewControlleren iOS 26.0, iPadOS 26.0 y Mac Catalyst 26.0, y otra que recibein window: NSWindowen macOS 26.0. La existencia de una variante conNSWindowpara la solicitud de rango de edad, y su ausencia para la hoja de reconocimiento citada en la nota 27, es la base para leer la omisión en macOS como un hueco y no como una decisión de política; Apple no ha publicado nada en ningún sentido. ↩ -
Apple, Requesting people’s age range information in your app, Declared Age Range. Fuente de la aritmética de umbrales citada en el cuerpo (“Puedes especificar hasta tres umbrales de edad, que crean hasta cuatro rangos de edad posibles”), de la restricción de separación (“Cada rango debe abarcar al menos dos años”) y de la semántica de los límites (“Cuando el valor de
lowerBoundesnil, la persona está por debajo de tu umbral de edad más bajo” y “CuandoupperBoundesnil, la persona alcanza o supera tu umbral de edad más alto”). Umbrales en 13, 16 y 18 devuelven exactamente los cuatro rangos que Apple publica para Texas, y ambos rangos acotados superan el mínimo de dos años: 13 a 15 abarca tres años y 16 a 17 abarca dos. El artículo también advierte que las regiones a las que pertenece la cuenta de una persona “determinan los umbrales de edad que el sistema usa para devolver los rangos de edad, y pueden diferir de los umbrales de edad que especificas en tu solicitud”. Leído desde el JSON de documentación de Apple el 26 de julio de 2026. ↩ -
Relevamiento del autor, 26 de julio de 2026: cómo se establecieron las afirmaciones sobre plataformas. Los valores de plataforma se leyeron del build setting
SUPPORTED_PLATFORMSde cada proyecto y no de las claves de deployment target, que Xcode escribe sin importar el destino: Reps declaraappletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulatormás un target aparte dewatchos watchsimulator, y Ace Citizenship declara únicamenteiphoneos iphonesimulator. Ese método subestima las plataformas, y Return es la prueba: el único valor deSUPPORTED_PLATFORMSde Return esiphoneos iphonesimulator macosx xros xrsimulator, mientras que sus targets de TV y reloj llevanSDKROOT = appletvosySDKROOT = watchosen su lugar, así que un barrido porSUPPORTED_PLATFORMSno ve ninguno de los dos. Por eso toda afirmación de “se distribuye en la plataforma X” en este artículo viene de App Store Connect y no de un build setting. Return: TV_OS 1.0 y 1.0.1 ambas en READY_FOR_DISTRIBUTION, IOS y MAC_OS 1.0.1 igual, y el target de iOS incorporaReturnWatch Watch App(com.941apps.Return.watchkitapp) mediante una fase Embed Watch Content, que es como una app de reloj llega a una muñeca. Reps: IOS 1.1 y MAC_OS 1.1 en READY_FOR_DISTRIBUTION, ninguna versión de TV_OS llegó nunca a ese estado, y TV_OS 1.2 lleva en WAITING_FOR_REVIEW desde el 2 de junio de 2026, así que Reps se distribuye en iOS y macOS. ↩ -
Relevamiento del autor, 26 de julio de 2026, en macOS 26.5.2 (build 25F84) con Xcode 26.6 (build 17F113) y Swift 6.3.3. Regla de selección, enunciada para que el alcance pueda verificarse y reproducirse en lugar de aceptarse a ciegas: cada fila de la tabla de Proyectos Activos en mi propia configuración de agente (
~/.claude/CLAUDE.md) que tiene detrás un proyecto de Xcode, lo que arroja exactamente siete:Reps,Return,Banana List(distribuida como Get Bananas),Ace-Citizenship,Water,ResumeGeniAppyYawara. La regla es también el defecto del relevamiento, porqueRandorino tiene fila en esa tabla yRandoriresultó ser el proyecto que importaba. Cinco de los siete tienen ficha en App Store Connect (Reps 6776044339, Return 6756242021, Get Bananas 6756241534, Ace Citizenship 6532592671, ResumeGeni 6771154645);WateryYawarano aparecen entre las 18 apps de la cuenta, así que ninguna fue enviada nunca y llamar app a cualquiera de las dos es exagerar. Cantidad de archivos Swift por proyecto: 77, 57, 55, 26, 34, 71 y 143. El escaneo de consentimiento cubrió*.swift,*.entitlements,*.plistyproject.pbxproj: 85, 65, 63, 34, 38, 74 y 149, que suman 508. Reproducir esos conteos requiere ocho nombres de directorio podados y no seis:build,DerivedData,.build,Pods,.git,worktreesy, solo en Reps,.venv(142 property lists entreReps/.venvyReps/server/.venv) y.xcode-state-backups(18). Si podas solo los primeros seis, Reps da 245 y el total da 668, así que la lista de exclusiones es determinante y todos los nombres de los que depende están impresos aquí. Los 16 patrones, todos sensibles a mayúsculas:PermissionKit,CommunicationLimits,AskPermission,AskCenter,SignificantAppUpdateTopic,SignificantUpdateAction,PermissionTopic,com.apple.developer.family-controls,FamilyControls,ManagedSettings,DeviceActivity,AuthorizationCenter,CKShare,sharedCloudDatabase,publicCloudDatabaseyageRatingCode. Cero archivos coincidentes en los siete proyectos,AskCenterincluido. Dos patrones de control ejecutados por la misma tubería devolvieron resultados distintos de cero (StoreKitcoincidió con tres archivos en Reps,CKContainer|NSPersistentCloudKitContainer|SwiftDatacoincidió con 17 en Banana List), y así es como sé que la tubería lee archivos en lugar de fallar en silencio. El onboarding de Ace Citizenship (IntroCarouselView,WelcomeView,AddStateView,AddRepresentativeView) no contiene ningún campo de edad ni de fecha de nacimiento, y suPrivacyInfo.xcprivacydeclara unNSPrivacyCollectedDataTypesvacío; la línea de elegibilidad “18 años o más” viene de~/Projects/acecitizenship.app/content/blog/n400-application-guide.md. ↩↩↩ -
Relevamiento del autor, 26 de julio de 2026: el escaneo ampliado y Randori. El escaneo ampliado corrió los mismos 16 patrones sobre todos los repositorios bajo
~/Projects, podando los mismos ocho nombres másnode_modulesy excluyendo rutas ignoradas por git, y coincidió con 21 archivos. Siete son código: seis archivos Swift bajoRandori/Randori/y_archive/Oishii-AI/Oishii AI/Services/CloudKitManager.swift(publicCloudDatabase, en un proyecto archivado). Los otros 14 son prosa o estado de máquina: nueve planes y documentos de diseño de Randori, tres posts en el propiocontent/blog/de este sitio (uno de ellos el borrador de este artículo), una nota de traspaso de Obsidian yobsidian-signals/40-Projects/blakecrosley-com/wwdc-2026/.pulse_state.json, que escribe un script y no una persona. Randori escom.wayofyawara.randori, app 6789693294 de App Store Connect con la versión 1.0 en PREPARE_FOR_SUBMISSION.CKShareysharedCloudDatabasecoinciden en 20 líneas repartidas en seis archivos:ConnectionStore.swift(10),RandoriApp.swift(5),ProfileCardView.swift(2) y una línea en cada uno deCKPostTransport.swift,CloudShareSheet.swiftySocialContracts.swift.ConnectionStoredescribe su propia forma en el comentario de cabecera como “one zone (ProfileCardZone), one record type (ConnectionCard), one share”, mantieneprivateCloudDatabaseysharedCloudDatabaselado a lado, e implementaaccept(_ metadata: CKShare.Metadata)en la línea 355,ensureOutboundShare()en la 547, y bloqueo y desbloqueo por cuenta.RandoriApp.swiftimplementauserDidAcceptCloudKitShareWithtanto en el delegado de la app (línea 73) como en el delegado de la escena de ventana (línea 97, comentado “Warm: the app is running when the link is opened”), mientras queRandoriSceneDelegate.scene(_:willConnectTo:options:)leeconnectionOptions.cloudKitShareMetadataen la línea 88 bajo el comentario “Cold start: the invitation rides the connection options”, que es la razón por la que el cuerpo asigna el arranque en frío a las connection options y no al callback de aceptación.TagConsentStorees el registro de consentimiento propio de la app, acotado por cuenta de iCloud, y su valor por defecto documentado es fallar en modo cerrado: un compañero cuyo registro de política no llegó no puede ser etiquetado.Randori/Randori.entitlementssolicita CloudKit, HealthKit yaps-environment. Todos los patrones de consentimiento salvo los dos de compartido en CloudKit devuelven cero en ese repositorio, y lo mismo pasa conStoreKit,signedPayloadynotificationType, así que la app no vende nada y no corre ningún servidor propio;docs/asc-metadata.mdprepara un precio gratuito y una clasificación por edad 4+. ↩↩↩↩ -
Relevamiento del autor, 26 de julio de 2026: puntos de llamada a StoreKit y evidencia del servidor. StoreKit aparece en tres proyectos:
Reps/Reps/Services/RepsProStore.swift(renovación automática, dos niveles),Ace Citizenship/StoreKitManager.swift(un no consumible) yResumeGeni/Subscription/(una suscripción mensual, controlada del lado del servidor). El manejo de.pendingcitado en la tabla está enRepsProStore.swift:134(case .userCancelled, .pending:),StoreKitManager.swift:69-73(uncase .pending:aparte cuyoreturn falsees la línea 73) ySubscriptionStore.swift:209-212(un resultado pendiente con nombre propio). Los puntos de llamada son los que dejan varado al usuario:RepsProPaywallView.swift:359-361cierra únicamente dentro deif await store.purchase(plan), yContentView.swift:484-485desbloquea únicamente dentro deif success, así que unfalsedevuelto por una compra pendiente no cambia nada en pantalla en ninguna de las dos apps. ResumeGeni, en cambio, llega aPaywallView.swift:526, una rama.pendingque muestra el toast “Waiting for approval. You’ll get access once it’s approved”. El endpoint de App Store Server Notifications de ResumeGeni esPOST /api/appstore/notificationsen~/Projects/resumegeni/app/routers/appstore.py; unPOSTde{"signedPayload":"probe"}devuelve HTTP 400{"status":"invalid"}, algo alcanzable solo después del flag de habilitación y dentro del verificador de cadena de certificados de Apple, yGETdevuelve 405. Ambas URLs de entorno están registradas, y la evidencia de eso es la entrega y no un runbook: la base de datos D1941-analytics, tablafunnel_events, contiene 56 filas dondeplatform='ios'y los metadatos traennotification_type, entre 2026-06-26T15:48:51Z y 2026-07-25T16:40:11Z, de las cuales 55 llevan unenvironmentdesandboxy una llevaproduction(2026-07-21T18:08:21Z). Toma 56 como cota inferior, porque el servicio mapea solo cinco nombres de evento a filas del embudo; los cuatro tipos realmente observados son DID_RENEW (42), SUBSCRIBED (6), EXPIRED (5) y DID_CHANGE_RENEWAL_STATUS (3).app/services/app_store_notification_service.pynombra 13 tipos de notificación, de los cuales ocho alcanzan una rama de despacho ejecutable en_funnel_event_name(líneas 75 a 89): SUBSCRIBED, OFFER_REDEEMED, DID_RENEW, REFUND, REVOKE, EXPIRED, DID_CHANGE_RENEWAL_STATUS y DID_FAIL_TO_RENEW. Los otros cinco aparecen únicamente dentro de comentarios: PRICE_INCREASE, RENEWAL_EXTENDED y METADATA_UPDATE en la línea 73, EXTERNAL_PURCHASE_TOKEN y TEST en la 187.RESCIND_CONSENT,CONSUMPTION_REQUEST,REFUND_DECLINEDyREFUND_REVERSEDdevuelven cero coincidencias en todo ese repositorio. Para dejar claro qué no es evidencia:docs/SUBSCRIPTION_GO_LIVE.md:83sí dice “configura AMBAS URL, la de producción y la del entorno de pruebas”, pero la línea es una instrucción de runbook bajo un encabezado que aclara que el paso “needs your re-auth”, así que registra una intención antes que un registro completado, y la afirmación sobre el registro se apoya en las notificaciones entregadas. ↩↩↩ -
Apple, Testing Ask to Buy in Xcode, StoreKit. Fuente de la descripción del mecanismo por parte de Apple (“With Ask to Buy, when a child wants to make an eligible purchase or download, the system sends the purchase request to the parent or guardian”) y del comportamiento ante el rechazo (“Your app doesn’t receive a transaction because you declined Ask to Buy”). El artículo también documenta el interruptor de Ask to Buy bajo Purchase Options en el editor de configuración de StoreKit, y el gestor de transacciones muestra Pending Ask to Buy, Ask to Buy Approved y Ask to Buy Declined. ↩↩↩
-
Apple, Product.PurchaseResult.pending y Transaction.updates, StoreKit, ambos disponibles desde iOS 15.0. Fuente del resumen del caso (“The purchase is pending, and requires action from the customer”), de la vía de resolución (“If a pending purchase succeeds, StoreKit delivers the resulting
Transactionin the transactionupdates”) y del propósito de la secuencia (“This sequence receives transactions that occur outside of the app, such as Ask to Buy transactions, offer code redemptions, and purchases that customers make in the App Store”). El enum Product.PurchaseResult publica tres casos:success(_:),pendingyuserCancelled. El propio ejemplo de Apple en la página del enum comenta la rama pendiente como “The purchase requires action from the customer. If the transaction completes, it’s available throughTransaction.updates”. ↩↩ -
Todos los símbolos del listado de ruteo anterior fueron verificados contra el JSON de documentación de Apple el 26 de julio de 2026.
AgeRangeService.sharedesstatic let shared: AgeRangeService(iOS 26.0). Los valores de entorno de SwiftUI sonrequestAgeRange,var requestAgeRange: DeclaredAgeRangeAction { get }(iOS 26.0), yshowSignificantUpdateAcknowledgment,var showSignificantUpdateAcknowledgment: SignificantUpdateAction { get }(iOS 26.4). El piso@available(iOS 26.5, *)del listado no viene de ninguno de esos dos:AgeRangeService.AgeRangeDeclaration.confirmedpublica iOS 26.5, iPadOS 26.5, Mac Catalyst 26.5 y macOS 26.5, un release después de la acción de reconocimiento, así que cualquier código que distinga a un adulto confirmado hereda el piso más alto. El propio proyecto de ejemplo de Apple publica la misma disponibilidad 26.5.6AgeRangeService.AgeRangeexponelowerBound,upperBound,ageRangeDeclarationdeclaradovar ageRangeDeclaration: AgeRangeService.AgeRangeDeclaration?, yactiveParentalControls. La comparación== .confirmedcontra ese opcional es válida porqueAgeRangeDeclarationconforma aEquatableyHashablesegún su sección de relaciones. El inicializador dePermissionButtonesinit(question: PermissionQuestion<Topic>, @ViewBuilder label: @escaping () -> Label), restringido aSignificantAppUpdateTopicen la sobrecarga usada aquí.26ChangedFeatureViewyAccountVerificationPromptson marcadores de posición para tus propias vistas, no símbolos de Apple. ↩↩↩ -
Apple, Age ratings values and definitions, Ayuda de App Store Connect, leído el 26 de julio de 2026 como fuente que confirma que los cambios del 18 de junio de 2026 anunciados en la nota 9 entraron en vigor. Bajo “Australia age rating values” la tabla ahora publica dos clasificaciones, 16+ y R 18+, sin fila de 15+. Ahora existe una sección aparte de “Vietnam age rating values”, presentada como “As required by Article 38 of Vietnam Decree 147”, y su fila 00+ se define como apps que “contain no objectionable material but may contain instances of the following content”, enumerando controles parentales, verificación de edad, contenido generado por usuarios, mensajería y chat, publicidad y concursos poco frecuentes. La lista de descriptores del 21 de mayo de Apple para la migración australiana no coincide con la lista actual de disparadores de 16+ de esta página, una discrepancia que no resolví y en la que no me apoyo, porque lo único que se afirma aquí es que el nivel 15+ desapareció y que la tabla de Vietnam existe. ↩↩↩