iOS In-App Purchase実装で必ず知っておきたい隠れた罠

## はじめに
Qiita初投稿です。
今回は入社一年目の会社でiOSのIn-App Purchaseの継続課金の実装、テストを任されました。その際に調べながら進めていったのですが、なかなかまとまっているものもなく、中にはとても探しずらかったり、たまたま方法を発見したり、調べても解決せずに問い合わせたものもあったので、この機会に詰まった部分や気をつけたことをまとめました。もっといい方法などもあると思うのでその際は気軽にコメントして頂けると助かります。

## 書くこと
　firebaseとstorekitの競合によって起こった課金障害（storekitはiOSが提供しているIn-App Purchaseを行うためのフレームワークです）やstorekitで困ったこと、テストで用いるAppleのsandbox環境に悩まされたことについて書きました。

　同じようにIn-App Purchaseで苦しんでいる人がこれを見て少しでも救われたら嬉しいです。特にsandbox環境にはとても苦しめられたのでIn-App Purchaseを実装する際には是非この記事の内容を一読してから開発してください。

## In-App Purchaseの実装
In-App Purchaseの実装に関しては色々な記事があるため、参考にした記事を書いておきますのでそちらを参考にすると良いと思います。

* [In-App Purchase プログラミングガイド（公式）](https://developer.apple.com/jp/documentation/StoreKitGuide.pdf)
公式のドキュメントですが、結構長いのでしっかり読み込むのは辛いです・・・。コード例は `objective-c` ですが、読めない人も流れは掴めると思います。

* [アプリ内課金の実装方法](https://terakoya.site/ios_dic/ios-dic-in-app-purchese/)
もし `objective-c` が全く読めないなどであればとても参考になると思います。

* [iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ](http://tech.vasily.jp/entry/ios_premium_validation)
今回はサーバーサイドでレシート検証を行っていたためとても参考になりました。

(2018/3/15 追記)

* [bizz84/SwiftyStoreKit](https://github.com/bizz84/SwiftyStoreKit)  
ご指摘いただいたのですが、リンクのようなstorekitをラップしたライブラリを使ったほうが綺麗にかけたりするので、実装する際はこちらも考慮に入れると良いと思います。自分はサーバーでの検証を前提としていたことと、継続課金を扱っていたためそこまで多機能なライブラリを入れる必要がないだろうと踏み切り、そのまま実装しましたが、振り返るとライブラリを使ってもよかったのかもしれないです。


## In-App Purchase実装時に気をつけること
### storekitの仕様
　はじめにドキュメントなどを読んでいきだいたい理解したあと、実装に取り掛かった時に一番驚いたことはstorekitのメソッドを呼ぶと、勝手にサインインなどのアラートが出て課金を進められることです。実装経験があれば当たり前に感じるのですが、最初は結構衝撃的で、アラートまでの流れを作りきらずに一回テストしておいてよかったと心底思いましたのでこれから実装する人は注意してください。

### storekitとfirebaseなどの解析系のSDKとの競合
#### 問題
　firebaseを導入することで全体の0.2%ほどで、課金障害が起きました。firebaseのサポートに問い合わせた結果、問題としてはstorekitの初期化処理とfirebaseの初期化処理が競合してstorekitの初期化に失敗していることが原因でした。また、この事象は課金の処理を自動的に計測してくれる他のSDK（facebookなど）でも起こりうる事象らしいです。facebookSDKをログインのみで使っているから大丈夫と思っていても自動的に計測されるので注意が必要です。

#### 対策
暫定の対応方法としては`SKPaymentTransactionObserver` の

```swift
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]){}
```

が完了したあとでfirebaseの初期化を行うということでした。しかし、`updatedTransactions` はtransactionが残っている時にしか呼ばれないため、確実にstorekitの初期化を行うことが難しい状態です。結果的にstorekit初期化後に、firebaseの初期化を遅延実行するという実装方針にしました。また、facebookSDKもすでに導入済みだったため、最終的には以下のフローで実装されています。しかし、遅延実行よりもより良い方法は全然ありそうなのでご意見ありましたらコメントいただけると助かります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/130374/e7c885c8-cd9e-ddb0-5fd2-5346ddec6ebd.png)

　アプリの形式にもよると思うので意見を出しにくいかもしれませんが、ログインまたは、サインアップした後に会員画面に遷移し、ログイン後にアプリの機能が使えるようなアプリです（ログイン必須なアプリ）。なのでログイン後にしか課金要素はありません。また、firebaseは一旦push通知目的で導入されていますので行動ログ集計は考慮されていません。行動ログ集計を考慮すると今の実装では不備が出てくるため変更しなければいけません。もしここに対応することができたら追記します。

## sandbox環境で気をつけること
### sandbox環境が応答せず課金が失敗する
#### 問題
　sandbox環境はとても不安定なため、ちゃんと実装されていてもiTunes Storeに接続できず失敗することが多々あります。経験上、夜に失敗することが多いような気がしています。これはレシート検証APIを叩く時も同様で、継続課金のテスト時などにバッチ処理を回して継続の有無を確認するような場合は注意が必要です。
#### 対策
　対策として決まったものは特になく、根気強く課金をリトライし続けるか、時間をずらして行うのが良いです。また、課金失敗時にiTunes Storeのアカウントを見るとsandboxユーザーでサインインされていることがあるのでサインアウトしておくと比較的成功率が上がっている気がします。

### iphoneに古いレシート情報がスタックし、課金に失敗することがある
#### 問題
　本来であればAppleIdを切り替えた時にレシート情報が更新されるのが普通のような気もしますが、実際にレシート検証APIを叩いて見ると別のAppleIdで購入した情報も残っていることが確認できます。イメージ的にはAppleIdとレシート情報が紐づくというよりは、端末とレシート情報が紐づいてしまっている感じです。購入情報が溜まりすぎると課金の失敗確率や、継続課金において継続されない確率が高くなっていると思います。
　iPhoneの初期化を行うことでレシート情報の初期化を行なっていたのですが、わざわざ初期化すると初期化・アクチベーション・アプリのビルドとそれだけで約10分程度の時間がかかってしまいますし、テスト項目の数によっては膨大な時間になってしまいます。
#### 対策
　たまたま見つけた方法としては、新しいAppleIdに対して（例: `test+init@gmail.com` のようなものを作っておく）以下のようなレシート更新メソッドを呼び出すことです。これによってiPhoneを初期することなく、レシートの更新に成功しました。これはiPhone初期化に比べて結構工数削減につながりますし、開発機のiPhoneを調達できない状況でもすることができるので是非取り入れてみてください。

```swift
fileprivate var request: SKRequest?

func refresh() {
    request = SKReceiptRefreshRequest()
    request?.delegate = self
    request?.start()
}
```

### 継続課金時に失効時間と購入時間の差が生まれることがある
#### 問題
　言葉で表しずらかったので図にしました。本来は上図となるべきですが、sandbox環境では下図となることがあります。なので継続課金時に何かしらの処理を行うテスト時にこれを知っていないと継続されていない判定となってしまい泥沼にはまっていくので注意です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/130374/49e0d1d6-cf1a-a748-6ed4-11be28a21ac8.png)
#### 対策
　継続課金確認バッチのようなものを回す前に自分でレシート検証APIを叩いてみて継続されていたらバッチを叩くようにするといいと思います。手間はかかりますが・・・。


### iOS11.0.0でsandbox環境における課金に失敗する
#### 問題
　これは結構はまりました。自分の実装が間違っているのかと何回も確認したあとに他の端末で試したらすんなり通るという悲しい出来事でした。エラー内容は `iTunes Storeに接続できません` の一点張りなので自分のミスなのか、sandbox環境のせいなのかという部分の切り分けが難しく時間を消費してしまったので注意が必要です。もしかしたら他にもできないOSバージョンが存在するかもしれません。
#### 対策
　対策はこの事象を知っておくことが重要です。頭に入れておけば他のOSバージョンで詰まってもこのパターンかもしれません。一応iOS11.2.5では問題なく課金できることを確認しています。

### sandboxユーザーを複数作るのが面倒
 これは知っている人も多いでしょうが、アカウントを一つ作り、以下のようにすると同アカウントで複数のメールアドレスの役割を持たせることができるので登録時に便利です。

```
例： test_iap_sandbox@gmail.com を作成した場合
test_iap_sandbox+1@gmail.com
test_iap_sandbox+2@gmail.com
test_iap_sandbox+3@gmail.com
```

(2018/3/14 追記)
ご指摘いただいたのですが、sandbox環境のテストユーザーは存在しないメールアドレスで登録できるようです。

### 課金テストに使ったデバイスにおいてアラートが高頻度で出続ける
#### 問題
　これは解決手段が見つからず、アプリを起動していない時も`サインインが必要です`というアラートが定期的に出てくるので困っています・・・。特定のAppleIdに対するサインインを求められるため、おそらくそのIDを用いた時に不整合が起きてしまったのかなという予想までしか立っておらず、アラートが出るたびにキャンセルすることで対応していて結構不便です。
#### 対策
　デバッグして見たところトランザクションも残っていないようなのでもし解決方法を知っている人がいればコメントしていただけると幸いです。根本的な対策手段ではないですが、普段使いのiPhoneで課金テストはなるべくしない方が良いでしょう。