iOS アプリ内課金で、必要なことをやってくれるお手軽Frameworkを作った

年末から冬休みの宿題として、iOS向けプロダクトを作っていまして、個人プロダクトで初めてアプリ内課金を実装してみようと思い、勢いでオレオレFrameworkを作ってみた私です。
おはこんばんちわ。

#In-App Purchase
フリーミアムモデルのアプリケーションで、収益化する際は「広告を入れる」or「一部機能を有償で解放する」の二択になると思います。後者のような一部の機能をプレミアム、有料化して、販売することがアプリ内課金によって実現することができます。

##In-App Purchaseの種類
アプリ内課金には、大きく分けて4つのタイプが用意されています。
###消耗型
ソーシャルゲームなどで、ライフであったりスタージュエルのようないわゆる石などのお助けアイテムやゲーム内通貨を購入する際に使われるタイプです。
名前の通り、一度消費してしまったら無くなります。

###非消耗型
一度購入すれば、ずっと使い続けることができるタイプです。イメージとして、フリーミアムのカメラアプリで、追加課金するとフィルムっぽい写真が撮れるフィルターを使えるようになるような機能追加に使うことが多いでしょう。
購入後もずっと使えるアイテムになるので、実装する際にはリストア処理が必要になります。

###自動更新サブスクリプション
フューチャーフォン時代によくあった月額課金制のアレです。一定期間サービスを利用するのにプレミアム登録が必要だったりするときに使うタイプです。
更新のタイミングに処理を行う場合、サーバー側での継続確認処理なども発生するので、少々面倒な奴。

###非更新サブスクリプション
一定期間のみプレミアムサービスを利用する際の期間利用権利を購入する際に使うタイプです。
自動更新はされないので、ユーザーが都度更新の手続きを行う必要があります。

個人開発のアプリでは、運用面からあまりサブスクリプションモデルを使うことは多くないと思います。
主にアプリ内課金で必要とするのは、非消耗型のタイプが多いのかなと考えてます。今回作ったプロダクトでも、非消耗型のタイプでのアプリ内課金を実装しました。

# YMTInAppPurchaseFramework,YMTInAppPurchaseAPI
今回、非消耗型タイプに特化したアプリ内課金の処理を担ってくれるFrameworkを作りました。
主に下記のことをやってくれます。

- AppStoreで販売中のアイテムの確認、取得
- 購入時のトランザクション、レシート検証
- 購入済みアイテムのリストア処理

基本的には、StoreKitの処理を自分なりに使いやすいようにラッパーしているようなFrameworkです。
また、レシート検証をサーバーサイドで行うためにnode.jsで書いた簡単なAPIも用意し、「YMTInAppPurchaseFramework」と「YMTInAppPurchaseAPI」を組み合わせて使う前提で作っています。
この記事では、作ったFrameworkの使い方を記載します。StoreKitが行なっているトランザクション処理などについては、公式のドキュメント等を参照していただけますと幸いです。

## YMTInAppPurchaseAPIのセットアップ
### インストール
APIを稼働させたいサーバーの適当なディレクトリに本プロジェクトを落としてください。

```
$ cd ./hoge/hoge
$ git clone https://github.com/MasamiYamate/YMTInAppPurchaseAPI.git
```

そのままですと、必要なモジュールが含まれていないので```npm install```を実行してAPI実行に必要なモジュールをインストールします。

```
$ cd YMTInAppPurchaseAPI
$ npm install
```

### nginxの設定
APIを外に公開するためにnginxのリバースプロキシを利用します。```./etc/nginx/conf.d/```にあるconfファイルに下記のようにAPIのロケーションを指定しましょう。
※1 すでにnginxがインストールしてサービスとして動いている前提です。
※2 また、素のnginxではhttps対応はしていませんので、別途Let's Encryptなどでhttps対応を行ってください。

```nginx
server {
  listen 443 ssl http2;
  server_name hogehoge;
  
  location /appleapi/ {
    proxy_pass http://localhost:3000;
  }
  
  error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   /usr/share/nginx/html;
  }  
}
```

設定後下記コマンドを実行し、nginxを再起動します。

```
$ sudo nginx -s reload
```

### APIを実行する

```node index.js```でも動かすことができますが、セッションが切れると止まってしまうので**forever**などのデーモン化ツールと組み合わせて常に待ち受けるようにします。

```
$ forever start index.js
```

※スクリプトが置いてあるディレクトリなどは適宜自分の環境に読み替えてください。

### APIのエンドポイント
上記の例のまま設定すると下記のURLがエンドポイントになります。
フレームワークの初期化時に必要になるので控えておきます。

#### Registration
https://【your-domain-name】/appleapi/regi
#### Restore
https://【your-domain-name】/appleapi/restore

## YMTInAppPurchaseFrameworkの使い方
### インストール
Cocoapodsに公開済みのため、pod installで組み込むことが可能です。
Podfileに下記のように追記します。

```Podfile.file
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'hogehoge' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for hogehoge
  pod 'YMTInAppPurchase'

  target 'hogehogeTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'hogehogeUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end
```

追記後、```pod install```を実行します。
Frameworkのインストール作業は以上で完了です。

### 利用方法
#### キー、検証APIのエンドポイント設定
アプリ側の実装前に下記の項目値を取得しておきましょう。

- YMTInAppPurchaseAPI Registration end point
- YMTInAppPurchaseAPI Restore end point
- App内課金共有シークレットキー

App内課金共有シークレットキーは、Appstore Connectより取得することができる16進数の文字列になります。

上記の3つの値を取得しましたら、Appdelegateの**didFinishLaunchingWithOptions**で、Frameworkの初期化を行います。

```AppDelegate.swift
//
//  AppDelegate.swift
//  YMTInAppPurchaseSampleApp
//

import UIKit

// Framework import
import YMTInAppPurchase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        // App内課金共有シークレットキーを登録します
        YMTInAppPurchase.shared.setAppShareKey("In-App Purchase Shared Secret Key")
        
        // YMTInAppPurchaseAPIのそれぞれのエンドポイントを設定します
        let registration = "registration url"
        let restore = "restore url"
        YMTInAppPurchase.shared.setValidationUrls(regist: registration, restore: restore)

        return true
    }

}
```

#### iTunes Storeの販売アイテムが有効か判別する
ここからは実際に販売アイテムを取り扱っていきます。
事前にAppStoreConnectから販売したいアイテムの情報などを登録する必要があります。
その際、プロダクトIDを独自で設定しますがこのIDを元に販売できるアイテムであるかということを判別することが求められます。
StoreKitを用いた場合では、下記のような実装になります。

```swift:StoreKitSample
func productValidation (ids: [String]) {
    //販売するアイテムのIDの配列を渡す
    let productReq = SKProductsRequest(productIdentifiers: Set(ids))
    //デリゲートの継承
    productReq.delegate = self
    //リクエストの開始
    productReq.start()
}

//リクエスト完了後コールされる
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    //有効なアイテムは、「SKProduct」オブジェクトの配列として返却
    effectiveProducts = response.products
    //無効なアイテムは、プロダクトIDの文字列の配列として返却
    invalidProductIds = response.invalidProductIdentifiers
}
```

YMTInAppPurchaseFrameworkでは、上記の実装をラッパーしたメソッドを用意しています。
ProductsIDの有効無効判定は、多少時間がかかるためアプリ起動時よりもアイテム販売ページの読み込み時に実行する方がよいと思います。

```swift:YMTInAppPurchaseFramework_Sample
import UIKit
import YMTInAppPurchase

class ViewController: UIViewController {

    //販売予定のプロダクトIDの配列
    let productsIds = ["itemOne" , "itemTwo"]

    override func viewDidLoad() {
        super.viewDidLoad()
    }

        override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        //取得済みのSKProductの件数が0の時に有効アイテムの判別を行う
        //アプリ起動後、一度でも判別を行い有効アイテムがある場合は実行する必要はない
        if Payment.shared.getProductsCnt() == 0 {
            Payment.shared.setProductIds(productsIds, callback: {
                //有効アイテムの取得後、アイテム購入画面に反映などの処理を行う
            })
        }
    }

}
```

#### 販売アイテムの情報を取得する
UITableViewなどに販売アイテムを表示して、該当するアイテムをタップすると購入プロセスを走らせるなどが一般的な課金アイテムの販売Viewになると思います。
その際、有効のアイテムを取得するには下記のメソッドを利用します。

```swift

/// 販売アイテムの総数を取得します
///
/// - Returns: Int
YMTInAppPurchase.shared.getProductsCnt()

/// index番号を元に特定アイテムのSKProductを取得します
///
/// - Parameter idx: Int  
/// - Returns: SKProduct?
YMTInAppPurchase.shared.getProduct(index)

/// 全ての販売アイテムを取得します
///
/// - Returns: [SKProduct]
YMTInAppPurchase.shared.getProducts()

/// 特定アイテムのローカライズ済みのアイテム名を取得します
///
/// - Parameter product: SKProduct
/// - Returns: String
YMTInAppPurchase.shared.getProductLocalizedTitle(PRODUCT)

/// 特定アイテムのローカライズ済みのアイテム説明文を取得します
///
/// - Parameter product: SKProduct
/// - Returns: String
YMTInAppPurchase.shared.getProductLocalizedBody(PRODUCT)

/// 特定アイテムのローカライズ済みのアイテム価格を取得します
///
/// - Parameter product: SKProduct
/// - Returns: String
YMTInAppPurchase.shared.getProductLocalizedPrice(PRODUCT)
```

#### 決済処理、リストア処理を行う
実際にユーザーがアイテムを選び、アプリ内に用意してあるであろう購入ボタンをタップした時にリクエストするメソッドです。
決済処理、リストア処理共に完了後にコールバックが呼ばれます。その際、引数として決済に成功したアイテムのプロダクトIDが渡されますので、アプリ側はプロダクトIDを元に有料機能の有効化などの処理を行ってください。

```swift
/// 決済処理を行う
///
/// - Parameters:
///   - product: SKProduct
///   - callback: ((String?) -> Void)?
YMTInAppPurchase.shared.startTransaction(product, callback: { productId in
    //プロダクトIDが含まれる場合は、有料機能の有効化処理を行い
    //nilの場合は、決済に失敗しているのでエラーアラートなどを出す
    if productId != nil {
        //有効化処理
    }else{
        //エラーアラートなど
    }
})

/// リストア処理を行う
///
/// - Parameter callback: ((String?) -> Void)?
YMTInAppPurchase.shared.startRestore(callback: { productId in
    //プロダクトIDが含まれる場合は、有料機能の有効化処理を行い
    //nilの場合は、決済に失敗しているのでエラーアラートなどを出す
    if productId != nil {
        //有効化処理
    }else{
        //エラーアラートなど
    }
})
```

# おわりに
トランザクションの検証部分などこれでいいのかという不安は抱えつつではありますが、今回Framework化に挑戦してみました。リリースしたアプリは、Appleの審査も通過しているので機能的には問題ないものになっていると思います。
まだまだ改善の余地は残されていると思いますので、少しづつ改良していきたいと思います。

[Github - YMTInAppPurchaseFramework](https://github.com/MasamiYamate/YMTInAppPurchaseFramework)
[Github - YMTInAppPurchaseAPI](https://github.com/MasamiYamate/YMTInAppPurchaseAPI)