【iOS】in-app purchase(自動更新契約)の実装時, sandboxでのテストで困ったことをまとめる

## この記事は何？

iOSでサブスク商品を販売したい時に詰まったことをまとめる記事。
全体の手順は書きません。あくまで、詰まったところを二度と詰まらないようにするための備忘録です。
以下のページに従ってサブスク商品を作成していく際に詰まった諸々をメモります。

>[qiita: どこよりもわかりやすいiOS最強課金まとめ](https://qiita.com/Masataka-n/items/6f98a5a9fee7b28ccd1f)
[Zenn: 初めてのiOSアプリ内課金実装](https://zenn.dev/ueshun/articles/2cd4b20b049b76eb26de)
[公式: レシート検証プログラミングガイド](https://developer.apple.com/jp/documentation/Receipt-Validation-Programming-Guide-JP.pdf)

## 「現在サンドボックスでInApp購入を行う権限がありません」

実機テスト時のアプリ内での購入テスト時の落とし穴。
testuserアカウントでサブスク契約を行なっても、「現在サンドボックスでInApp購入を行う権限がありません」と表示され、購入が完了できない問題が発生した。
結論から言うと、sandbox環境の不具合だったらしく、test accountを作り直したら解決した。

### はまっていった経緯
最初に調べると以下のページがヒットした。

> [bushimichiの日記: 現在サンドボックスでInApp購入を行う権限がありません](https://bushimichi.hatenablog.com/entry/20160905/1473056527)

どうやら問題はiphone側の設定らしい、とのことだったので、以下を試した。

- iPhoneからサインアウト
- iPhone初期化

ここまでしてもまだ同様のメッセージが表示され続けたため調査を続けると、以下のページにたどり着いた。

> [おおばログ: 現在サンドボックスでInApp購入を行う権限がありません。](https://tworks.hatenablog.jp/entry/2014/01/03/000000)

どうやら、テストユーザとしての登録がうまくいっていないと上記のメッセージが表示されるらしい。でも自分は、正しくtestuserの登録がなされている。何かがおかしい。
さらに調べると、以下のような記事が見つかった。

> [SPINNERS: iOSアプリの購入テストでSandboxアカウントを作って使うマニュアル](https://spinners.work/posts/ios-sandbox/)
[qiita: iOS In-App Purchase実装で必ず知っておきたい隠れた罠](https://qiita.com/you12724/items/374942dec7b3ea14721f)

なんと、そもそも**sandbox環境は不安定**らしい。
sandbox環境自体にできることはないので、自分が変更できる要素を再設定しようと言うことで、testuserを作り直すことにした。
Appleが提供しているシステムがそんな不安定なことがあるのかと言う疑問を持ちつつも、半信半疑ながらもテストユーザを作り直すと、成功した。

### はまった原因

- システムを信用しすぎた。
- 調査力不足

自分のコードが間違っているに決まっていると言う考えしかなかったので、testuserを作り直すという発想がなかった。
そのため、検索時にもそういう目線で記事を探してしまっていたため、真の原因を発見できなかった。
無駄にiPhoneの初期化までしてしまった。。。フラットな意識での調べ力も大事。

## ステータスコード「21002」

pythonでrequestsを用いてエンドポイントを叩く挙動確認を行った際、レシートデータがあってるはずが常にステータスコード「21002」が返ってきてしまうと言う落とし穴。

### はまっていった経緯
レシートデータは、実機でtest accountでサブスク契約した際に返ってきたデータを用いており、またそのデータを用いてcurlでレシート検証用のエンドポイントを叩き、正しい取引情報が返ってきていることを確認済みであった。
ゆえに、問題はcurlコマンドをpythonを用いて実現する部分に問題があったと考えられたが、その部分でつまずいた。

```:curlコマンド
curl -X POST -H "Content-Type: application/json" -d '{"receipt-data": "レシートデータを入れる", "password":"とってきたパスワードを入れる"}' https://sandbox.itunes.apple.com/verifyReceipt
```

```python:変換したと思っていたはずのpythonコマンド
import requests
import pprint
import json

VERIFY_URL = "https://sandbox.itunes.apple.com/verifyReceipt"

if __name__ == '__main__':
    data = { "receipt-data": "取得したデータを入力", "password": "パスワードを入力" }
    headers = {'Content-type': 'application/json'}
    response = (requests.post(VERIFY_URL, json.dumps(data).encode("utf-8"), headers=headers)).json()
    pprint.pprint(response, depth=2, compact=True)
 
```

```python:正しいpythonコマンド
import requests
import pprint
import json

VERIFY_URL = "https://sandbox.itunes.apple.com/verifyReceipt"

if __name__ == '__main__':
    data = '{ "receipt-data": "取得したデータを入力", "password": "パスワードを入力" }'
    headers = {'Content-type': 'application/json'}
    response = (requests.post(VERIFY_URL, data=data, headers=headers)).json()
    pprint.pprint(response, depth=2, compact=True)
```

```python:変数対応
import requests
import pprint
import json

PASSWORD = "パスワードを入力"
VERIFY_URL = "https://sandbox.itunes.apple.com/verifyReceipt"

if __name__ == '__main__':
    receipt_data = "取得したデータを入力"
    data = '{ "receipt-data": "%s", "password": "%s" }'
    headers = {'Content-type': 'application/json'}
    response = (requests.post(VERIFY_URL, data=(data % (receipt_data, PASSWORD)), headers=headers)).json()
    pprint.pprint(response, depth=2, compact=True)
```

### はまった原因
curlでjsonデータをpostしているし、[公式](https://developer.apple.com/jp/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store/)にも`JSONオブジェクトを、HTTP POSTリクエストのペイロードとして送信します`とあるので、pythonコマンドでもjsonオブジェクトとしてデータを送信すべきかと思っていた。
しかしながらそれではreceipt-dataをきちんと認識されない。
jsonとしてではなく、文字列としてdataを送信しなければならなかった、というのに気づくまでに時間がかかった。


## まとめ
実機でのレシート取得と取得したレシートデータによるサーバでの検証のテストが終わったので、機能実装に移って行こうかと思います。
上記の問題に関して、自分の理解が間違っている部分があればコメントいただけると嬉しいです！(そもそもjsonオブジェクトを送るの意味を誤解しているよ、みたいな。)