WWDC Quick Look 💓 By SwiftGGTeam
What’s new in StoreKit and In-App Purchase

What’s new in StoreKit and In-App Purchase

元の動画を見る

Highlight

3 つのパート: コアフレームワークの強化、署名リクエスト、新しい SwiftUI ストアビュー。


主な内容

サブスクリプション系 App では、よくこんな悩みがあります。ユーザーが iPad で App を購入し、Mac でも同じ App をダウンロードしたとき、開発者にとってこの 2 回のインストールが同一の Apple Account に紐づくものなのか判別が難しい、という問題です。また、ユーザーがサブスクリプションをキャンセルした後にウィンバックを仕掛けようとしても、そもそもそのユーザーが「有料 App として購入したのか」「無料ダウンロード後にサブスクライブしたのか」が分からない、ということもあります。これらの情報が欠けていると、ビジネス施策は推測ベースに頼らざるを得ません。

WWDC25 の StoreKit アップデートはこの課題を中心に展開されています。AppTransactionTransactionRenewalInfo という 3 つのコア型に appTransactionIDoriginalPlatformappAccountToken などのフィールドが追加され、offer code はサブスクリプションだけでなく消耗型と非更新型サブスクリプションにも拡張されました。署名リクエストは JWS フォーマットに切り替わり、App Store Server Library を使えば署名処理は数行のコードに収まります。さらに、新しい SwiftUI ビュー SubscriptionOfferView は、ユーザーの現在のサブスクリプション状態に応じてどのプランを表示するかを自動で選んでくれます。今回のアップデート全体の狙いはひとつ。十分に正確なユーザー識別と購入状態を開発者に渡し、マーケティング判断を App 側に取り戻すことです。


詳細

AppTransaction と Transaction の新しいフィールド

AppTransaction は、ユーザーがその App を最初にダウンロードしたときのトランザクションを表します。iOS 18.4 から 2 つのフィールドが追加されました (01:42)。

  • appTransactionID: App をダウンロードしたことのある Apple Account ごとにグローバルで一意な値が割り当てられ、iOS 15 までバックデプロイされます。Family Sharing 環境では家族メンバーごとに独立した値になります。
  • originalPlatform: 型は新しい AppStore.Platform。値は iOS / macOS / tvOS / visionOS です。watchOS でダウンロードされたものは iOS としてマークされます。

取得方法は次の通りです。

let result = try await AppTransaction.shared
guard case .verified(let appTransaction) = result else { return }

let downloadID = appTransaction.appTransactionID
let platform = appTransaction.originalPlatform

ポイント:

  • AppTransaction.shared が返すのは VerificationResult です。StoreKit が JSON web signature の検証を済ませているので、開発者は .verified 分岐を取り出すだけで済みます。
  • appTransactionID のおかげで、サーバー間リクエストを送らずに、同一ユーザーの複数の originalTransactionID を関連付けられるようになります (02:00)。
  • originalPlatform は、「有料 App」から「無料 + In-App Purchase」モデルへ移行する際に既存ユーザーへ権利を付与する判定に向いています。新モデル下で無償提供してしまうのを防げます。

currentEntitlements API のアップデート

iOS 18.4 から、従来の Transaction.currentEntitlement(for:)(単数形)は非推奨となり、Transaction.currentEntitlements(for:)(複数形)に置き換わりました。

for await result in Transaction.currentEntitlements(for: "com.app.pro") {
    guard case .verified(let transaction) = result else { continue }
    unlock(productID: transaction.productID)
}

ポイント:

  • 戻り値は AsyncSequence<VerificationResult<Transaction>> で、複数のレコードを非同期で反復処理できます。
  • 1 つのプロダクトに対して有効な transaction が複数存在することがあります。たとえば、ユーザー自身がサブスクライブしている上に、Family Sharing 経由でもアクセス権を取得しているケースです (03:58)。旧 API では 1 件しか返せず、情報が欠落します。
  • 戻り値は引き続き VerificationResult なので、.verified 分岐を経由する必要があります。

署名リクエストと App Store Server Library

iOS 18.4 では JWS 署名を必要とする 2 つの purchase option、introductoryOfferEligibilitypromotionalOffer が導入され、iOS 15 までバックデプロイされます (10:29)。クライアント側では新しい SwiftUI view modifier 経由で利用します。

SubscriptionStoreView(productIDs: ["pro.monthly"])
    .subscriptionPromotionalOffer { product in
        longestFreeTrialOffer(for: product)
    } compactJWS: { product, offer in
        try await NetworkLayer.signPromotionalOffer(
            productID: product.id,
            offerID: offer.id
        )
    }

ポイント:

  • 1 つ目のクロージャは、現在のプロダクトに対してどの promotional offer を適用するかを決定します。
  • 2 つ目のクロージャは compact JWS 文字列を返すもので、署名は自前のサーバーで発行する形です。署名鍵をクライアントに持たせるべきではありません。
  • App Store は JWS を検証してから購入を許可します。これが「このプロモーションは開発者が承認したものである」ことの証跡になります。

サーバー側では App Store Server Library(Swift / Java / Python / Node.js の 4 言語に対応)を使って署名します。

import AppStoreServerLibrary

let creator = try PromotionOfferV2SignatureCreator(
    bundleID: "com.example.SKDemo",
    signingKey: signingKey,
    keyID: keyID,
    issuerID: issuerID
)

let jws = try creator.createSignature(
    productID: productID,
    offerID: offerID,
    transactionID: appTransactionID
)

ポイント:

  • bundleIDsigningKeykeyIDissuerID は、App Store Connect の Users & Access → Integrations → In-App Purchases から取得できます (11:35)。
  • transactionID フィールドはオプショナルですが、渡すことが推奨されます。appTransactionID でも、そのユーザーの任意の Transaction の ID でも構いません (13:31)。これを付けると、App Store がより正確に購入の帰属を検証できます。

SubscriptionOfferView とサブスクリプションの関係

新しい SubscriptionOfferView は、特定のサブスクリプションプランを 1 行で宣言的にショーケース表示し、さらにユーザーの現在のサブスクリプションに応じてプランを自動選択できます。

SubscriptionOfferView(
    groupID: "21181796",
    visibleRelationship: .upgrade
) {
    Image("PlaceholderIcon")
}
.subscriptionOfferViewDetailAction {
    showFullStore = true
}

ポイント:

  • visibleRelationship の値は upgrade / downgrade / crossgrade / current / all です (17:23)。upgrade は現在のプランより 1 段階上のプランを表示し、crossgrade は同等プラン内でコストパフォーマンスの良いものを選びます。current はリテンション施策で offer と組み合わせる用途に適しています。
  • subscriptionOfferViewDetailAction を指定すると、ビュー上に detailLink ボタンが描画され、タップで App 自身のサブスクリプションストアへ遷移できます。
  • iOS 17 で導入された subscriptionStatusTask と組み合わせて SubscriptionStatus を取得し、それを App 内部の SKDemoPlusStatus のようなモデルに変換します。さらに environment 経由で下流の view に注入し、どのプランを表示するかを決定するというパターンになります (16:08)。

Offer code の拡張と購入 UI コンテキスト

Offer code は消耗型、非消耗型、非更新型サブスクリプションをカバーするようになり、iOS 16.3 までバックデプロイされます (07:54)。Transaction.Offer.PaymentMode には oneTime モードが追加され、iOS 17.2 までバックデプロイ。それより古いシステムでは offerPaymentModeStringRepresentation という文字列フィールドから読み取れて、こちらは iOS 15 までバックデプロイされます。

iOS 18.2 から、UIKit / AppKit の purchase メソッドには UI context を渡す必要があります。iOS / macCatalyst / tvOS / visionOS では UIViewController、macOS では NSWindow を渡します。watchOS では渡しません (09:29)。SwiftUI では purchase environment から PurchaseAction を取り出して callAsFunction するだけで済みます。StoreKit views ならそもそも気にする必要がありません。


ポイント

1. appTransactionID でユーザー識別子を再構築する

なぜやる価値があるか: これまでは複数の注文を同一ユーザーに紐づけるのに、サーバー間で App Store Server API を呼ぶ必要がありました。appTransactionID はクライアント側で取得でき、iOS 15 までバックデプロイされ、Family Sharing でもメンバーを区別できます。

始め方: App 起動時に一度 AppTransaction.shared を呼び、appTransactionID をローカルのユーザー識別の一部としてキャッシュします。ログイン後はそれをサーバーのアカウントに紐づけ、1 つの Apple Account がマルチデバイスで別々のユーザーとして認識されてしまうのを防ぎます。

2. originalPlatform を権利付与ロジックに組み込む

なぜやる価値があるか: 有料 App から In-App Purchase モデルへ移行するとき、もっとも離脱しやすいのが既存ユーザーです。originalPlatform を見れば、ユーザーが最初に iOS / macOS / tvOS / visionOS のどのプラットフォームで購入したかを区別でき、既存ユーザーをホワイトリスト化しやすくなります。

始め方: 新しい課金モデルをリリースする前に、各 appTransactionID に紐づく originalPlatformoriginalPurchaseDate を記録しておき、「特定の日付以前にダウンロード済み」のユーザーには旧来の権利を残します。

3. SubscriptionOfferView + visibleRelationship で手書きのペイウォールを置き換える

なぜやる価値があるか: ユーザーの現在のサブスクリプションプランに応じて「アップグレード / ダウングレード / 同等プランへの切り替え」を動的に出し分けるのは頻出のニーズです。これを自前で書こうとすると、SubscriptionStatus の取得、グループ内のプラン価格比較、offer 資格判定などコード量が膨らみます。

始め方: まず AppsubscriptionStatusTask で状態を自前の enum(例: none / standard / pro)に正規化し、environment で配信します。リテンション枠では .currentsubscriptionPromotionalOffer を、アップセル枠では .upgrade を使い分けます。

4. App Store Server Library で手書き JWS を置き換える

なぜやる価値があるか: promotional offer や introductory offer の署名は一度ミスると購入そのものが失敗します。Apple 公式ライブラリは Swift / Java / Python / Node.js をカバーしており、署名ロジックは Apple 自身がメンテナンスしてくれます。

始め方: 署名サーバー側に App Store Server Library を導入し、productIDofferIDtransactionID を引数とする /sign-promotional-offer ルートをラップしておきます。クライアントは subscriptionPromotionalOffer modifier の 2 つ目のクロージャからこのルートを GET するだけで済みます。


関連 Session

コメント

GitHub Issues · utterances