mcpField
S
storekit

storekit

github.com/dpearson2699開発1.2k4.1k 回インストール13 週間前に更新
リポジトリ

storekit について

アプリ内課金やサブスクリプションを実装するSwift開発者向けのStoreKit 2ガイドです。商品取得、取引検証、権利管理、復元、SwiftUIのストアビューを扱います。

主な機能

  1. 01商品の取得と購入処理
  2. 02取引の検証と更新イベントの処理
  3. 03サブスクリプション状態と権利の確認
  4. 04ストアビューによる購入画面の構築
  5. 05購入復元と取り消しへの対応

活用シーン

  1. 01消耗型または買い切り購入の実装
  2. 02自動更新サブスクリプションとペイウォールの作成
  3. 03返金や更新後のアクセス権の反映

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill storekit

インストール後、Claude Code で/storekitを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
app-review-guidelines.md
storekit-advanced.md

詳細

StoreKit 2 のアプリ内課金とサブスクリプション

StoreKit 2 を使って、アプリ内課金、サブスクリプション、ペイウォール、StoreKit のテストを実装します。最新の Swift ベースの Product、Transaction、PurchaseAction、StoreView、SubscriptionStoreView API を使用してください。レガシー OS のサポートが必要な場合を除き、従来のアプリ内課金 API(SKProduct、SKPaymentQueue)は使用しないでください。

StoreKit のビューは購入を自動的に開始します。カスタムコントロールでは、SwiftUI の PurchaseAction、UIKit/AppKit の purchase(confirmIn:options:)、watchOS の product.purchase(options:) を使用してください。

目次

製品タイプ

タイプEnum ケース動作
消耗型.consumable1 回使用すると消費され、再購入できます(ジェム、コインなど)
非消耗型.nonConsumable1 回購入すると永続的に利用できます(プレミアム機能のロック解除など)
自動更新サブスクリプション.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:
        // Ask to Buy or deferred approval: show pending UI, no unlock yet.
        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 のコントロールスタイルにあるコンテナ背景とヘッダーのパターンを使用してください。

階層レイアウト

iOS 18 以降のオプションの整理には、SubscriptionOptionGroup、SubscriptionOptionSection、または SubscriptionPeriodGroupSet を使用してください。サブスクリプショングループの管理を参照してください。

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支払いに失敗しましたが、猶予期間中はアクセスが継続します
.revokedApple がサブスクリプションを返金または取り消しました

購入の復元

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
            // Migration logic for users who paid before subscription model
        case .unverified:
            // Potentially tampered -- restrict features as appropriate
            break
        }
    } catch { /* Could not retrieve app transaction */ }
}

購入オプション

// App account token for server-side reconciliation
try await product.purchase(options: [.appAccountToken(UUID())])

// Consumable quantity
try await product.purchase(options: [.quantity(5)])

// Simulate Ask to Buy in sandbox
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 を開始しない
// WRONG: No listener -- misses renewals, refunds, Ask to Buy approvals
@main struct MyApp: App {
    var body: some Scene { WindowGroup { ContentView() } }
}
// CORRECT: Start listener in App init (see Transaction.updates section above)
2. transaction.finish() を忘れる
// WRONG: Never finished -- reappears in unfinished queue forever
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)

// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.
let transaction = try checkVerified(verification)
try await recordDelivery(transaction)
await transaction.finish()
3. 検証結果を無視する
// WRONG: Using unverified transaction -- security risk
let transaction = verification.unsafePayloadValue

// CORRECT: Verify before using
let transaction = try checkVerified(verification)
4. 新しい StoreKit 2 のコードで従来のアプリ内課金 API を使用する
// AVOID: Original In-App Purchase APIs
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)

// PREFERRED: StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
5. revocationDate を確認しない
// WRONG: Grants access to refunded purchases
if case .verified(let transaction) = result {
    purchased.insert(transaction.productID)
}

// CORRECT: Skip revoked transactions
if case .verified(let transaction) = result, transaction.revocationDate == nil {
    purchased.insert(transaction.productID)
}
6. 価格をハードコードする
// WRONG: Wrong for other currencies and regions
Text("Buy Premium for $4.99")

// CORRECT: Localized price from Product
Text("Buy \(product.displayName) for \(product.displayPrice)")
7. .pending の購入結果を処理しない
// WRONG: Silently drops pending Ask to Buy
default: break

// CORRECT: Explain approval is pending; unlock only after Transaction.updates
case .pending:
    showPendingApprovalMessage()
8. 起動時に一度だけエンタイトルメントを確認する
// WRONG: Check once, never update
func appDidFinish() { Task { await updateEntitlements() } }

// CORRECT: Re-check on Transaction.updates AND on foreground return
// Transaction.updates listener handles mid-session changes.
// Also use .task { await storeManager.updateEntitlements() } on content views.
9. 購入の復元ボタンがない
// WRONG: No restore option -- App Store rejection risk
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(groupID: "group_id")
    .storeButton(.visible, for: .restorePurchases)
10. ポリシーへのリンクがないサブスクリプションビュー
// WRONG: No terms or privacy policy
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(groupID: "group_id")
    .subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
    .subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)

レビューチェックリスト

  • Transaction.updates リスナーがアプリ起動時に App init で開始される
  • アクセスを許可する前に、すべてのトランザクションが検証される
  • transaction.finish() は、コンテンツの提供が確実に完了した後にのみ呼び出される
  • 取り消し/返金されたトランザクションを除外し、エンタイトルメントの状態を更新する
  • .pending の結果に対して、「承認と購入の共有」/承認待ちを示すフィードバックを表示する
  • 購入画面とストアビューに購入の復元ボタンが表示される
  • サブスクリプションビューに利用規約とプライバシーポリシーへのリンクがある
  • 価格は product.displayPrice を使って表示し、ハードコードしない
  • サブスクリプションの条件(価格、期間、更新)が明確に表示される
  • 無料トライアルでは、トライアル終了後の価格が明確に表示される
  • レガシー OS のサポートに必要な場合を除き、従来のアプリ内課金 API(SKProduct、SKPaymentQueue)を使用しない
  • 商品 ID は定数として定義し、文字列を各所に散在させない
  • StoreKit テストで、プロモーションオファー、再獲得オファー、オファーコード、「承認と購入の共有」、更新、返金、取り消しを網羅する
  • Transaction.updates とアプリがフォアグラウンドになったときにエンタイトルメントを再確認する
  • 該当する場合、サーバー側の検証に jwsRepresentation を使用する
  • 消耗型アイテムは速やかに提供し、トランザクションを完了する
  • 並行処理の境界を越えて共有される場合、トランザクションオブザーバーの型と商品モデルの型は Sendable にする

参考資料

  • IAP のルール(ガイドライン 3.1.1)、サブスクリプションの表示要件、リジェクト防止については、references/app-review-guidelines.md を参照してください。
  • サブスクリプションの制御スタイル、オファー管理、テストパターン、高度なサブスクリプション処理については、references/storekit-advanced.md を参照してください。
  • 提出、プライバシー、メタデータ、スクリーンショット、リジェクトリスクの監査には app-store-review を使用してください。
  • キーワード、スクリーンショットのキャプション、ランキング、コンバージョン戦略には app-store-optimization を使用してください。
  • Apple 公式ドキュメント:StoreKit API の選択、Transaction.updates、Transaction.currentEntitlements、 SubscriptionStoreView、PurchaseAction。

コメント