🏪

StoreKit からユーザーが「App Store から購入したとき」の App のバージョン・購入日時を取得する

2022/06/22に公開

Summary

  • StoreKit に AppTransaction が追加された(iOS 16.0+、iPadOS 16.0+、macOS 13.0+、Mac Catalyst 16.0+、tvOS 16.0+、watchOS 9.0+、Xcode 14.0+)
  • AppTransaction は JWS によって署名された App の購入情報で、これを使えば App の購入を端末内で確認できる
  • AppTransaction の情報は StoreKit が必要に応じて自動更新するが、この更新を手動で行うこともでき、その際ユーザーには認証が求められる

AppTransaction が StoreKit に追加

iOS 16.0+、iPadOS 16.0+、macOS 13.0+、Mac Catalyst 16.0+、tvOS 16.0+、watchOS 9.0+ では、StoreKit に AppTransaction が追加されています。

AppTransaction は App の購入を確認するための新しい API です。これまで、App 内課金のトランザクションを見るための Transaction が StoreKit にありましたが、この AppTransaction は App 内課金ではなく App 自体のトランザクションについての情報を持っています。

日本語コメントは一般公開済みのドキュメントを参考にした筆者によるもの
// App Store によって暗号で署名された、ユーザーによる App の購入を表す情報。
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
public struct AppTransaction : Sendable, Hashable, CustomDebugStringConvertible {
    // この App のバージョンに紐付いてキャッシュされた、App Store によって署名された App のトランザクション情報を取得する。まだキャッシュされていない場合は App Store サーバー側へリクエストも行う。
    public static var shared: VerificationResult<AppTransaction> { get async throws }
    
    // このトランザクションが作成されたサーバー環境。
    public let environment: AppStore.Environment
    
    // このトランザクションが対応する Bundle ID。
    public let bundleID: String
    
    // このトランザクションが対応する App のバージョン。
    public let appVersion: String
    
    // ユーザーが最初に App Store から購入した時点での App のバージョン。
    public let originalAppVersion: String
    
    // App Store が App の識別に使用する一意の ID。
    public let appID: UInt64?
    
    // App Store が App のバージョンを一意に識別するために使用する番号。
    public let appVersionID: UInt64?
    
    // ユーザーが最初に App Store から App を購入した日時。
    public let originalPurchaseDate: Date
    
    // ユーザーが App Store で予約注文したときの日時。
    public let preorderDate: Date?
    
    // App のトランザクションがデバイスに属しているかどうかを確認するために使用するデバイス検証値。
    public let deviceVerification: Data
    
    // デバイス検証値の計算に使用される UUID。
    public let deviceVerificationNonce: UUID
    
    // App Store が JWS で App のトランザクションに署名した日時。
    public let signedDate: Date
    
    // App のトランザクション情報の生の JSON 表現。
    public var jsonRepresentation: Data { get }
    
    // App Store サーバーから App Store によって署名された App のトランザクション情報を取得する。このメソッドを呼ぶと、App Store の認証をユーザーに求めるプロンプトが表示される。
    public static func refresh() async throws -> VerificationResult<AppTransaction>
}

とくに、ユーザーが初めてその App を App Store で購入(無料 App も含む)したときの日時やそのときの App のバージョンを、App Store 側による検証込みで手軽に取得できるようになったのは嬉しい点です。AppTransaction.shared でインスタンスを得て、その中の originalAppVersionoriginalPurchaseDate を取得するだけです。

具体例: 有料 App が無料 App(一部機能を App 内課金)化するが、有料提供時代に App を購入したユーザーには引き続きその一部機能を有効にする場合

以下のような変更が行われる App の場合、この AppTransaction を使えばサーバー側との通信を行わずとも対応できます。

  • メジャーバージョン1〜3では、App Store で有料で配信していた
  • メジャーバージョン4以降は、App Store での配信は無料化し、一部の機能は App 内課金によりアンロックされるようになった
  • しかし、メジャーバージョン1〜3時代に有料で App Store から購入したユーザーには、その一部の機能は App 内課金することなく提供する

コード例は以下のとおりです。例えばあらかじめ自分のサーバー側に、ユーザーの初回起動時バージョンを記録しておくだとか、自分のサーバーから App Store Server へ問い合わせることなく、安全にこの条件分岐を行えるようになったのはとても楽ですね。

import StoreKit

/// 一部機能が有料化されたメジャーバージョン
let newBusinessModelMajorVersion = "4"

/// App Store 署名付きの App の情報を取得する
let shared = try await AppTransaction.shared
switch shared {
case .verified(let appTransaction):
    /// ユーザーが初めて App Store でこの App を購入したときのバージョン
    let originalAppVersion = appTransaction.originalAppVersion
    /// セマンティックバージョニングの「メジャー」「マイナー」「パッチ」に分ける
    let versionComponents = originalAppVersion.split(separator: ".")
    /// ユーザーが初めて App Store でこの App を購入したときのメジャーバージョン
    let originalMajorAppVersion = versionComponents[0]
    
    if originalMajorAppVersion < newBusinessModelMajorVersion {
        // 一部機能が有料化される前に、App Store から有料で App を購入済みのユーザーなので、その一部機能はアンロック扱いにする
    } else {
        // 一部機能が有料化された後に、App Store から無料で App を手に入れたユーザーなので、その一部の機能のアンロックは App 内課金の結果による
    }
case .unverified(let appTransaction, let verificationError):
    // StoreKit の自動検証に失敗した
    // 例えば AppTransaction.refresh() を呼ぶなどして、情報の更新を試みる
    break
}

参考

https://developer.apple.com/videos/play/wwdc2022/10007/

DeNA Engineers

Discussion