1
/
5

【開発日誌 #13】iOSのサブスクリプション実装!バックエンドのタスクについて

Photo by Mathieu Stern on Unsplash

はじめに

こんにちは!
バックエンドエンジニア兼ディレクターを務めています、奥村と申します。
iOSアプリ開発案件における、自動更新サブスクリプションのナレッジが溜まってきましたので書きます。

iOSアプリの課金形態は以下の4種類に分類されます。

  • 消耗型
  • 非消耗型
  • 自動更新サブスクリプション
  • 非自動更新サブスクリプション

消耗型と非消耗型は買い切り型のアイテム、
自動更新サブスクリプションと非自動更新サブスクリプションは一定期間の機能開放となります。

App内課金
https://developer.apple.com/jp/documentation/storekit/in-app_purchase/

今回は自動更新サブスクリプションを実装する大まかな流れと、バックエンドで必要なタスクや開発の中で気付いたTipsについて書いていこうと思います!

購入の流れ

購入におけるフェーズは大きく分けて3つに分かれます。以下はAppleの公式ドキュメントに記載されている図です。

Requesting a Payment from the App Store:
https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/loading_in-app_product_identifiers
  1. 製品情報の取得
  2. 購入のリクエスト
  3. 製品の提供

以上がアプリ課金の基本的なフローとなります。
基本的に上記の作業に関してはアプリ側(のエンジニア)がやってくれますので、

サーバサイドとして気にするのは

  • 製品情報取得のお手伝い
  • 購入情報(レシート)を受け取りユーザと紐付けてサブスクリプション有効期限を管理する

主にこの2つとなります。

バックエンドのタスク

製品情報取得のお手伝い

サブスクリプションは一つ一つに識別子が付いています。これをProduct IDと呼びます。
(Product IDはAppStoreConnectで登録します)
保守性を考えると、Product IDは基本的にサーバ側で保持した方が良いでしょう。

製品IDの一覧を返すエンドポイントを作ってあげます。
アプリで購入ボタン実装時に間違いを防ぐ目的で説明文も入れてあげると親切です。
例:

{
    "id": 1,
    "name": "月会費コース",
    "product_id": "jp.example.app.plan1",
    "comment": "期間1ヶ月・500円"
},
{
    "id": 2,
    "name": "年会費コース",
    "product_id": "jp.example.app.plan2",
    "comment": "期間1年・5000円"
}

アプリ側ではこのレスポンスからProduct IDを取得し、Appleサーバへ問い合わせを行います。
その結果Product IDが有効であれば、ストアUIをアプリ上で表示します。


購入情報の保存

アプリで無事購入処理が完了したら、Appleから購入情報(レシートと呼びます)が返ってきます。
アプリからサーバにレシートを送ってもらいます。

サーバからレシートをAppleに送ると、レシートの詳細情報が返ってきます。

参考:
https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info

最低限、以下のデータはユーザと紐付けて保持しておきましょう

  • latest_receipt_info.transaction_id:購入1回につき固有のIDになります。
  • latest_receipt_info.original_transaction_id:同Apple ID, 同Product IDでの一連の購入における固有のIDです。初回購入の際はtransaction_idとoriginaltransaction_idは同一となります。
  • latest_receipt:最新レシート。再度購入情報を問い合わせる際に必要です。
  • latest_receipt_info.expire_date:サブスクリプション期限が切れる日時。
  • latest_receipt_info.purchase_date:サブスクリプション購入日時。
  • status:ステータスコード。(https://developer.apple.com/documentation/appstorereceipts/status)

Appleサーバからの通知を受ける(必要に応じて)

こちらからレシートを送って問い合わせる以外にも、ユーザーがサブスクリプションのキャンセルや更新をした時には、Appleサーバからの通知を受け取ることができます。

レスポンスの中身
https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv1
通知種類
https://developer.apple.com/documentation/appstoreservernotifications/notification_type

すべての通知を処理する必要は無いと思いますが、よく利用するのはこの辺りです。

CANCEL:解約された時(返金など)。
RENEWAL:失敗した更新が成功した時。
INITIAL BUY: 初回購入時。稀にアプリ外での購入処理が成立する場合があるため必要。
INTERACTIVE_RENEWAL:ユーザーが手動でサブスクリプションを更新した時。アプリ外でも更新できるので。
DID_CHANGE_RENEWAL_STATUS:更新ステータスが変更された時。
DID_CHANGE_RENEWAL_PREF:プランを変更した時。
DID_FAIL_TO_RENEW:更新に失敗した時。
DID_RECOVER:更新に失敗したサブスクリプションが更新に成功した時。
DID_RENEW: 自動更新が成功した時。
REFUND:ユーザへの返金があった時。

※2021年10月にApp Store Server Notifications V2が公開されました。上記はV1のものとなります。
https://developer.apple.com/jp/news/releases/?id=10212021ef

レシートの管理はどこまですべきなのか

レシートの有効期限をもとにユーザに有料コンテンツを開放するか判断しますが、どの様に管理すれば良いでしょうか。ポーリングをしてユーザとレシートの状態を更新するべきでしょうか。それともAppleサーバからの通知を待っているだけで良いのでしょうか。

これはアプリに求められている仕様によって変わってきます。

一番簡単なのは、ユーザーがアプリを起動する度にAppleサーバにレシートを送り、最新レシート情報から有効期限を取得する方法です。これならばバッチ処理で有効期限を管理する必要もなく、Appleサーバからの通知を受ける必要もありません。ただし以下の条件ではこの手は使えません。

  • アプリ外で何かしらのサブスクリプション特典を受けられる(マルチプラットフォームのアプリ等)
  • アプリ起動時のローディング時間を許容できない

iOSではアプリ外からサブスクリプションの更新・キャンセル・新規購入が可能です。(新規購入ができることは私も想定できておらず、思わぬバグを引き起こしたりしました。。)
これらにどう対応するべきか、仕様検討の段階で考慮しておきましょう。

アプリがアンインストールされない限り、レシートは端末に残ります。不測の事態が起きた際は、アプリ側からレシートを受け取って検証できるようにエンドポイントを作っておくと良いかもしれません。
また、サブスクリプション購入画面に遷移する前にはアプリ側でレシートが無いかチェックし、もしあればレシートの検証をすることで重複課金を防ぐ策をとるのも良いかと思います。

アプリの要求仕様に合わせた最適なレシートのハンドリングを検討しましょう👍

参考

【Apple公式ドキュメント】
Original API for In-App Purchase
https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase
サブスクリプションに関するトピック
https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers
サブスクリプション購入の処理
https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/handling_subscriptions_billing

【参考にさせて頂いたQiitaの記事】
https://qiita.com/ckm/items/b8cf23ba4bd0ae5bbf34
https://qiita.com/Masataka-n/items/6f98a5a9fee7b28ccd1f

株式会社コムデでは一緒に働く仲間を募集しています
11 いいね!
11 いいね!
今週のランキング
株式会社コムデからお誘い
この話題に共感したら、メンバーと話してみませんか?