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

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

[SwiftyStoreKit](https://github.com/bizz84/SwiftyStoreKit)

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

<h2>実施環境</h2>
Xcode 10.1
Swift 4.2

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


<h1>インストール</h1>
CocoaPodsが嫌いな自分はCarthage（カートハゲw、でなくカーセッジ）でインストールです。

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

```file:Cartfile
github "bizz84/SwiftyStoreKit" == 0.14.0
```
SwiftyStoreKitのビルド

```
# carthage update --platform iOS SwiftyStoreKit
```
`{ProjectRoot}/Carthage/Build/iOS/SwiftyStoreKit.framework`を追加
![ShadowMatch_xcodeproj1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151314/bad2c164-50f8-293e-65f1-d86c813a3ca7.png)

<b>Run Script</b> の設定
Carthageのパスは各自のインストールパスで。
`Run Script Only installing`にチェックを入れるとストアアップ時に必要なライブラリがアップされず、運が悪いとリリース後にアプリがコケる原因となるので気をつけてください（経験済）。
![ShadowMatch_xcodeproj2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151314/24e07a10-8730-e4ee-aca4-42d53ed6de29.png)


<h1>実装編</h1>
アプリ起動時にトランザクションの監視を開始します。（アップルさん推奨）


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

<b>トランザクションの種類</b>

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

<b>プロダクト情報取得</b>

```Swift
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の管理画面でプロダクトを登録しても、「契約／税金／口座情報」の設定ができてないと取得に失敗しますので注意です。

<b>購入（プロダクトIDをパラメタに）</b>

```swift

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)
        }
    }
}
```
`atomically`が`true`であれば、<b>if product.needsFinishTransaction{}</b> のブロックは不要です。finishTransaction() は購入成功のステータス検知後直ちに実行されます。

<b>購入（SKProductオブジェクトをパラメタに）</b>

```swift

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)
        }
    }
}
```

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

<b>リストア</b>

```swift

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については前述通りです。

<b>ローカルレシートの取得</b>

```swift

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

<b>アップルからレシート取得</b>

```swift

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の場合、ローカルレシートが取得結果に更新されます。

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

```swift
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です。サンドボックスへの分岐は<b>SwiftyStoreKit</b>がやってくれます。すばらしい（＾＾
期限切れか否かの判定をしてくれるのが助かりますね。
残念ながらローカルレシートの検証は非サポートみたいです。
必要な場合は[レシート検証 プログラミングガイド](https://developer.apple.com/jp/documentation/Receipt-Validation-Programming-Guide-JP.pdf)を参考にがんばるしかなさそうです。

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

```swift

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への接続回数を減らすことができます。