StoreKit 2 Mülakatı: Abonelik Yönetimi ve Makbuz Doğrulama

StoreKit 2, abonelik yönetimi, makbuz doğrulama ve uygulama içi satın alma uygulaması hakkında iOS mülakat sorularında pratik Swift kod örnekleriyle uzmanlaşın.

iOS StoreKit 2 abonelik mimarisi ve makbuz doğrulama

StoreKit 2, iOS'ta uygulama içi satın alımların ele alınma şeklinde temel bir değişimi temsil eder. iOS 15 ile tanıtılan bu modern framework, abonelik yönetimi ve işlem doğrulaması için gereken kodu önemli ölçüde basitleştirir. Teknik iOS mülakatları, geliştiricilerin uygulama monetizasyonu konusundaki uzmanlığını değerlendirmek için bu konuyu düzenli olarak ele alır.

Önemli Mülakat İçgörüsü

StoreKit 2, async/await ile yerel bir Swift API'sinden yararlanır ve karmaşık callback'ler ile istemci tarafı makbuz doğrulamaya olan ihtiyacı ortadan kaldırır. Mülakatçılar, StoreKit 1'den temel farkları ifade edebilen adayları takdir eder.

StoreKit 2 Mimarisine Genel Bakış

StoreKit 2, Swift Concurrency'den tam olarak yararlanan modern bir mimari üzerine inşa edilmiştir. StoreKit 1'in aksine, tüm işlemler asenkrondur ve yerel Swift türlerini kullanır.

Mevcut Ürünleri Getirme

İlk adım, App Store Connect'te yapılandırılmış ürünleri yüklemeyi içerir. StoreKit 2, bu işlemi bildirimsel bir API ile basitleştirir.

ProductService.swiftswift
import StoreKit

actor ProductService {

    // App Store Connect'te yapılandırılmış ürün tanımlayıcıları
    private let productIdentifiers: Set<String> = [
        "com.app.subscription.monthly",
        "com.app.subscription.yearly",
        "com.app.premium.lifetime"
    ]

    // Tekrarlanan ağ çağrılarını önlemek için ürün önbelleği
    private var cachedProducts: [Product] = []

    func loadProducts() async throws -> [Product] {
        // Mevcutsa önbelleği döndür
        guard cachedProducts.isEmpty else {
            return cachedProducts
        }

        // App Store'a asenkron istek
        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 }
    }
}

Bir actor kullanmak, ürün önbelleğini yönetirken iş parçacığı güvenliğini garanti eder.

StoreKit 2 Ürün Türleri

StoreKit 2, farklı ürün türlerini açıkça ayırt eder. Bu ayrımı anlamak mülakatlar için önemlidir.

ProductType+Extensions.swiftswift
import StoreKit

extension Product.ProductType {

    var displayName: String {
        switch self {
        case .consumable:
            // Tüketilebilir ürünler (krediler, canlar vb.)
            return "Consumable"
        case .nonConsumable:
            // Kalıcı satın alımlar (özellik kilidini açma)
            return "Non-Consumable"
        case .autoRenewable:
            // Otomatik yenilemeli abonelikler
            return "Auto-Renewable Subscription"
        case .nonRenewable:
            // Yenilemesiz abonelikler (geçici geçiş kartı)
            return "Non-Renewable Subscription"
        @unknown default:
            return "Unknown"
        }
    }
}

Otomatik yenilemeli abonelikler, modern iOS uygulamaları için baskın iş modelini temsil eder.

Satın Alma İşlemlerinin Yönetimi

StoreKit 2'deki satın alma süreci, async/await sayesinde doğrusal hale gelir. StoreKit 1'in karmaşık delegate ve iç içe callback'leri yok artık.

Bir Satın Alma Başlatma

PurchaseManager.swiftswift
import StoreKit

actor PurchaseManager {

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

    func purchase(_ product: Product) async throws -> Transaction {
        // Sistem modal'ı ile satın alma başlat
        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            // İşlem imzasını doğrula
            let transaction = try checkVerification(verification)

            // İşlemi tamamlanmış olarak işaretle
            await transaction.finish()

            return transaction

        case .userCancelled:
            // Kullanıcı satın alma modal'ını iptal etti
            throw PurchaseError.purchaseCancelled

        case .pending:
            // İşlem bekliyor (ebeveyn onayı vb.)
            throw PurchaseError.purchasePending

        @unknown default:
            throw PurchaseError.unknown
        }
    }

    private func checkVerification<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .verified(let safe):
            // İşlem App Store tarafından imzalandı ve doğrulandı
            return safe
        case .unverified(_, let error):
            // Geçersiz veya bozulmuş imza
            throw PurchaseError.verificationFailed
        }
    }
}

finish() yöntemi çok önemlidir: App Store'a içeriğin kullanıcıya teslim edildiğini bildirir.

Mülakat Uyarısı

İçeriği teslim ettikten sonra transaction.finish() çağırmayı asla unutmayın. Tamamlanmamış bir işlem bir sonraki uygulama başlatımında geri yüklenir ve beklenmedik davranışlara neden olur.

Arka Plan İşlemlerini Dinleme

StoreKit 2, uygulamanın dışında gerçekleşen işlemleri (yenilemeler, family sharing satın alımları vb.) yayınlayan asenkron bir akış olan Transaction.updates'i tanıtır.

TransactionObserver.swiftswift
import StoreKit

actor TransactionObserver {

    private var updateTask: Task<Void, Never>?

    func startObserving() {
        // Önceki gözlemi iptal et
        updateTask?.cancel()

        updateTask = Task(priority: .background) {
            // İşlem güncellemelerinin sonsuz akışı
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerification(result)

                    // İşlemi türüne göre işle
                    await self.handleTransaction(transaction)

                    // Her zaman işlemi tamamla
                    await transaction.finish()
                } catch {
                    // Hata ayıklama için hatayı kaydet
                    print("Transaction verification failed: \(error)")
                }
            }
        }
    }

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

    private func handleTransaction(_ transaction: Transaction) async {
        switch transaction.productType {
        case .autoRenewable:
            // Abonelik durumunu güncelle
            await updateSubscriptionStatus(transaction)
        case .nonConsumable:
            // Premium özellik kilidini aç
            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 {
        // Durum güncelleme uygulaması
    }

    private func unlockFeature(_ productID: String) async {
        // Özellik kilidini açma uygulaması
    }

    enum PurchaseError: Error {
        case verificationFailed
    }
}

İşlem gözlemi, herhangi bir güncellemeyi kaçırmamak için uygulama başlar başlamaz başlatılmalıdır.

StoreKit 2 Abonelikleri Hakkında Yaygın Mülakat Soruları

iOS mülakatları sıklıkla abonelik yönetimi hakkında belirli sorular içerir. İşte en yaygın olanlar ve ayrıntılı cevapları.

Soru 1: Mevcut Abonelik Durumu Nasıl Kontrol Edilir?

StoreKit 2, aktif yetkilendirmelere Transaction.currentEntitlements aracılığıyla doğrudan erişim sağlar.

SubscriptionStatusChecker.swiftswift
import StoreKit

struct SubscriptionStatusChecker {

    struct SubscriptionStatus {
        let isActive: Bool
        let expirationDate: Date?
        let willRenew: Bool
        let productID: String?
    }

    static func checkCurrentStatus() async -> SubscriptionStatus {
        // Tüm aktif işlemleri al
        var latestTransaction: Transaction?

        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                // Yalnızca abonelikleri filtrele
                if transaction.productType == .autoRenewable {
                    // En son işlemi sakla
                    if latestTransaction == nil ||
                       transaction.purchaseDate > latestTransaction!.purchaseDate {
                        latestTransaction = transaction
                    }
                }
            }
        }

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

        // Aboneliğin hala aktif olup olmadığını kontrol et
        let isActive = transaction.expirationDate ?? Date.distantPast > Date()

        // Yenileme bilgilerini al
        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
        }

        // Yenileme durumunu al
        let status = try? await subscription.status.first
        return status?.renewalInfo
    }
}

Bu yaklaşım, sunucu çağrıları olmadan kesin abonelik durumu sağlar.

iOS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Soru 2: Ücretsiz Denemeler ve Promosyon Teklifleri Nasıl Yönetilir?

StoreKit 2, teklif bilgilerine erişimi basitleştirir. Mülakatçılar genellikle farklı teklif türlerinin anlaşılmasını test eder.

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 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:
            // Ücretsiz deneme süresi
            return .freeTrial(duration: formatPeriod(offer.period))

        case .payAsYouGo:
            // Birden fazla dönem boyunca indirimli fiyat
            return .payAsYouGo(
                periods: offer.periodCount,
                price: offer.price
            )

        case .payUpFront:
            // Bir dönem için tek seferlik ödeme
            return .payUpFront(
                duration: formatPeriod(offer.period),
                price: offer.price
            )

        @unknown default:
            return .none
        }
    }

    static func checkEligibility(for product: Product) async -> Bool {
        // Kullanıcının tekliften yararlanıp yararlanamayacağını kontrol et
        guard let subscription = product.subscription else {
            return false
        }

        // isEligibleForIntroOffer yeni bir aboneyi gösterir
        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 ""
        }
    }
}

isEligibleForIntroOffer yöntemi, kullanılamayan teklifleri görüntülemekten kaçınmak için önemlidir.

Soru 3: Önceki Satın Alımlar Nasıl Geri Yüklenir?

Satın alma geri yükleme, App Store yönergelerine göre zorunlu bir özelliktir.

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] = []

        // App Store ile senkronize et
        try await AppStore.sync()

        // Tüm kullanıcı işlemleri arasında dolaş
        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else {
                continue
            }

            switch transaction.productType {
            case .nonConsumable:
                // Kalıcı satın alımları geri yükle
                restoredProducts.append(transaction.productID)

            case .autoRenewable:
                // Aboneliğin aktif olup olmadığını kontrol et
                if let expiration = transaction.expirationDate,
                   expiration > Date() {
                    activeSubscriptions.append(transaction.productID)
                }

            default:
                // Tüketilebilirler geri yüklenebilir değildir
                break
            }
        }

        return RestoreResult(
            restoredProducts: restoredProducts,
            activeSubscriptions: activeSubscriptions
        )
    }
}

AppStore.sync(), Apple sunucularıyla senkronizasyonu zorlar ve yeniden yükleme sonrası faydalıdır.

Sunucu Tarafı Makbuz Doğrulama

Kritik abonelikleri olan uygulamalar için sunucu tarafı doğrulama önerilmeye devam etmektedir. StoreKit 2, modern doğrulama için JWS (JSON Web Signatures) tanıtır.

Sunucu için İşlem Verilerini Çıkarma

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
}

actor ServerValidationService {

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

    func validateWithServer(_ transaction: Transaction) async throws -> Bool {
        // Sunucu için yük oluştur
        let payload = TransactionPayload(
            transactionID: String(transaction.id),
            originalTransactionID: String(transaction.originalID),
            productID: transaction.productID,
            purchaseDate: transaction.purchaseDate,
            expirationDate: transaction.expirationDate,
            jwsRepresentation: transaction.jwsRepresentation ?? ""
        )

        // JSON'a kodla
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let jsonData = try encoder.encode(payload)

        // Sunucuya gönder
        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
        }

        // Sunucu yanıtını çöz
        let validationResponse = try JSONDecoder().decode(
            ValidationResponse.self,
            from: data
        )

        return validationResponse.isValid
    }
}

struct ValidationResponse: Codable {
    let isValid: Bool
    let subscriptionStatus: String?
}

jwsRepresentation, sunucu tarafından doğrulanabilir kriptografik imza içerir.

App Store Server API

Apple artık sunucu tarafı doğrulama için App Store Server API'sini sağlıyor. Bu modern API, kullanımdan kaldırılan verifyReceipt uç noktasının yerini alıyor ve sunucudan sunucuya bildirimler gibi gelişmiş özellikler sunuyor.

Sunucu Bildirimlerini Yapılandırma

Server Notifications V2, gerçek zamanlı olay alımını sağlar.

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

Bu bildirimler, abonelik durumunun backend ile senkronize tutulmasını sağlar.

Hata Yönetimi ve Sınır Durumları

Mülakatlar sıklıkla hata senaryolarının ve uygun şekilde işlenmesinin anlaşılmasını test eder.

StoreKit 2'deki Yaygın Hatalar

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 {
        // StoreKit hatası olup olmadığını kontrol et
        if let storeKitError = error as? StoreKitError {
            switch storeKitError {
            case .networkError:
                return .networkError
            case .userCancelled:
                // Hata gösterme, kullanıcı iptal etti
                return .unknown
            case .notAvailableInStorefront:
                return .productNotAvailable
            case .notEntitled:
                return .notAuthorized
            default:
                return .unknown
            }
        }

        // Satın alma hataları
        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
    }
}

Net hata yönetimi, kullanıcı deneyimini geliştirir ve hata ayıklamayı basitleştirir.

Çevrimdışı Mod Desteği

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

        // Yerel olarak kaydet
        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 [:]
        }

        // Süresi dolmuş yetkilendirmeleri filtrele
        let now = Date()
        return entitlements.filter { _, expiration in
            expiration > now
        }
    }

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

Yerel önbellekleme, bağlantı olmadan bile premium özelliklere erişimi korur.

Üretim için En İyi Uygulamalar

Önerilen Mimari

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() {
        // İşlem gözlemini başlat
        startTransactionObserver()

        // Başlangıç durumunu yükle
        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()
    }
}

Bu merkezileştirilmiş mimari, uygulama genelinde satın alma durumu yönetimini basitleştirir.

Sonuç

StoreKit 2, uygulama içi satın alma yönetimini modern ve güvenli bir geliştirme deneyimine dönüştürür. Bir mülakat için hatırlanacak önemli noktalar:

✅ Yerel async/await API'si: StoreKit 1'in karmaşık delegate ve callback'lerine son

✅ Otomatik doğrulama: VerificationResult işlemlerin kriptografik doğrulamasını yönetir

Transaction.updates: arka plan işlemleri için asenkron akış

Transaction.currentEntitlements: kullanıcının aktif yetkilendirmelerine doğrudan erişim

✅ Promosyon teklifleri: uygunluğu kontrol etmek için isEligibleForIntroOffer

✅ Sunucu doğrulama: gelişmiş güvenlik için JWS ve App Store Server API

transaction.finish(): içerik teslimi sonrası zorunlu

✅ Çevrimdışı işleme: kesintisiz deneyim için yetkilendirmelerin yerel önbelleklenmesi

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

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

Paylaş

İlgili makaleler