Tüm Yazılar
KategoriiOS
Okuma Süresi
22 dk
Yayın Tarihi
...
Kelime Sayısı
3.135kelime

Kahveni hazırla - bu içerikli bir makale!

Modern StoreKit API ile subscription ve one-time purchase implementasyonu. Transaction handling, receipt validation ve testing.

StoreKit 2 ile Modern In-App Purchase

# 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


🎯 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 StoreKit
2 
3// MARK: - Product IDs - Type-Safe Tanımlama
4enum ProductID: String, CaseIterable {
5 // ═══════════════════════════════════════════
6 // 🪙 CONSUMABLES - Her seferinde tekrar satın alınabilir
7 // ═══════════════════════════════════════════
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ır
14 // ═══════════════════════════════════════════
15 case premiumUnlock = "com.myapp.premium"
16 case removeAds = "com.myapp.removeads"
17 case allThemes = "com.myapp.themes.all"
18
19 // ═══════════════════════════════════════════
20 // 📅 AUTO-RENEWABLE SUBSCRIPTIONS
21 // ═══════════════════════════════════════════
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 helper
28 var type: Product.ProductType {
29 switch self {
30 case .coins100, .coins500, .coins1000:
31 return .consumable
32 case .premiumUnlock, .removeAds, .allThemes, .lifetimePro:
33 return .nonConsumable
34 case .weeklyPro, .monthlyPro, .yearlyPro:
35 return .autoRenewable
36 }
37 }
38
39 // 🐣 Easter Egg: Özel debug product ID
40 // 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 StoreKit
2import Combine
3 
4// MARK: - Store Manager - Ana Yönetici Sınıfı
5@MainActor
6final class StoreManager: ObservableObject {
7 // ═══════════════════════════════════════════
8 // Published Properties - SwiftUI Binding
9 // ═══════════════════════════════════════════
10 @Published private(set) var products: [Product] = []
11 @Published private(set) var purchasedProductIDs: Set<String> = []
12 @Published private(set) var isLoading = false
13 @Published private(set) var error: StoreError?
14
15 // Subscription specific
16 @Published private(set) var subscriptionStatus: SubscriptionStatus = .notSubscribed
17 @Published private(set) var subscriptionGroupStatus: [Product.SubscriptionInfo.Status]?
18
19 // ═══════════════════════════════════════════
20 // Private Properties
21 // ═══════════════════════════════════════════
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 // Lifecycle
30 // ═══════════════════════════════════════════
31 init() {
32 // Transaction listener'ı başlat
33 transactionListener = listenForTransactions()
34
35 // İlk yükleme
36 Task {
37 await loadProducts()
38 await updatePurchasedProducts()
39 }
40 }
41
42 deinit {
43 transactionListener?.cancel()
44 }
45}
46 
47// MARK: - Error Types
48enum StoreError: LocalizedError {
49 case failedVerification
50 case productNotFound
51 case purchaseFailed(Error)
52 case networkError
53 case userCancelled
54 case pending
55
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 Status
75enum SubscriptionStatus: Equatable {
76 case notSubscribed
77 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 true
86 default:
87 return false
88 }
89 }
90}
91 
92enum SubscriptionTier: String, Comparable {
93 case weekly
94 case monthly
95 case yearly
96 case lifetime
97
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 Loading
2extension StoreManager {
3 /// App Store'dan ürünleri yükle
4 func loadProducts() async {
5 isLoading = true
6 defer { isLoading = false }
7
8 do {
9 // Tüm product ID'lerini al
10 let productIDs = Set(ProductID.allCases.map { $0.rawValue })
11
12 // App Store'dan ürünleri fetch et
13 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 DEBUG
19 print("📦 Yüklenen ürün sayısı: \(products.count)")
20 products.forEach { product in
21 print(" - \(product.displayName): \(product.displayPrice)")
22 }
23 #endif
24
25 } catch {
26 self.error = .networkError
27 print("❌ Ürün yükleme hatası: \(error)")
28 }
29 }
30
31 /// Belirli bir ürünü ID ile bul
32 func product(for id: ProductID) -> Product? {
33 products.first { $0.id == id.rawValue }
34 }
35
36 /// Subscription ürünlerini filtrele
37 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 filtrele
44 var nonConsumableProducts: [Product] {
45 products.filter { $0.type == .nonConsumable }
46 }
47
48 /// Consumable ürünleri filtrele
49 var consumableProducts: [Product] {
50 products.filter { $0.type == .consumable }
51 }
52}

4. Satın Alma İşlemi

swift
1// MARK: - Purchase Flow
2extension StoreManager {
3 /// Ana satın alma fonksiyonu
4 @discardableResult
5 func purchase(_ product: Product) async throws -> Transaction? {
6 isLoading = true
7 defer { isLoading = false }
8
9 // Satın alma isteği gönder
10 let result: Product.PurchaseResult
11
12 do {
13 // 💡 Pro Tip: appAccountToken ile kendi user ID'ni bağla
14 let options: Set<Product.PurchaseOption> = [
15 .appAccountToken(UUID()) // Kendi user ID sisteminle değiştir
16 ]
17 result = try await product.purchase(options: options)
18 } catch {
19 throw StoreError.purchaseFailed(error)
20 }
21
22 // Sonucu işle
23 switch result {
24 case .success(let verification):
25 // Transaction'ı doğrula
26 let transaction = try checkVerified(verification)
27
28 // Entitlements'ı güncelle
29 await updatePurchasedProducts()
30
31 // Transaction'ı bitir (ÇOK ÖNEMLİ!)
32 await transaction.finish()
33
34 // Analytics event gönder
35 await trackPurchase(product: product, transaction: transaction)
36
37 return transaction
38
39 case .userCancelled:
40 throw StoreError.userCancelled
41
42 case .pending:
43 // Ask to Buy veya SCA (Strong Customer Authentication)
44 throw StoreError.pending
45
46 @unknown default:
47 return nil
48 }
49 }
50
51 /// Transaction doğrulama
52 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.failedVerification
57 case .verified(let safe):
58 return safe
59 }
60 }
61
62 /// Analytics tracking
63 private func trackPurchase(product: Product, transaction: Transaction) async {
64 // Firebase Analytics, Mixpanel vs. entegrasyonu
65 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 Listener
2extension StoreManager {
3 /// Background'da transaction'ları dinle
4 private func listenForTransactions() -> Task<Void, Error> {
5 Task.detached { [weak self] in
6 // Transaction.updates sonsuz bir async sequence
7 for await result in Transaction.updates {
8 do {
9 let transaction = try await self?.checkVerified(result)
10
11 // Ana thread'de UI güncelle
12 await MainActor.run { [weak self] in
13 Task {
14 await self?.updatePurchasedProducts()
15 }
16 }
17
18 // Transaction'ı bitir
19 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üncelle
31 func updatePurchasedProducts() async {
32 var purchased: Set<String> = []
33
34 // Tüm aktif entitlement'ları kontrol et
35 for await result in Transaction.currentEntitlements {
36 do {
37 let transaction = try checkVerified(result)
38
39 // Revoked değilse ekle
40 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üncelle
50 await MainActor.run {
51 self.purchasedProductIDs = purchased
52 self.updateSubscriptionStatus()
53 }
54 }
55
56 /// Subscription durumunu güncelle
57 private func updateSubscriptionStatus() {
58 // En yüksek tier'ı bul
59 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 = .notSubscribed
69 }
70 }
71
72 private func getExpirationDate(for productID: ProductID) -> Date? {
73 // Transaction'dan expiration date al
74 Task {
75 for await result in Transaction.currentEntitlements {
76 if case .verified(let transaction) = result,
77 transaction.productID == productID.rawValue {
78 return transaction.expirationDate
79 }
80 }
81 return nil
82 }
83 return nil
84 }
85}

📱 SwiftUI Entegrasyonu

6. Paywall View

swift
1import SwiftUI
2 
3// MARK: - Modern Paywall View
4struct PaywallView: View {
5 @StateObject private var store = StoreManager.shared
6 @State private var selectedProduct: Product?
7 @State private var isPurchasing = false
8 @State private var showError = false
9 @Environment(\.dismiss) private var dismiss
10
11 var body: some View {
12 NavigationStack {
13 ScrollView {
14 VStack(spacing: 32) {
15 // Hero Section
16 headerSection
17
18 // Features
19 featuresSection
20
21 // Products
22 productsSection
23
24 // Trust badges
25 trustSection
26
27 // Legal
28 legalSection
29 }
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 in
51 showError = error != nil
52 }
53 .overlay {
54 if isPurchasing {
55 purchaseOverlay
56 }
57 }
58 }
59 }
60
61 // MARK: - Header Section
62 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: .bottomTrailing
70 ))
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 Section
81 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 Section
95 private var productsSection: some View {
96 VStack(spacing: 12) {
97 ForEach(store.subscriptionProducts) { product in
98 ProductCard(
99 product: product,
100 isSelected: selectedProduct?.id == product.id,
101 isPurchased: store.purchasedProductIDs.contains(product.id)
102 ) {
103 selectedProduct = product
104 }
105 }
106
107 // Purchase button
108 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 Section
127 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 Section
138 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 Overlay
154 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 Action
172 private func purchase(_ product: Product) async {
173 isPurchasing = true
174 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 et
183 } catch {
184 // Hata store.error'a yazılacak
185 }
186 }
187}
188 
189// MARK: - Supporting Views
190struct FeatureRow: View {
191 let icon: String
192 let title: String
193 let subtitle: String
194
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: Product
217 let isSelected: Bool
218 let isPurchased: Bool
219 let onTap: () -> Void
220
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: String
290 let text: String
291
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 Helpers
2#if DEBUG
3extension StoreManager {
4 /// Sandbox ortamında tüm subscription'ları hızlandır
5 /// Xcode > Product > Scheme > Edit Scheme > Run > Options >
6 /// StoreKit Configuration > Select your .storekit file
7
8 /// Test: Satın alma simülasyonu
9 func simulatePurchase(productID: ProductID) async {
10 guard let product = product(for: productID) else { return }
11
12 // Test modunda doğrudan entitlement ekle
13 purchasedProductIDs.insert(product.id)
14 updateSubscriptionStatus()
15
16 print("🧪 Simulated purchase: \(productID.rawValue)")
17 }
18
19 /// Test: Subscription expire simülasyonu
20 func simulateExpiration() async {
21 purchasedProductIDs.removeAll()
22 subscriptionStatus = .notSubscribed
23
24 print("🧪 Simulated expiration")
25 }
26
27 /// Test: Refund simülasyonu
28 func simulateRefund(productID: ProductID) async {
29 purchasedProductIDs.remove(productID.rawValue)
30 updateSubscriptionStatus()
31
32 print("🧪 Simulated refund: \(productID.rawValue)")
33 }
34}
35#endif
36 
37// MARK: - StoreKit Configuration Checklist
38/*
39 📋 STOREKIT TEST CHECKLIST:
40
41 1. ☐ File > New > File > StoreKit Configuration File
42 2. ☐ Tüm product'ları ekle (consumable, non-consumable, subscription)
43 3. ☐ Subscription group oluştur
44 4. ☐ Price tier'ları ayarla
45 5. ☐ Scheme > Edit Scheme > Run > Options > StoreKit Configuration
46 6. ☐ Simulator'da test et
47
48 SANDBOX TEST CHECKLIST:
49
50 1. ☐ App Store Connect'te Sandbox Tester oluştur
51 2. ☐ Cihazda Settings > App Store > Sandbox Account
52 3. ☐ Gerçek cihazda test et
53 4. ☐ Subscription renewal hızlandırma aktif (5 dk = 1 ay)
54 5. ☐ Tüm durumları test et:
55 - ☐ Başarılı satın alma
56 - ☐ İptal
57 - ☐ Ask to Buy (pending)
58 - ☐ Restore
59 - ☐ Subscription expiration
60 - ☐ Grace period
61 - ☐ Billing retry
62*/

🔐 Server-Side Receipt Validation

8. Backend Entegrasyonu

swift
1// MARK: - Server-Side Validation (Recommended for Production)
2extension StoreManager {
3 /// Server-side receipt validation
4 func validateReceiptOnServer(transaction: Transaction) async throws -> Bool {
5 // JWS formatında signed transaction
6 guard let jwsRepresentation = transaction.jwsRepresentation else {
7 return false
8 }
9
10 // Backend'e gönder
11 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.rawValue
19 ]
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 false
27 }
28
29 // Backend response'u parse et
30 let result = try JSONDecoder().decode(ValidationResponse.self, from: data)
31 return result.isValid
32 }
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: Bool
47 let expirationDate: Date?
48 let productID: String?
49}

markdown
1# 🎁 STOREKIT 2 PRODUCTION CHECKLIST
2 
3## App Store Connect Setup
4- [ ] Products oluşturuldu (Consumable, Non-Consumable, Subscription)
5- [ ] Subscription Groups tanımlandı
6- [ ] Price tiers ayarlandı (tüm bölgeler)
7- [ ] Screenshots/Preview hazır
8- [ ] Review notes yazıldı
9- [ ] Tax & banking bilgileri tamamlandı
10 
11## Code Implementation
12- [ ] Product loading
13- [ ] Purchase flow
14- [ ] Transaction listener (background)
15- [ ] Entitlement check
16- [ ] Restore purchases
17- [ ] Error handling
18- [ ] Offline support
19 
20## Testing
21- [ ] StoreKit Configuration File test
22- [ ] Sandbox account test
23- [ ] Tüm purchase türleri test edildi
24- [ ] Refund senaryosu test edildi
25- [ ] Network error senaryosu test edildi
26- [ ] Ask to Buy test edildi
27 
28## Analytics & Monitoring
29- [ ] Purchase events tracked
30- [ ] Revenue analytics entegre
31- [ ] Error logging aktif
32- [ ] Crash reporting aktif
33 
34## Legal & Compliance
35- [ ] Terms of Service linki
36- [ ] Privacy Policy linki
37- [ ] Subscription disclosure metni
38- [ ] Restore button mevcut
39- [ ] Price displayed correctly
40 
41## Server-Side (Recommended)
42- [ ] Receipt validation endpoint
43- [ ] Webhook entegrasyonu
44- [ ] User entitlement database
45- [ ] Refund handling

Okuyucu Ödülü

💡 Pro Tips ve Best Practices

  1. Transaction.finish() çağırmayı UNUTMA! - Aksi halde kullanıcı aynı ürünü tekrar satın alamaz.
  1. Offline desteği ekle - Transaction'ları local cache'le ve internet gelince sync et.
  1. Price'ı hardcode yapma - Her zaman product.displayPrice kullan.
  1. Introductory offers - product.subscription?.introductoryOffer ile free trial sun.
  1. Grace period handling - Subscription expired olsa bile grace period'da erişim ver.
  1. Promotional offers - Server-signed promotional offer'lar ile churn azalt.
  1. App Store Server Notifications v2 - Webhook ile real-time billing events al.
  1. 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.shared
5
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.

Etiketler

#StoreKit#In-App Purchase#iOS#Subscriptions#Monetization#SwiftUI
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz