every Tech Blog

株式会社エブリーのTech Blogです。

IAP, IABレシートとユーザー状態の管理について

DELISH KITCHENの定期購読

こんにちは、DELISH KITCHEN開発部でバックエンド開発を担当している南です。 主にDELISH KITCHENのプレミアムユーザー向けの機能の開発を行っております。

DELISH KITCHENでは、人気順検索、プレミアムレシピ(ダイエット、ヘルスケア、美容・健康、作りおき)、 プレミアム献立など、さまざまな機能を提供するプレミアムサービスという定期購読(サブスクリプション)商品を販売しております。 プレミアムサービスは、おもにiOSやAndroidのプラットフォーム上で管理、販売されておりDELISH KITCHENアプリ内から購入できます。

ここではiOSの課金をIAP(In-App-Purchase), Androidの課金をIAB(In-App-Billing)と呼んで区別したいと思います。

IAPとIABとDELISH KITCHEN

iOSやAndroidのプラットフォームに対してAPIを実行することでそれぞれ課金状態を表したレシートを取得できます。 IAPレシートもIABレシートも課金状態を表現するものという点では共通しているのですが、 表現の仕方がことなるためDELISH KITCHENのサーバー側で違いを吸収する必要があります。

IAPとIABの課金状態について

IAPもIABもレシートが表現する課金状態はほぼ同じですが、IABにのみ一時停止という状態があります。

課金状態名と、それがDELISH KITCHENにおいて、どのような状態かを説明した表です。

AndroidのIABレシート

IABレシートの構造はシンプルで、現時点の購読状態のみ返します。 購読が更新されれば、expiryTimeMillis の日時が増加し、支払いに関して変化がおきたら paymentState の値が変化します。

{
    "kind": "xxxxxxx",
    "startTimeMillis": 1111111111111,
    "expiryTimeMillis": 2222222222222,
    "autoRenewing": true,
    "priceCurrencyCode": "JPY",
    "priceAmountMicros": 480000000,
    "countryCode": "JP",
    "developerPayload": "",
    "paymentState": 1,
    "cancelReason": 0,
    "userCancellationTimeMillis": 0,
    "orderId": "GPA.0000-0000-0000-0000",
    "linkedPurchaseToken": "",
    "purchaseType": 0
}

IABレシートとユーザー状態

レシートの値からユーザーの課金状態を把握する必要があります。 IABは返す情報がシンプルで、情報と状態を結びつける資料も整備されているので判別することが簡単です。 一方で履歴のような過去の情報が一切ないため、状態の変遷をAPIから知る方法がありません。

Google Play の課金システム > 定期購入を販売する

(*) 一時停止状態とはユーザーがPlayストアの定期購読一覧から指定した期間だけ購読を中断して、期間がすぎたら再び自動再開する仕組みです。

AppleStoreのIAPレシート

一方でIAPレシートは、IABレシートと比べると構造が複雑で情報も多めです。

latest_receipt_infoには定期購読商品の購入履歴が含まれています。 履歴の1つ1つには、「どんな商品を購入したか?」、「何時購入したか?」、「何時期限切れになるか?」といった変化しない情報が含まれています。 (例外として返金キャンセルが発生すると履歴の値が変化します)

また状況に応じて刻々と値が変わるpending_renewal_infoという項目があります。 pending_renewal_infoからは「次回の更新で購入する予定の情報」、「定期購読を継続するか否か」、「期限切れになった理由」、といった状況に応じて変化する情報が含まれています。

{
    ...
    "latest_receipt_info": [
        {
            "quantity": "1",
            "product_id": "delishkitchen",
            "transaction_id": "111111111111111",
            "original_transaction_id": "111111111111111",
            "purchase_date_ms": "1629307052000",
            "purchase_date": "2022-02-01 07:00:00 Etc/GMT",
            "purchase_date_pst": "2021-02-01 00:00:00 America/Los_Angeles",
            "original_purchase_date_ms": "1643698800000",
            "expires_date_ms": "1646118000000",
            "expires_date": "2021-03-01 07:00:00 Etc/GMT",
            "expires_date_pst": "2021-03-01 00:00:00 America/Los_Angeles",
            "cancellation_date_ms": "0",
            "cancellation_date": 0,
            "cancellation_date_pst": 0,
            "web_order_line_item_id": "333333333333333",
            "is_trial_period": "true",
            "is_in_intro_offer_period": "false",
            "promotional_offer_id": ""
        },
        {
            "quantity": "1",
            "product_id": "delishkitchen",
            "transaction_id": "222222222222222",
            "original_transaction_id": "111111111111111",
            "purchase_date_ms": "1646118000000",
            "purchase_date": "2022-03-01 07:00:00 Etc/GMT",
            "purchase_date_pst": "2021-03-01 00:00:00 America/Los_Angeles",
            "original_purchase_date_ms": "1629307053000",
            "expires_date_ms": "1648796400000",
            "expires_date": "2022-04-01 07:00:00 Etc/GMT",
            "expires_date_pst": "2021-04-01 00:00:00 America/Los_Angeles",
            "cancellation_date_ms": "0",
            "cancellation_date": 0,
            "cancellation_date_pst": 0,
            "web_order_line_item_id": "444444444444444",
            "is_trial_period": "false",
            "is_in_intro_offer_period": "false",
            "promotional_offer_id": ""
        }
    ],
    "pending_renewal_info": [
        {
            "expiration_intent": "",
            "auto_renew_product_id": "delishkitchen",
            "original_transaction_id": "111111111111111",
            "is_in_billing_retry_period": "",
            "product_id": "delishkitchen",
            "auto_renew_status": "1"
        }
    ]
}

IAPレシートとユーザー状態

IAPレシートは情報が多めですが、レシートからユーザーの状態を把握する際は以下の情報を用いています。

  • latest_receipt_infoの最新のレシート
    • is_trial_period: 無料トライアルか否か
    • expires_date_ms: 有効期限の日時
    • cancellation_date_ms: 返金した日時
  • pending_renewal_info
    1. auto_renew_status: 購読を継続するか否かを表す。
    2. expiration_intent: レシートが期限切れになった理由を表す。期限内は常に空
    3. is_in_billing_retry_period: 支払いリトライ中か否かを表す。ExpirationIntent=2以外のときは空
    4. grace_period_expiration_date: 猶予期間の期限
    5. auto_renew_product_id: 次回の更新に購入するプロダクトID

課金状態と注意点

猶予期間保留中(支払いリトライ状態)一時停止の扱いは気をつけないといけません。

猶予期間

期限が切れたユーザーを引き続き課金状態として扱うため、IABではexpiryTimeMillisの日時が自動的に伸びます。 一方IAPでは、latest_receipt_infoのexpires_date_msは伸びません。代わりに、grace_period_expiration_date_msに猶予期間の日時が入ります。

保留中(支払いリトライ状態)

猶予期間後も支払いに失敗しつづけている状態です。 期限切れになり無料ユーザー状態となりますが、一定期間(デフォルトでiOSは60日間、Androidは30日間)支払いをリトライし続けます。 リトライによって支払いが成功すると購読状態に戻りますが、一定期間以上失敗し続けると、プラットフォームが自動的に解約状態にしてリトライをやめます。 また支払いリトライ期間中に解約することもできます。

一時停止

IAB特有のユーザー状態です。こちらも一度期限が切れるため、一見解約したように見えます。 しかし指定した期間をすぎると何事も無かったかのように購読を再開するため、一時停止状態中だと判別できていないと解約したユーザーが再び戻ってきたかのように見えてしまいます。 また定期購読一時停止中に解約することもできます。

定期購読と状態管理

Choosing a Receipt Validation Technique

こちらで述べられている通り定期購読状態を適切に扱うにはサーバーで購読状態を管理し、同期する必要があります。 ですが、これだけでは下記のようなユーザーの行動の変遷を追うことはできません。 ユーザー状態をと経緯を正確に判断するためにも、レシートの履歴をサービスのサーバー側で保存することも大切です。

  • ユーザーが猶予期間中になっていたか?
  • 保留中から戻ったのか?、それともキャンセルしたのか?
  • 一時停止から戻ったのか?、それともキャンセルしたのか?

まとめ

定期購読の難しいところでも述べられておりますが、 一見単純そうにみえる定期購読ですが、正しくやろうとすると実は面倒なことが多いです。 またIAP、IABの仕様追加にも追従していく必要がありサーバー側の保守コストがかかります。

ですがDELISH KITCHENのプレミアム機能を多くの方に提供し続けるためにも定期購読の管理・アップデートを続けていきたいと思っております。

参考資料

  1. Google Play の課金システム > 定期購入を販売する
  2. App Store Receipt Data Types
  3. Choosing a Receipt Validation Technique
  4. Question About Ios Receipt Fields Addition on July 19 2017
  5. App Store の In-App Purchase の Grace Period対応
  6. アップルはApp Storeのサブスク期限切れに「猶予期間」を導入
  7. Engineering Subscriptions(WWDC 2018)
  8. Auto Renewing Subscriptions for iOS Apps