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.

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.
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.
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.
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
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.
İç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.
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.
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.
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.
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
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.
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.
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
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
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
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
Paylaş
İlgili makaleler

Swift Testing Framework Mülakat 2026: #expect ve #require Makroları XCTest Karşısında
iOS mülakatları için yeni Swift Testing Framework'ünde uzmanlaş: #expect ve #require makroları, XCTest geçişi, ileri seviye desenler ve sık yapılan hatalar.

iOS Push Notifications Mülakatı 2026: APNs, token'lar ve sorun giderme
iOS mülakatlarına hazırlık için kapsamlı rehber: Push Notifications, APNs, token yönetimi ve sorun giderme. Sık sorulan sorular ve ayrıntılı yanıtlar.

iOS Geliştiricileri İçin En Önemli 25 Swift Mülakat Sorusu
iOS mülakatlarına hazırlık: En sık sorulan 25 Swift sorusu — optionals, closures, ARC, protokoller, async/await ve ileri düzey kalıplar detaylı açıklamalarla.