使用 StoreKit 2 實作、審查或改善應用程式內購買與訂閱功能。適用於使用 SubscriptionStoreView 或 ProductView 建立付費牆、使用 Product 與 Transaction API 處理交易、驗證權益、處理購買流程(消耗型、非消耗型、自動續訂型)、實作優惠代碼或促銷/回饋/入門優惠、管理訂閱狀態與續訂狀態、設定 StoreKit 測試設定檔,或整合家庭共享、詢問購買、退款處理與帳單重試邏輯。
StoreKit 2 應用程式內購買與訂閱
使用 StoreKit 2 實作應用程式內購買、訂閱、付費牆與 StoreKit 測試。使用現代 Swift 基礎的 Product、Transaction、PurchaseAction、StoreView 與 SubscriptionStoreView API。除非需要支援舊版作業系統,否則避免使用原始應用程式內購買 API(SKProduct、SKPaymentQueue)。
StoreKit 視圖會自動發起購買。若需自訂控制項,在 SwiftUI 中使用 PurchaseAction,在 UIKit/AppKit 中使用 purchase(confirmIn:options:),在 watchOS 中使用 product.purchase(options:)。
目錄
- 產品類型
- 載入產品
- 購買流程
- Transaction.updates 監聽器
- 權益檢查
- SubscriptionStoreView (iOS 17+)
- StoreView (iOS 17+)
- 訂閱狀態檢查
- 還原購買
- 應用程式交易(應用程式購買驗證)
- 購買選項
- SwiftUI 購買回呼
- 常見錯誤
- 審查檢查清單
- 參考資料
產品類型
| 類型 | 列舉值 | 行為 |
|---|---|---|
| 消耗型 | .consumable |
使用一次,可重複購買(寶石、金幣) |
| 非消耗型 | .nonConsumable |
永久購買一次(解鎖高級功能) |
| 自動續訂型 | .autoRenewable |
定期扣款,自動續訂 |
| 非續訂型 | .nonRenewing |
限時存取,不會自動續訂 |
載入產品
將產品 ID 定義為常數。使用 Product.products(for:) 取得產品。
import StoreKit
enum ProductID {
static let premium = "com.myapp.premium"
static let gems100 = "com.myapp.gems100"
static let monthlyPlan = "com.myapp.monthly"
static let yearlyPlan = "com.myapp.yearly"
static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
}
let products = try await Product.products(for: ProductID.all)
for product in products {
print("\(product.displayName): \(product.displayPrice)")
}
購買流程
標準付費牆建議使用 StoreKit 視圖,因為它們會自動發起購買、還原購買並顯示政策控制項。若需自訂 SwiftUI 購買按鈕,建議從環境中使用 PurchaseAction。在 watchOS 上使用直接 product.purchase(options:),在 UIKit 或 AppKit 中則使用 purchase(confirmIn:options:) 進行確認。務必處理每個 PurchaseResult,在存取前驗證,確保內容可靠傳遞後再完成交易。
@Environment(\.purchase) private var purchase
func purchaseProduct(_ product: Product) async throws {
let result = try await purchase(product, options: [
.appAccountToken(userAccountToken)
])
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await deliverContent(for: transaction)
await transaction.finish()
case .userCancelled:
break
case .pending:
// 詢問購買或延遲核准:顯示待處理 UI,但不要解鎖。
showPendingApprovalMessage()
@unknown default:
break
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let value): return value
case .unverified(_, let error): throw error
}
}
Transaction.updates 監聽器
在應用程式啟動時啟動,而不是在付費牆出現時。可捕捉來自其他裝置的購買、家庭共享變更、續訂、詢問購買核准、退款、撤銷,以及 Apple 在啟動後立即發出的未完成交易。保留此任務在應用程式生命週期內持續執行。
@main
struct MyApp: App {
private let transactionListener: Task<Void, Never>
init() {
transactionListener = Self.listenForTransactions()
}
var body: some Scene {
WindowGroup { ContentView() }
}
static func listenForTransactions() -> Task<Void, Never> {
Task(priority: .background) {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
await StoreManager.shared.updateEntitlements()
await transaction.finish()
}
}
}
}
權益檢查
Transaction.currentEntitlements 會發出非消耗型產品、有效或寬限期內的自動續訂訂閱,以及最新的非續訂訂閱交易(包括已完成的交易)。它排除消耗型產品以及已退款或已撤銷的產品。請分別追蹤消耗型產品的交付情況,並在授予存取權限前,對非續訂訂閱套用應用程式的到期政策。
@Observable
@MainActor
class StoreManager {
static let shared = StoreManager()
var purchasedProductIDs: Set<String> = []
var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }
func updateEntitlements() async {
var purchased = Set<String>()
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result,
transaction.revocationDate == nil {
if transaction.productType == .nonRenewing,
transaction.expirationDate.map({ $0 <= .now }) ?? true {
continue
}
purchased.insert(transaction.productID)
}
}
purchasedProductIDs = purchased
}
}
SwiftUI .currentEntitlementTask 修飾詞
struct PremiumGatedView: View {
@State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading
var body: some View {
Group {
switch state {
case .loading: ProgressView()
case .failure: PaywallView()
case .success(.some(.verified(let transaction))) where transaction.revocationDate == nil:
PremiumContentView()
case .success:
PaywallView()
}
}
.currentEntitlementTask(for: ProductID.premium) { state in
self.state = state
}
}
}
SubscriptionStoreView (iOS 17+)
內建的 SwiftUI 訂閱付費牆視圖。自動處理產品載入、購買 UI 與還原購買。
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
.subscriptionStoreControlStyle(.prominentPicker)
.subscriptionStoreButtonLabel(.multiline)
.storeButton(.visible, for: .restorePurchases)
.storeButton(.visible, for: .redeemCode)
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
自訂行銷內容
使用 SubscriptionStoreView Control Styles 中的容器背景與標頭模式。
階層式佈局
使用 SubscriptionOptionGroup、SubscriptionOptionSection 或 SubscriptionPeriodGroupSet 來組織 iOS 18+ 的選項;請參閱 Subscription Group Management。
StoreView (iOS 17+)
展示多個產品,包含本地化名稱、價格與購買按鈕。
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
.productViewStyle(.large)
.storeButton(.visible, for: .restorePurchases)
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
單一產品的 ProductView
ProductView(id: ProductID.premium) { iconPhase in
switch iconPhase {
case .success(let image): image.resizable().scaledToFit()
case .loading: ProgressView()
default: Image(systemName: "star.fill")
}
}
.productViewStyle(.large)
訂閱狀態檢查
func checkSubscriptionActive(groupID: String) async throws -> Bool {
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
for status in statuses {
guard case .verified = status.renewalInfo,
case .verified = status.transaction else { continue }
if status.state == .subscribed || status.state == .inGracePeriod {
return true
}
}
return false
}
續訂狀態
| 狀態 | 意義 |
|---|---|
.subscribed |
訂閱有效 |
.expired |
訂閱已過期 |
.inBillingRetryPeriod |
付款失敗,Apple 正在重試 |
.inGracePeriod |
付款失敗,但寬限期內仍可存取 |
.revoked |
Apple 已退款或撤銷訂閱 |
還原購買
StoreKit 2 透過 Transaction.currentEntitlements 處理還原。請加入還原按鈕或明確呼叫 AppStore.sync()。
func restorePurchases() async throws {
try await AppStore.sync()
await StoreManager.shared.updateEntitlements()
}
在商店視圖上:.storeButton(.visible, for: .restorePurchases)
應用程式交易(應用程式購買驗證)
驗證應用程式安裝的合法性。用於商業模式變更或偵測遭篡改的安裝(iOS 16+)。
func verifyAppPurchase() async {
do {
let result = try await AppTransaction.shared
switch result {
case .verified(let appTransaction):
let originalVersion = appTransaction.originalAppVersion
let purchaseDate = appTransaction.originalPurchaseDate
// 針對在訂閱模式前付費的使用者進行遷移邏輯
case .unverified:
// 可能遭篡改 — 適度限制功能
break
}
} catch { /* 無法取得應用程式交易 */ }
}
購買選項
// 用於伺服器端對帳的應用程式帳戶代碼
try await product.purchase(options: [.appAccountToken(UUID())])
// 消耗型數量
try await product.purchase(options: [.quantity(5)])
// 在沙盒中模擬詢問購買
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
SwiftUI 購買回呼
.onInAppPurchaseStart { product in
await analytics.trackPurchaseStarted(product.id)
}
.onInAppPurchaseCompletion { product, result in
if case .success(.success(.verified(let transaction))) = result {
await deliverContent(for: transaction)
await transaction.finish()
}
}
.inAppPurchaseOptions { product in
[.appAccountToken(userAccountToken)]
}
常見錯誤
1. 未在應用程式啟動時啟動 Transaction.updates
// 錯誤:沒有監聽器 — 錯過續訂、退款、詢問購買核准
@main struct MyApp: App {
var body: some Scene { WindowGroup { ContentView() } }
}
// 正確:在 App init 中啟動監聽器(請參閱上方 Transaction.updates 章節)
2. 忘記呼叫 transaction.finish()
// 錯誤:從未完成 — 交易會一直留在未完成佇列中
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
// 正確:確保內容可靠傳遞後再完成。若傳遞失敗,則不要完成。
let transaction = try checkVerified(verification)
try await recordDelivery(transaction)
await transaction.finish()
3. 忽略驗證結果
// 錯誤:使用未驗證的交易 — 安全風險
let transaction = verification.unsafePayloadValue
// 正確:使用前先驗證
let transaction = try checkVerified(verification)
4. 在新的 StoreKit 2 程式碼中使用原始應用程式內購買 API
// 避免:原始應用程式內購買 API
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
// 建議:StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
5. 未檢查 revocationDate
// 錯誤:對已退款的購買授予存取權
if case .verified(let transaction) = result {
purchased.insert(transaction.productID)
}
// 正確:跳過已撤銷的交易
if case .verified(let transaction) = result, transaction.revocationDate == nil {
purchased.insert(transaction.productID)
}
6. 硬編碼價格
// 錯誤:對其他貨幣與地區不正確
Text("購買高級版 $4.99")
// 正確:使用 Product 提供的本地化價格
Text("購買 \(product.displayName) 只需 \(product.displayPrice)")
7. 未處理 .pending 購買結果
// 錯誤:靜默忽略待處理的詢問購買
default: break
// 正確:說明核准正在等待;僅在 Transaction.updates 後才解鎖
case .pending:
showPendingApprovalMessage()
8. 僅在啟動時檢查一次權益
// 錯誤:只檢查一次,從不更新
func appDidFinish() { Task { await updateEntitlements() } }
// 正確:在 Transaction.updates 以及回到前景時重新檢查
// Transaction.updates 監聽器處理會話中的變更。
// 同時在內容視圖中使用 .task { await storeManager.updateEntitlements() }。
9. 缺少還原購買按鈕
// 錯誤:沒有還原選項 — 可能被 App Store 拒絕
SubscriptionStoreView(groupID: "group_id")
// 正確
SubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)
10. 訂閱視圖缺少政策連結
// 錯誤:沒有條款或隱私權政策
SubscriptionStoreView(groupID: "group_id")
// 正確
SubscriptionStoreView(groupID: "group_id")
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
審查檢查清單
- [ ]
Transaction.updates監聽器在應用程式啟動時於 App init 中啟動 - [ ] 所有交易在授予存取權前皆已驗證
- [ ] 僅在內容可靠傳遞後才呼叫
transaction.finish() - [ ] 排除已撤銷/已退款的交易,並更新權益狀態
- [ ]
.pending結果顯示詢問購買/延遲核准的回饋 - [ ] 還原購買按鈕在付費牆與商店視圖上可見
- [ ] 訂閱視圖上包含服務條款與隱私權政策連結
- [ ] 使用
product.displayPrice顯示價格,絕不硬編碼 - [ ] 清楚顯示訂閱條款(價格、期間、續訂)
- [ ] 免費試用狀態清楚顯示試用後的價格
- [ ] 除非需要支援舊版作業系統,否則不使用原始應用程式內購買 API(
SKProduct、SKPaymentQueue) - [ ] 產品 ID 定義為常數,而非散落的字串
- [ ] StoreKit 測試涵蓋促銷優惠、回饋優惠、優惠代碼、詢問購買、續訂、退款與撤銷
- [ ] 在 Transaction.updates 與應用程式回到前景時重新檢查權益
- [ ] 若適用,伺服器端驗證使用
jwsRepresentation - [ ] 消耗型產品立即交付並完成
- [ ] 交易觀察者類型與產品模型類型在跨並發邊界共享時為
Sendable
參考資料
- 請參閱 references/app-review-guidelines.md 了解 IAP 規則(準則 3.1.1)、訂閱顯示要求與拒絕預防。
- 請參閱 references/storekit-advanced.md 了解訂閱控制樣式、優惠管理、測試模式與進階訂閱處理。
- 關於送審、隱私、後設資料、截圖與拒絕風險審計,請使用
app-store-review。 - 關於關鍵字、截圖說明、排名與轉換策略,請使用
app-store-optimization。 - Apple 官方文件:Choosing a StoreKit API、Transaction.updates、Transaction.currentEntitlements、
SubscriptionStoreView 與 PurchaseAction。




