storekit

storekit

熱門

使用 StoreKit 2 實作、審查或改善應用程式內購買與訂閱功能。適用於使用 SubscriptionStoreView 或 ProductView 建立付費牆、使用 Product 與 Transaction API 處理交易、驗證權益、處理購買流程(消耗型、非消耗型、自動續訂型)、實作優惠代碼或促銷/回饋/入門優惠、管理訂閱狀態與續訂狀態、設定 StoreKit 測試設定檔,或整合家庭共享、詢問購買、退款處理與帳單重試邏輯。

931星標
0分支
更新於 2026/7/26
SKILL.md
唯讀
名稱
storekit
描述

使用 StoreKit 2 實作、審查或改善應用程式內購買與訂閱功能。適用於使用 SubscriptionStoreView 或 ProductView 建立付費牆、使用 Product 與 Transaction API 處理交易、驗證權益、處理購買流程(消耗型、非消耗型、自動續訂型)、實作優惠代碼或促銷/回饋/入門優惠、管理訂閱狀態與續訂狀態、設定 StoreKit 測試設定檔,或整合家庭共享、詢問購買、退款處理與帳單重試邏輯。

StoreKit 2 應用程式內購買與訂閱

使用 StoreKit 2 實作應用程式內購買、訂閱、付費牆與 StoreKit 測試。使用現代 Swift 基礎的 ProductTransactionPurchaseActionStoreViewSubscriptionStoreView API。除非需要支援舊版作業系統,否則避免使用原始應用程式內購買 API(SKProductSKPaymentQueue)。

StoreKit 視圖會自動發起購買。若需自訂控制項,在 SwiftUI 中使用 PurchaseAction,在 UIKit/AppKit 中使用 purchase(confirmIn:options:),在 watchOS 中使用 product.purchase(options:)

目錄

產品類型

類型 列舉值 行為
消耗型 .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 中的容器背景與標頭模式。

階層式佈局

使用 SubscriptionOptionGroupSubscriptionOptionSectionSubscriptionPeriodGroupSet 來組織 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(SKProductSKPaymentQueue
  • [ ] 產品 ID 定義為常數,而非散落的字串
  • [ ] StoreKit 測試涵蓋促銷優惠、回饋優惠、優惠代碼、詢問購買、續訂、退款與撤銷
  • [ ] 在 Transaction.updates 與應用程式回到前景時重新檢查權益
  • [ ] 若適用,伺服器端驗證使用 jwsRepresentation
  • [ ] 消耗型產品立即交付並完成
  • [ ] 交易觀察者類型與產品模型類型在跨並發邊界共享時為 Sendable

參考資料