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.

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.
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.
import StoreKit
actor ProductService {
// Identificadores de productos configurados en App Store Connect
private let productIdentifiers: Set<String> = [
"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.
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
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<T>(_ result: VerificationResult<T>) 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.
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.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
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<T>(_ result: VerificationResult<T>) 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.
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.
¿Listo para aprobar tus entrevistas de iOS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
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.
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.
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
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.
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.
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
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
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
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<String> = []
@Published private(set) var subscriptionStatus: SubscriptionStatus = .none
private var transactionObserver: Task<Void, Never>?
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<String> = []
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
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Swift Testing Framework Entrevista 2026: Macros #expect y #require vs XCTest
Domina el nuevo Swift Testing Framework para entrevistas iOS: macros #expect y #require, migración desde XCTest, patrones avanzados y errores comunes.

Entrevista iOS Push Notifications 2026: APNs, tokens y troubleshooting
Guía completa para preparar entrevistas iOS sobre Push Notifications, APNs, gestión de tokens y troubleshooting. Preguntas frecuentes con respuestas detalladas.

Las 25 preguntas de entrevista Swift más importantes para desarrolladores iOS
Preparación para entrevistas técnicas iOS con las 25 preguntas Swift más frecuentes: optionals, closures, ARC, protocolos, async/await y patrones avanzados.