[G1-Sync] Manual knowledge update

This commit is contained in:
Antigravity Agent
2026-05-09 21:08:02 +09:00
parent f0befc887a
commit 93ec7e9056
363 changed files with 68333 additions and 64 deletions
@@ -0,0 +1,139 @@
---
id: ios-storekit-2-patterns
title: StoreKit 2 — IAP / Subscription
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [ios, storekit, iap, subscription, vibe-coding]
tech_stack: { language: "Swift / StoreKit 2", applicable_to: ["iOS 15+"] }
applied_in: []
aliases: [in-app purchase, Transaction, JWS, server-side validation]
---
# StoreKit 2
> iOS 15+ 의 새 IAP API. async/await + 타입 안전. **Transaction 자체가 JWS 서명** — server-side validation 가능. 옛 receipt validation 시대 끝.
## 📖 핵심 개념
- Product: App Store 등록 상품.
- Purchase → Transaction → finish.
- Transaction.currentEntitlements: 현재 active 구독 / 영구 구매.
- Transaction.updates: 백그라운드 갱신 / 환불.
## 💻 코드 패턴
### Product fetch + 구매
```swift
import StoreKit
final class Store: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedIds: Set<String> = []
init() {
Task { await loadProducts(); await listenForUpdates() }
}
func loadProducts() async {
do {
products = try await Product.products(for: ["pro_yearly", "pro_monthly"])
} catch { print("load failed: \(error)") }
}
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updateEntitlements()
await transaction.finish()
return transaction
case .userCancelled, .pending:
return nil
@unknown default:
return nil
}
}
func updateEntitlements() async {
var ids = Set<String>()
for await result in Transaction.currentEntitlements {
if case .verified(let t) = result {
ids.insert(t.productID)
}
}
purchasedIds = ids
}
func listenForUpdates() async {
for await result in Transaction.updates {
if case .verified(let t) = result {
await updateEntitlements()
await t.finish()
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let value): return value
case .unverified(_, let error): throw error
}
}
}
```
### Server-side validation
```swift
// Transaction jwsRepresentation
let jws = transaction.jwsRepresentation
await api.verifyPurchase(jws: jws, userId: user.id)
```
서버:
```ts
// JWS 검증 — Apple 의 root cert chain 으로
import { decodeRevoked } from '@apple/app-store-server-library';
const decoded = await verifyJWS(jws, { rootCertificates });
if (decoded.appAccountToken === userId) await grantEntitlement(...);
```
### Restore
```swift
// " "
try await AppStore.sync()
// currentEntitlements
```
### 가격 표시 (locale)
```swift
Text(product.displayPrice) // $9.99 / 12,000
```
## 🤔 의사결정 기준
| 상품 | 종류 |
|---|---|
| 1회 구매 | non-consumable |
| 사용 후 소진 (코인) | consumable |
| 월/년 구독 | auto-renewable subscription |
| 정기 (1년 단발) | non-renewing subscription |
## ❌ 안티패턴
- **client-side validation 만**: 우회 가능. 서버 JWS 검증 필수.
- **Transaction.finish 안 함**: queue 누적. OS 가 다시 trigger.
- **listenForUpdates 안 함**: 백그라운드 갱신 / 환불 놓침.
- **userId 매핑 없음**: 어떤 사용자 구매인지 모름. appAccountToken.
- **Sandbox 와 Production 혼동**: TestFlight 는 sandbox.
- **에러 메시지 generic**: 결제 실패 종류별 (cancel / decline / parental) 다른 UX.
- **price hardcode**: 통화 변경 / 지역 미지원.
## 🤖 LLM 활용 힌트
- StoreKit 2 (iOS 15+). 옛 코드 마이그레이션 시 RestoreCompletedTransactions → AppStore.sync.
- 서버 JWS 검증 필수.
## 🔗 관련 문서
- [[Web_JWT_Patterns]]
- [[iOS_Keychain_Storage]]