# StoreKit 2 ile Modern In-App Purchase: A'dan Z'ye Rehber 💰
Merhaba değerli iOS geliştiricisi! Bugün seninle Apple'ın en güçlü monetizasyon aracı olan StoreKit 2'yi derinlemesine inceleyeceğiz. Bu rehber sonunda, profesyonel düzeyde in-app purchase sistemi kurmayı öğreneceksin.
İçindekiler
- StoreKit 2 Nedir ve Neden Önemli?
- Proje Kurulumu
- Product Loading ve Purchase Flow
- Transaction Listener ve Entitlements
- SwiftUI Entegrasyonu
- Testing ve Sandbox
- Server-Side Receipt Validation
- Sürpriz Hediye: Complete StoreKit 2 Checklist
- Pro Tips ve Best Practices
- Easter Egg: Debug Menu
- Sonuç
🎯 Bu Yazıda Öğreneceklerin
- StoreKit 2'nin temelleri ve StoreKit 1'den farkları
- Product configuration ve App Store Connect entegrasyonu
- Transaction handling ve async/await kullanımı
- Subscription lifecycle management
- Receipt validation (server-side & on-device)
- Sandbox ve production testing
- Real-world best practices
📚 StoreKit 2 Nedir ve Neden Önemli?
StoreKit 2, iOS 15+ ile tanıtılan modern in-app purchase framework'üdür. StoreKit 1'e göre dramatik iyileştirmeler sunar:
Özellik | StoreKit 1 | StoreKit 2 |
|---|---|---|
API Stili | Delegate-based | async/await |
Transaction Handling | SKPaymentQueue | Transaction.updates |
Receipt Validation | Receipt parsing gerekli | JWS otomatik doğrulama |
Testing | Sandbox account zorunlu | StoreKit Configuration File |
Code Complexity | 500+ satır | ~100 satır |
💡 Altın İpucu: StoreKit 2, iOS 15+ gerektirir ama App Store Server API ile iOS 7+'ya kadar geriye dönük destek sağlayabilirsin!
Dış Kaynaklar:
🏗️ Proje Kurulumu
1. Product ID'leri Tanımla
İlk adım olarak product ID'lerini enum ile type-safe hale getir:
swift
1import StoreKit2 3// MARK: - Product IDs - Type-Safe Tanımlama4enum ProductID: String, CaseIterable {5 // ═══════════════════════════════════════════6 // 🪙 CONSUMABLES - Her seferinde tekrar satın alınabilir7 // ═══════════════════════════════════════════8 case coins100 = "com.myapp.coins.100"9 case coins500 = "com.myapp.coins.500"10 case coins1000 = "com.myapp.coins.1000"11 12 // ═══════════════════════════════════════════13 // 🔓 NON-CONSUMABLES - Bir kez satın alınır14 // ═══════════════════════════════════════════15 case premiumUnlock = "com.myapp.premium"16 case removeAds = "com.myapp.removeads"17 case allThemes = "com.myapp.themes.all"18 19 // ═══════════════════════════════════════════20 // 📅 AUTO-RENEWABLE SUBSCRIPTIONS21 // ═══════════════════════════════════════════22 case weeklyPro = "com.myapp.pro.weekly"23 case monthlyPro = "com.myapp.pro.monthly"24 case yearlyPro = "com.myapp.pro.yearly"25 case lifetimePro = "com.myapp.pro.lifetime"26 27 // Product type helper28 var type: Product.ProductType {29 switch self {30 case .coins100, .coins500, .coins1000:31 return .consumable32 case .premiumUnlock, .removeAds, .allThemes, .lifetimePro:33 return .nonConsumable34 case .weeklyPro, .monthlyPro, .yearlyPro:35 return .autoRenewable36 }37 }38 39 // 🐣 Easter Egg: Özel debug product ID40 // EASTER_EGG_2024: Bu ID ile test modunda sınırsız coins!41 static var debugAllAccess: String { "com.myapp.debug.allaccess" }42}2. Ana Store Manager Sınıfı
swift
1import StoreKit2import Combine3 4// MARK: - Store Manager - Ana Yönetici Sınıfı5@MainActor6final class StoreManager: ObservableObject {7 // ═══════════════════════════════════════════8 // Published Properties - SwiftUI Binding9 // ═══════════════════════════════════════════10 @Published private(set) var products: [Product] = []11 @Published private(set) var purchasedProductIDs: Set<String> = []12 @Published private(set) var isLoading = false13 @Published private(set) var error: StoreError?14 15 // Subscription specific16 @Published private(set) var subscriptionStatus: SubscriptionStatus = .notSubscribed17 @Published private(set) var subscriptionGroupStatus: [Product.SubscriptionInfo.Status]?18 19 // ═══════════════════════════════════════════20 // Private Properties21 // ═══════════════════════════════════════════22 private var transactionListener: Task<Void, Error>?23 private var cancellables = Set<AnyCancellable>()24 25 // Singleton (opsiyonel - DI tercih edilebilir)26 static let shared = StoreManager()27 28 // ═══════════════════════════════════════════29 // Lifecycle30 // ═══════════════════════════════════════════31 init() {32 // Transaction listener'ı başlat33 transactionListener = listenForTransactions()34 35 // İlk yükleme36 Task {37 await loadProducts()38 await updatePurchasedProducts()39 }40 }41 42 deinit {43 transactionListener?.cancel()44 }45}46 47// MARK: - Error Types48enum StoreError: LocalizedError {49 case failedVerification50 case productNotFound51 case purchaseFailed(Error)52 case networkError53 case userCancelled54 case pending55 56 var errorDescription: String? {57 switch self {58 case .failedVerification:59 return "İşlem doğrulanamadı. Lütfen tekrar deneyin."60 case .productNotFound:61 return "Ürün bulunamadı."62 case .purchaseFailed(let error):63 return "Satın alma başarısız: \(error.localizedDescription)"64 case .networkError:65 return "İnternet bağlantınızı kontrol edin."66 case .userCancelled:67 return "Satın alma iptal edildi."68 case .pending:69 return "Satın alma onay bekliyor."70 }71 }72}73 74// MARK: - Subscription Status75enum SubscriptionStatus: Equatable {76 case notSubscribed77 case subscribed(tier: SubscriptionTier, expiresAt: Date?)78 case expired(tier: SubscriptionTier)79 case inGracePeriod(tier: SubscriptionTier)80 case inBillingRetry(tier: SubscriptionTier)81 82 var isActive: Bool {83 switch self {84 case .subscribed, .inGracePeriod:85 return true86 default:87 return false88 }89 }90}91 92enum SubscriptionTier: String, Comparable {93 case weekly94 case monthly95 case yearly96 case lifetime97 98 static func < (lhs: SubscriptionTier, rhs: SubscriptionTier) -> Bool {99 let order: [SubscriptionTier] = [.weekly, .monthly, .yearly, .lifetime]100 return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!101 }102}🛒 Product Loading ve Purchase Flow
3. Ürünleri Yükleme
swift
1// MARK: - Product Loading2extension StoreManager {3 /// App Store'dan ürünleri yükle4 func loadProducts() async {5 isLoading = true6 defer { isLoading = false }7 8 do {9 // Tüm product ID'lerini al10 let productIDs = Set(ProductID.allCases.map { $0.rawValue })11 12 // App Store'dan ürünleri fetch et13 let storeProducts = try await Product.products(for: productIDs)14 15 // Fiyata göre sırala (düşükten yükseğe)16 products = storeProducts.sorted { $0.price < $1.price }17 18 #if DEBUG19 print("📦 Yüklenen ürün sayısı: \(products.count)")20 products.forEach { product in21 print(" - \(product.displayName): \(product.displayPrice)")22 }23 #endif24 25 } catch {26 self.error = .networkError27 print("❌ Ürün yükleme hatası: \(error)")28 }29 }30 31 /// Belirli bir ürünü ID ile bul32 func product(for id: ProductID) -> Product? {33 products.first { $0.id == id.rawValue }34 }35 36 /// Subscription ürünlerini filtrele37 var subscriptionProducts: [Product] {38 products.filter { $0.type == .autoRenewable }39 .sorted { ($0.subscription?.subscriptionPeriod.value ?? 0) < 40 ($1.subscription?.subscriptionPeriod.value ?? 0) }41 }42 43 /// Non-consumable ürünleri filtrele44 var nonConsumableProducts: [Product] {45 products.filter { $0.type == .nonConsumable }46 }47 48 /// Consumable ürünleri filtrele49 var consumableProducts: [Product] {50 products.filter { $0.type == .consumable }51 }52}4. Satın Alma İşlemi
swift
1// MARK: - Purchase Flow2extension StoreManager {3 /// Ana satın alma fonksiyonu4 @discardableResult5 func purchase(_ product: Product) async throws -> Transaction? {6 isLoading = true7 defer { isLoading = false }8 9 // Satın alma isteği gönder10 let result: Product.PurchaseResult11 12 do {13 // 💡 Pro Tip: appAccountToken ile kendi user ID'ni bağla14 let options: Set<Product.PurchaseOption> = [15 .appAccountToken(UUID()) // Kendi user ID sisteminle değiştir16 ]17 result = try await product.purchase(options: options)18 } catch {19 throw StoreError.purchaseFailed(error)20 }21 22 // Sonucu işle23 switch result {24 case .success(let verification):25 // Transaction'ı doğrula26 let transaction = try checkVerified(verification)27 28 // Entitlements'ı güncelle29 await updatePurchasedProducts()30 31 // Transaction'ı bitir (ÇOK ÖNEMLİ!)32 await transaction.finish()33 34 // Analytics event gönder35 await trackPurchase(product: product, transaction: transaction)36 37 return transaction38 39 case .userCancelled:40 throw StoreError.userCancelled41 42 case .pending:43 // Ask to Buy veya SCA (Strong Customer Authentication)44 throw StoreError.pending45 46 @unknown default:47 return nil48 }49 }50 51 /// Transaction doğrulama52 private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {53 switch result {54 case .unverified(_, let error):55 print("⚠️ Doğrulama hatası: \(error)")56 throw StoreError.failedVerification57 case .verified(let safe):58 return safe59 }60 }61 62 /// Analytics tracking63 private func trackPurchase(product: Product, transaction: Transaction) async {64 // Firebase Analytics, Mixpanel vs. entegrasyonu65 let params: [String: Any] = [66 "product_id": product.id,67 "price": product.price,68 "currency": product.priceFormatStyle.currencyCode,69 "transaction_id": String(transaction.id)70 ]71 // Analytics.logEvent("purchase", parameters: params)72 }73}🔄 Transaction Listener ve Entitlements
5. Background Transaction Handling
swift
1// MARK: - Transaction Listener2extension StoreManager {3 /// Background'da transaction'ları dinle4 private func listenForTransactions() -> Task<Void, Error> {5 Task.detached { [weak self] in6 // Transaction.updates sonsuz bir async sequence7 for await result in Transaction.updates {8 do {9 let transaction = try await self?.checkVerified(result)10 11 // Ana thread'de UI güncelle12 await MainActor.run { [weak self] in13 Task {14 await self?.updatePurchasedProducts()15 }16 }17 18 // Transaction'ı bitir19 await transaction?.finish()20 21 print("✅ Transaction güncellendi: \(transaction?.productID ?? "unknown")")22 23 } catch {24 print("❌ Transaction listener hatası: \(error)")25 }26 }27 }28 }29 30 /// Mevcut entitlement'ları güncelle31 func updatePurchasedProducts() async {32 var purchased: Set<String> = []33 34 // Tüm aktif entitlement'ları kontrol et35 for await result in Transaction.currentEntitlements {36 do {37 let transaction = try checkVerified(result)38 39 // Revoked değilse ekle40 if transaction.revocationDate == nil {41 purchased.insert(transaction.productID)42 }43 44 } catch {45 print("⚠️ Entitlement doğrulama hatası: \(error)")46 }47 }48 49 // UI güncelle50 await MainActor.run {51 self.purchasedProductIDs = purchased52 self.updateSubscriptionStatus()53 }54 }55 56 /// Subscription durumunu güncelle57 private func updateSubscriptionStatus() {58 // En yüksek tier'ı bul59 if purchasedProductIDs.contains(ProductID.lifetimePro.rawValue) {60 subscriptionStatus = .subscribed(tier: .lifetime, expiresAt: nil)61 } else if purchasedProductIDs.contains(ProductID.yearlyPro.rawValue) {62 subscriptionStatus = .subscribed(tier: .yearly, expiresAt: getExpirationDate(for: .yearlyPro))63 } else if purchasedProductIDs.contains(ProductID.monthlyPro.rawValue) {64 subscriptionStatus = .subscribed(tier: .monthly, expiresAt: getExpirationDate(for: .monthlyPro))65 } else if purchasedProductIDs.contains(ProductID.weeklyPro.rawValue) {66 subscriptionStatus = .subscribed(tier: .weekly, expiresAt: getExpirationDate(for: .weeklyPro))67 } else {68 subscriptionStatus = .notSubscribed69 }70 }71 72 private func getExpirationDate(for productID: ProductID) -> Date? {73 // Transaction'dan expiration date al74 Task {75 for await result in Transaction.currentEntitlements {76 if case .verified(let transaction) = result,77 transaction.productID == productID.rawValue {78 return transaction.expirationDate79 }80 }81 return nil82 }83 return nil84 }85}📱 SwiftUI Entegrasyonu
6. Paywall View
swift
1import SwiftUI2 3// MARK: - Modern Paywall View4struct PaywallView: View {5 @StateObject private var store = StoreManager.shared6 @State private var selectedProduct: Product?7 @State private var isPurchasing = false8 @State private var showError = false9 @Environment(\.dismiss) private var dismiss10 11 var body: some View {12 NavigationStack {13 ScrollView {14 VStack(spacing: 32) {15 // Hero Section16 headerSection17 18 // Features19 featuresSection20 21 // Products22 productsSection23 24 // Trust badges25 trustSection26 27 // Legal28 legalSection29 }30 .padding()31 }32 .navigationTitle("Pro'ya Yükselt")33 .navigationBarTitleDisplayMode(.inline)34 .toolbar {35 ToolbarItem(placement: .cancellationAction) {36 Button("Kapat") { dismiss() }37 }38 ToolbarItem(placement: .primaryAction) {39 Button("Geri Yükle") {40 Task { try? await store.restorePurchases() }41 }42 .font(.footnote)43 }44 }45 .alert("Hata", isPresented: $showError) {46 Button("Tamam") { store.error = nil }47 } message: {48 Text(store.error?.localizedDescription ?? "Bir hata oluştu")49 }50 .onChange(of: store.error) { _, error in51 showError = error != nil52 }53 .overlay {54 if isPurchasing {55 purchaseOverlay56 }57 }58 }59 }60 61 // MARK: - Header Section62 private var headerSection: some View {63 VStack(spacing: 16) {64 Image(systemName: "crown.fill")65 .font(.system(size: 60))66 .foregroundStyle(.linearGradient(67 colors: [.yellow, .orange],68 startPoint: .topLeading,69 endPoint: .bottomTrailing70 ))71 72 Text("Tüm Özelliklerin Kilidini Aç")73 .font(.title.bold())74 75 Text("Sınırsız erişim, reklamsız deneyim")76 .foregroundStyle(.secondary)77 }78 }79 80 // MARK: - Features Section81 private var featuresSection: some View {82 VStack(alignment: .leading, spacing: 12) {83 FeatureRow(icon: "infinity", title: "Sınırsız Kullanım", subtitle: "Hiçbir limit yok")84 FeatureRow(icon: "bolt.fill", title: "Tüm Özellikler", subtitle: "Premium araçlar")85 FeatureRow(icon: "xmark.circle.fill", title: "Reklamsız", subtitle: "Temiz deneyim")86 FeatureRow(icon: "icloud.fill", title: "Cloud Sync", subtitle: "Tüm cihazlarda")87 FeatureRow(icon: "heart.fill", title: "Öncelikli Destek", subtitle: "7/24 yardım")88 }89 .padding()90 .background(.ultraThinMaterial)91 .clipShape(RoundedRectangle(cornerRadius: 16))92 }93 94 // MARK: - Products Section95 private var productsSection: some View {96 VStack(spacing: 12) {97 ForEach(store.subscriptionProducts) { product in98 ProductCard(99 product: product,100 isSelected: selectedProduct?.id == product.id,101 isPurchased: store.purchasedProductIDs.contains(product.id)102 ) {103 selectedProduct = product104 }105 }106 107 // Purchase button108 if let product = selectedProduct {109 Button {110 Task { await purchase(product) }111 } label: {112 HStack {113 Text("Devam Et")114 .fontWeight(.semibold)115 Text("- \(product.displayPrice)")116 }117 .frame(maxWidth: .infinity)118 .padding()119 }120 .buttonStyle(.borderedProminent)121 .disabled(isPurchasing || store.purchasedProductIDs.contains(product.id))122 }123 }124 }125 126 // MARK: - Trust Section127 private var trustSection: some View {128 HStack(spacing: 24) {129 TrustBadge(icon: "lock.fill", text: "Güvenli Ödeme")130 TrustBadge(icon: "arrow.counterclockwise", text: "İstediğin Zaman İptal")131 TrustBadge(icon: "star.fill", text: "4.9 ★")132 }133 .font(.caption)134 .foregroundStyle(.secondary)135 }136 137 // MARK: - Legal Section138 private var legalSection: some View {139 VStack(spacing: 8) {140 Text("Abonelik otomatik olarak yenilenir. Dönem sonundan en az 24 saat önce iptal edilmezse ücretlendirilirsiniz.")141 142 HStack {143 Link("Kullanım Şartları", destination: URL(string: "https://myapp.com/terms")!)144 Text("•")145 Link("Gizlilik Politikası", destination: URL(string: "https://myapp.com/privacy")!)146 }147 }148 .font(.caption2)149 .foregroundStyle(.secondary)150 .multilineTextAlignment(.center)151 }152 153 // MARK: - Purchase Overlay154 private var purchaseOverlay: some View {155 ZStack {156 Color.black.opacity(0.5)157 .ignoresSafeArea()158 159 VStack(spacing: 16) {160 ProgressView()161 .scaleEffect(1.5)162 Text("İşleniyor...")163 .font(.headline)164 }165 .padding(32)166 .background(.ultraThinMaterial)167 .clipShape(RoundedRectangle(cornerRadius: 16))168 }169 }170 171 // MARK: - Purchase Action172 private func purchase(_ product: Product) async {173 isPurchasing = true174 defer { isPurchasing = false }175 176 do {177 let transaction = try await store.purchase(product)178 if transaction != nil {179 dismiss()180 }181 } catch StoreError.userCancelled {182 // Kullanıcı iptal etti, sessizce devam et183 } catch {184 // Hata store.error'a yazılacak185 }186 }187}188 189// MARK: - Supporting Views190struct FeatureRow: View {191 let icon: String192 let title: String193 let subtitle: String194 195 var body: some View {196 HStack(spacing: 16) {197 Image(systemName: icon)198 .font(.title2)199 .foregroundStyle(.green)200 .frame(width: 32)201 202 VStack(alignment: .leading) {203 Text(title).fontWeight(.medium)204 Text(subtitle).font(.caption).foregroundStyle(.secondary)205 }206 207 Spacer()208 209 Image(systemName: "checkmark.circle.fill")210 .foregroundStyle(.green)211 }212 }213}214 215struct ProductCard: View {216 let product: Product217 let isSelected: Bool218 let isPurchased: Bool219 let onTap: () -> Void220 221 var body: some View {222 Button(action: onTap) {223 HStack {224 VStack(alignment: .leading, spacing: 4) {225 HStack {226 Text(product.displayName)227 .fontWeight(.semibold)228 229 if isBestValue {230 Text("EN POPÜLER")231 .font(.caption2.bold())232 .padding(.horizontal, 8)233 .padding(.vertical, 2)234 .background(.orange)235 .foregroundStyle(.white)236 .clipShape(Capsule())237 }238 }239 240 Text(product.description)241 .font(.caption)242 .foregroundStyle(.secondary)243 }244 245 Spacer()246 247 if isPurchased {248 Image(systemName: "checkmark.circle.fill")249 .foregroundStyle(.green)250 } else {251 VStack(alignment: .trailing) {252 Text(product.displayPrice)253 .fontWeight(.bold)254 if let period = product.subscription?.subscriptionPeriod {255 Text(periodText(period))256 .font(.caption)257 .foregroundStyle(.secondary)258 }259 }260 }261 }262 .padding()263 .background(isSelected ? Color.accentColor.opacity(0.1) : Color(.secondarySystemBackground))264 .clipShape(RoundedRectangle(cornerRadius: 12))265 .overlay(266 RoundedRectangle(cornerRadius: 12)267 .stroke(isSelected ? Color.accentColor : .clear, lineWidth: 2)268 )269 }270 .buttonStyle(.plain)271 }272 273 private var isBestValue: Bool {274 product.id.contains("yearly")275 }276 277 private func periodText(_ period: Product.SubscriptionPeriod) -> String {278 switch period.unit {279 case .day: return "/ gün"280 case .week: return "/ hafta"281 case .month: return "/ ay"282 case .year: return "/ yıl"283 @unknown default: return ""284 }285 }286}287 288struct TrustBadge: View {289 let icon: String290 let text: String291 292 var body: some View {293 VStack(spacing: 4) {294 Image(systemName: icon)295 Text(text)296 }297 }298}🧪 Testing ve Sandbox
7. StoreKit Configuration File ile Test
swift
1// MARK: - Test Helpers2#if DEBUG3extension StoreManager {4 /// Sandbox ortamında tüm subscription'ları hızlandır5 /// Xcode > Product > Scheme > Edit Scheme > Run > Options > 6 /// StoreKit Configuration > Select your .storekit file7 8 /// Test: Satın alma simülasyonu9 func simulatePurchase(productID: ProductID) async {10 guard let product = product(for: productID) else { return }11 12 // Test modunda doğrudan entitlement ekle13 purchasedProductIDs.insert(product.id)14 updateSubscriptionStatus()15 16 print("🧪 Simulated purchase: \(productID.rawValue)")17 }18 19 /// Test: Subscription expire simülasyonu20 func simulateExpiration() async {21 purchasedProductIDs.removeAll()22 subscriptionStatus = .notSubscribed23 24 print("🧪 Simulated expiration")25 }26 27 /// Test: Refund simülasyonu28 func simulateRefund(productID: ProductID) async {29 purchasedProductIDs.remove(productID.rawValue)30 updateSubscriptionStatus()31 32 print("🧪 Simulated refund: \(productID.rawValue)")33 }34}35#endif36 37// MARK: - StoreKit Configuration Checklist38/*39 📋 STOREKIT TEST CHECKLIST:40 41 1. ☐ File > New > File > StoreKit Configuration File42 2. ☐ Tüm product'ları ekle (consumable, non-consumable, subscription)43 3. ☐ Subscription group oluştur44 4. ☐ Price tier'ları ayarla45 5. ☐ Scheme > Edit Scheme > Run > Options > StoreKit Configuration46 6. ☐ Simulator'da test et47 48 SANDBOX TEST CHECKLIST:49 50 1. ☐ App Store Connect'te Sandbox Tester oluştur51 2. ☐ Cihazda Settings > App Store > Sandbox Account52 3. ☐ Gerçek cihazda test et53 4. ☐ Subscription renewal hızlandırma aktif (5 dk = 1 ay)54 5. ☐ Tüm durumları test et:55 - ☐ Başarılı satın alma56 - ☐ İptal57 - ☐ Ask to Buy (pending)58 - ☐ Restore59 - ☐ Subscription expiration60 - ☐ Grace period61 - ☐ Billing retry62*/🔐 Server-Side Receipt Validation
8. Backend Entegrasyonu
swift
1// MARK: - Server-Side Validation (Recommended for Production)2extension StoreManager {3 /// Server-side receipt validation4 func validateReceiptOnServer(transaction: Transaction) async throws -> Bool {5 // JWS formatında signed transaction6 guard let jwsRepresentation = transaction.jwsRepresentation else {7 return false8 }9 10 // Backend'e gönder11 let url = URL(string: "https://api.myapp.com/validate-receipt")!12 var request = URLRequest(url: url)13 request.httpMethod = "POST"14 request.setValue("application/json", forHTTPHeaderField: "Content-Type")15 16 let body: [String: Any] = [17 "signedTransaction": jwsRepresentation,18 "environment": AppStore.environment.rawValue19 ]20 request.httpBody = try? JSONSerialization.data(withJSONObject: body)21 22 let (data, response) = try await URLSession.shared.data(for: request)23 24 guard let httpResponse = response as? HTTPURLResponse,25 httpResponse.statusCode == 200 else {26 return false27 }28 29 // Backend response'u parse et30 let result = try JSONDecoder().decode(ValidationResponse.self, from: data)31 return result.isValid32 }33 34 /// App Store environment kontrolü35 var currentEnvironment: String {36 switch AppStore.environment {37 case .production: return "Production"38 case .sandbox: return "Sandbox"39 case .xcode: return "Xcode"40 @unknown default: return "Unknown"41 }42 }43}44 45struct ValidationResponse: Codable {46 let isValid: Bool47 let expirationDate: Date?48 let productID: String?49}markdown
1# 🎁 STOREKIT 2 PRODUCTION CHECKLIST2 3## App Store Connect Setup4- [ ] Products oluşturuldu (Consumable, Non-Consumable, Subscription)5- [ ] Subscription Groups tanımlandı6- [ ] Price tiers ayarlandı (tüm bölgeler)7- [ ] Screenshots/Preview hazır8- [ ] Review notes yazıldı9- [ ] Tax & banking bilgileri tamamlandı10 11## Code Implementation12- [ ] Product loading13- [ ] Purchase flow14- [ ] Transaction listener (background)15- [ ] Entitlement check16- [ ] Restore purchases17- [ ] Error handling18- [ ] Offline support19 20## Testing21- [ ] StoreKit Configuration File test22- [ ] Sandbox account test23- [ ] Tüm purchase türleri test edildi24- [ ] Refund senaryosu test edildi25- [ ] Network error senaryosu test edildi26- [ ] Ask to Buy test edildi27 28## Analytics & Monitoring29- [ ] Purchase events tracked30- [ ] Revenue analytics entegre31- [ ] Error logging aktif32- [ ] Crash reporting aktif33 34## Legal & Compliance35- [ ] Terms of Service linki36- [ ] Privacy Policy linki37- [ ] Subscription disclosure metni38- [ ] Restore button mevcut39- [ ] Price displayed correctly40 41## Server-Side (Recommended)42- [ ] Receipt validation endpoint43- [ ] Webhook entegrasyonu44- [ ] User entitlement database45- [ ] Refund handlingOkuyucu Ödülü
💡 Pro Tips ve Best Practices
- Transaction.finish() çağırmayı UNUTMA! - Aksi halde kullanıcı aynı ürünü tekrar satın alamaz.
- Offline desteği ekle - Transaction'ları local cache'le ve internet gelince sync et.
- Price'ı hardcode yapma - Her zaman
product.displayPricekullan.
- Introductory offers -
product.subscription?.introductoryOfferile free trial sun.
- Grace period handling - Subscription expired olsa bile grace period'da erişim ver.
- Promotional offers - Server-signed promotional offer'lar ile churn azalt.
- App Store Server Notifications v2 - Webhook ile real-time billing events al.
- StoreKit Testing in Xcode - Production'a çıkmadan önce tüm edge case'leri test et.
swift
1// Debug build'lerde gizli menü2// Paywall'da 7 kez logo'ya tıkla!3struct SecretDebugMenu: View {4 @StateObject private var store = StoreManager.shared5 6 var body: some View {7 List {8 Section("Debug Actions") {9 Button("Simulate Pro Purchase") {10 Task { await store.simulatePurchase(productID: .yearlyPro) }11 }12 Button("Simulate Expiration") {13 Task { await store.simulateExpiration() }14 }15 Button("Clear All Purchases") {16 Task { await store.simulateRefund(productID: .yearlyPro) }17 }18 }19 20 Section("Status") {21 LabeledContent("Environment", value: store.currentEnvironment)22 LabeledContent("Subscription", value: "\(store.subscriptionStatus)")23 }24 }25 }26}📖 Sonuç
StoreKit 2, iOS'ta in-app purchase'ı devrimsel şekilde basitleştirdi. async/await, otomatik JWS doğrulama ve SwiftUI entegrasyonu ile artık daha az kod yazarak daha güvenli monetizasyon sistemleri kurabilirsin.
Unutma: Kullanıcı deneyimi her şeyden önemli. Paywall'ını A/B test et, pricing stratejini optimize et ve her zaman değer sun!
Bir sonraki yazıda görüşmek üzere! 🚀
*Bu yazıyı faydalı bulduysan, Twitter'da paylaş ve beni takip et!*
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

