💰

StoreKit2ではStoreKit1のReceiptは扱えない

2024/07/29に公開

結論から言いますと表題の通りStoreKit2ではStoreKit1のReceiptは扱えないようです。

StoreKit1

StoreKit1ではBundle.main.appStoreReceiptURLに最新のレシートが入っています。

StoreKit2

一方StoreKit2ではSKReceiptRefreshRequestでリフレッシュしても最新のレシートが入ってきませんでした。ここで私はだいぶハマってしまいましたが、以下のtapple様の記事を見つけてStoreKit2ではReceiptを扱ってはいけないのだとわかりました。
https://speakerdeck.com/yuheiito/storekit2woshi-tutake-jin-sisutemunohururiniyuaru?slide=53

StoreKit2での購入の検証

ではStoreKit2での購入の検証はどうすばいいのかと言いますと、StoreKit2でpurchase()した後に得られるoriginalTransactionIdをキーにして検証を行うようです。この検証は私が調査した限りではiOS端末で行うのではなく、サーバーサイドで行います。

検証の詳細は以下のフリュー株式会社様の記事が大変参考になりました。
https://qiita.com/maya_yan/items/30b7ea39e6edfdfdfb45

こちらの記事ではjavaのライブラリで検証していますが、他にもtypescriptSwift(server side swift?)も用意されています。

私は試しにMacアプリのプロジェクトで以下のようなコードで試してみました。(各定数やファイル名は適宜置き換えてください)

import Cocoa
import AppStoreServerLibrary

class ViewController: NSViewController {
    let issuerId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    let keyId = "XXXXXXXXXX"
    let bundleId = "xx.xx.xxx.xxxxx.xxx"
    let environment = Environment.sandbox
    let appAppleId: Int64 = 0000000000
    let transactionId = "0000000000000000"
    
    override func viewDidLoad() {
        super.viewDidLoad()

        guard let path = Bundle.main.path(forResource:"Key_File_Name", ofType:"p8") else {
            return
        }

        let encodedKey = try! String(contentsOfFile: path)
        let client = try! AppStoreServerAPIClient(
            signingKey: encodedKey,
            keyId: keyId,
            issuerId: issuerId,
            bundleId: bundleId,
            environment: environment
        )
        
        Task {
            let response = await client.getTransactionInfo(transactionId: transactionId)
            switch response {
            case .success(let response):
                print(response)
                getJWSTransactionDecodedPayload(transactionInfo: response)
            case .failure(let errorCode, let rawApiError, let apiError, let errorMessage, let causedBy):
                print(errorCode)
                print(rawApiError)
                print(apiError)
                print(errorMessage)
                print(causedBy)
            }
        }
    }
    
    func getJWSTransactionDecodedPayload(transactionInfo: TransactionInfoResponse) {
        guard let certPath = Bundle.main.path(forResource: "Cer_File_Name", ofType: "cer") else {
            print("証明書ファイルが見つかりません")
            return
        }
        guard let certData = try? Data(contentsOf: URL(fileURLWithPath: certPath)) else {
            print("証明書ファイルの読み込みに失敗しました")
            return
        }
        do {
            let signedDataVerifier = try SignedDataVerifier(
                rootCertificates: [certData],
                bundleId: bundleId,
                appAppleId: appAppleId,
                environment: environment,
                enableOnlineChecks: true
            )
            Task {
                let response = await signedDataVerifier.verifyAndDecodeTransaction(
                    signedTransaction: transactionInfo.signedTransactionInfo ?? ""
                )
                switch response {
                case .valid(let jwsTransactionDecodedPayload):
                    print(jwsTransactionDecodedPayload)
                case .invalid(let error):
                    print(error)
                }
            }
        } catch {
            print(error)
        }
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

実行するとSubscriptionのアイテムの購入した場合以下のような値が返ってきます。(実際の値は伏せています)

JWSTransactionDecodedPayload(
    originalTransactionId: Optional("0000000000000000"),
    transactionId: Optional("0000000000000000"),
    webOrderLineItemId: Optional("0000000000000000"),
    bundleId: Optional("xx.xx.xxx.xxxxx.xxx"),
    productId: Optional("xx.xx.xxx.xxxxx.xxx.xxxxx"),
    subscriptionGroupIdentifier: Optional("0000000000"),
    purchaseDate: Optional(2024-07-18 00:39:41 +0000),
    originalPurchaseDate: Optional(2024-07-18 00:39:42 +0000),
    expiresDate: Optional(2024-07-18 00:42:41 +0000),
    quantity: Optional(1),
    rawType: Optional("Auto-Renewable Subscription"),
    appAccountToken: nil,
    rawInAppOwnershipType: Optional("PURCHASED"),
    signedDate: Optional(2024-07-19 01:42:36 +0000),
    rawRevocationReason: nil,
    revocationDate: nil,
    isUpgraded: nil,
    rawOfferType: nil,
    offerIdentifier: nil,
    rawEnvironment: Optional("Sandbox"),
    storefront: Optional("JPN"),
    storefrontId: Optional("000000"),
    rawTransactionReason: Optional("PURCHASE"),
    currency: Optional("JPY"),
    price: Optional(200000),
    rawOfferDiscountType: nil
)

これでサーバーサイドで検証できるかと思います。

Discussion