Colloquio StoreKit 2: Gestione Abbonamenti e Validazione Ricevute
Padroneggia le domande di colloquio iOS su StoreKit 2, gestione abbonamenti, validazione ricevute e implementazione degli acquisti in-app con esempi pratici di codice Swift.

StoreKit 2 rappresenta un cambiamento fondamentale nel modo in cui vengono gestiti gli acquisti in-app su iOS. Introdotto con iOS 15, questo framework moderno semplifica drasticamente il codice necessario per la gestione degli abbonamenti e la validazione delle transazioni. I colloqui tecnici iOS affrontano regolarmente questo argomento per valutare l'esperienza degli sviluppatori nella monetizzazione delle app.
StoreKit 2 sfrutta un'API Swift nativa con async/await, eliminando la necessità di callback complessi e validazione delle ricevute lato client. Gli intervistatori apprezzano i candidati che sanno articolare le differenze fondamentali con StoreKit 1.
Panoramica dell'Architettura StoreKit 2
StoreKit 2 è costruito su un'architettura moderna che sfrutta appieno Swift Concurrency. A differenza di StoreKit 1, tutte le operazioni sono asincrone e utilizzano tipi nativi di Swift.
Recupero dei Prodotti Disponibili
Il primo passo consiste nel caricare i prodotti configurati su App Store Connect. StoreKit 2 semplifica questa operazione con un'API dichiarativa.
import StoreKit
actor ProductService {
// Identificatori dei prodotti configurati su App Store Connect
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// Cache dei prodotti per evitare chiamate di rete ripetute
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// Restituisce la cache se disponibile
guard cachedProducts.isEmpty else {
return cachedProducts
}
// Richiesta asincrona all'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'utilizzo di un actor garantisce la thread-safety nella gestione della cache dei prodotti.
Tipi di Prodotti in StoreKit 2
StoreKit 2 distingue chiaramente tra i diversi tipi di prodotti. Comprendere questa distinzione è essenziale per i colloqui.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// Prodotti consumabili (crediti, vite, ecc.)
return "Consumable"
case .nonConsumable:
// Acquisti permanenti (sblocco funzionalità)
return "Non-Consumable"
case .autoRenewable:
// Abbonamenti con rinnovo automatico
return "Auto-Renewable Subscription"
case .nonRenewable:
// Abbonamenti senza rinnovo (pass temporaneo)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}Gli abbonamenti con rinnovo automatico rappresentano il modello di business dominante per le applicazioni iOS moderne.
Gestione delle Transazioni di Acquisto
Il processo di acquisto in StoreKit 2 diventa lineare grazie ad async/await. Niente più delegate e callback annidati di StoreKit 1.
Avvio di un Acquisto
import StoreKit
actor PurchaseManager {
enum PurchaseError: Error {
case productNotFound
case purchaseCancelled
case purchasePending
case verificationFailed
case unknown
}
func purchase(_ product: Product) async throws -> Transaction {
// Avvia l'acquisto con il modale di sistema
let result = try await product.purchase()
switch result {
case .success(let verification):
// Verifica la firma della transazione
let transaction = try checkVerification(verification)
// Contrassegna la transazione come completata
await transaction.finish()
return transaction
case .userCancelled:
// L'utente ha annullato il modale di acquisto
throw PurchaseError.purchaseCancelled
case .pending:
// Acquisto in attesa (approvazione genitoriale, ecc.)
throw PurchaseError.purchasePending
@unknown default:
throw PurchaseError.unknown
}
}
private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
// Transazione firmata e verificata dall'App Store
return safe
case .unverified(_, let error):
// Firma non valida o corrotta
throw PurchaseError.verificationFailed
}
}
}Il metodo finish() è cruciale: segnala all'App Store che il contenuto è stato consegnato all'utente.
Non dimenticare mai di chiamare transaction.finish() dopo aver consegnato il contenuto. Una transazione non finalizzata verrà ripristinata al successivo avvio dell'app, causando comportamenti imprevisti.
Ascolto delle Transazioni in Background
StoreKit 2 introduce Transaction.updates, uno stream asincrono che emette le transazioni che avvengono al di fuori dell'app (rinnovi, acquisti family sharing, ecc.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// Annulla qualsiasi osservazione precedente
updateTask?.cancel()
updateTask = Task(priority: .background) {
// Stream infinito di aggiornamenti delle transazioni
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// Elabora la transazione in base al suo tipo
await self.handleTransaction(transaction)
// Finalizza sempre la transazione
await transaction.finish()
} catch {
// Registra l'errore per il debug
print("Transaction verification failed: \(error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// Aggiorna lo stato dell'abbonamento
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// Sblocca la funzionalità 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 {
// Implementazione dell'aggiornamento dello stato
}
private func unlockFeature(_ productID: String) async {
// Implementazione dello sblocco delle funzionalità
}
enum PurchaseError: Error {
case verificationFailed
}
}L'osservazione delle transazioni dovrebbe iniziare non appena l'app viene avviata per evitare di perdere aggiornamenti.
Domande Comuni di Colloquio sugli Abbonamenti StoreKit 2
I colloqui iOS includono spesso domande specifiche sulla gestione degli abbonamenti. Ecco le più comuni con risposte dettagliate.
Domanda 1: Come Verificare lo Stato Attuale dell'Abbonamento?
StoreKit 2 fornisce accesso diretto agli entitlement attivi tramite 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 tutte le transazioni attive
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// Filtra solo gli abbonamenti
if transaction.productType == .autoRenewable {
// Mantiene la transazione più recente
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 se l'abbonamento è ancora attivo
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// Recupera le informazioni di rinnovo
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 lo stato di rinnovo
let status = try? await subscription.status.first
return status?.renewalInfo
}
}Questo approccio fornisce lo stato preciso dell'abbonamento senza chiamate al server.
Pronto a superare i tuoi colloqui su iOS?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Domanda 2: Come Gestire Periodi di Prova Gratuiti e Offerte Promozionali?
StoreKit 2 semplifica l'accesso alle informazioni sulle offerte. Gli intervistatori spesso testano la comprensione dei diversi tipi di offerte.
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:
// Periodo di prova gratuito
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// Prezzo ridotto su più periodi
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// Pagamento unico per un periodo
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// Verifica se l'utente può beneficiare dell'offerta
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer indica se è un nuovo abbonato
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 ""
}
}
}Il metodo isEligibleForIntroOffer è essenziale per evitare di mostrare offerte non disponibili.
Domanda 3: Come Ripristinare gli Acquisti Precedenti?
Il ripristino degli acquisti è una funzionalità obbligatoria secondo le linee guida dell'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] = []
// Sincronizza con l'App Store
try await AppStore.sync()
// Itera attraverso tutte le transazioni dell'utente
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// Ripristina gli acquisti permanenti
restoredProducts.append(transaction.productID)
case .autoRenewable:
// Verifica se l'abbonamento è attivo
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// I consumabili non sono ripristinabili
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}AppStore.sync() forza la sincronizzazione con i server Apple, utile dopo una reinstallazione.
Validazione delle Ricevute Lato Server
La validazione lato server rimane consigliata per le applicazioni con abbonamenti critici. StoreKit 2 introduce JWS (JSON Web Signatures) per una validazione moderna.
Estrazione dei Dati di Transazione per il Server
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 il payload per il server
let payload = TransactionPayload(
transactionID: String(transaction.id),
originalTransactionID: String(transaction.originalID),
productID: transaction.productID,
purchaseDate: transaction.purchaseDate,
expirationDate: transaction.expirationDate,
jwsRepresentation: transaction.jwsRepresentation ?? ""
)
// Codifica in JSON
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// Invia al 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
}
// Decodifica la risposta del server
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 crittografica verificabile dal server.
Apple ora fornisce l'App Store Server API per la validazione lato server. Questa API moderna sostituisce l'endpoint deprecato verifyReceipt e offre funzionalità avanzate come le notifiche server-to-server.
Configurazione delle Notifiche del Server
Le Server Notifications V2 consentono la ricezione di eventi in tempo reale.
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?
}
}Queste notifiche consentono di mantenere lo stato degli abbonamenti sincronizzato con il backend.
Gestione degli Errori e Casi Limite
I colloqui spesso testano la comprensione degli scenari di errore e della loro corretta gestione.
Errori Comuni in 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 se è un errore StoreKit
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// Non mostrare errore, l'utente ha annullato
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// Errori di acquisto
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
}
}Una gestione chiara degli errori migliora l'esperienza utente e semplifica il debug.
Supporto alla Modalità Offline
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
}
}
// Salva 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 gli entitlement scaduti
let now = Date()
return entitlements.filter { _, expiration in
expiration > now
}
}
func hasValidEntitlement(for productID: String) -> Bool {
let cached = getCachedEntitlements()
return cached[productID] != nil
}
}La cache locale mantiene l'accesso alle funzionalità premium anche senza connettività.
Best Practice per la Produzione
Architettura Consigliata
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() {
// Avvia l'osservazione delle transazioni
startTransactionObserver()
// Carica lo stato iniziale
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()
}
}Questa architettura centralizzata semplifica la gestione dello stato degli acquisti in tutta l'applicazione.
Conclusione
StoreKit 2 trasforma la gestione degli acquisti in-app in un'esperienza di sviluppo moderna e sicura. Punti chiave da ricordare per un colloquio:
✅ API nativa async/await: niente più delegate e callback complessi di StoreKit 1
✅ Verifica automatica: VerificationResult gestisce la validazione crittografica delle transazioni
✅ Transaction.updates: stream asincrono per le transazioni in background
✅ Transaction.currentEntitlements: accesso diretto agli entitlement attivi dell'utente
✅ Offerte promozionali: isEligibleForIntroOffer per verificare l'idoneità
✅ Validazione server: JWS e App Store Server API per maggiore sicurezza
✅ transaction.finish(): obbligatorio dopo la consegna del contenuto
✅ Gestione offline: cache locale degli entitlement per un'esperienza fluida
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Swift Testing Framework Colloquio 2026: Macro #expect e #require vs XCTest
Padroneggia il nuovo Swift Testing Framework per i colloqui iOS: macro #expect e #require, migrazione da XCTest, pattern avanzati ed errori comuni.

Colloquio iOS Push Notifications 2026: APNs, token e troubleshooting
Guida completa per preparare i colloqui iOS su Push Notifications, APNs, gestione dei token e troubleshooting. Domande frequenti con risposte dettagliate.

Le 25 domande più frequenti nei colloqui Swift per sviluppatori iOS
Preparazione ai colloqui iOS con le 25 domande Swift più comuni: optionals, closures, ARC, protocolli, async/await e pattern avanzati.