StoreKit 2 en entretien : gestion des abonnements et validation des receipts

Préparez vos entretiens iOS avec ce guide complet sur StoreKit 2, la gestion des abonnements, la validation des receipts et les nouvelles fonctionnalités iOS 18.4.

StoreKit 2 iOS architecture des abonnements et validation receipts

StoreKit 2 représente une évolution majeure pour la gestion des achats in-app sur iOS. Introduit avec iOS 15, ce framework moderne simplifie considérablement le code nécessaire pour gérer les abonnements et la validation des transactions. Avec iOS 18, StoreKit 1 est désormais officiellement déprécié, faisant de StoreKit 2 la seule voie supportée. Les entretiens iOS techniques abordent régulièrement ce sujet pour évaluer la maîtrise des développeurs sur la monétisation des applications.

Point clé pour l'entretien

StoreKit 2 utilise une API Swift native avec async/await, éliminant le besoin de callbacks complexes et de la validation manuelle des receipts côté client. Les recruteurs apprécient les candidats capables d'expliquer les différences fondamentales avec StoreKit 1, ainsi que les nouvelles fonctionnalités iOS 18.4 comme appTransactionID et SubscriptionOfferView.

Architecture de StoreKit 2

StoreKit 2 repose sur une architecture moderne qui tire parti de Swift Concurrency. Contrairement à StoreKit 1, toutes les opérations sont asynchrones et utilisent les types natifs Swift.

Récupération des produits disponibles

La première étape consiste à charger les produits configurés dans App Store Connect. StoreKit 2 simplifie cette opération avec une API déclarative.

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 }
    }
}

L'utilisation d'un actor garantit la thread-safety lors de la gestion du cache des produits.

Types de produits StoreKit 2

StoreKit 2 distingue clairement les différents types de produits. Cette distinction est importante à connaître pour les entretiens.

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"
        }
    }
}

Les abonnements auto-renouvelables constituent le modèle économique dominant pour les applications iOS modernes.

Gestion des transactions d'achat

Le processus d'achat avec StoreKit 2 devient linéaire grâce à async/await. Fini les delegates et les callbacks imbriqués de StoreKit 1.

Initiation d'un achat

À partir d'iOS 18.2, les achats nécessitent un paramètre de contexte UI. Ce changement garantit que la feuille d'achat apparaît correctement dans la hiérarchie des vues de l'application.

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
        }
    }
}

La méthode finish() est cruciale : elle indique à l'App Store que le contenu a été livré à l'utilisateur.

Attention en entretien

Ne jamais oublier d'appeler transaction.finish() après avoir délivré le contenu. Une transaction non terminée sera restaurée au prochain lancement de l'application, causant des comportements inattendus.

Écoute des transactions en arrière-plan

StoreKit 2 introduit Transaction.updates, un stream asynchrone qui émet les transactions survenues en dehors de l'application (renouvellements, achats familiaux, 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
    }
}

L'observation des transactions doit démarrer dès le lancement de l'application pour ne manquer aucune mise à jour.

Questions fréquentes sur les abonnements StoreKit 2

Les entretiens iOS comportent souvent des questions spécifiques sur la gestion des abonnements. Voici les plus courantes avec leurs réponses détaillées. Pour d'autres sujets d'entretien Swift, consultez le guide sur Swift structured concurrency.

Question 1 : Comment vérifier le statut actuel d'un abonnement ?

StoreKit 2 fournit un accès direct aux entitlements actifs. Notez que Transaction.currentEntitlement(for:) est déprécié depuis iOS 18.4. La nouvelle méthode currentEntitlements(for:) retourne une séquence asynchrone pour gérer plusieurs entitlements (par exemple, abonnement plus accès 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
    }
}

Cette approche permet d'obtenir un état précis de l'abonnement sans appel serveur. Le champ appTransactionID, introduit avec iOS 18.4, permet le suivi des clients à travers les transactions sans appels serveur à serveur.

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Question 2 : Comment gérer les périodes d'essai et les offres promotionnelles ?

StoreKit 2 simplifie l'accès aux informations sur les offres. Les recruteurs testent souvent la compréhension des différents types d'offres. Depuis iOS 18.4, les offres promotionnelles peuvent être signées avec JWS via le nouveau PromotionOfferV2SignatureCreator disponible dans 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 ""
        }
    }
}

La méthode isEligibleForIntroOffer est essentielle pour éviter d'afficher des offres non disponibles. Notez que les codes d'offre, auparavant limités aux abonnements auto-renouvelables, supportent désormais les consommables, non-consommables et abonnements non-renouvelables depuis iOS 16.3.

Question 3 : Comment restaurer les achats précédents ?

La restauration des achats est une fonctionnalité obligatoire selon les 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() force une synchronisation avec les serveurs Apple, utile après une réinstallation.

SubscriptionOfferView : présentation moderne des abonnements

iOS 18.4 introduit SubscriptionOfferView, un composant SwiftUI qui affiche les options d'abonnement avec chargement automatique des métadonnées produit. C'est un sujet d'entretien courant pour les développeurs créant des applications basées sur les abonnements.

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()
    }
}

Le paramètre visibleRelationship contrôle quels niveaux d'abonnement apparaissent : .upgrade affiche les plans d'un niveau supérieur, .downgrade les niveaux inférieurs, .crossgrade les niveaux équivalents avec meilleure valeur, et .all affiche tous les plans du groupe.

Validation des receipts côté serveur

La validation serveur reste recommandée pour les applications avec des abonnements critiques. StoreKit 2 utilise les JWS (JSON Web Signatures) pour une validation moderne. L'endpoint déprécié verifyReceipt a été remplacé par l'App Store Server API.

Extraction des données de transaction pour le serveur

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?
}

Le jwsRepresentation contient la signature cryptographique vérifiable par le serveur. Le nouveau champ appTransactionID permet un suivi précis des clients sans allers-retours serveur supplémentaires.

App Store Server API

L'App Store Server API d'Apple fournit une validation côté serveur avec des tokens JWS, les Server Notifications V2 pour les événements d'abonnement en temps réel, et la nouvelle SubscriptionStatus API qui accepte les Transaction IDs (iOS 18.4+) pour interroger directement le statut.

Configuration des notifications serveur

Les Server Notifications V2 permettent de recevoir les événements en temps réel.

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?
    }
}

Ces notifications permettent de maintenir l'état des abonnements synchronisé avec le backend.

Gestion des erreurs et cas limites

Les entretiens testent souvent la compréhension des scénarios d'erreur et leur gestion appropriée. Pour plus d'informations sur les patterns d'architecture iOS, consultez le guide d'entretien senior iOS sur les design patterns.

Erreurs courantes 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
    }
}

Une gestion des erreurs claire améliore l'expérience utilisateur et facilite le debugging.

Gestion du mode hors ligne

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
    }
}

Le cache local permet de maintenir l'accès aux fonctionnalités premium même sans connexion.

Bonnes pratiques pour la production

Architecture recommandée

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()
    }
}

Cette architecture centralisée facilite la gestion de l'état des achats dans toute l'application.

Sources

Points clés pour les entretiens StoreKit 2

StoreKit 2 transforme la gestion des achats in-app en une expérience de développement moderne et sécurisée. Points essentiels à retenir pour un entretien :

  • L'API native async/await élimine les delegates et callbacks complexes de StoreKit 1, désormais déprécié depuis iOS 18
  • La vérification automatique via VerificationResult gère la validation cryptographique des transactions
  • Transaction.updates fournit un stream asynchrone pour les transactions en arrière-plan incluant les renouvellements et le family sharing
  • Transaction.currentEntitlements(for:) remplace le déprécié currentEntitlement(for:) pour gérer plusieurs entitlements
  • Le champ appTransactionID (iOS 18.4+) permet le suivi des clients sans appels serveur à serveur
  • SubscriptionOfferView fournit une UI intégrée pour la présentation des abonnements avec visibleRelationship pour les options upgrade, downgrade et crossgrade
  • Les offres promotionnelles peuvent être signées avec JWS via PromotionOfferV2SignatureCreator
  • Les codes d'offre supportent désormais les consommables, non-consommables et abonnements non-renouvelables (iOS 16.3+)
  • La validation serveur utilise l'App Store Server API avec des tokens JWS, remplaçant l'endpoint déprécié verifyReceipt
  • Toujours appeler transaction.finish() après la livraison du contenu pour éviter un comportement de restauration inattendu
  • Implémenter le cache local des entitlements pour une expérience utilisateur fluide sans connectivité

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Défi du jour

Tu saurais repérer le bug en iOS ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 20 octobre 2026

Tags

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

Partager

Articles similaires