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

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

StoreKit 2 ile modern async/await satın alma akışı, JWS receipt validation, subscription offers, family sharing ve server-side validation tam rehberi.

StoreKit 2 Production Rehberi: Async/Await ile Modern IAP

# 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

Ö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 StoreKit
2 
3@Observable
4final 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: String
3 let displayName: String
4 let description: String
5 let price: Decimal
6 let displayPrice: String // "$9.99" localized
7 let type: ProductType // .consumable, .nonConsumable, .nonRenewable, .autoRenewable
8 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 bekleniyor
13 break
14 
15 case .userCancelled:
16 break
17 
18 @unknown default:
19 break
20 }
21 }
22 
23 func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
24 switch result {
25 case .unverified(_, let error):
26 throw error
27 case .verified(let safe):
28 return safe
29 }
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 server
2import 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 chain
10 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 signature
16 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?.introductoryOffer
2 
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.id
5)
6 
7// 2. Purchase with offer
8let 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.timestamp
15 )
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 handler
2app.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.currentEntitlements
2for 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 UI
2@Environment(\.requestReview) var requestReview
3@Environment(\.openURL) var openURL
4 
5// Refund request
6if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
7 let result = try await Transaction.beginRefundRequest(
8 for: transactionID,
9 in: windowScene
10 )
11}

Server'da Refund Notification

typescript
1case 'REFUND':
2 // notification.data.signedRenewalInfo → revoked
3 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

  1. Sandbox Tester oluştur: App Store Connect > Users and Access > Sandbox Testers
  2. Settings > App Store > Sandbox Account: Test user ile giriş
  3. 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 store
3 @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 in
11 ProductCard(
12 product: product,
13 isSelected: selectedProduct?.id == product.id
14 )
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.*

Etiketler

#iOS#Swift#StoreKit 2#IAP#Subscription#App Store#JWS
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