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 auto_renew_status: 購読を継続するか否かを表す。 expiration_intent : レシートが期限切れになった理由を表す。期限内は常に空 is_in_billing_retry_period: 支払いリトライ中か否かを表す。ExpirationIntent=2以外のときは空 grace_period_expiration_date: 猶予期間の期限 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のプレミアム機能を多くの方に提供し続けるためにも定期購読の管理・アップデートを続けていきたいと思っております。 参考資料 Google Play の課金システム > 定期購入を販売する App Store Receipt Data Types Choosing a Receipt Validation Technique Question About Ios Receipt Fields Addition on July 19 2017 App Store の In-App Purchase の Grace Period対応 アップルはApp Storeのサブスク期限切れに「猶予期間」を導入 Engineering Subscriptions(WWDC 2018) Auto Renewing Subscriptions for iOS Apps