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 y las nuevas funcionalidades iOS 18.4 con ejemplos prácticos en Swift.

Arquitectura de suscripciones iOS StoreKit 2 y validación de recibos

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. Con iOS 18, StoreKit 1 está oficialmente deprecado, haciendo de StoreKit 2 el único camino soportado. 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, y las nuevas funcionalidades de iOS 18.4 como appTransactionID y SubscriptionOfferView.

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.

ProductService.swiftswift
import StoreKit

actor ProductService {

    // Product identifiers configured in App Store Connect
    private let productIdentifiers: Set<String> = [
        "com.app.subscription.monthly",
        "com.app.subscription.yearly",
        "com.app.premium.lifetime"
    ]

    // Product cache to avoid repeated network calls
    private var cachedProducts: [Product] = []

    func loadProducts() async throws -> [Product] {
        // Return cache if available
        guard cachedProducts.isEmpty else {
            return cachedProducts
        }

        // Asynchronous request to the 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 el 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.

ProductType+Extensions.swiftswift
import StoreKit

extension Product.ProductType {

    var displayName: String {
        switch self {
        case .consumable:
            // Consumable products (credits, lives, etc.)
            return "Consumable"
        case .nonConsumable:
            // Permanent purchases (feature unlock)
            return "Non-Consumable"
        case .autoRenewable:
            // Subscriptions with automatic renewal
            return "Auto-Renewable Subscription"
        case .nonRenewable:
            // Subscriptions without renewal (temporary pass)
            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

A partir de iOS 18.2, las compras requieren un parámetro de contexto de UI. Este cambio asegura que la hoja de compra aparezca correctamente en la jerarquía de vistas de la aplicación.

PurchaseManager.swiftswift
import StoreKit
import UIKit

actor PurchaseManager {

    enum PurchaseError: Error {
        case productNotFound
        case purchaseCancelled
        case purchasePending
        case verificationFailed
        case unknown
    }

    func purchase(_ product: Product, in scene: UIWindowScene) async throws -> Transaction {
        // Initiate purchase with UI context (iOS 18.2+)
        let result = try await product.purchase(confirmIn: scene)

        switch result {
        case .success(let verification):
            // Verify the transaction signature
            let transaction = try checkVerification(verification)

            // Mark the transaction as finished
            await transaction.finish()

            return transaction

        case .userCancelled:
            // User cancelled the purchase modal
            throw PurchaseError.purchaseCancelled

        case .pending:
            // Purchase pending (parental approval, etc.)
            throw PurchaseError.purchasePending

        @unknown default:
            throw PurchaseError.unknown
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            // Transaction signed and verified by the App Store
            return safe
        case .unverified(_, let error):
            // Invalid or corrupted signature
            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.).

TransactionObserver.swiftswift
import StoreKit

actor TransactionObserver {

    private var updateTask: Task<Void, Never>?

    func startObserving() {
        // Cancel any previous observation
        updateTask?.cancel()

        updateTask = Task(priority: .background) {
            // Infinite stream of transaction updates
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerification(result)

                    // Process the transaction based on its type
                    await self.handleTransaction(transaction)

                    // Always finish the transaction
                    await transaction.finish()
                } catch {
                    // Log the error for debugging
                    print("Transaction verification failed: (error)")
                }
            }
        }
    }

    func stopObserving() {
        updateTask?.cancel()
        updateTask = nil
    }

    private func handleTransaction(_ transaction: Transaction) async {
        switch transaction.productType {
        case .autoRenewable:
            // Update subscription status
            await updateSubscriptionStatus(transaction)
        case .nonConsumable:
            // Unlock premium feature
            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 {
        // Implementation of status update
    }

    private func unlockFeature(_ productID: String) async {
        // Implementation of feature unlock
    }

    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. Para más temas de entrevista Swift, consulta la guía de Swift structured concurrency.

Pregunta 1: ¿Cómo Verificar el Estado Actual de una Suscripción?

StoreKit 2 proporciona acceso directo a los entitlements activos. Nota que Transaction.currentEntitlement(for:) está deprecado desde iOS 18.4. El nuevo currentEntitlements(for:) retorna una secuencia asíncrona para manejar múltiples entitlements (por ejemplo, suscripción más acceso Family Sharing).

SubscriptionStatusChecker.swiftswift
import StoreKit

struct SubscriptionStatusChecker {

    struct SubscriptionStatus {
        let isActive: Bool
        let expirationDate: Date?
        let willRenew: Bool
        let productID: String?
        let appTransactionID: UInt64?  // iOS 18.4+
    }

    static func checkCurrentStatus(for productID: String) async -> SubscriptionStatus {
        // Use the new currentEntitlements(for:) API (iOS 18.4+)
        var latestTransaction: Transaction?

        for await result in Transaction.currentEntitlements(for: productID) {
            if case .verified(let transaction) = result {
                if transaction.productType == .autoRenewable {
                    if latestTransaction == nil ||
                       transaction.purchaseDate > latestTransaction!.purchaseDate {
                        latestTransaction = transaction
                    }
                }
            }
        }

        guard let transaction = latestTransaction else {
            return SubscriptionStatus(
                isActive: false,
                expirationDate: nil,
                willRenew: false,
                productID: nil,
                appTransactionID: nil
            )
        }

        // Check if the subscription is still active
        let isActive = transaction.expirationDate ?? Date.distantPast > Date()

        // Retrieve renewal information
        let renewalInfo = await getRenewalInfo(for: transaction.productID)

        return SubscriptionStatus(
            isActive: isActive,
            expirationDate: transaction.expirationDate,
            willRenew: renewalInfo?.willAutoRenew ?? false,
            productID: transaction.productID,
            appTransactionID: transaction.appTransactionID
        )
    }

    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
        }

        // Retrieve renewal status
        let status = try? await subscription.status.first
        return status?.renewalInfo
    }
}

Este enfoque proporciona el estado preciso de la suscripción sin llamadas al servidor. El campo appTransactionID, introducido en iOS 18.4, permite el seguimiento de clientes a través de transacciones sin llamadas servidor a 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. Desde iOS 18.4, las ofertas promocionales pueden firmarse usando JWS con el nuevo PromotionOfferV2SignatureCreator disponible en la App Store Server Library.

OfferManager.swiftswift
import StoreKit

struct OfferManager {

    enum OfferType {
        case freeTrial(duration: String)
        case payAsYouGo(periods: Int, price: Decimal)
        case payUpFront(duration: String, price: Decimal)
        case oneTime(price: Decimal)  // iOS 17.2+: offer codes for all product types
        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:
            // Free trial period
            return .freeTrial(duration: formatPeriod(offer.period))

        case .payAsYouGo:
            // Reduced price over multiple periods
            return .payAsYouGo(
                periods: offer.periodCount,
                price: offer.price
            )

        case .payUpFront:
            // Single payment for a period
            return .payUpFront(
                duration: formatPeriod(offer.period),
                price: offer.price
            )

        case .oneTime:
            // One-time offer code (consumables, non-consumables, non-renewing)
            return .oneTime(price: offer.price)

        @unknown default:
            return .none
        }
    }

    static func checkEligibility(for product: Product) async -> Bool {
        // Check if user can benefit from the offer
        guard let subscription = product.subscription else {
            return false
        }

        // isEligibleForIntroOffer indicates if this is a new subscriber
        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. Nota que los códigos de oferta, anteriormente limitados a suscripciones auto-renovables, ahora soportan consumibles, no consumibles y suscripciones no renovables desde iOS 16.3.

Pregunta 3: ¿Cómo Restaurar Compras Anteriores?

La restauración de compras es una funcionalidad obligatoria según las App Store Review Guidelines.

RestoreManager.swiftswift
import StoreKit

actor RestoreManager {

    struct RestoreResult {
        let restoredProducts: [String]
        let activeSubscriptions: [String]
    }

    func restorePurchases() async throws -> RestoreResult {
        var restoredProducts: [String] = []
        var activeSubscriptions: [String] = []

        // Synchronize with the App Store
        try await AppStore.sync()

        // Iterate through all user transactions
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            switch transaction.productType {
            case .nonConsumable:
                // Restore permanent purchases
                restoredProducts.append(transaction.productID)

            case .autoRenewable:
                // Check if subscription is active
                if let expiration = transaction.expirationDate,
                   expiration > Date() {
                    activeSubscriptions.append(transaction.productID)
                }

            default:
                // Consumables are not restorable
                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.

SubscriptionOfferView: Presentación Moderna de Suscripciones

iOS 18.4 introduce SubscriptionOfferView, un componente SwiftUI que muestra las opciones de suscripción con carga automática de metadatos del producto. Este es un tema común de entrevista para desarrolladores que construyen aplicaciones basadas en suscripciones.

SubscriptionOfferViewExample.swiftswift
import SwiftUI
import StoreKit

struct SubscriptionSettingsView: View {
    let subscriptionGroupID = "com.app.subscription.group"
    
    var body: some View {
        VStack(spacing: 16) {
            // Show upgrade options for current subscribers
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.upgrade)
                .prefersPromotionalIcon(true)
                .subscriptionOfferViewDetailAction { path in
                    path.append("subscriptionDetail")
                }
            
            // Show current plan details
            SubscriptionOfferView(groupID: subscriptionGroupID)
                .visibleRelationship(.current)
        }
        .padding()
    }
}

El parámetro visibleRelationship controla qué niveles de suscripción aparecen: .upgrade muestra planes un nivel más alto, .downgrade muestra niveles inferiores, .crossgrade muestra niveles equivalentes con mejor valor, y .all muestra todos los planes del grupo.

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 usa JWS (JSON Web Signatures) para validación moderna. El endpoint deprecado verifyReceipt ha sido reemplazado por la App Store Server API.

Extracción de Datos de Transacción para el Servidor

ServerValidation.swiftswift
import StoreKit

struct TransactionPayload: Codable {
    let transactionID: String
    let originalTransactionID: String
    let productID: String
    let purchaseDate: Date
    let expirationDate: Date?
    let jwsRepresentation: String
    let appTransactionID: String?  // iOS 18.4+
}

actor ServerValidationService {

    private let apiEndpoint = "https://api.example.com/verify"

    func validateWithServer(_ transaction: Transaction) async throws -> Bool {
        // Create payload for the server
        let payload = TransactionPayload(
            transactionID: String(transaction.id),
            originalTransactionID: String(transaction.originalID),
            productID: transaction.productID,
            purchaseDate: transaction.purchaseDate,
            expirationDate: transaction.expirationDate,
            jwsRepresentation: transaction.jwsRepresentation ?? "",
            appTransactionID: transaction.appTransactionID.map { String($0) }
        )

        // Encode to JSON
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let jsonData = try encoder.encode(payload)

        // Send to server
        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
        }

        // Decode server response
        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. El nuevo campo appTransactionID permite el seguimiento preciso de clientes sin viajes adicionales al servidor.

App Store Server API

La App Store Server API de Apple proporciona validación del lado del servidor con tokens JWS, Server Notifications V2 para eventos de suscripción en tiempo real, y la nueva SubscriptionStatus API que acepta Transaction IDs (iOS 18.4+) para consultar el estado directamente.

Configuración de Notificaciones del Servidor

Las Server Notifications V2 permiten la recepción de eventos en tiempo real.

ServerNotificationTypes.swiftswift
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. Para más información sobre patrones de arquitectura iOS, consulta la guía de entrevista senior iOS sobre design patterns.

Errores Comunes en StoreKit 2

PurchaseErrorHandler.swiftswift
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 {
        // Check if it's a StoreKit error
        if let storeKitError = error as? StoreKitError {
            switch storeKitError {
            case .networkError:
                return .networkError
            case .userCancelled:
                // Don't show error, user cancelled
                return .unknown
            case .notAvailableInStorefront:
                return .productNotAvailable
            case .notEntitled:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        // Purchase errors
        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

OfflineCapabilityManager.swiftswift
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
            }
        }

        // Save locally
        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 [:]
        }

        // Filter expired entitlements
        let now = Date()
        return entitlements.filter { _, expiration in
            expiration > now
        }
    }

    func hasValidEntitlement(for productID: String) -> Bool {
        let cached = getCachedEntitlements()
        return cached[productID] != nil
    }
}

El caché local mantiene el acceso a las funciones premium incluso sin conectividad.

Mejores Prácticas para Producción

Arquitectura Recomendada

SubscriptionService.swiftswift
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() {
        // Start transaction observation
        startTransactionObserver()

        // Load initial state
        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.

Sources

Puntos Clave para Entrevistas de StoreKit 2

StoreKit 2 transforma la gestión de compras integradas en una experiencia de desarrollo moderna y segura. Puntos clave para recordar para una entrevista:

  • La API nativa async/await elimina los delegados y callbacks complejos de StoreKit 1, que está deprecado desde iOS 18
  • La verificación automática mediante VerificationResult maneja la validación criptográfica de transacciones
  • Transaction.updates proporciona un flujo asíncrono para transacciones en segundo plano incluyendo renovaciones y family sharing
  • Transaction.currentEntitlements(for:) reemplaza el deprecado currentEntitlement(for:) para manejar múltiples entitlements
  • El campo appTransactionID (iOS 18.4+) permite el seguimiento de clientes sin llamadas servidor a servidor
  • SubscriptionOfferView proporciona UI integrada para presentación de suscripciones con visibleRelationship para opciones de upgrade, downgrade y crossgrade
  • Las ofertas promocionales pueden firmarse usando JWS con PromotionOfferV2SignatureCreator
  • Los códigos de oferta ahora soportan consumibles, no consumibles y suscripciones no renovables (iOS 16.3+)
  • La validación del servidor usa la App Store Server API con tokens JWS, reemplazando el endpoint deprecado verifyReceipt
  • Siempre llamar a transaction.finish() después de la entrega del contenido para evitar comportamiento de restauración inesperado
  • Implementar caché local de entitlements para una experiencia de usuario fluida sin conectividad

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Reto diario

¿Sabrías detectar el bug en iOS?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador de SharpSkill

Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.

Actualizado el 20 de octubre de 2026

Etiquetas

#ios
#storekit
#in-app-purchase
#swift
#interview

Compartir

Artículos relacionados