# StoreKit 2 Production Rehberi: Async/Await ile Modern IAP
StoreKit 2 (iOS 15+) Apple'ın IAP API'sini yeniden yazdığı sürüm. Eski StoreKit 1 delegate-pattern, callback hell, manual receipt parsing — hepsi gitti. Yerine Swift Concurrency, JWS-based verification, strongly-typed Product/Transaction/Purchase API'leri geldi. Bu rehber production-ready StoreKit 2 implementasyonu, server-side validation, subscription offers, family sharing, error handling ve test stratejisi için kapsamlı kılavuz.
💡 Pro Tip: StoreKit 1'den StoreKit 2'ye migration tek günlük refactor değil — 2-3 haftalık proje. Legacy receipt validation'ı StoreKit 2'nin JWS validation'ıyla value parity'ye getirmek en zor kısım.
İçindekiler
- StoreKit 2 vs StoreKit 1
- Product ve Transaction API
- Purchase Flow: Modern async/await
- JWS Receipt Validation (Server-Side)
- Subscription Offers: Intro, Promo, Winback
- Server Notifications V2
- Family Sharing
- Refund Handling
- Testing: Sandbox + TestFlight
- SwiftUI Paywall Örneği
StoreKit 2 vs StoreKit 1
Özellik | StoreKit 1 | StoreKit 2 |
|---|---|---|
Pattern | Delegate + Notification | async/await |
Type safety | Weak (AnyObject) | Strong (Codable struct) |
Receipt | Binary PKCS#7 | JWS (signed JSON) |
Server validation | Apple endpoint /verifyReceipt | App Store Server API |
iOS min | iOS 3 | iOS 15+ |
Subscription offers | Manual JWT | Type-safe API |
Parallel verification | Manuel | Otomatik |
Product ve Transaction API
Product Load
swift
1import StoreKit2 3@Observable4final class StoreVM {5 var products: [Product] = []6 var purchasedProductIDs: Set<String> = []7 8 func loadProducts() async throws {9 let ids = ["com.myapp.pro.monthly", "com.myapp.pro.yearly", "com.myapp.coin.100"]10 products = try await Product.products(for: ids)11 }12}Product Structure
swift
1struct Product {2 let id: String3 let displayName: String4 let description: String5 let price: Decimal6 let displayPrice: String // "$9.99" localized7 let type: ProductType // .consumable, .nonConsumable, .nonRenewable, .autoRenewable8 let subscription: SubscriptionInfo?9}Purchase Flow: Modern async/await
swift
1extension StoreVM {2 func purchase(_ product: Product) async throws {3 let result = try await product.purchase()4 5 switch result {6 case .success(let verification):7 let transaction = try checkVerified(verification)8 await handleTransactionCompletion(transaction)9 await transaction.finish()10 11 case .pending:12 // Aile onayı veya Apple ID doğrulama bekleniyor13 break14 15 case .userCancelled:16 break17 18 @unknown default:19 break20 }21 }22 23 func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {24 switch result {25 case .unverified(_, let error):26 throw error27 case .verified(let safe):28 return safe29 }30 }31}Transaction Updates Listener
swift
1Task.detached {2 for await update in Transaction.updates {3 if case .verified(let transaction) = update {4 await handleTransactionCompletion(transaction)5 await transaction.finish()6 }7 }8}App lifecycle'ı boyunca transactions listener çalışır — offline purchase, renewal, refund otomatik.
JWS Receipt Validation (Server-Side)
StoreKit 2 JWS (JSON Web Signature) kullanır — ECDSA ile imzalı. Server'da validate:
1. JWS Decode
typescript
1// Node.js server2import jwt from 'jsonwebtoken';3import { X509Certificate } from 'node:crypto';4 5async function verifyJWS(signedPayload: string): Promise { 6 const [headerB64] = signedPayload.split('.');7 const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString());8 9 // 1. Get Apple public keys from x5c chain10 const certChain = header.x5c.map(c => new X509Certificate(Buffer.from(c, 'base64')));11 12 // 2. Verify chain against Apple Root CA (G3)13 await verifyCertChain(certChain, APPLE_ROOT_CA_G3);14 15 // 3. Verify signature16 const leafCert = certChain[0];17 const publicKey = leafCert.publicKey;18 const decoded = jwt.verify(signedPayload, publicKey, { algorithms: ['ES256'] });19 20 return decoded as TransactionInfo;21}2. App Store Server API
Apple'ın REST API ile transaction state sorgula:
typescript
1const response = await fetch(2 `https://api.storekit.itunes.apple.com/inApps/v1/transactions/${transactionId}`,3 {4 headers: {5 'Authorization': `Bearer ${generateJWT(APP_STORE_KEY_ID, ISSUER_ID)}`,6 },7 }8);9const { signedTransactionInfo } = await response.json();10const transaction = await verifyJWS(signedTransactionInfo);Bu approach Apple'ın endpoint'ine her seferinde gitmeden local cache ile de çalışır.
Subscription Offers: Intro, Promo, Winback
Intro Offer (İlk Defa Abone Olanlar)
swift
1let offer = product.subscription?.introductoryOffer2 3if let offer {4 print("İlk (offer.period.formatted): (offer.displayPrice)")5 // UI'da göster: "İlk 7 gün ücretsiz, sonra ayda $9.99"6}Promotional Offer (Mevcut Aboneler)
swift
1// 1. Server'da signature oluştur (App Store Connect'te tanımlı offer)2let signedOffer = await myServer.signPromoOffer(3 offerID: "winter_discount",4 userUUID: currentUser.id5)6 7// 2. Purchase with offer8let result = try await product.purchase(options: [9 .promotionalOffer(10 offerID: signedOffer.id,11 keyID: signedOffer.keyID,12 nonce: signedOffer.nonce,13 signature: signedOffer.signature,14 timestamp: signedOffer.timestamp15 )16])Winback Offer (Cancel Etmiş Aboneler)
iOS 18+ yeni özellik. App Store'da gösterilen offer'ı app'ten de tetikleyebilirsin:
swift
1let winbackOffers = product.subscription?.winBackOffers ?? []2if let firstOffer = winbackOffers.first {3 try await product.purchase(options: [.winBackOfferID(firstOffer.id)])4}Server Notifications V2
Apple, subscription state değişikliklerini webhook olarak gönderir:
typescript
1// Express webhook handler2app.post('/apple/webhook', async (req, res) => {3 const { signedPayload } = req.body;4 const notification = await verifyJWS(signedPayload);5 6 switch (notification.notificationType) {7 case 'SUBSCRIBED':8 await grantEntitlement(notification);9 break;10 case 'DID_RENEW':11 await extendEntitlement(notification);12 break;13 case 'DID_FAIL_TO_RENEW':14 await warnUser(notification);15 break;16 case 'EXPIRED':17 await revokeEntitlement(notification);18 break;19 case 'REFUND':20 await processRefund(notification);21 break;22 case 'GRACE_PERIOD_EXPIRED':23 await downgradeUser(notification);24 break;25 }26 27 res.status(200).send('OK');28});Important: Webhook idempotent olmalı — Apple retry eder, duplicate processing yapma.
Family Sharing
swift
1let purchases = Transaction.currentEntitlements2for await entitlement in purchases {3 if case .verified(let transaction) = entitlement {4 if transaction.ownershipType == .familyShared {5 print("Bu satın alma aile paylaşımıyla geldi")6 } else {7 print("Kullanıcı kendisi satın aldı")8 }9 }10}Family-sharable product config'i App Store Connect'te "Family Sharing" toggle'ıyla aktif.
Refund Handling
App'ten Refund Request (iOS 15+)
swift
1// iOS 15+ Apple resmi refund UI2@Environment(\.requestReview) var requestReview3@Environment(\.openURL) var openURL4 5// Refund request6if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {7 let result = try await Transaction.beginRefundRequest(8 for: transactionID,9 in: windowScene10 )11}Server'da Refund Notification
typescript
1case 'REFUND':2 // notification.data.signedRenewalInfo → revoked3 await db.transaction(async (tx) => {4 await tx.update('entitlements', { status: 'refunded' });5 await tx.insert('refund_log', { ... });6 });7 break;Kullanıcıya entitlement'ı derhal kaldır. Grace period yok.
Testing: Sandbox + TestFlight
Sandbox Setup
- Sandbox Tester oluştur: App Store Connect > Users and Access > Sandbox Testers
- Settings > App Store > Sandbox Account: Test user ile giriş
- App'te purchase: Sandbox mode otomatik algılanır
StoreKit Test in Xcode
.storekit dosyası — Xcode'da test catalog:
json
1{2 "products": [{3 "id": "com.myapp.pro.monthly",4 "type": "auto_renewable_subscription",5 "displayPrice": "9.99",6 "subscriptionGroupID": "pro_group"7 }]8}TestFlight
- Beta Apple ID'leri sandbox'a düşürülmez — real purchases yapılır ama Apple'a ödeme gitmez, TestFlight expires.
- Subscription offerings TestFlight'ta gerçek config'ten çekilir.
SwiftUI Paywall Örneği
swift
1struct PaywallView: View {2 @Environment(StoreVM.self) private var store3 @State private var selectedProduct: Product?4 5 var body: some View {6 VStack(spacing: 16) {7 Text("Pro'ya Yükselt")8 .font(.largeTitle)9 10 ForEach(store.products) { product in11 ProductCard(12 product: product,13 isSelected: selectedProduct?.id == product.id14 )15 .onTapGesture { selectedProduct = product }16 }17 18 Button {19 Task {20 guard let product = selectedProduct else { return }21 try await store.purchase(product)22 }23 } label: {24 Text("Satın Al")25 .frame(maxWidth: .infinity)26 .padding()27 }28 .buttonStyle(.borderedProminent)29 30 Text("Her zaman iptal edebilirsiniz")31 .font(.caption)32 .foregroundStyle(.secondary)33 }34 .padding()35 .task { try? await store.loadProducts() }36 }37}ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
IAP'ı production'a çıkarırken: 1. ✅ Server-side JWS validation entegre et 2. ✅ Webhook signature verification 3. ✅ Idempotent transaction processing (transaction_id unique key) 4. ✅ Grace period handling (billing retry) 5. ✅ Refund webhook → immediate entitlement revoke 6. ✅ Family Sharing UI (ownership type göster) 7. ✅ Localized prices (displayPrice, currency) 8. ✅ Restore purchases button (App Store zorunlu) 9. ✅ Privacy policy link (data handling) 10. ✅ Sandbox test flow + TestFlight final test 11. ✅ Production key'leri server'da secret manager'da (AWS Secrets, 1Password) 12. ✅ Monitoring: purchase success rate, refund rate, renewal rate **External Resources:** - [StoreKit 2 documentation](https://developer.apple.com/documentation/storekit) - [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi) - [Server Notifications V2](https://developer.apple.com/documentation/appstoreservernotifications/app_store_server_notifications_v2) - [JWS signed transaction verification](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction) - [RevenueCat StoreKit 2 guide](https://www.revenuecat.com/blog/engineering/storekit-2/)
Sonuç
StoreKit 2 modern iOS IAP'ın zorunlu standardı. async/await ile temiz kod, JWS ile güvenli verification, server notifications V2 ile reliable webhook, winback offers ile churn reduction. Migration StoreKit 1'den 2-3 haftalık iş ama ROI yüksek. Direct IAP yapma kararı RevenueCat vs self-built tradeoff'u (başka blog konusu). Production-ready kod için yukarıdaki checklist kritik — özellikle server-side validation adımı atlanmaz.
*İlgili yazılar: StoreKit Subscription, Async/Await Best Practices, Swift 6 Concurrency.*

