# Entrevista StoreKit 2: Gestión de Suscripciones y Validación de Recibos > Domina las preguntas de entrevista iOS sobre StoreKit 2, gestión de suscripciones, validación de recibos e implementación de compras integradas con ejemplos prácticos en Swift. - Published: 2026-03-09 - Updated: 2026-04-29 - Author: SharpSkill - Tags: ios, storekit, in-app-purchase, swift, interview - Reading time: 14 min --- StoreKit 2 representa un cambio fundamental en la forma en que se gestionan las compras integradas en iOS. Introducido con iOS 15, este framework moderno simplifica drásticamente el código requerido para la gestión de suscripciones y la validación de transacciones. Las entrevistas técnicas iOS abordan regularmente este tema para evaluar la experiencia de los desarrolladores en monetización de aplicaciones. > **Punto Clave para la Entrevista** > > StoreKit 2 aprovecha una API nativa de Swift con async/await, eliminando la necesidad de callbacks complejos y validación de recibos del lado del cliente. Los entrevistadores valoran a los candidatos que pueden articular las diferencias fundamentales con StoreKit 1. ## Visión General de la Arquitectura StoreKit 2 StoreKit 2 está construido sobre una arquitectura moderna que aprovecha al máximo Swift Concurrency. A diferencia de StoreKit 1, todas las operaciones son asíncronas y utilizan tipos nativos de Swift. ### Obtención de Productos Disponibles El primer paso consiste en cargar los productos configurados en App Store Connect. StoreKit 2 simplifica esta operación con una API declarativa. ```swift // ProductService.swift import StoreKit actor ProductService { // Identificadores de productos configurados en App Store Connect private let productIdentifiers: Set = [ "com.app.subscription.monthly", "com.app.subscription.yearly", "com.app.premium.lifetime" ] // Caché de productos para evitar llamadas de red repetidas private var cachedProducts: [Product] = [] func loadProducts() async throws -> [Product] { // Devuelve la caché si está disponible guard cachedProducts.isEmpty else { return cachedProducts } // Solicitud asíncrona a la App Store let products = try await Product.products(for: productIdentifiers) cachedProducts = products return products } func product(for identifier: String) async throws -> Product? { let products = try await loadProducts() return products.first { $0.id == identifier } } } ``` El uso de un `actor` garantiza la seguridad de hilos al gestionar la caché de productos. ### Tipos de Productos en StoreKit 2 StoreKit 2 distingue claramente entre los diferentes tipos de productos. Comprender esta distinción es esencial para las entrevistas. ```swift // ProductType+Extensions.swift import StoreKit extension Product.ProductType { var displayName: String { switch self { case .consumable: // Productos consumibles (créditos, vidas, etc.) return "Consumable" case .nonConsumable: // Compras permanentes (desbloqueo de funciones) return "Non-Consumable" case .autoRenewable: // Suscripciones con renovación automática return "Auto-Renewable Subscription" case .nonRenewable: // Suscripciones sin renovación (pase temporal) return "Non-Renewable Subscription" @unknown default: return "Unknown" } } } ``` Las suscripciones con renovación automática representan el modelo de negocio dominante para las aplicaciones iOS modernas. ## Manejo de Transacciones de Compra El proceso de compra en StoreKit 2 se vuelve lineal gracias a async/await. Se acabaron los delegados y callbacks anidados de StoreKit 1. ### Iniciar una Compra ```swift // PurchaseManager.swift import StoreKit actor PurchaseManager { enum PurchaseError: Error { case productNotFound case purchaseCancelled case purchasePending case verificationFailed case unknown } func purchase(_ product: Product) async throws -> Transaction { // Inicia la compra con el modal del sistema let result = try await product.purchase() switch result { case .success(let verification): // Verifica la firma de la transacción let transaction = try checkVerification(verification) // Marca la transacción como finalizada await transaction.finish() return transaction case .userCancelled: // El usuario canceló el modal de compra throw PurchaseError.purchaseCancelled case .pending: // Compra pendiente (aprobación parental, etc.) throw PurchaseError.purchasePending @unknown default: throw PurchaseError.unknown } } private func checkVerification(_ result: VerificationResult) throws -> T { switch result { case .verified(let safe): // Transacción firmada y verificada por la App Store return safe case .unverified(_, let error): // Firma inválida o corrupta throw PurchaseError.verificationFailed } } } ``` El método `finish()` es crucial: indica a la App Store que el contenido ha sido entregado al usuario. > **Alerta de Entrevista** > > Nunca olvides llamar a `transaction.finish()` después de entregar el contenido. Una transacción no finalizada se restaurará en el próximo lanzamiento de la app, causando comportamientos inesperados. ### Escuchar Transacciones en Segundo Plano StoreKit 2 introduce `Transaction.updates`, un flujo asíncrono que emite las transacciones que ocurren fuera de la aplicación (renovaciones, compras de family sharing, etc.). ```swift // TransactionObserver.swift import StoreKit actor TransactionObserver { private var updateTask: Task? func startObserving() { // Cancela cualquier observación previa updateTask?.cancel() updateTask = Task(priority: .background) { // Flujo infinito de actualizaciones de transacciones for await result in Transaction.updates { do { let transaction = try self.checkVerification(result) // Procesa la transacción según su tipo await self.handleTransaction(transaction) // Siempre finaliza la transacción await transaction.finish() } catch { // Registra el error para depuración print("Transaction verification failed: \(error)") } } } } func stopObserving() { updateTask?.cancel() updateTask = nil } private func handleTransaction(_ transaction: Transaction) async { switch transaction.productType { case .autoRenewable: // Actualiza el estado de la suscripción await updateSubscriptionStatus(transaction) case .nonConsumable: // Desbloquea la función premium await unlockFeature(transaction.productID) default: break } } private func checkVerification(_ result: VerificationResult) throws -> T { switch result { case .verified(let safe): return safe case .unverified: throw PurchaseError.verificationFailed } } private func updateSubscriptionStatus(_ transaction: Transaction) async { // Implementación de la actualización del estado } private func unlockFeature(_ productID: String) async { // Implementación del desbloqueo de funciones } enum PurchaseError: Error { case verificationFailed } } ``` La observación de transacciones debe iniciarse tan pronto como se lance la aplicación para evitar perder cualquier actualización. ## Preguntas Comunes de Entrevista sobre Suscripciones en StoreKit 2 Las entrevistas iOS frecuentemente incluyen preguntas específicas sobre la gestión de suscripciones. Aquí están las más comunes con respuestas detalladas. ### Pregunta 1: ¿Cómo Verificar el Estado Actual de una Suscripción? StoreKit 2 proporciona acceso directo a los entitlements activos a través de `Transaction.currentEntitlements`. ```swift // SubscriptionStatusChecker.swift import StoreKit struct SubscriptionStatusChecker { struct SubscriptionStatus { let isActive: Bool let expirationDate: Date? let willRenew: Bool let productID: String? } static func checkCurrentStatus() async -> SubscriptionStatus { // Recupera todas las transacciones activas var latestTransaction: Transaction? for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { // Filtra solo las suscripciones if transaction.productType == .autoRenewable { // Conserva la transacción más reciente if latestTransaction == nil || transaction.purchaseDate > latestTransaction!.purchaseDate { latestTransaction = transaction } } } } guard let transaction = latestTransaction else { return SubscriptionStatus( isActive: false, expirationDate: nil, willRenew: false, productID: nil ) } // Verifica si la suscripción sigue activa let isActive = transaction.expirationDate ?? Date.distantPast > Date() // Recupera la información de renovación let renewalInfo = await getRenewalInfo(for: transaction.productID) return SubscriptionStatus( isActive: isActive, expirationDate: transaction.expirationDate, willRenew: renewalInfo?.willAutoRenew ?? false, productID: transaction.productID ) } private static func getRenewalInfo(for productID: String) async -> Product.SubscriptionInfo.RenewalInfo? { guard let product = try? await Product.products(for: [productID]).first, let subscription = product.subscription else { return nil } // Recupera el estado de renovación let status = try? await subscription.status.first return status?.renewalInfo } } ``` Este enfoque proporciona el estado preciso de la suscripción sin llamadas al servidor. ### Pregunta 2: ¿Cómo Manejar Pruebas Gratuitas y Ofertas Promocionales? StoreKit 2 simplifica el acceso a la información de las ofertas. Los entrevistadores a menudo prueban la comprensión de los diferentes tipos de ofertas. ```swift // OfferManager.swift import StoreKit struct OfferManager { enum OfferType { case freeTrial(duration: String) case payAsYouGo(periods: Int, price: Decimal) case payUpFront(duration: String, price: Decimal) case none } static func getIntroductoryOffer(for product: Product) -> OfferType { guard let subscription = product.subscription, let offer = subscription.introductoryOffer else { return .none } switch offer.paymentMode { case .freeTrial: // Período de prueba gratuita return .freeTrial(duration: formatPeriod(offer.period)) case .payAsYouGo: // Precio reducido durante varios períodos return .payAsYouGo( periods: offer.periodCount, price: offer.price ) case .payUpFront: // Pago único por un período return .payUpFront( duration: formatPeriod(offer.period), price: offer.price ) @unknown default: return .none } } static func checkEligibility(for product: Product) async -> Bool { // Verifica si el usuario puede beneficiarse de la oferta guard let subscription = product.subscription else { return false } // isEligibleForIntroOffer indica si es un nuevo suscriptor return await subscription.isEligibleForIntroOffer } private static func formatPeriod(_ period: Product.SubscriptionPeriod) -> String { let value = period.value switch period.unit { case .day: return "\(value) day\(value > 1 ? "s" : "")" case .week: return "\(value) week\(value > 1 ? "s" : "")" case .month: return "\(value) month\(value > 1 ? "s" : "")" case .year: return "\(value) year\(value > 1 ? "s" : "")" @unknown default: return "" } } } ``` El método `isEligibleForIntroOffer` es esencial para evitar mostrar ofertas no disponibles. ### Pregunta 3: ¿Cómo Restaurar Compras Anteriores? La restauración de compras es una funcionalidad obligatoria según las directrices de la App Store. ```swift // RestoreManager.swift import StoreKit actor RestoreManager { struct RestoreResult { let restoredProducts: [String] let activeSubscriptions: [String] } func restorePurchases() async throws -> RestoreResult { var restoredProducts: [String] = [] var activeSubscriptions: [String] = [] // Sincroniza con la App Store try await AppStore.sync() // Itera por todas las transacciones del usuario for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } switch transaction.productType { case .nonConsumable: // Restaura las compras permanentes restoredProducts.append(transaction.productID) case .autoRenewable: // Verifica si la suscripción está activa if let expiration = transaction.expirationDate, expiration > Date() { activeSubscriptions.append(transaction.productID) } default: // Los consumibles no son restaurables break } } return RestoreResult( restoredProducts: restoredProducts, activeSubscriptions: activeSubscriptions ) } } ``` `AppStore.sync()` fuerza la sincronización con los servidores de Apple, útil después de una reinstalación. ## Validación de Recibos del Lado del Servidor La validación del lado del servidor sigue siendo recomendada para aplicaciones con suscripciones críticas. StoreKit 2 introduce JWS (JSON Web Signatures) para una validación moderna. ### Extracción de Datos de Transacción para el Servidor ```swift // ServerValidation.swift import StoreKit struct TransactionPayload: Codable { let transactionID: String let originalTransactionID: String let productID: String let purchaseDate: Date let expirationDate: Date? let jwsRepresentation: String } actor ServerValidationService { private let apiEndpoint = "https://api.example.com/verify" func validateWithServer(_ transaction: Transaction) async throws -> Bool { // Crea el payload para el servidor let payload = TransactionPayload( transactionID: String(transaction.id), originalTransactionID: String(transaction.originalID), productID: transaction.productID, purchaseDate: transaction.purchaseDate, expirationDate: transaction.expirationDate, jwsRepresentation: transaction.jwsRepresentation ?? "" ) // Codifica a JSON let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let jsonData = try encoder.encode(payload) // Envía al servidor var request = URLRequest(url: URL(string: apiEndpoint)!) request.httpMethod = "POST" request.httpBody = jsonData request.setValue("application/json", forHTTPHeaderField: "Content-Type") let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return false } // Decodifica la respuesta del servidor let validationResponse = try JSONDecoder().decode( ValidationResponse.self, from: data ) return validationResponse.isValid } } struct ValidationResponse: Codable { let isValid: Bool let subscriptionStatus: String? } ``` La `jwsRepresentation` contiene la firma criptográfica verificable por el servidor. > **App Store Server API** > > Apple ahora proporciona la App Store Server API para la validación del lado del servidor. Esta API moderna reemplaza el endpoint obsoleto verifyReceipt y ofrece funciones avanzadas como notificaciones servidor a servidor. ### Configuración de Notificaciones del Servidor Las Server Notifications V2 permiten la recepción de eventos en tiempo real. ```swift // ServerNotificationTypes.swift enum NotificationType: String, Codable { case subscribed = "SUBSCRIBED" case didRenew = "DID_RENEW" case didFailToRenew = "DID_FAIL_TO_RENEW" case didChangeRenewalStatus = "DID_CHANGE_RENEWAL_STATUS" case expired = "EXPIRED" case gracePeriodExpired = "GRACE_PERIOD_EXPIRED" case offerRedeemed = "OFFER_REDEEMED" case priceIncrease = "PRICE_INCREASE" case refund = "REFUND" case renewalExtended = "RENEWAL_EXTENDED" case revoke = "REVOKE" } struct ServerNotification: Codable { let notificationType: NotificationType let subtype: String? let data: NotificationData struct NotificationData: Codable { let bundleId: String let environment: String let signedTransactionInfo: String let signedRenewalInfo: String? } } ``` Estas notificaciones permiten mantener el estado de las suscripciones sincronizado con el backend. ## Manejo de Errores y Casos Límite Las entrevistas a menudo prueban la comprensión de los escenarios de error y su manejo apropiado. ### Errores Comunes en StoreKit 2 ```swift // PurchaseErrorHandler.swift import StoreKit struct PurchaseErrorHandler { enum UserFacingError { case networkError case paymentFailed case notAuthorized case productNotAvailable case unknown var message: String { switch self { case .networkError: return "Connection failed. Please check your internet connection." case .paymentFailed: return "Payment failed. Please verify your payment information." case .notAuthorized: return "In-app purchases are disabled on this device." case .productNotAvailable: return "This product is not currently available." case .unknown: return "An unexpected error occurred." } } } static func handle(_ error: Error) -> UserFacingError { // Verifica si es un error de StoreKit if let storeKitError = error as? StoreKitError { switch storeKitError { case .networkError: return .networkError case .userCancelled: // No muestra error, el usuario canceló return .unknown case .notAvailableInStorefront: return .productNotAvailable case .notEntitled: return .notAuthorized default: return .unknown } } // Errores de compra if let purchaseError = error as? Product.PurchaseError { switch purchaseError { case .invalidQuantity: return .unknown case .productUnavailable: return .productNotAvailable case .purchaseNotAllowed: return .notAuthorized default: return .unknown } } return .unknown } } ``` Un manejo claro de errores mejora la experiencia del usuario y simplifica la depuración. ### Soporte para Modo Sin Conexión ```swift // OfflineCapabilityManager.swift import StoreKit actor OfflineCapabilityManager { private let userDefaults = UserDefaults.standard private let entitlementsKey = "cached_entitlements" func cacheCurrentEntitlements() async { var entitlements: [String: Date] = [:] for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { entitlements[transaction.productID] = transaction.expirationDate } } // Guarda localmente if let data = try? JSONEncoder().encode(entitlements) { userDefaults.set(data, forKey: entitlementsKey) } } func getCachedEntitlements() -> [String: Date] { guard let data = userDefaults.data(forKey: entitlementsKey), let entitlements = try? JSONDecoder().decode( [String: Date].self, from: data ) else { return [:] } // Filtra los entitlements expirados let now = Date() return entitlements.filter { _, expiration in expiration > now } } func hasValidEntitlement(for productID: String) -> Bool { let cached = getCachedEntitlements() return cached[productID] != nil } } ``` La caché local mantiene el acceso a las funciones premium incluso sin conectividad. ## Mejores Prácticas para Producción ### Arquitectura Recomendada ```swift // SubscriptionService.swift import StoreKit import Combine @MainActor final class SubscriptionService: ObservableObject { static let shared = SubscriptionService() @Published private(set) var products: [Product] = [] @Published private(set) var purchasedProductIDs: Set = [] @Published private(set) var subscriptionStatus: SubscriptionStatus = .none private var transactionObserver: Task? enum SubscriptionStatus { case none case active(expirationDate: Date) case expired case inGracePeriod } private init() { // Inicia la observación de transacciones startTransactionObserver() // Carga el estado inicial Task { await loadProducts() await updateSubscriptionStatus() } } private func startTransactionObserver() { transactionObserver = Task.detached(priority: .background) { for await result in Transaction.updates { if case .verified(let transaction) = result { await transaction.finish() await self.updateSubscriptionStatus() } } } } func loadProducts() async { do { let productIDs = [ "com.app.subscription.monthly", "com.app.subscription.yearly" ] products = try await Product.products(for: productIDs) } catch { print("Failed to load products: \(error)") } } func purchase(_ product: Product) async throws { let result = try await product.purchase() switch result { case .success(let verification): if case .verified(let transaction) = verification { await transaction.finish() await updateSubscriptionStatus() } case .userCancelled: break case .pending: break @unknown default: break } } func updateSubscriptionStatus() async { var purchasedIDs: Set = [] var latestSubscription: Transaction? for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result { purchasedIDs.insert(transaction.productID) if transaction.productType == .autoRenewable { if latestSubscription == nil || transaction.purchaseDate > latestSubscription!.purchaseDate { latestSubscription = transaction } } } } purchasedProductIDs = purchasedIDs if let subscription = latestSubscription, let expiration = subscription.expirationDate { if expiration > Date() { subscriptionStatus = .active(expirationDate: expiration) } else { subscriptionStatus = .expired } } else { subscriptionStatus = .none } } deinit { transactionObserver?.cancel() } } ``` Esta arquitectura centralizada simplifica la gestión del estado de las compras en toda la aplicación. ## Conclusión StoreKit 2 transforma la gestión de compras integradas en una experiencia de desarrollo moderna y segura. Puntos clave para recordar de cara a una entrevista: ✅ API nativa async/await: se acabaron los delegados y callbacks complejos de StoreKit 1 ✅ Verificación automática: `VerificationResult` gestiona la validación criptográfica de las transacciones ✅ `Transaction.updates`: flujo asíncrono para transacciones en segundo plano ✅ `Transaction.currentEntitlements`: acceso directo a los entitlements activos del usuario ✅ Ofertas promocionales: `isEligibleForIntroOffer` para verificar la elegibilidad ✅ Validación del servidor: JWS y App Store Server API para mayor seguridad ✅ `transaction.finish()`: obligatorio después de la entrega del contenido ✅ Manejo sin conexión: caché local de entitlements para una experiencia fluida --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/ios/storekit-2-subscriptions-receipts-validation-interview