Highlight
3 つのパート: コアフレームワークの強化、署名リクエスト、新しい SwiftUI ストアビュー。
主な内容
サブスクリプション系 App では、よくこんな悩みがあります。ユーザーが iPad で App を購入し、Mac でも同じ App をダウンロードしたとき、開発者にとってこの 2 回のインストールが同一の Apple Account に紐づくものなのか判別が難しい、という問題です。また、ユーザーがサブスクリプションをキャンセルした後にウィンバックを仕掛けようとしても、そもそもそのユーザーが「有料 App として購入したのか」「無料ダウンロード後にサブスクライブしたのか」が分からない、ということもあります。これらの情報が欠けていると、ビジネス施策は推測ベースに頼らざるを得ません。
WWDC25 の StoreKit アップデートはこの課題を中心に展開されています。AppTransaction、Transaction、RenewalInfo という 3 つのコア型に appTransactionID、originalPlatform、appAccountToken などのフィールドが追加され、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、introductoryOfferEligibility と promotionalOffer が導入され、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
)
ポイント:
bundleID、signingKey、keyID、issuerIDは、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 に紐づく originalPlatform と originalPurchaseDate を記録しておき、「特定の日付以前にダウンロード済み」のユーザーには旧来の権利を残します。
3. SubscriptionOfferView + visibleRelationship で手書きのペイウォールを置き換える
なぜやる価値があるか: ユーザーの現在のサブスクリプションプランに応じて「アップグレード / ダウングレード / 同等プランへの切り替え」を動的に出し分けるのは頻出のニーズです。これを自前で書こうとすると、SubscriptionStatus の取得、グループ内のプラン価格比較、offer 資格判定などコード量が膨らみます。
始め方: まず App の subscriptionStatusTask で状態を自前の enum(例: none / standard / pro)に正規化し、environment で配信します。リテンション枠では .current と subscriptionPromotionalOffer を、アップセル枠では .upgrade を使い分けます。
4. App Store Server Library で手書き JWS を置き換える
なぜやる価値があるか: promotional offer や introductory offer の署名は一度ミスると購入そのものが失敗します。Apple 公式ライブラリは Swift / Java / Python / Node.js をカバーしており、署名ロジックは Apple 自身がメンテナンスしてくれます。
始め方: 署名サーバー側に App Store Server Library を導入し、productID、offerID、transactionID を引数とする /sign-promotional-offer ルートをラップしておきます。クライアントは subscriptionPromotionalOffer modifier の 2 つ目のクロージャからこのルートを GET するだけで済みます。
関連 Session
- Automate your development process with the App Store Connect API — App Store Connect に webhook が追加され、ビルド、審査、サブスクリプション状態の変化をリアルタイムで受け取れます。
- Optimize your monetization with App Analytics — App Analytics にサブスクリプションと offer の軸が追加されており、本セッションのアップデートとセットでマネタイズ効果を確認できます。
- What’s new in AdAttributionKit — AdAttributionKit の新機能。広告ネットワークをまたいだアトリビューション計測をカバーします。
- What’s new in App Store Connect — 新しい Web UI と、offer code の設定エントリーポイントもこちらにあります。
コメント
GitHub Issues · utterances