どこよりもわかりやすいiOS最強課金まとめ

はじめに

わかりにくいiOSのアプリ内課金についてどの記事よりもわかりやすいものを目指して書きました。
実際に実装していて困ったことなどもまとめたので同じ状況で悩んでる人の助けになれば幸いです。
今現在まだ実装途中でして、まだ細かい部分など書ききれていない部分もありますが、都度更新していきたいと思います。
間違ってることなどあればご指摘くださいmm

課金について

課金の種類

種類 説明
消耗型 ゲーム内でのスタミナやガチャを引くためのアイテムなど 一度使うとなくなり、再度購入が可能
非消耗型 広告の非表示や使える機能の拡張など 一度の購入だけで無制限に使用できる
自動更新サブスクリプション 月間サービスのクラウドストレージや週刊雑誌のサブスクリプションなど、サービスや定期的にアップデートされるコンテンツ ユーザーが解約するまで定期的に課金される 
非更新サブスクリプション ストリーミングコンテンツのシーズンパスなど 自動的に更新されないため、ユーザー自身が毎回更新する必要がある

準備

1. 契約 / 税金 / 口座情報の設定

App内課金を提供するには、有料App契約に署名し、税金および口座情報を設定する必要があります。
App Store Connectの「契約 / 税金 / 口座情報」のページで各種情報を入力をしてください。

スクリーンショット 2019-08-30 16.23.42.png

※ダウンロードが無料のアプリであっても、アプリ内課金を提供する場合、上の画像の有料Appのステータスがアクティブになっている必要があります。
アクティブでない場合、課金アイテムの取得などでエラーになります。
また直接ここが関係しているかは定かではないですが、課金アイテムの追加で自動更新サブスクリプションの項目が表示されませんでした。

2. Appの追加

App Store Connectでアプリのページを作成します。
「マイ App」で新規Appを選択して追加します。

3. 課金アイテムの追加

アプリのページを作成したら課金アイテムを追加します。
課金アイテムはアプリページ内の「機能」→ 「App 内課金」から作成できます。
⊕ボタンを押すと下の画像のポップアップが出るので作成したい種類のコンテンツを選択して作成してください。
スクリーンショット 2019-08-30 16.29.44.png

自動更新サブスクリプションは最初に参照名と製品IDの入力とを求められます。
参照名は後ほど変更できますが、製品IDは変更できません。
スクリーンショット 2019-08-30 18.01.55.png

参照名
App Store Connect上のみで表示されます。
(例)
自動更新型: 〇〇プラン(3ヶ月)
消耗型: 〇〇石(10個)
など

製品ID
アプリから課金アイテムを取得する際に必要になる固有のIDです。
アプリのBundle Identifierを先頭に付けてproductIDを設定します。

決済完了後、コンテンツを付与するときに課金の種類によって処理が分かれると思うので、productIDでどのタイプの課金アイテムなのか判別できるようにします。
後からの変更ができないのでチームで開発する際は命名規則を決めると良さそうです。
(例)
自動更新型: com.hoge.application. autoSubscription .plan1
消耗型: com.hoge.application. consumable .item1
など

また自動更新型の場合、どのサブスクリプショングループに追加するかを選択します。
サブスクリプショングループがまだ無い場合は作成してください。

スクリーンショット 2019-08-30 19.02.27.png

サブスクリプショングループとは

提供するサブスクリプションはすべて、1つのサブスクリプショングループに割り当てる必要があります。サブスクリプショングループは、アクセスレベル、価格、期間が異なる複数のサブスクリプションで構成されているため、ユーザーが自分のニーズに最適なオプションを選択できるようになっています。ユーザーが1回に購入できるのはグループ内の1つのサブスクリプションのみであるため、ほとんどのAppでは、グループを1つだけ作成することがベストプラクティスです。これにより、ユーザーが複数のサブスクリプションを誤って購入してしまう事態を避けることができます。
ユーザーがAppで複数のサブスクリプションを購入できるようにする必要がある場合(ストリーミングAppで複数のチャンネルのサブスクリプションを提供する場合など)は、各サブスクリプションをそれぞれ異なるグループに追加することもできます。複数のグループでサブスクリプションを購入したユーザーには、サブスクリプションごとに請求が行われます。また、ユーザーがあるサブスクリプショングループから別のグループに移動した場合、サブスクリプションの更新日は変更され、有料サービスの日数もリセットされます。1つの有効なサブスクリプションのみ存在することが通常予想されるAppに、複数のサブスクリプショングループを設定することは推奨されません。

このサブスクリプショングループにもローカリゼーションの設定が必要です。

スクリーンショット 2019-11-01 12.45.39.png

自動更新型

サブスクリプション期間
登録したサブスクリプションが自動更新されるまでの期間です。
設定できる期間は下記の通りです。

期間
1週間
1ヶ月
2ヶ月
3ヶ月
6ヶ月
1年

サブスクリプション価格
通貨を日本円(JPY)にして価格を選択して次へを押すと他のテリトリでの価格を自動計算してくれます。
テリトリごとに異なる価格を設定することも可能です。
スクリーンショット 2019-08-30 19.32.40.png
スクリーンショット 2019-08-30 19.33.30.png
無料トライアルやお試し価格などの設定もできます。
トライアル期間が終わると自動的に通常のサブスクリプション価格が請求されます。

自動更新型以外

価格
USDでの表記になっているので「その他の通貨」を押して出てくるポップアップを参考に適切な価格を設定しましょう。

共通

App Store 情報
App Storeに表示するApp内課金の表示名と説明を設定します。
スクリーンショット 2019-08-30 19.43.15.png
この情報はAppStoreに表示されるものになるのでユーザーに分かりやすい表示名と説明を設定しましょう。

App Store プロモーション(オプション)
App 内課金を App Store でのプロモーションに使用する場合は1024x1024ピクセルのプロモーション用イメージを追加します。
プロモーション用の情報なので必須ではないです。

審査に関する情報
課金アイテムにも審査があり、その際に必要な情報です。
実際のプロダクト一覧画面や購入画面のスクリーンショットと
審査の際に必要なメモ(アプリにログインできるアカウント情報や、課金画面表示への案内など)を記載します。

※開発時はスクリーンショット、メモ共にダミーのものでも大丈夫です。申請するときに差し替えてください。

上記のApp Store プロモーション以外の設定が完了すると下の画像の用に課金アイテムのステータスが「送信準備完了」となります。
(情報が不足していると「メタデータが不足」になります。自動更新型の場合サブスクリプショングループの設定も必要です。)
スクリーンショット 2019-08-30 20.22.43.png
このステータスが送信準備完了の状態にならないとアプリから課金アイテムの取得をする際にエラーになってしまうので注意です。

また、自動更新型の課金を提供する場合は決済後のレシートの検証で App 用共有シークレット というものが必要になるので上の画像右上にあるApp 用共有シークレットから生成しておきましょう。(生成後は同じところから確認、再生成することができます)

スクリーンショット 2019-11-19 15.56.56.png

4. Xcode側での設定

Xcodeではプロジェクトの設定から「TARGETS」 → 「Signing & Capabilities」で In-App Purchase を追加します。
スクリーンショット 2019-11-18 18.34.50.png

課金アイテムの取得

AppStoreから課金アイテムを取得する
取得と言いつつも、予めアプリ側で課金アイテムのproductIDを知っている必要があります。
AppStoreにproductIDで問い合わせることでその課金アイテムの価格などの情報と販売可能な状態かがわかります。

このproductIDをアプリ側で管理する方法は2通りあります。

・Appバンドル内へのプロダクトIDを埋め込む
・自前のサーバーから取得

広告の削除や機能の有効化など固定されている場合は、Appバンドルにリストを埋め込みでも良いですが、
課金アイテムを増やすことが多い場合は、アプリの更新をせずすぐに反映できるようにサーバーから取得するようにしておくのが良いです。

Appバンドルに埋め込み サーバーからフェッチ
購入目的 機能のアンロック コンテンツ配信
プロダクトのリストの変更 Appの更新時に可能 いつでも可能
サーバの必要性 不可

公式ドキュメント: https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html

レシート検証について

レシートの検証とは課金アイテムを購入した際に発行されるレシートをAppleに問い合わせることで、
不正な購入や意図しない購入でないかを検証するものです。
また、自動更新型の状態(継続、停止など)を確認する際にも使います。

検証の方法

レシート検証の方法は2つあります。
・ ローカルでの検証
・ AppStoreを使用した検証

基本的にはAppStoreを使用した検証をおすすめします。

ローカルでの検証についてはこちらの記事をご覧ください
iOSで課金のレシートをローカルで判定する方法

AppStoreを使用した検証

リクエストURL

環境 URL
production https://buy.itunes.apple.com/verifyReceipt
sandbox https://sandbox.itunes.apple.com/verifyReceipt

リクエストBody

キー
receipt-data base64 でエンコードされたレシートデータ
password 自動更新購読が含まれているレシートにのみ使用。アプリケーションの共有シークレット(16 進数文字列)
exclude-old-transactions 自動更新または非更新購読が含まれる iOS7 スタイルの App レシートの場合にのみ使用します。値がtrueの場合、応答には購読の最新の更新トランザクションのみが含まれます。

ステータスコード

状態コード 説明
21000 App Storeは、指定されたJSONオブジェクトを読み取ることができませんでした。
21002 receipt-dataプロパティのデータの形式が正しくないか、欠落しています。
21003 領収書を認証できませんでした。
21004 指定した共有秘密は、アカウントのファイルにある共有秘密と一致しません。
21005 レシートサーバーは現在利用できません。
21006 この領収書は有効ですが、サブスクリプションの有効期限が切れています。このステータスコードがサーバーに返されると、レシートデータもデコードされ、応答の一部として返されます。
自動更新可能なサブスクリプションのiOS 6スタイルのトランザクションレシートに対してのみ返されます。
21007 このレシートはテスト環境からのものですが、検証のために実稼働環境に送信されました。代わりにテスト環境に送信してください。
21008 この領収書は実稼働環境からのものですが、検証のためにテスト環境に送信されました。代わりに本番環境に送信してください。
21010 この領収書は承認されませんでした。これは、購入したことがない場合と同じように扱います。
21100-21199 内部データアクセスエラー。

まず、上の表にあるproductionのURLに問い合わせ、statusが 21007 で返ってきたらsandboxのURLに再度問い合わせるようにします。

このようなレスポンスが返ってきます。

{
    "status": 0,
    "environment": "Sandbox",
    "receipt": {
        "receipt_type": "ProductionSandbox",
        "adam_id": 0,
        "app_item_id": 0,
        "bundle_id": " ",
        "application_version": "1",
        "download_id": 0,
        "version_external_identifier": 0,
        "receipt_creation_date": "2019-11-12 08:55:19 Etc/GMT",
        "receipt_creation_date_ms": "1573548919000",
        "receipt_creation_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
        "request_date": "2019-11-18 12:58:25 Etc/GMT",
        "request_date_ms": "1574081905053",
        "request_date_pst": "2019-11-18 04:58:25 America/Los_Angeles",
        "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
        "original_purchase_date_ms": "1375340400000",
        "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
        "original_application_version": "1.0",
        "in_app": [
            {
                "quantity": "1",
                "product_id": " ",
                "transaction_id": "1000000590999315",
                "original_transaction_id": "1000000590999315",
                "purchase_date": "2019-11-12 08:55:18 Etc/GMT",
                "purchase_date_ms": "1573548918000",
                "purchase_date_pst": "2019-11-12 00:55:18 America/Los_Angeles",
                "original_purchase_date": "2019-11-12 08:55:19 Etc/GMT",
                "original_purchase_date_ms": "1573548919000",
                "original_purchase_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
                "expires_date": "2019-11-12 08:58:18 Etc/GMT",
                "expires_date_ms": "1573549098000",
                "expires_date_pst": "2019-11-12 00:58:18 America/Los_Angeles",
                "web_order_line_item_id": "1000000048178827",
                "is_trial_period": "false",
                "is_in_intro_offer_period": "false"
            }
        ]
    },
    "latest_receipt_info": [
        {
            "quantity": "1",
            "product_id": " ",
            "transaction_id": "1000000590999315",
            "original_transaction_id": "1000000590999315",
            "purchase_date": "2019-11-12 08:55:18 Etc/GMT",
            "purchase_date_ms": "1573548918000",
            "purchase_date_pst": "2019-11-12 00:55:18 America/Los_Angeles",
            "original_purchase_date": "2019-11-12 08:55:19 Etc/GMT",
            "original_purchase_date_ms": "1573548919000",
            "original_purchase_date_pst": "2019-11-12 00:55:19 America/Los_Angeles",
            "expires_date": "2019-11-12 08:58:18 Etc/GMT",
            "expires_date_ms": "1573549098000",
            "expires_date_pst": "2019-11-12 00:58:18 America/Los_Angeles",
            "web_order_line_item_id": "1000000048178827",
            "is_trial_period": "false",
            "is_in_intro_offer_period": "false",
            "subscription_group_identifier": "20567235"
        }
    ],
    "latest_receipt": " ",
    "pending_renewal_info": [
        {
            "expiration_intent": "1",
            "auto_renew_product_id": " ",
            "original_transaction_id": "1000000590999315",
            "is_in_billing_retry_period": "0",
            "product_id": " ",
            "auto_renew_status": "0"
        }
    ]
}

AppStoreを使用した検証はアプリ内から直接レシート検証のAPIを叩くこともできるのですが、推奨されていません。
↓公式ドキュメントより

信頼できるサーバーを使用して、App Storeと通信します。独自のサーバーを使用すると、サーバーのみを認識および信頼するようにアプリを設計し、サーバーがApp Storeサーバーに接続することを確認できます。ユーザーのデバイスとApp Storeの間に信頼できる接続を直接構築することはできません。その接続のどちらの端も制御しないため、中間者攻撃の影響を受けやすいためです。

公式ドキュメント: https://developer.apple.com/jp/documentation/Receipt-Validation-Programming-Guide-JP.pdf

自動更新型の状態チェックについて

自動更新型は1度登録するとユーザーが自身でキャンセルをしない限り自動で課金されるため、
その更新のタイミングでサービスの提供を次の更新日まで伸ばさないといけません。

その状態を確認し、ユーザーのステータスを更新するには、Appleが提供しているレシート検証APIをポーリングする必要があります。
ただ、このポーリングだけでは即座に対応できない場合があります。
そのため下に記載しているAppleサーバー通知と併用することをおすすめします。

Appleサーバー通知

自動更新サブスクリプションのステータスが変更したときにAppleからの通知を受け取ることができます。
この通知を受け取ることでユーザーがAppleに問い合わせてキャンセルされた場合(返金など)や、アプリ以外からサブスクリプションを再度登録した場合など、
こちら側でハンドリングできないケースに対応することができます。

※Appleサーバー通知ではサブスクリプションが正常に更新された場合、通知されません。
そのためAppleサーバー通知のみでサブスクリプションの更新に対応することはできなそうです。

通知の種類とタイミングはこちらです。

通知タイプ 通知タイミング
INITIAL_BUY サブスクリプションの初回購入時
CANCEL Appleカスタマーサポートによって返金などの理由で解約されたとき
※ユーザーが手動で購読の自動更新を停止した場合は通知されない
RENEWAL 過去に更新に失敗した期限切れのサブスクリプションの自動更新が成功したとき
INTERACTIVE_RENEWAL 解約していたサブスクリプションを、ユーザが再度登録したとき
DID_CHANGE_RENEWAL_STATUS ユーザーがiOSの設定、App Storeアプリ、またはAppleサポートを通じてサブスクリプションをキャンセルまたは再度有効にした
DID_CHANGE_RENEWAL_PREF 定期購読のプランをユーザがアップグレード/ダウングレードしたとき

アプリページ内のサブスクリプションステータスURLにURLを登録すると上記のタイミングでAppleの通知を受け取ることができます。
スクリーンショット 2019-11-18 20.52.51.png

実際に通知で受け取ることができるレスポンスです。

{
    "auto_renew_product_id": " ",
    "auto_renew_status": "false",
    "auto_renew_status_change_date": "2019-11-08 08:13:55 Etc/GMT",
    "auto_renew_status_change_date_ms": "1573200835000",
    "auto_renew_status_change_date_pst": "2019-11-08 00:13:55 America/Los_Angeles",
    "environment": "Sandbox",
    "latest_receipt": " ", // Base64エンコードされたレシート (長いので省略
    "latest_receipt_info": {
        "bid": " ",
        "bvrs": "1",
        "expires_date": "1573200886000",
        "expires_date_formatted": "2019-11-08 08:14:46 Etc/GMT",
        "expires_date_formatted_pst": "2019-11-08 00:14:46 America/Los_Angeles",
        "is_in_intro_offer_period": "false",
        "is_trial_period": "false",
        "item_id": "1486750613",
        "original_purchase_date": "2019-11-06 10:17:19 Etc/GMT",
        "original_purchase_date_ms": "1573035439000",
        "original_purchase_date_pst": "2019-11-06 02:17:19 America/Los_Angeles",
        "original_transaction_id": "1000000588791509",
        "product_id": " ",
        "purchase_date": "2019-11-08 08:11:46 Etc/GMT",
        "purchase_date_ms": "1573200706000",
        "purchase_date_pst": "2019-11-08 00:11:46 America/Los_Angeles",
        "quantity": "1",
        "subscription_group_identifier": "20567235",
        "transaction_id": "1000000589784549",
        "unique_identifier": "3bf0388bbca73e22cbfbce8e7b5b4c8181047dbc",
        "unique_vendor_identifier": "D89BF8D6-3B46-4F1E-895A-1061ECD31178",
        "version_external_identifier": "0",
        "web_order_line_item_id": "1000000048103370"
    },
    "notification_type": "DID_CHANGE_RENEWAL_STATUS",
    "password": " "
}

期限切れ来る通知の場合、下記2つのKeyが別のキーで返ってくるので注意です。
中身は同じでした。

{
    "latest_receipt": " ",
    "latest_receipt_info": " "
}

↓ こうなる

{
    "latest_expired_receipt": " ",
    "latest_expired_receipt_info": " "
}

公式ドキュメント: https://developer.apple.com/documentation/storekit/in-app_purchase/enabling_server-to-server_notifications

Sandbox環境でのテスト

Sandbox環境での課金テストはApp Store Connectでテスターアカウントを作って行います。

App Store Connect → ユーザとアクセス → Sandbox → テスター から作成できます。

スクリーンショット 2019-11-19 15.33.10.png

Sandbox用のアカウントなので、入力する情報は適当で大丈夫です。
ただパスワードは忘れた場合にあとから確認できないのと、Sandboxアカウントは作成後に編集ができないことに注意してください。

※メールアドレスも架空のものでOKです。(既に使われているものは使えない)
※iTunesなどのプロダクション環境に誤ってサインインした場合は、Sandboxアカウントは無効になり、以降使用できなくなります。
※iPhoneの設定でこのSandboxアカウントでログインする必要はありません。(Sandboxアカウントのログインは別にあります。詳しくは下をご覧ください)

公式ドキュメント: https://help.apple.com/app-store-connect/?lang=ja#/dev8b997bee1

Sandboxアカウントの切り替え

Sandbox環境で決済処理を呼び出すとStoreKitが自動で下のアラートを表示してくれます。

IMG_0012.jpg
このアラートでSandboxのアカウントを入力して購入するを押すと
iPhoneの設定のiTunes StoreとApp Storeの画面の下にSANDBOXアカウントの項目が追加されます。

IMG_6971.PNG

以降ここでSandboxアカウントの切り替えができます。

Sandbox環境におけるサブスクリプションの更新間隔

Sandbox環境ではサブスクリプションの更新間隔が短く設定されています。
更新の間隔は以下のとおりです。

期間 Sandboxの期間
1週間 3分
1ヶ月 5分
2ヶ月 10分
3ヶ月 15分
6ヶ月 30分
1年 1時間

Sandbox環境での自動更新について

Sandboxでは自動更新型のサブスクリプションは最大6回更新され、その後自動的に期限切れになります。
実装中に何度かテストしていたところ、自動更新されなりました。

公式ドキュメントには

更新と期限切れの頻度が増しているために、サブスクリプションの期間に短い間隔を残したまま、システムがサブスクリプションの更新を実行しようとする前に、サブスクリプションが期限切れになる場合があります。

と書かれており、おそらく期限切れになったサブスクリプションへの再登録を高頻度で行ったためかと思います。
時間を置いて再度登録しても更新がされなかったので、レシート検証APIを叩いてみたところ
pending_renewal_info 内の auto_renew_status が0で返ってきていたので、
自動更新されない状態になってしまったようです。

解決策としては、新しいSandboxユーザーを作るのが良さそうです。

auto_renew_status
「1」 - 現在の購読期間の終了時に購読が更新される。
「0」 - お客様が購読の自動更新をオフにした。

おすすめのライブラリ

iOS: SwiftyStoreKit
javascript: in-app-purchase
ruby: itunes_receipt_validator
ruby: venice

参考資料

iOSの消耗型課金のサーバーサイドTipsまとめ
iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ
iOS課金まとめ
(公式)レシート検証 プログラミングガイド

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away