mcpField
A
axiom-storekit-ref

axiom-storekit-ref

github.com/charleswiltgen開発1.2k181 回インストール今日更新
リポジトリ

axiom-storekit-ref について

axiom-storekit-refは、Appleプラットフォーム開発者向けのStoreKit 2 APIリファレンススキルです。購入、トランザクション、サブスクリプションの実装や問題調査に役立つ、iOS 18.4の追加項目も扱います。

主な機能

  1. 01StoreKit 2 APIの参照
  2. 02商品とサブスクリプションの種類
  3. 03購入フローと結果処理
  4. 04トランザクション検証と権利確認
  5. 05iOS 18.4の追加項目

活用シーン

  1. 01アプリ内課金の実装
  2. 02サブスクリプション設計
  3. 03トランザクション処理の調査

インストール

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

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-storekit-ref

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

詳細

StoreKit 2 — API 完全リファレンス

概要

StoreKit 2 は、async/await API、自動レシート検証、SwiftUI との統合を備えた、Apple の最新のアプリ内課金フレームワークです。このリファレンスでは、すべての API、iOS 18.4 の機能強化、WWDC 2025 の包括的なコード例を取り上げます。

対応する製品タイプ

消耗型:

  • 複数回購入できる製品です
  • 例:コイン、ヒント、一時的なブースト
  • 新しいデバイスでは復元されません

非消耗型:

  • 一度購入すれば、永続的に所有できる製品です
  • 例:プレミアム機能、レベルパック、広告の削除
  • 新しいデバイスでは必ず復元されます

自動更新サブスクリプション:

  • 自動的に更新されるサブスクリプションです
  • サブスクリプショングループに整理されます
  • 新しいデバイスでは必ず復元されます
  • 対応する内容:無料トライアル、導入オファー、プロモーションオファー、再獲得オファー

非更新サブスクリプション:

  • 期間が固定されたサブスクリプションです(自動更新なし)
  • 例:シーズンパス
  • 新しいデバイスでは必ず復元されます
StoreKit 1 からの主な改善点
  • Async/Await:デリゲート/クロージャーの代わりに最新の並行処理を使用
  • 自動検証:JSON Web Signature(JWS)検証を標準で搭載
  • トランザクションの型:SKPaymentTransaction の代わりに、型安全な Swift 型を使用
  • テスト:ローカルテスト用の StoreKit 構成ファイル
  • SwiftUI ビュー:購入用の事前構築済み UI(ProductView、SubscriptionStoreView)
  • サーバー API:App Store Server API と Server Notifications

このリファレンスを使用する場面

次のような場合に、このリファレンスを使用してください:

  • StoreKit 2 でアプリ内課金を実装する
  • iOS 18.4 の新しいフィールド(appTransactionID、offerPeriod など)を理解する
  • 特定の API シグネチャやパラメーターを調べる
  • サブスクリプションのアーキテクチャを計画する
  • トランザクションの問題をデバッグする
  • StoreKit Views を実装する
  • App Store Server API と統合する

関連スキル:

  • axiom-in-app-purchases — テスト優先のワークフローとアーキテクチャパターンを扱う、規律を重視したスキル
  • (将来:既存の IAP コードを監査する iap-auditor エージェント)
  • (将来:IAP をゼロから実装する iap-implementation エージェント)

Product

概要

Product は、App Store Connect または StoreKit 構成ファイルで設定されたアプリ内課金アイテムを表します。

製品の読み込み

基本的な読み込み:

import StoreKit

let productIDs = [
    "com.app.coins_100",
    "com.app.premium",
    "com.app.pro_monthly"
]

let products = try await Product.products(for: productIDs)
WWDC 2021-10114 より

製品が見つからない場合の処理:

let products = try await Product.products(for: productIDs)

// Check what loaded
let loadedIDs = Set(products.map { $0.id })
let missingIDs = Set(productIDs).subtracting(loadedIDs)

if !missingIDs.isEmpty {
    print("Missing products: \(missingIDs)")
    // Products not configured in App Store Connect or .storekit file
}
製品のプロパティ

基本プロパティ:

let product: Product

product.id // "com.app.premium"
product.displayName // "Premium Upgrade"
product.description // "Unlock all features"
product.displayPrice // "$4.99"
product.price // Decimal(4.99)
product.type // .nonConsumable

製品タイプの列挙型:

switch product.type {
case .consumable:
    // Coins, hints, boosts
case .nonConsumable:
    // Premium features, level packs
case .autoRenewable:
    // Monthly/annual subscriptions
case .nonRenewing:
    // Seasonal passes
@unknown default:
    break
}
サブスクリプション固有のプロパティ

製品がサブスクリプションかどうかを確認する:

if let subscriptionInfo = product.subscription {
    // Product is auto-renewable subscription
    let groupID = subscriptionInfo.subscriptionGroupID
    let period = subscriptionInfo.subscriptionPeriod
}

サブスクリプション期間:

let period = product.subscription?.subscriptionPeriod

switch period?.unit {
case .day:
    print("\(period?.value ?? 0) days")
case .week:
    print("\(period?.value ?? 0) weeks")
case .month:
    print("\(period?.value ?? 0) months")
case .year:
    print("\(period?.value ?? 0) years")
default:
    break
}

導入オファー:

if let introOffer = product.subscription?.introductoryOffer {
    print("Free trial: \(introOffer.period.value) \(introOffer.period.unit)")
    print("Price: \(introOffer.displayPrice)")

    switch introOffer.paymentMode {
    case .freeTrial:
        print("Free trial - no charge")
    case .payAsYouGo:
        print("Discounted price per period")
    case .payUpFront:
        print("One-time discounted price")
    @unknown default:
        break
    }
}

プロモーションオファー:

let offers = product.subscription?.promotionalOffers ?? []

for offer in offers {
    print("Offer ID: \(offer.id)")
    print("Price: \(offer.displayPrice)")
    print("Period: \(offer.period.value) \(offer.period.unit)")
}
購入メソッド

UI コンテキストを指定して購入する(iOS 18.2 以降):

let product: Product
let scene: UIWindowScene

let result = try await product.purchase(confirmIn: scene)
WWDC 2025-241:9:32 より

オプションを指定して購入する:

let accountToken = UUID()

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .appAccountToken(accountToken)
    ]
)
WWDC 2025-241:11:01 より

プロモーションオファーを指定して購入する(JWS 形式):

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .promotionalOffer(offerID: "promo_winback", signature: jwsSignature)
    ]
)
WWDC 2025-241:10:55 より

カスタムの導入オファー適格性を指定して購入する:

let jwsSignature: String // From your server

let result = try await product.purchase(
    confirmIn: scene,
    options: [
        .introductoryOfferEligibility(signature: jwsSignature)
    ]
)
WWDC 2025-241:10:42 より

SwiftUI で購入する(Environment を使用):

struct ProductView: View {
    let product: Product
    @Environment(\.purchase) private var purchase

    var body: some View {
        Button("Buy \(product.displayPrice)") {
            Task {
                do {
                    let result = try await purchase(product)
                    // Handle result
                } catch {
                    print("Purchase failed: \(error)")
                }
            }
        }
    }
}
WWDC 2025-241:9:50 より
PurchaseResult

購入結果の処理:

let result = try await product.purchase(confirmIn: scene)

switch result {
case .success(let verificationResult):
    // Purchase succeeded - verify transaction
    guard let transaction = try? verificationResult.payloadValue else {
        print("Transaction verification failed")
        return
    }

    // Grant entitlement
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .userCancelled:
    // User tapped "Cancel" in payment sheet
    print("User cancelled purchase")

case .pending:
    // Purchase requires action (Ask to Buy, payment issue)
    // Transaction will arrive via Transaction.updates when approved
    print("Purchase pending approval")

@unknown default:
    break
}
WWDC 2025-241 より

Transaction

概要

Transaction は、成功したアプリ内課金を表します。購入メタデータ、製品 ID、購入日、およびサブスクリプションの場合は有効期限が含まれます。

新しいフィールド(iOS 18.4)

appTransactionID:

let transaction: Transaction
let appTransactionID = transaction.appTransactionID
// Unique ID for app download (same across all purchases by same Apple Account)
WWDC 2025-241:4:13 より

offerPeriod:

if let offerPeriod = transaction.offer?.period {
    print("Offer duration: \(offerPeriod)")
    // ISO 8601 duration format (e.g., "P1M" for 1 month)
}
WWDC 2025-249:3:11 より

advancedCommerceInfo:

if let advancedInfo = transaction.advancedCommerceInfo {
    // Only present for Advanced Commerce API purchases
    // nil for standard IAP
}
WWDC 2025-241:4:42 より
重要なプロパティ

基本フィールド:

let transaction: Transaction

transaction.id // Unique transaction ID
transaction.originalID // Original transaction ID (consistent across renewals)
transaction.productID // "com.app.pro_monthly"
transaction.productType // .autoRenewable
transaction.purchaseDate // Date of purchase
transaction.appAccountToken // UUID set at purchase time (if provided)

サブスクリプションフィールド:

transaction.expirationDate // When subscription expires
transaction.isUpgraded // true if user upgraded to higher tier
transaction.revocationDate // Date of refund (nil if not refunded)
transaction.revocationReason // .developerIssue or .other

オファーフィールド:

if let offer = transaction.offer {
    offer.type // .introductory or .promotional or .code
    offer.id // Offer identifier from App Store Connect
    offer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront, .oneTime
}
WWDC 2025-241:8:00 より
現在のエンタイトルメント

現在のエンタイトルメントをすべて取得する:

var purchasedProductIDs: Set<String> = []

for await result in Transaction.currentEntitlements {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // Only include non-refunded transactions
    if transaction.revocationDate == nil {
        purchasedProductIDs.insert(transaction.productID)
    }
}
WWDC 2025-241 より

特定の製品のエンタイトルメントを取得する(iOS 18.4 以降):

let productID = "com.app.premium"

for await result in Transaction.currentEntitlements(for: productID) {
    if let transaction = try? result.payloadValue,
       transaction.revocationDate == nil {
        // User owns this product
        return true
    }
}
WWDC 2025-241:3:31 より

非推奨 API(iOS 18.4):

// ❌ Deprecated in iOS 18.4
let entitlement = await Transaction.currentEntitlement(for: productID)

// ✅ Use this instead (returns sequence, handles Family Sharing)
for await result in Transaction.currentEntitlements(for: productID) {
    // ...
}
WWDC 2025-241:3:31 より
トランザクション履歴

すべてのトランザクションを取得:

for await result in Transaction.all {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    print("Transaction: \(transaction.productID) on \(transaction.purchaseDate)")
}

製品のトランザクションを取得:

for await result in Transaction.all(matching: productID) {
    guard let transaction = try? result.payloadValue else {
        continue
    }

    // All transactions for this product
}
トランザクションリスナー

リアルタイム更新を監視する(必須):

func listenForTransactions() -> Task<Void, Never> {
    Task.detached {
        for await verificationResult in Transaction.updates {
            await handleTransaction(verificationResult)
        }
    }
}

func handleTransaction(_ result: VerificationResult<Transaction>) async {
    guard let transaction = try? result.payloadValue else {
        return
    }

    // Grant or revoke entitlement
    if transaction.revocationDate != nil {
        await revokeEntitlement(for: transaction.productID)
    } else {
        await grantEntitlement(for: transaction)
    }

    // CRITICAL: Always finish transaction
    await transaction.finish()
}
WWDC 2021-10114 より

トランザクションの発生元:

  • アプリ内課金
  • App Store からの購入(プロモーション対象のアプリ内課金)
  • オファーコードの引き換え
  • サブスクリプションの更新
  • ファミリー共有のトランザクション
  • 保留中の購入(承認と購入)が完了した場合
  • 払い戻し通知
検証

VerificationResult:

let result: VerificationResult<Transaction>

switch result {
case .verified(let transaction):
    // ✅ Transaction signed by App Store
    await grantEntitlement(for: transaction)
    await transaction.finish()

case .unverified(let transaction, let error):
    // ❌ Transaction signature invalid
    print("Unverified: \(error)")
    // DO NOT grant entitlement
    await transaction.finish() // Still finish to clear queue
}

検証で確認されること:

  • トランザクションが App Store によって署名されている(不正ではない)
  • トランザクションがこのアプリのものである(バンドル ID が一致)
  • トランザクションがこのデバイスのものである
トランザクションの完了

必ず finish() を呼び出す:

await transaction.finish()

完了するタイミング:

  • ✅ ユーザーに利用権を付与した後
  • ✅ トランザクションのレシート/ID を保存した後
  • ✅ 未検証のトランザクションでも(キューをクリアするため)
  • ✅ 払い戻し済みのトランザクションでも

完了しなかった場合に起こること:

  • 次回のアプリ起動時にトランザクションが再配信される
  • Transaction.updates がトランザクションを再度発行する
  • 時間とともにキューが蓄積する

AppTransaction

概要

AppTransaction はアプリの最初のダウンロードを表します。AppTransaction.shared から利用できます。

新しいフィールド(iOS 18.4)

appTransactionID:

let appTransaction = try await AppTransaction.shared

switch appTransaction {
case .verified(let transaction):
    let appTransactionID = transaction.appTransactionID
    // Globally unique ID for this Apple Account + app
    // Same value appears in Transaction and RenewalInfo

case .unverified(_, let error):
    print("AppTransaction verification failed: \(error)")
}
WWDC 2025-241:1:42 より

originalPlatform:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    let platform = appTransaction.originalPlatform

    switch platform {
    case .iOS:
        print("Originally downloaded on iPhone/iPad")
    case .macOS:
        print("Originally downloaded on Mac")
    case .tvOS:
        print("Originally downloaded on Apple TV")
    case .visionOS:
        print("Originally downloaded on Vision Pro")
    @unknown default:
        break
    }
}
WWDC 2025-241:2:11 より

注:watchOS でダウンロードされたアプリの場合、originalPlatform = .iOS になります。

主なプロパティ
let appTransaction: AppTransaction

appTransaction.appVersion // "1.2.3"
appTransaction.originalAppVersion // "1.0.0"
appTransaction.originalPurchaseDate // First download date
appTransaction.bundleID // "com.company.app"
appTransaction.deviceVerification // UUID for device
appTransaction.deviceVerificationNonce // Nonce for verification
ユースケース

アプリのバージョンを確認する:

if let appTransaction = try? await AppTransaction.shared.payloadValue {
    if appTransaction.appVersion != currentVersion {
        // Prompt user to update
    }
}
WWDC 2025-241:0:51 より

ビジネスモデルの移行:

// Moving from paid app to free app with IAP
if appTransaction.originalPlatform == .iOS,
   appTransaction.originalPurchaseDate < migrationDate {
    // User paid for app before migration - grant premium
    await grantPremiumAccess()
}
WWDC 2025-241:2:32 より

Product.SubscriptionInfo.RenewalInfo

概要

RenewalInfo は、自動更新サブスクリプションの更新状態に関する情報を提供します。更新されるかどうか、有効期限切れの理由、今後のオファーなどが含まれます。

新しいフィールド(iOS 18.4)

appTransactionID:

let renewalInfo: RenewalInfo
let appTransactionID = renewalInfo.appTransactionID
WWDC 2025-241:6:40 より

offerPeriod:

if let offerPeriod = renewalInfo.offerPeriod {
    print("Next renewal offer period: \(offerPeriod)")
    // ISO 8601 duration (applies at next renewal)
}
WWDC 2025-249:3:11 より

appAccountToken:

if let token = renewalInfo.appAccountToken {
    // UUID associating subscription with your server account
}
WWDC 2025-241:6:56 より

advancedCommerceInfo:

if let advancedInfo = renewalInfo.advancedCommerceInfo {
    // Only for Advanced Commerce API subscriptions
}
WWDC 2025-241:6:50 より
主なプロパティ

更新状態:

let renewalInfo: RenewalInfo

renewalInfo.willAutoRenew // true if subscription will renew
renewalInfo.autoRenewPreference // Product ID customer will renew to
renewalInfo.expirationReason // Why subscription expired (if expired)

有効期限切れの理由:

switch renewalInfo.expirationReason {
case .autoRenewDisabled:
    // User turned off auto-renewal
case .billingError:
    // Payment method issue
case .didNotConsentToPriceIncrease:
    // User didn't accept price increase - show win-back offer!
case .productUnavailable:
    // Product no longer available
case .unknown:
    // Unknown reason
@unknown default:
    break
}
WWDC 2025-241:5:38 より

猶予期間:

if let gracePeriodExpiration = renewalInfo.gracePeriodExpirationDate {
    // Subscription in grace period - billing issue
    // Show update payment method UI
}

価格改定への同意:

if let consentStatus = renewalInfo.priceIncreaseStatus {
    switch consentStatus {
    case .agreed:
        // User accepted price increase
    case .notYetResponded:
        // User hasn't responded - show consent UI
    @unknown default:
        break
    }
}
RenewalInfo へのアクセス

SubscriptionStatus から:

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

for status in statuses {
    switch status.renewalInfo {
    case .verified(let renewalInfo):
        print("Will renew: \(renewalInfo.willAutoRenew)")
    case .unverified(_, let error):
        print("Renewal info verification failed: \(error)")
    }
}

Product.SubscriptionInfo.Status

概要

SubscriptionStatus は自動更新サブスクリプションの現在の状態を表します。アクティブか、有効期限切れか、猶予期間中か、請求再試行中かなどが含まれます。

サブスクリプションの状態

状態の列挙型:

let status: Product.SubscriptionInfo.Status

switch status.state {
case .subscribed:
    // User has active subscription - full access

case .expired:
    // Subscription expired - show resubscribe/win-back offer

case .inGracePeriod:
    // Billing issue but access maintained - show update payment UI

case .inBillingRetryPeriod:
    // Apple retrying payment - maintain access

case .revoked:
    // Family Sharing access removed - revoke access

@unknown default:
    break
}
WWDC 2025-241 より
サブスクリプションの状態を取得する

サブスクリプショングループの場合:

let groupID = "pro_tier"

let statuses = try await Product.SubscriptionInfo.status(for: groupID)

// Find highest service level
let activeStatus = statuses
    .filter { $0.state == .subscribed }
    .max { $0.transaction.productID < $1.transaction.productID }
WWDC 2025-241:6:22 より

特定のトランザクションの場合(iOS 18.4 以降):

let transactionID = transaction.id

let status = try await Product.SubscriptionInfo.status(for: transactionID)
WWDC 2025-241:6:40 より

状態の更新を監視する:

for await statuses in Product.SubscriptionInfo.Status.updates(for: groupID) {
    // Process updated statuses
    for status in statuses {
        print("Status: \(status.state)")
    }
}
状態のプロパティ
let status: Product.SubscriptionInfo.Status

status.state // .subscribed, .expired, etc.
status.transaction // VerificationResult<Transaction>
status.renewalInfo // VerificationResult<RenewalInfo>

StoreKit ビュー

ProductView(iOS 17 以降)

基本的な使い方:

import StoreKit

struct ContentView: View {
    let productID = "com.app.premium"

    var body: some View {
        ProductView(id: productID)
    }
}
WWDC 2023-10013 より

読み込み済みの製品を使用する場合:

struct ContentView: View {
    let product: Product

    var body: some View {
        ProductView(for: product)
    }
}

カスタムアイコン:

ProductView(id: productID) {
    Image(systemName: "star.fill")
        .foregroundStyle(.yellow)
}

コントロールスタイル:

ProductView(id: productID)
    .productViewStyle(.regular)  // Default

ProductView(id: productID)
    .productViewStyle(.compact)  // Smaller

ProductView(id: productID)
    .productViewStyle(.large)  // Prominent
StoreView(iOS 17 以降)

基本的なストア:

struct ContentView: View {
    let productIDs = [
        "com.app.coins_100",
        "com.app.coins_500",
        "com.app.coins_1000"
    ]

    var body: some View {
        StoreView(ids: productIDs)
    }
}
WWDC 2023-10013 より

読み込み済みの製品を使用する場合:

struct ContentView: View {
    let products: [Product]

    var body: some View {
        StoreView(products: products)
    }
}
SubscriptionStoreView(iOS 17 以降)

基本的なサブスクリプションストア:

struct SubscriptionView: View {
    let groupID = "pro_tier"

    var body: some View {
        SubscriptionStoreView(groupID: groupID) {
            // Marketing content above subscription options
            VStack {
                Image("app-icon")
                Text("Go Pro")
                    .font(.largeTitle.bold())
                Text("Unlock all features")
            }
        }
    }
}
WWDC 2023-10013 より

コントロールスタイル:

SubscriptionStoreView(groupID: groupID) {
    // Marketing content
}
.subscriptionStoreControlStyle(.automatic)    // Default
.subscriptionStoreControlStyle(.picker)       // Horizontal picker
.subscriptionStoreControlStyle(.buttons)      // Stacked buttons
.subscriptionStoreControlStyle(.prominentPicker) // Large picker (iOS 18.4+)
WWDC 2025-241 より
SubscriptionOfferView(iOS 18.4 以降)

基本的なオファービュー:

struct ContentView: View {
    let productID = "com.app.pro_monthly"

    var body: some View {
        SubscriptionOfferView(id: productID)
    }
}
WWDC 2025-241:14:27 より

読み込み済みの製品を使用する場合:

let product: Product // Already loaded via Product.products(for:)

SubscriptionOfferView(product: product)

プロモーションアイコンを使用する場合:

SubscriptionOfferView(
    id: productID,
    prefersPromotionalIcon: true
)

// Also available as modifier
SubscriptionOfferView(id: productID)
    .prefersPromotionalIcon(true)

カスタムアイコンを使用する場合:

SubscriptionOfferView(id: productID) {
    Image("custom-icon")
        .resizable()
        .frame(width: 60, height: 60)
} placeholder: {
    Image(systemName: "photo")
        .foregroundStyle(.gray)
}
WWDC 2025-241:15:14 より

詳細アクションを使用する場合:

@State private var showStore = false

var body: some View {
    SubscriptionOfferView(id: productID)
        .subscriptionOfferViewDetailAction {
            showStore = true
        }
        .sheet(isPresented: $showStore) {
            SubscriptionStoreView(groupID: "pro_tier")
        }
}
WWDC 2025-241:15:38 より

表示する関連付け:

// Only show if customer can upgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .upgrade
)

// Only show if customer can downgrade
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .downgrade
)

// Show crossgrade options (same tier, different billing period)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .crossgrade
)

// Show current subscription (only if offer available)
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .current
)

// Show any plan in group
SubscriptionOfferView(
    groupID: "pro_tier",
    visibleRelationship: .all
)
WWDC 2025-241:17:44 より

アプリアイコンを使用する場合:

SubscriptionOfferView(
    groupID: groupID,
    visibleRelationship: .all,
    useAppIcon: true
)
WWDC 2025-241:19:06 より
オファー修飾子

プロモーションオファー(JWS):

SubscriptionStoreView(groupID: groupID)
    .subscriptionPromotionalOffer(
        for: { subscription in
            // Return offer for this subscription
            return subscription.promotionalOffers.first
        },
        signature: { subscription, offer in
            // Get JWS signature from server
            let signature = try await server.signOffer(
                productID: subscription.id,
                offerID: offer.id
            )
            return signature
        }
    )
WWDC 2025-241:12:17 より
subscriptionStatusTask モディファイア(iOS 18.4以降)

SwiftUI モディファイアで、アプリレベルのサブスクリプション状況を追跡します。状況の変化に自動的に反応するため、手動でのポーリングは不要です。

基本的な使い方:

@main
struct MyApp: App {
    @State private var customerStatus: CustomerStatus = .unknown

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.customerSubscriptionStatus, customerStatus)
                .subscriptionStatusTask(for: "your.group.id") { statuses in
                    if statuses.contains(where: { $0.state == .subscribed }) {
                        customerStatus = .subscribed
                    } else if statuses.contains(where: { $0.state == .expired }) {
                        customerStatus = .expired
                    } else {
                        customerStatus = .notSubscribed
                    }
                }
        }
    }
}

主な動作:

  • アプリ起動時に現在の状況を使って呼び出されます
  • サブスクリプション状況が変化すると、再度呼び出されます(更新、期限切れ、アップグレード)
  • StoreKit の状況をアプリのモデルに変換します。ドメインモデルはシンプルに保ちましょう
  • ビュー階層の最上位(App またはルートの WindowGroup)に追加します

オファーコード(iOS 18.2以降)

概要

オファーコードがすべての製品タイプに対応しました(以前はサブスクリプションのみでした):

  • 消耗型
  • 非消耗型
  • 非更新サブスクリプション
  • 自動更新サブスクリプション
アプリ内での引き換え

UIKit:

func showOfferCodeSheet() {
    guard let scene = view.window?.windowScene else { return }

    StoreKit.AppStore.presentOfferCodeRedeemSheet(in: scene)
}
WWDC 2025-241:7:38 より

SwiftUI:

.offerCodeRedemption(isPresented: $showRedeemSheet)
支払いモード

新機能:.oneTime:

let transaction: Transaction

if let offer = transaction.offer {
    switch offer.paymentMode {
    case .freeTrial:
        // No charge during offer period
    case .payAsYouGo:
        // Discounted price per billing period
    case .payUpFront:
        // One-time discounted price for entire duration
    case .oneTime:
        // ✨ New: One-time offer code redemption (iOS 17.2+)
    @unknown default:
        break
    }
}
WWDC 2025-241:8:17 より

旧バージョンでのアクセス(iOS 15~17.1):

if let offerMode = transaction.offerPaymentModeStringRepresentation {
    // String representation for older OS versions
    print(offerMode) // "oneTime"
}
WWDC 2025-241:8:49 より

App Store Server Library

概要

IAP リクエストへの署名と、サーバー API レスポンスのデコードに使うオープンソースライブラリです。Swift、Java、Python、Node.js で利用できます。

プロモーションオファーの署名を作成する

Swift の例:

import AppStoreServerLibrary

// Configure signing
let signingKey = "YOUR_PRIVATE_KEY"
let keyID = "YOUR_KEY_ID"
let issuerID = "YOUR_ISSUER_ID"
let bundleID = "com.app.bundle"

let creator = PromotionalOfferV2SignatureCreator(
    privateKey: signingKey,
    keyID: keyID,
    issuerID: issuerID,
    bundleID: bundleID
)

// Create signature
let productID = "com.app.pro_monthly"
let offerID = "promo_winback"
let transactionID = transaction.id // Optional but recommended

let signature = try creator.createSignature(
    productIdentifier: productID,
    subscriptionOfferIdentifier: offerID,
    applicationUsername: nil,
    nonce: UUID(),
    timestamp: Date().timeIntervalSince1970,
    transactionIdentifier: transactionID
)

// Send signature to app
return signature // Compact JWS string
WWDC 2025-241:12:44、2025-249 より

サーバーエンドポイントの例:

app.get("promo-offer") { req async throws -> String in
    let productID = try req.query.get(String.self, at: "productID")
    let offerID = try req.query.get(String.self, at: "offerID")

    let signature = try creator.createSignature(
        productIdentifier: productID,
        subscriptionOfferIdentifier: offerID,
        transactionIdentifier: nil
    )

    return signature
}
WWDC 2025-241:12:52 より

App Store Server API

App Account Token を設定する

エンドポイント:

PATCH /inApps/v1/transactions/{originalTransactionId}

リクエストボディ:

{
  "appAccountToken": "550e8400-e29b-41d4-a716-446655440000"
}

使い方:

  • オファーコードや App Store など、アプリ外で行われた購入の appAccountToken を設定します
  • アカウントの所有者が変わったときに appAccountToken を更新します
  • サーバー上で取引と顧客アカウントを関連付けます
WWDC 2025-249:5:19 より
App Transaction Info を取得する

エンドポイント:

GET /inApps/v2/appTransaction/{transactionId}

レスポンス:

{
  "signedAppTransactionInfo": "eyJhbGc..."
}

使い方:

  • サーバー上でアプリのダウンロード情報を取得します
  • アプリのバージョン、プラットフォーム、環境を確認します
  • 2025年後半に利用可能になります
WWDC 2025-249:10:48 より
消費情報 V2 を送信する

エンドポイント:

PUT /inApps/v2/transactions/consumption/{transactionId}

リクエストボディ:

{
  "customerConsented": true,
  "sampleContentProvided": false,
  "deliveryStatus": "DELIVERED",
  "refundPreference": "GRANT_PRORATED",
  "consumptionPercentage": 25000
}

フィールド:

  • customerConsented(必須):ユーザーが消費データの送信に同意したかどうか
  • sampleContentProvided(任意):購入前にサンプルが提供されたかどうか
  • deliveryStatus(必須):"DELIVERED" または複数の UNDELIVERED ステータス
  • refundPreference(任意):"NO_REFUND"、"GRANT_REFUND"、"GRANT_PRORATED"
  • consumptionPercentage(任意):0~100000(ミリパーセント。例:25000 = 25%)

按分返金:

  • 2025年の新機能です
  • 一部消費に対応します(消耗型、非消耗型、非更新サブスクリプション)
  • 自動更新サブスクリプションの場合、App Store が残り期間に基づいて計算します
WWDC 2025-249:16:09 より
返金通知

REFUND 通知:

{
  "notificationType": "REFUND",
  "data": {
    "signedTransactionInfo": "...",
    "refundPercentage": 75,
    "revocationType": "REFUND_PRORATED"
  }
}

revocationType の値:

  • REFUND_FULL:全額返金。すべてのアクセス権を取り消します
  • REFUND_PRORATED:一部返金。返金割合に応じてアクセス権を取り消します
  • FAMILY_REVOKE:ファミリー共有から削除。アクセス権を取り消します
WWDC 2025-249:20:17 より

エッジケース

ファミリー共有

ファミリー共有の取引を検出する:

// appAccountToken is NOT available for family shared transactions
let transaction: Transaction

if transaction.appAccountToken == nil {
    // Might be family shared (or appAccountToken not set)
    // Check ownershipType (if available)
}

ファミリー共有のサブスクリプション状況:

// Each family member has unique appTransactionID
// Use appTransactionID to identify individual family members
WWDC 2025-241:1:54 より
返金

返金を処理する:

func handleTransaction(_ transaction: Transaction) async {
    if let revocationDate = transaction.revocationDate {
        // Transaction was refunded
        print("Refunded on \(revocationDate)")

        switch transaction.revocationReason {
        case .developerIssue:
            // Refund due to app issue
        case .other:
            // Other refund reason
        @unknown default:
            break
        }

        // Revoke entitlement
        await revokeEntitlement(for: transaction.productID)
    }
}
Advanced Commerce API

Advanced Commerce API は、次のような用途に対応します:

  • 大規模なコンテンツカタログ向けのアプリ内購入
  • クリエイター向けの体験(チップ、支援)
  • オプションのアドオン付きサブスクリプション

取引が Advanced Commerce を使用しているか確認する:

if transaction.advancedCommerceInfo != nil {
    // Transaction from Advanced Commerce API
    // Large catalogs, creator experiences, subscriptions with add-ons
}

Transaction と RenewalInfo の両方にある advancedCommerceInfo フィールドからアクセスできます。標準的な IAP 取引では nil を返します。

WWDC 2025-241:4:51 より
ウィンバックオファー

期限切れのサブスクリプション向けにウィンバックオファーを表示する:

let renewalInfo: RenewalInfo

if renewalInfo.expirationReason == .didNotConsentToPriceIncrease {
    // Perfect time for win-back offer!
    SubscriptionOfferView(
        groupID: groupID,
        visibleRelationship: .current
    )
    .preferredSubscriptionOffer(offer: winBackOffer)
}
WWDC 2025-241:5:38 より

テスト

StoreKit 構成ファイル

作成:

  1. Xcode → ファイル → 新規 → StoreKit 構成ファイル
  2. 商品を追加します(消耗型、非消耗型、サブスクリプション)
  3. 価格、画像、説明を設定します

スキームで有効にする:

  1. スキーム → スキームを編集 → 実行 → オプション
  2. StoreKit 構成:.storekit ファイルを選択します

テストシナリオ:

  • 購入の成功
  • 購入のキャンセル
  • サブスクリプションの更新(時間を短縮)
  • サブスクリプションの期限切れ
  • アップグレード/ダウングレード
  • オファーコードの利用
  • ファミリー共有(構成ファイルで有効にします)
トランザクションマネージャー

テスト中にトランザクションを確認、操作するには、Xcode のトランザクションマネージャーウィンドウを使用します。

  • トランザクションを手動で作成します(特定の購入フローをテストします)
  • トランザクションのプロパティを変更します(有効期限、更新状態)
  • サブスクリプションのオファーシナリオをテストします
  • トランザクションの詳細と検証状況を確認します

開く:デバッグ → StoreKit → トランザクションを管理(StoreKit 構成を使用して実行中に操作します)

サンドボックステスト

サンドボックスアカウントを作成する:

  1. App Store Connect → ユーザーとアクセス → サンドボックステスター
  2. テスト用 Apple ID を作成します
  3. デバイスの「設定」→「App Store」→「サンドボックスアカウント」でサインインします

購入履歴を消去する:

  • 「設定」→「App Store」→「サンドボックスアカウント」→「購入履歴を消去」

StoreKit 1 からの移行

主な変更点

デリゲート → Async/Await:

// StoreKit 1
class StoreObserver: NSObject, SKPaymentTransactionObserver {
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        // Handle transactions
    }
}

// StoreKit 2
for await result in Transaction.updates {
    // Handle transactions
}

レシート → トランザクション:

// StoreKit 1
let receiptURL = Bundle.main.appStoreReceiptURL
let receipt = try Data(contentsOf: receiptURL!)

// StoreKit 2
let transaction: Transaction // Automatically verified!

商品 → Product.products(for:):

// StoreKit 1
let request = SKProductsRequest(productIdentifiers: Set(productIDs))
request.delegate = self
request.start()

// StoreKit 2
let products = try await Product.products(for: productIDs)

リソース

WWDC:2025-241, 2025-249, 2024-10061, 2024-10062, 2024-10110, 2023-10013, 2023-10140, 2022-10007, 2022-110404, 2021-10114

ドキュメント:/storekit

スキル:axiom-in-app-purchases


クイックリファレンス

商品タイプ
  • .consumable - 複数回購入できます(コイン、ブースト)
  • .nonConsumable - 一度購入すれば永久に所有できます(プレミアム、レベルパック)
  • .autoRenewable - 自動更新サブスクリプション
  • .nonRenewing - 期間が固定されたサブスクリプション
トランザクションの状態
  • success - 購入が完了しました
  • userCancelled - ユーザーがキャンセルをタップしました
  • pending - 操作が必要です(「承認と購入のリクエスト」)
サブスクリプションの状態
  • .subscribed - サブスクリプションは有効です
  • .expired - サブスクリプションの期限が切れました
  • .inGracePeriod - 請求に問題がありますが、アクセスは維持されます
  • .inBillingRetryPeriod - Apple が支払いを再試行しています
  • .revoked - ファミリー共有が解除されました
必須の呼び出し
// Load products
try await Product.products(for: productIDs)

// Purchase
try await product.purchase(confirmIn: scene)

// Current entitlements
Transaction.currentEntitlements(for: productID)

// Transaction listener
Transaction.updates

// Subscription status
Product.SubscriptionInfo.status(for: groupID)

// Restore purchases
try await AppStore.sync()

// Finish transaction (REQUIRED)
await transaction.finish()

コメント