LoginSignup
43
34

More than 3 years have passed since last update.

【Swift4.2】SwityStoreKitを使う【iOS】

Last updated at Posted at 2019-10-27

In-App Purchase。iOSの購入処理ですが、StoreKitで素でやるとなるとpaymentQueueなんたらにいろいろ書いていくわけですがこれがなかなかめんどくさい。サンドボックス環境とプロダクション環境の分岐をロジックにあらかじめ入れておく必要がある(=リリース後にはじめて通るブロックがある)というのも厄介です。
最初は苦労しながらやってましたけど、今はこれ使っちゃってます。めっちゃ楽。

SwiftyStoreKit

今回は自動更新型プロダクト限定です。
In-App PurchaseのCertificationやCapabilityの設定、AppStoreConnectでのプロダクト登録処理などは割愛。

実施環境

Xcode 10.1
Swift 4.2

大人の事情でまだSwift5に移行できてないんです…

インストール

CocoaPodsが嫌いな自分はCarthage(カートハゲw、でなくカーセッジ)でインストールです。

Cartfileはこんな感じです。
Swift 4.2でビルドしたいのでそれに対応したバージョンを指定してます。

Cartfile
github "bizz84/SwiftyStoreKit" == 0.14.0

SwiftyStoreKitのビルド

# carthage update --platform iOS SwiftyStoreKit

{ProjectRoot}/Carthage/Build/iOS/SwiftyStoreKit.frameworkを追加
ShadowMatch_xcodeproj1.png

Run Script の設定
Carthageのパスは各自のインストールパスで。
Run Script Only installingにチェックを入れるとストアアップ時に必要なライブラリがアップされず、運が悪いとリリース後にアプリがコケる原因となるので気をつけてください(経験済)。
ShadowMatch_xcodeproj2.png

実装編

アプリ起動時にトランザクションの監視を開始します。(アップルさん推奨)

AppDelegate.swift
import SwiftyStoreKit

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // see notes below for the meaning of Atomic / Non-Atomic
    SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
        for purchase in purchases {
            switch purchase.transaction.transactionState {
            case .purchased, .restored:
                if purchase.needsFinishTransaction {
                    // Deliver content from server, then:
                    SwiftyStoreKit.finishTransaction(purchase.transaction)
                }
                // Unlock content
            case .failed, .purchasing, .deferred:
                break // do nothing
            }
        }
    }
    return true
}

completeTransactionsのatomicallyfalseの場合、purchase.needsFinishTransactiontrueになります。PaymentQueueのステータスを購入完了に更新する前に何か処理をしたい場合はatomicallyfalseをセットします。
上記のコードの場合、atomically=true(購入完了のトランザクションは分解不可)となっていますので、if purchase.needsFinishTransaction{} のブロックは通らず、purchased、restoredの検知の直後にfinishTransaction()が実行されます。

トランザクションの種類

transactionState(SKPaymentTransactionState) 状態
purchased 購入済み
restored リストア(購入復元)済み
failed トランザクション異常終了
purchasing Appストアが処理中
deferred 親垢(AppleID)の承認待ち(ペアレンタルコントロール)

プロダクト情報取得

SwiftyStoreKit.retrieveProductsInfo(["your-product-id"]) { result in
    if let product = result.retrievedProducts.first {
        let priceString = product.localizedPrice!
        print("Product: \(product.localizedDescription), price: \(priceString)")
    }
    else if let invalidProductId = result.invalidProductIDs.first {
        print("Invalid product identifier: \(invalidProductId)")
    }
    else {
        print("Error: \(result.error)")
    }
}

AppStoreConnectの管理画面でプロダクトを登録しても、「契約/税金/口座情報」の設定ができてないと取得に失敗しますので注意です。

購入(プロダクトIDをパラメタに)


SwiftyStoreKit.purchaseProduct("your-product-id", quantity: 1, atomically: false) { result in
    switch result {
    case .success(let product):
        // fetch content from your server, then:
        if product.needsFinishTransaction {
            SwiftyStoreKit.finishTransaction(product.transaction)
        }
        print("Purchase Success: \(product.productId)")
    case .error(let error):
        switch error.code {
        case .unknown: print("Unknown error. Please contact support")
        case .clientInvalid: print("Not allowed to make the payment")
        case .paymentCancelled: break
        case .paymentInvalid: print("The purchase identifier was invalid")
        case .paymentNotAllowed: print("The device is not allowed to make the payment")
        case .storeProductNotAvailable: print("The product is not available in the current storefront")
        case .cloudServicePermissionDenied: print("Access to cloud service information is not allowed")
        case .cloudServiceNetworkConnectionFailed: print("Could not connect to the network")
        case .cloudServiceRevoked: print("User has revoked permission to use this cloud service")
        default: print((error as NSError).localizedDescription)
        }
    }
}

atomicallytrueであれば、if product.needsFinishTransaction{} のブロックは不要です。finishTransaction() は購入成功のステータス検知後直ちに実行されます。

購入(SKProductオブジェクトをパラメタに)


SwiftyStoreKit.retrieveProductsInfo(["your-product-id"]) { result in
    if let product = result.retrievedProducts.first {
        SwiftyStoreKit.purchaseProduct(product, quantity: 1, atomically: true) { result in
            // handle result (same as above)
        }
    }
}

プロダクト情報の取得と購入処理を1セッションで行いたい場合に使えますね。

リストア


SwiftyStoreKit.restorePurchases(atomically: false) { results in
    if results.restoreFailedPurchases.count > 0 {
        print("Restore Failed: \(results.restoreFailedPurchases)")
    }
    else if results.restoredPurchases.count > 0 {
        for purchase in results.restoredPurchases {
            // fetch content from your server, then:
            if purchase.needsFinishTransaction {
                SwiftyStoreKit.finishTransaction(purchase.transaction)
            }
        }
        print("Restore Success: \(results.restoredPurchases)")
    }
    else {
        print("Nothing to Restore")
    }
}

複数のプロダクトがある場合はproductIdプロパティ(上のfor文中ならpurchase.productId)を調べてそれぞれに応じた処理を書きましょう。atomicallyについては前述通りです。

ローカルレシートの取得


let receiptData = SwiftyStoreKit.localReceiptData
let receiptString = receiptData.base64EncodedString(options: [])
// do your receipt validation here

アップルからレシート取得


SwiftyStoreKit.fetchReceipt(forceRefresh: true) { result in
    switch result {
    case .success(let receiptData):
        let encryptedReceipt = receiptData.base64EncodedString(options: [])
        print("Fetch receipt success:\n\(encryptedReceipt)")
    case .error(let error):
        print("Fetch receipt failed: \(error)")
    }
}

forceRefreshがtrueの場合、ローカルレシートが取得結果に更新されます。

自動更新型のレシート検証と継続中か期限切れかのチェック

let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
    switch result {
    case .success(let receipt):
        let productId = "subscription_product_identifier"
        // Verify the purchase of a Subscription
        let purchaseResult = SwiftyStoreKit.verifySubscription(
            ofType: .autoRenewable, // or .nonRenewing (see below)
            productId: productId,
            inReceipt: receipt)

        switch purchaseResult {
        case .purchased(let expiryDate, let items):
            print("\(productId) is valid until \(expiryDate)\n\(items)\n")
        case .expired(let expiryDate, let items):
            print("\(productId) is expired since \(expiryDate)\n\(items)\n")
        case .notPurchased:
            print("The user has never purchased \(productId)")
        }

    case .error(let error):
        print("Receipt verification failed: \(error)")
    }
}

引数のservice.productionで常時OKです。サンドボックスへの分岐はSwiftyStoreKitがやってくれます。すばらしい(^^
期限切れか否かの判定をしてくれるのが助かりますね。
残念ながらローカルレシートの検証は非サポートみたいです。
必要な場合はレシート検証 プログラミングガイドを参考にがんばるしかなさそうです。

自動更新型の購入とレシート検証を同時にやる


let productId = "your-product-id"
SwiftyStoreKit.purchaseProduct(productId, atomically: true) { result in

    if case .success(let purchase) = result {
        // Deliver content from server, then:
        if purchase.needsFinishTransaction {
            SwiftyStoreKit.finishTransaction(purchase.transaction)
        }

        let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: "your-shared-secret")
        SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in

            if case .success(let receipt) = result {
                let purchaseResult = SwiftyStoreKit.verifySubscription(
                    ofType: .autoRenewable,
                    productId: productId,
                    inReceipt: receipt)

                switch purchaseResult {
                case .purchased(let expiryDate, let receiptItems):
                    print("Product is valid until \(expiryDate)")
                case .expired(let expiryDate, let receiptItems):
                    print("Product is expired since \(expiryDate)")
                case .notPurchased:
                    print("This product has never been purchased")
                }

            } else {
                // receipt verification error
            }
        }
    } else {
        // purchase error
    }
}

SwiftyStoreKit.verifyReceipt(using: appleValidator)の部分、実はローカルレシートかApple持ちのレシートかを指定できます。ローカルレシートを指定する場合は、
SwiftyStoreKit.verifyReceipt(using: appleValidator,forcerefresh:false)とします。こうすることでStoreへの接続回数を減らすことができます。

43
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
34