Entrevista StoreKit 2: Gerenciamento de Assinaturas e Validação de Recibos
Domine perguntas de entrevista iOS sobre StoreKit 2, gerenciamento de assinaturas, validação de recibos e implementação de compras no aplicativo com exemplos práticos em Swift.

O StoreKit 2 representa uma mudança fundamental na forma como as compras no aplicativo são tratadas no iOS. Introduzido com o iOS 15, este framework moderno simplifica drasticamente o código necessário para o gerenciamento de assinaturas e validação de transações. As entrevistas técnicas iOS abordam regularmente este tema para avaliar a experiência dos desenvolvedores em monetização de aplicativos.
O StoreKit 2 aproveita uma API nativa do Swift com async/await, eliminando a necessidade de callbacks complexos e validação de recibos no lado do cliente. Os entrevistadores valorizam candidatos que conseguem articular as diferenças fundamentais em relação ao StoreKit 1.
Visão Geral da Arquitetura StoreKit 2
O StoreKit 2 é construído sobre uma arquitetura moderna que aproveita ao máximo o Swift Concurrency. Diferente do StoreKit 1, todas as operações são assíncronas e usam tipos nativos do Swift.
Buscando Produtos Disponíveis
O primeiro passo envolve carregar os produtos configurados no App Store Connect. O StoreKit 2 simplifica esta operação com uma API declarativa.
import StoreKit
actor ProductService {
// Identificadores de produtos configurados no App Store Connect
private let productIdentifiers: Set<String> = [
"com.app.subscription.monthly",
"com.app.subscription.yearly",
"com.app.premium.lifetime"
]
// Cache de produtos para evitar chamadas de rede repetidas
private var cachedProducts: [Product] = []
func loadProducts() async throws -> [Product] {
// Retorna o cache se disponível
guard cachedProducts.isEmpty else {
return cachedProducts
}
// Solicitação assíncrona à 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 }
}
}O uso de um actor garante a segurança de threads ao gerenciar o cache de produtos.
Tipos de Produtos no StoreKit 2
O StoreKit 2 distingue claramente entre os diferentes tipos de produtos. Compreender essa distinção é essencial para entrevistas.
import StoreKit
extension Product.ProductType {
var displayName: String {
switch self {
case .consumable:
// Produtos consumíveis (créditos, vidas, etc.)
return "Consumable"
case .nonConsumable:
// Compras permanentes (desbloqueio de recursos)
return "Non-Consumable"
case .autoRenewable:
// Assinaturas com renovação automática
return "Auto-Renewable Subscription"
case .nonRenewable:
// Assinaturas sem renovação (passe temporário)
return "Non-Renewable Subscription"
@unknown default:
return "Unknown"
}
}
}As assinaturas com renovação automática representam o modelo de negócio dominante para aplicativos iOS modernos.
Tratando Transações de Compra
O processo de compra no StoreKit 2 torna-se linear graças ao async/await. Acabaram-se os delegates e callbacks aninhados do StoreKit 1.
Iniciando uma 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 a compra com o modal do sistema
let result = try await product.purchase()
switch result {
case .success(let verification):
// Verifica a assinatura da transação
let transaction = try checkVerification(verification)
// Marca a transação como finalizada
await transaction.finish()
return transaction
case .userCancelled:
// Usuário cancelou o modal de compra
throw PurchaseError.purchaseCancelled
case .pending:
// Compra pendente (aprovação 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):
// Transação assinada e verificada pela App Store
return safe
case .unverified(_, let error):
// Assinatura inválida ou corrompida
throw PurchaseError.verificationFailed
}
}
}O método finish() é crucial: sinaliza à App Store que o conteúdo foi entregue ao usuário.
Nunca esqueça de chamar transaction.finish() após entregar o conteúdo. Uma transação não finalizada será restaurada na próxima inicialização do app, causando comportamentos inesperados.
Escutando Transações em Segundo Plano
O StoreKit 2 introduz Transaction.updates, um stream assíncrono que emite as transações que ocorrem fora do aplicativo (renovações, compras de family sharing, etc.).
import StoreKit
actor TransactionObserver {
private var updateTask: Task<Void, Never>?
func startObserving() {
// Cancela qualquer observação anterior
updateTask?.cancel()
updateTask = Task(priority: .background) {
// Stream infinito de atualizações de transações
for await result in Transaction.updates {
do {
let transaction = try self.checkVerification(result)
// Processa a transação de acordo com seu tipo
await self.handleTransaction(transaction)
// Sempre finaliza a transação
await transaction.finish()
} catch {
// Registra o erro para depuração
print("Transaction verification failed: \(error)")
}
}
}
}
func stopObserving() {
updateTask?.cancel()
updateTask = nil
}
private func handleTransaction(_ transaction: Transaction) async {
switch transaction.productType {
case .autoRenewable:
// Atualiza o status da assinatura
await updateSubscriptionStatus(transaction)
case .nonConsumable:
// Desbloqueia o recurso 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 {
// Implementação da atualização de status
}
private func unlockFeature(_ productID: String) async {
// Implementação do desbloqueio de recursos
}
enum PurchaseError: Error {
case verificationFailed
}
}A observação de transações deve começar assim que o aplicativo for iniciado para evitar perder qualquer atualização.
Perguntas Comuns de Entrevista sobre Assinaturas StoreKit 2
As entrevistas iOS frequentemente incluem perguntas específicas sobre o gerenciamento de assinaturas. Aqui estão as mais comuns com respostas detalhadas.
Pergunta 1: Como Verificar o Status Atual da Assinatura?
O StoreKit 2 fornece acesso direto aos entitlements ativos via 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 as transações ativas
var latestTransaction: Transaction?
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
// Filtra apenas assinaturas
if transaction.productType == .autoRenewable {
// Mantém a transação mais 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 a assinatura ainda está ativa
let isActive = transaction.expirationDate ?? Date.distantPast > Date()
// Recupera as informações de renovação
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 o status de renovação
let status = try? await subscription.status.first
return status?.renewalInfo
}
}Essa abordagem fornece o estado preciso da assinatura sem chamadas ao servidor.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Pergunta 2: Como Tratar Trials Gratuitos e Ofertas Promocionais?
O StoreKit 2 simplifica o acesso às informações das ofertas. Os entrevistadores frequentemente testam o entendimento dos 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 avaliação gratuita
return .freeTrial(duration: formatPeriod(offer.period))
case .payAsYouGo:
// Preço reduzido durante vários períodos
return .payAsYouGo(
periods: offer.periodCount,
price: offer.price
)
case .payUpFront:
// Pagamento único por um período
return .payUpFront(
duration: formatPeriod(offer.period),
price: offer.price
)
@unknown default:
return .none
}
}
static func checkEligibility(for product: Product) async -> Bool {
// Verifica se o usuário pode se beneficiar da oferta
guard let subscription = product.subscription else {
return false
}
// isEligibleForIntroOffer indica se é um novo assinante
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 ""
}
}
}O método isEligibleForIntroOffer é essencial para evitar exibir ofertas indisponíveis.
Pergunta 3: Como Restaurar Compras Anteriores?
A restauração de compras é uma funcionalidade obrigatória conforme as diretrizes da 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 com a App Store
try await AppStore.sync()
// Itera por todas as transações do usuário
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
switch transaction.productType {
case .nonConsumable:
// Restaura compras permanentes
restoredProducts.append(transaction.productID)
case .autoRenewable:
// Verifica se a assinatura está ativa
if let expiration = transaction.expirationDate,
expiration > Date() {
activeSubscriptions.append(transaction.productID)
}
default:
// Consumíveis não podem ser restaurados
break
}
}
return RestoreResult(
restoredProducts: restoredProducts,
activeSubscriptions: activeSubscriptions
)
}
}O AppStore.sync() força a sincronização com os servidores da Apple, útil após uma reinstalação.
Validação de Recibos no Lado do Servidor
A validação no lado do servidor permanece recomendada para aplicativos com assinaturas críticas. O StoreKit 2 introduz JWS (JSON Web Signatures) para validação moderna.
Extraindo Dados da Transação para o 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 {
// Cria o payload para o 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 para JSON
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let jsonData = try encoder.encode(payload)
// Envia para o 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 a resposta do servidor
let validationResponse = try JSONDecoder().decode(
ValidationResponse.self,
from: data
)
return validationResponse.isValid
}
}
struct ValidationResponse: Codable {
let isValid: Bool
let subscriptionStatus: String?
}A jwsRepresentation contém a assinatura criptográfica verificável pelo servidor.
A Apple agora fornece a App Store Server API para validação no lado do servidor. Esta API moderna substitui o endpoint depreciado verifyReceipt e oferece recursos avançados como notificações servidor a servidor.
Configurando Notificações do Servidor
As Server Notifications V2 permitem o recebimento de eventos em tempo 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?
}
}Essas notificações permitem manter o estado das assinaturas sincronizado com o backend.
Tratamento de Erros e Casos Extremos
As entrevistas frequentemente testam o entendimento dos cenários de erro e seu tratamento adequado.
Erros Comuns no 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 é um erro do StoreKit
if let storeKitError = error as? StoreKitError {
switch storeKitError {
case .networkError:
return .networkError
case .userCancelled:
// Não exibe erro, o usuário cancelou
return .unknown
case .notAvailableInStorefront:
return .productNotAvailable
case .notEntitled:
return .notAuthorized
default:
return .unknown
}
}
// Erros 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
}
}Um tratamento claro de erros melhora a experiência do usuário e simplifica a depuração.
Suporte ao Modo 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 os 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
}
}O cache local mantém o acesso aos recursos premium mesmo sem conectividade.
Melhores Práticas para Produção
Arquitetura 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 a observação de transações
startTransactionObserver()
// Carrega o 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()
}
}Essa arquitetura centralizada simplifica o gerenciamento do estado das compras em todo o aplicativo.
Conclusão
O StoreKit 2 transforma o gerenciamento de compras no aplicativo em uma experiência de desenvolvimento moderna e segura. Pontos-chave para lembrar para uma entrevista:
✅ API nativa async/await: acabaram-se os delegates e callbacks complexos do StoreKit 1
✅ Verificação automática: VerificationResult gerencia a validação criptográfica das transações
✅ Transaction.updates: stream assíncrono para transações em segundo plano
✅ Transaction.currentEntitlements: acesso direto aos entitlements ativos do usuário
✅ Ofertas promocionais: isEligibleForIntroOffer para verificar a elegibilidade
✅ Validação no servidor: JWS e App Store Server API para maior segurança
✅ transaction.finish(): obrigatório após a entrega do conteúdo
✅ Tratamento offline: cache local de entitlements para uma experiência fluida
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

Swift Testing Framework Entrevista 2026: Macros #expect e #require vs XCTest
Domine o novo Swift Testing Framework para entrevistas iOS: macros #expect e #require, migração do XCTest, padrões avançados e armadilhas comuns.

Entrevista iOS Push Notifications 2026: APNs, tokens e troubleshooting
Guia completo para preparar entrevistas iOS sobre Push Notifications, APNs, gestão de tokens e troubleshooting. Perguntas frequentes com respostas detalhadas.

As 25 perguntas de entrevista Swift mais importantes para desenvolvedores iOS
Preparação para entrevistas técnicas iOS com as 25 perguntas Swift mais frequentes: optionals, closures, ARC, protocolos, async/await e padrões avançados.