BlogRight arrowTutorialRight ArrowAndroidアプリ内購入、パート 5: サーバーサイドでの購入の検証
BlogRight arrowTutorialRight ArrowAndroidアプリ内購入、パート 5: サーバーサイドでの購入の検証

Androidアプリ内購入、パート 5: サーバーサイドでの購入の検証

Androidアプリ内購入、パート 5: サーバーサイドでの購入の検証
Listen to the episode
Androidアプリ内購入、パート 5: サーバーサイドでの購入の検証

サーバーサイド (server-side) の検証は、購入の信頼性を検証するのに役立ちます。デバイスはGoogleサーバーにリクエストを送信し、購入が実際に行われたかどうか、および購入が有効かどうかを確認します。

このガイドでは、Androidアプリのサーバーサイド検証を構成する方法について説明します。

購入を検証する理由

サーバーサイドの検証は必須ではないため、ご注意ください。検証しなくてもアプリ内購入を完了できますが、いくつかの重要なメリットがあります。

1. 高度な支払い分析:有効化後に発生するすべてのことは、デバイスによって処理されないため、定期購入 (subscription) において特に重要です。サーバーサイドの購入処理がないため、現在の定期購入ステータスを取得したり、ユーザーが定期購入を更新したりキャンセルしたりしたかどうか、支払いの問題があるかどうかなどを把握できません。

2. 購入の信頼性を確認できる:トランザクションが不正ではなく、ユーザーが実際にアイテムの代金を支払ったことを確認できます.

3.クロスプラットフォームの定期購入:ユーザーの定期購入ステータスをリアルタイムで確認できれば、他のプラットフォームと同期できます。たとえば、iOSデバイスから定期購入を購入したユーザーは、Android、ウェブサイト、およびその他のプラットフォームで使用できます。

4. サーバーサイドからコンテンツへのアクセスを制御できるため、サーバーへのリクエストを実行するだけで、定期購入せずにデータにアクセスしようとするユーザーから保護されます。

経験上、サーバーサイドの購入処理を設定するには、最初のメリットだけで十分です。

⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

支払いの検証

この方法を通して、Androidの支払いの検証についての要約を説明します。

Google Play Developer APIリクエストの認証

Google Play Developer APIを使用するには、まずリクエストに署名するためのキーを生成する必要があります。まず、Google Play Consoleアカウント (アプリを管理する場所) を Google Cloud アカウント (リクエスト署名用のキーを生成する場所) にリンクする必要があります。すべての設定が完了したら、ユーザーに購入管理権限を付与する必要があります。このプロセスについては、専用の記事で説明する必要があります。幸いにも、Adaptyのドキュメントにある手順ごとのガイドで既に説明されています。

通常、キーを生成してから機能を利用開始できるようになるまで、24時間以上かかります。これを回避するには、アプリ内アイテムまたは定期購入の説明を更新するだけで、すぐにキーが有効になります。

公式のgoogle-api-python-clientライブラリを使用して、Google Play Developer APIを操作します。このライブラリは、一般的な言語の大部分で利用できます。必要なすべてのメソッドをサポートしているため、使用することをお勧めします。

定期購入のトランザクションの検証

iOSのサーバーサイドの検証とは異なり、Androidでは、定期購入とその他のアイテムの検証の両方がさまざまな方法で実装されます。そのため、トランザクションを検証するときは、アイテムまたは定期購入のいずれかを販売しているかを把握する必要があります。実際には、モバイルアプリからこのデータを転送して、トークンの再検証が必要な場合に備えてデータベースにフラグを保持しなければなりません。

2つ目の重要な違いは、Androidでは各トランザクションが独自のトークンを持っているのに対し、すべてのiOSトランザクションはアプリ固有の共有シークレットを使用して、トランザクション履歴全体を保存することです。そのため、ユーザーの購入をいつでも復元できるようにする場合は、任意の1つのトークンを選択するのではなく、すべての購入トークンを保存する必要があることを意味します。

定期購入を検証するには、purchases.subscriptions.getメソッドを呼び出す必要があります。基本的に、以下のGETリクエスト呼び出しとなります。

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}

次のすべてのパラメータが必要です。

packageName:アプリの識別子 (com.adapty.sample_app など)。

subscriptionId:検証される定期購入の定期購入識別子 (com.adapty.sample_app.weekly_sub など)。

token:一意のトランザクショントークン。モバイルアプリ側で購入が処理されると表示されます。

まず、すべてが意図したとおりに機能することを確認するため、対処する必要があるエラーメッセージを見てみましょう。

400, Invalid grant: account not found:このエラーメッセージは、リクエスト認証キーが正しく生成されなかったことを意味します。アカウントがリンクされていること、十分な権限を持つ適切なアカウントを使用していること、必要なすべての API が有効になっていることを確認してください。すべてを設定する方法については、以下のセクションを参照してください。自動的に返金されるアイテム説明の更新に関するヒントを確認しましょう。

400, The purchase token does not match the package name:このエラーメッセージは通常、不正なトランザクションで発生します。テスト中に表示された場合は、別のアプリに属する​​アプリ購入トークンを使用していないことを確認してください。

403, Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'androidpublisher.googleapis.com':これは、Google APIリクエストの1日あたりの上限を超えたことを意味します。デフォルトでは、1日あたり最大200,000件のリクエストを実行できます。この上限を引き上げることはできますが、ほとんどのアプリでは十分な回数です。この制限に達した場合は、アプリのロジックを再確認して、すべてが正しいことを確認する必要があります。

410, The subscription purchase is no longer available for query because it has been expired for too long:このエラーメッセージは、定期購入が60日以上前に期限切れになったトランザクションで表示されます。実際のエラーメッセージではないため、そのように処理するべきではありません。

Start for free

You don't need to write server code yourself,

because we did it for you. Try Adapty SDK!

Start for free

定期購入のトランザクション

検証が完了した場合は、応答としてトランザクションデータを受け取ります。

トランザクションデータ (定期購入の場合):

{
    "expiryTimeMillis": "1631116261362",
    "paymentState": 1,
    "acknowledgementState": 1,
    "kind": "androidpublisher#subscriptionPurchase",
    "orderId": "GPA.3382-9215-9042-70164",
    "startTimeMillis": "1630504367892",
    "autoRenewing": true,
    "priceCurrencyCode": "USD",
    "priceAmountMicros": "1990000",
    "countryCode": "US",
    "developerPayload": ""
}

ユーザーがアプリにより提供される有料オプションにアクセスできるかどうか (有効な定期購入があるかどうか) を把握するには、次のことを行う必要があります。

1. startTimeMillisおよびexpiryTimeMillisパラメーターを確認します。現在の時刻は、開始時間と終了時間の範囲内となります。

2. さらに、paymentStateパラメーターの値が「0」でないことを確認する必要があります。定期購入の購入がまだ保留中であるため、ユーザーに有料機能へのアクセスをまだ許可する必要はありません。

3. トランザクションにautoResumeTimeMillisプロパティがある場合、定期購入は一時停止されます。これは、指定された日付より前に有料機能へのアクセス権をユーザーに付与してはならないことを意味します。

定期購入のトランザクションの主要なプロパティを見てみましょう。

kind:トランザクションのタイプ。定期購入の場合、常にandroidpublisher#subscriptionPurchase値があります。このパラメーターを使用すると、定期購入とアイテムのいずれかを販売しているかを把握して、処理ロジックを適宜選択できます。

paymentState:支払いステータス。このプロパティは、期限切れのトランザクションには存在しません。想定される値は次のとおりです。

0:この購入はまだ処理されていません。一部の国では、ユーザーはオンサイトで定期購入の料金を支払うことができます。つまり、ユーザーは自分のデバイスから定期購入の購入を開始して、最寄りの端末で支払います。全体として非常にまれなケースですが、留意しておく必要があります。

1:定期購入が購入されました。

2:定期購入は試用期間中です。

3:定期購入は次の期間にアップグレードまたはダウングレードされます。これは、定期購入プランが変更されることを意味します。

acknowledgementState:購入承認ステータス。これは、ユーザーが料金を支払ったアイテムにアクセスできるようになったかどうかを確認する重要なパラメーターです。「0」の値は、アクセス権を付与されていないことを意味し、「1」は付与されたことを意味します。開発者はこのステータスを定義する責任があり、モバイルアプリとサーバーサイドの両方で行うことができます。購入後3日以内に承認しなかった場合は、自動的に返金されます。次のロジックを実装することをお勧めします。acknowledgementState=0を含むトランザクションを受信すると、パラメーターはサーバーによって変更されます。以下でその方法を説明します。

orderId:一意のトランザクション識別子。各定期購入の購入または更新には独自の識別子があり、トランザクションが以前に処理されたかどうかを確認するのに使用できます。各更新IDの前半は一定で、そこに2個のドットと定期購入の更新回数 (0から始まる) が追加されます。定期購入が有効化されたときにGPA.3382-9215-9042-70164識別子が指定されている場合、最初の更新はGPA.3382-9215-9042-70164..0で識別され、2回目の更新はGPA.3382-9215-9042-70164..1などで識別されます。このようにして、トランザクションチェーンを構築して、更新回数を追跡できます。

startTimeMillis:定期購入の開始日。

expiryTimeMillis:定期購入の有効期限。

autoRenewing:定期購入が次の期間に更新されるかどうかを示すフラグ。

priceCurrencyCode:USDなどの3文字形式の購入通貨。

priceAmountMicros:購入価格。通常の価格値を取得するには、この値を1000000で割ります。つまり、1990000は実際には1.99を意味します。

countryCode:購入国を2文字形式で表します (USなど)。

purchaseType:購入タイプ。ほとんどの場合、このキーは存在しません。購入がサンドボックス環境で行われたかどうかを理解するのに役立つため、依然として考慮することは重要です。想定される値は次のとおりです。

0:購入はサンドボックス環境で行われたため、分析データに含めるべきではありません。

1:購入はプロモーションコードを使用して行われました。

autoResumeTimeMillis:定期購入の更新日。以前に一時停止された定期購入にのみ存在します。このパラメーターが存在する場合、指定された日付までユーザーに有料機能へのアクセス権を許可する必要はありません。

cancelReason:定期購入が更新されない理由。想定される値は次のとおりです。

0:ユーザーが定期購入の自動更新をキャンセルしました。

1:定期購入はシステムによってキャンセルされました。ほとんどの場合、課金に関する問題 (billing issue) が原因です。

2:ユーザーが別の定期購入プランに切り替えました。

3:開発者が定期購入をキャンセルしました。

userCancellationTimeMillis:定期購入の更新キャンセルに関するデータ。 cancelReasonが0の場合にのみ存在します。定期購入は引き続き有効である可能性があります。確認するには、expiryTimeMillisパラメーターの値を参照してください。

cancelSurveyResult:定期購入のキャンセルの理由を格納するオブジェクト。ユーザーがこの問題についてフィードバックを投稿した場合に表示されます。

introductoryPriceInfo:紹介価格データを格納するオブジェクト。たとえば、1か月間の50%オフの特別オファーなどです。

promotionType:定期購入の有効化に使用されたプロモーションコードタイプ。想定される値は次のとおりです。

0:1回限りのプロモーションコード。

1:複数のお客様が適用できるカスタムプロモーションコード。このようなコードは通常、ブロガーのパートナーシップで使用されます。

promotionCode:定期購入の有効化に使用されたカスタムプロモーションコード。このパラメーターは、1回限りのプロモーションコードには存在しません。

priceChange:今後の価格変更データと、ユーザーがそれに同意したかどうかを格納するオブジェクト

定期購入の承認

前述のとおり、購入後3日以内に定期購入が承認されなかった場合、定期購入はキャンセルされ、自動的に返金されます。正直なところ、その背後にあるロジックは不明です。また、iOSを含め、他の支払い処理システムで発生したことはありません。それでも、acknowledgementState=0を含むトランザクションを受信した場合は、定期購入を承認する必要があります。

これを行うには、purchases.subscriptions.acknowledgeメソッドを呼び出す必要があります。このメソッドにより、POSTリクエストを実行します

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge

パラメーターは、リクエストの検証と同じです。リクエストが正常に実行されると、定期購入が承認されます。つまり、売上を喪失することはありません。

定期購入がまだ完全に購入されていない場合は、承認する必要はありません。

定期購入の更新のキャンセル、取り消し、返金、延長

定期購入の検証と承認とは別に、Google Play Developer APIは他の定期購入の操作にも使用できます。非常にまれですが、更新を除いて、Google Play Consoleで操作可能です。さまざまな API ソリューションを全般的に理解できるように、一覧にまとめました。これらのすべてのリクエストには、前述のメソッドと同じパラメーター (packageNamesubscriptionIdtoken) が必要です。

● 更新のキャンセル (purchases.subscriptions.cancelメソッド):選択した定期購入の自動更新をキャンセルします。ただし、定期購入は、現在の請求期間中は引き続き有効となります。

● 定期購入の返金 (purchases.subscriptions.refundメソッド):定期購入を返金します。ただし、ユーザーは引き続き定期購入を利用でき、次の期間に自動的に更新されます。ほとんどの場合、返金を行う際に定期購入も取り消す必要があります。

● 定期購入の取り消し (purchases.subscriptions.revokeメソッド):ただちに定期購入が取り消され、ユーザーは有料機能を利用できなくなります。定期購入は更新されません。このメソッドは通常、返金の実行と一緒に呼び出されます。

● 定期購入の延長 (purchases.subscriptions.deferメソッド):指定された日付まで、定期購入の期間を延長します。リクエストでは、定期購入の有効期限と、それを置き換える日付を指定します。後者は、前者よりも定期購入の期間が長くなる必要があります。

アイテム (定期購入以外) の検証

アイテムの検証は、定期購入の検証に似ています。 GETリクエストを実行するには、 purchases.products.getメソッドを呼び出す必要があります。

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}

上記の例では、これらすべてのパラメーターについて詳しく学びました。

トランザクションデータ (アイテムの場合):

{
  "purchaseTimeMillis": "1630529397125",
  "purchaseState": 0,
  "consumptionState": 0,
  "developerPayload": "",
  "orderId": "GPA.3374-2691-3583-90384",
  "acknowledgementState": 1,
  "kind": "androidpublisher#productPurchase",
  "regionCode": "RU"
}

アイテムのトランザクションに含まれるプロパティは、定期購入のトランザクションよりもはるかに少ないです。重要なプロパティをいくつか見てみましょう。

kind:トランザクションのタイプ。アイテムの場合、常にandroidpublisher#productPurchase 値があります。このパラメーターを使用すると、定期購入とアイテムのいずれかを販売しているかを把握して、処理ロジックを適宜選択できます。

purchaseState:支払いステータス。ここでのキー値は、定期購入のpaymentStateパラメーターとは異なるため、ご注意ください。想定される値は次のとおりです。

0:購入が完了しました。

1:購入がキャンセルされました。これは、購入が保留されていたものの、ユーザーが支払いをしなかったことを意味します。

2:購入は保留中です。一部の国では、ユーザーはオンサイトで定期購入の料金を支払うことができます。つまり、ユーザーは自分のデバイスから定期購入の購入を開始して、最寄りの端末で支払います。全体として非常にまれなケースですが、留意しておく必要があります。

acknowledgementState:購入承認ステータス。これは、ユーザーが料金を支払ったアイテムにアクセスできるようになったかどうかを確認する重要なパラメーターです。「0」の値は、アクセス権を付与されていないことを意味し、「1」は付与されたことを意味します。開発者はこのステータスを定義する責任があり、モバイルアプリとサーバーサイドの両方で行うことができます。購入後3日以内に承認しなかった場合は、自動的に返金されます。次のロジックを実装することをお勧めします。acknowledgementState=0を含むトランザクションを受信すると、パラメーターはサーバーによって変更されます。以下でその方法を説明します。

consumptionState:アイテムの消費ステータス。iOSでは「消費可能アイテム」と呼ばれます。モバイルアプリ側で定義されます。値が「0」の場合、アイテムが消費されなかったことを意味します。「1」なら、消費されたということです。アプリまたは特定の有料機能への無期限アクセスを販売している場合、そのようなアイテムは消費されないものとします。つまり、ステータスは「0」でなければなりません。ユーザーが何度でも購入できるコインを販売している場合、そのようなアイテムは消費されるものとします。つまり、ステータスは「1」でなければなりません。consumptionState=0はアイテムを1回しか購入できないのに対して、consumptionState=1は何度も購入できることを意味します。

orderId:一意のトランザクション識別子。各定期購入の購入または更新には独自の識別子があり、トランザクションが以前に処理されたかどうかを確認するのに使用できます。

purchaseTimeMillis:購入日。

regionCode:購入国を2文字形式で表します (USなど)。countryCodeという定期購入のパラメーターとは異なるため、ご注意ください。

purchaseType:購入タイプ。ほとんどの場合、このキーは存在しません。購入がサンドボックス環境で行われたかどうかを理解するのに役立つため、依然として考慮することは重要です。想定される値は次のとおりです。

0:購入はサンドボックス環境で行われたため、分析データに含めるべきではありません。

1:購入はプロモーションコードを使用して行われました。

2:支払いの代わりにアプリ内広告を視聴するなど、目的のアクションに対して購入が許可されました。

ご覧のとおり、アイテムの検証は定期購入の検証と非常によく似ています。ただし、注意すべき点がいくつかあります。

● 価格は返されませんが、分析には非常に便利です。

purchaseStateパラメーターの値は、定期購入のpaymentStateパラメーターの値とは大きく異なります。違いを考慮しなかった場合はバグにつながります。

● 定期購入ではcountryCodeですが、regionCodeが返されます。

定期購入の購入と同様に、アイテムの購入も確認する必要があります。これを行うには、POSTリクエストを実行するpurchases.products.acknowledgeメソッドを呼び出します。

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge

購入がまだ完了していない場合は、承認する必要はありません。

定期購入とアイテムの返金追跡

高精度の分析には、返金を考慮することが不可欠です。残念ながら、返金データはトランザクションに存在せず、iOSで機能するため、別のイベントとしてプロンプトも表示されません。返金されたトランザクションのリストを受け取るには、たとえば1日1回など、定期的にpurchases.voidedpurchases.listを呼び出す必要があります。このメソッドにより、以下のGETリクエストを実行します。

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/voidedpurchases

リクエストに応じて、返金されたすべてのトランザクションのリストを受け取ります。 purchaseTokenパラメーターではなく、orderIdパラメーターでデータベース内のトランザクションを検索することをお勧めします。時間の短縮になるだけでなく、すべての定期購入の更新で同じトークンが共有されるため、最新のトークンを取得するだけで済みます。

トランザクションのサーバー通知 (server notifications)

サーバー通知 (リアルタイムデベロッパー通知) は、Google 側、サーバー上、およびほぼリアルタイムで発生したイベントについて把握するのに役立ちます。これらが設定されると、新しい購入、更新、支払いの問題などについて通知されます。これにより、効果的に分析を収集できるだけでなく、定期購入ユーザーのステータス管理がはるかに簡単になります。

サーバー通知の受信を開始するには、目的のアドレスに通知を送信するGoogle Cloud Pub/Subトピックを作成する必要があります。このトピックは、Google Play Consoleの「収益化のセットアップ」セクションに表示されます。スクリーンショットを含む詳細なガイドについては、Adaptyドキュメントを参照してください。

サーバー通知:

{
  "message": {
    "data": "eyJ2ZXJzaW9uIjoiMS4wIiwicGFja2FnZU5hbWUiOiJjb20uYWRhcHR5LnNhbXBsZV9hcHAiLCJldmVudFRpbWVNaWxsaXMiOiIxNjMwNTI5Mzk3MTI1Iiwic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjp7InZlcnNpb24iOiIxLjAiLCJub3RpZmljYXRpb25UeXBlIjo2LCJwdXJjaGFzZVRva2VuIjoiY2o3anAuQU8tSjFPelIxMjMiLCJzdWJzY3JpcHRpb25JZCI6ImNvbS5hZGFwdHkuc2FtcGxlX2FwcC53ZWVrbHlfc3ViIn19",
    "messageId": "2829603729517390",
    "message_id": "2829603729517390",
    "publishTime": "2021-09-01T20:49:59.124Z",
    "publish_time": "2021-08-04T20:49:59.124Z"
  },
  "subscription": "projects/935083/subscriptions/adapty-rtdn"
}

主に関心があるのは、base64でエンコードされたトランザクションデータを含むデータキーに関心があります。messageIdキーはメッセージの重複排除に使用できるため、重複したメッセージを処理する必要はありません。

サーバー通知のトランザクションは次のとおりです。

{
  "version": "1.0",
  "packageName": "com.adapty.sample_app",
  "eventTimeMillis": "1630529397125",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 6,
    "purchaseToken": "cj7jp.AO-J1OzR123",
    "subscriptionId": "com.adapty.sample_app.weekly_sub"
  }
}

packageNameキーは、このイベントがどのアプリに属しているかを理解するのに役立ちます。subscriptionIdキーはどの定期購入が関連しているかを示し、 purchaseTokenは特定のトランザクションを見つけるのに役立ちます。定期購入では、このイベントが属する更新チェーンで最新のトランザクションを常に探します。notificationTypeキーには、イベント タイプが含まれます。これらは定期購入で最も便利なキーと言えます。

(2) SUBSCRIPTION_RENEWED:定期購入は正常に更新されました。

(3) SUBSCRIPTION_CANCELED:ユーザーが定期購入の自動更新を無効にしました。自動更新が無効になっている場合は、有効な定期購入ユーザーに戻す必要があります。

(5) SUBSCRIPTION_ON_HOLD, (6) SUBSCRIPTION_IN_GRACE_PERIOD:支払いの問題により定期購入を更新できませんでした。定期購入が自動的にキャンセルされないように、ユーザーに通知する必要があります。

(12) SUBSCRIPTION_REVOKED:定期購入は取り消されました。これは、ユーザーが以前に定期購入によって付与された有料機能を利用できなくなることを意味します。

アイテム (定期購入以外) では、subscriptionNotificationキーの代わりにoneTimeProductNotificationを受け取ります。また、subscriptionIdキーの代わりにskuキーも含まれます。さらに、アイテムの2種類のイベントのみを受け取ります。

(1) ONE_TIME_PRODUCT_PURCHASED:アイテムの購入が正常に完了しました。

(2) ONE_TIME_PRODUCT_CANCELED:ユーザーが代金を支払っていないため、アイテムの購入がキャンセルされました。

まとめ

サーバーサイドの検証により、アプリについて収集できる分析が強化されます。これにより、不正ユーザーが有料コンテンツにアクセスすることが難しくなり、クロスプラットフォームの定期購入の実装に使用できます。ただし、特に高精度のデータが必要な場合は、サーバーサイドの検証の実装にかなりの時間がかかることがあります。高品質のデータを提供するには、定期購入のアップグレード、定期購入のクロスグレード、試用期間、プロモーションオファー (promo offer)、紹介オファー (intro offer)、猶予期間 (grace period)、返金など、多数のサイドケースを考慮する必要があります。また、Googleでは一年以上更新された定期購入に対して (30%ではなく) 15%の手数料しか請求しないなど、すべてのポリシーの詳細を把握して、考慮する必要があります。

Further reading

Adapty API outage and what we've learned from it
Adapty API outage and what we've learned from it
April 8, 2021
5 min read
Adapty November update
Adapty November update
December 1, 2020
4 min read
Tutorial: how to implement in-app purchases into a flutter app
Tutorial: how to implement in-app purchases into a flutter app
January 10, 2022
12 min read