プログラミング

【Android】Realtime Developer Notificationを実装する

android-realtime-developer-notification

この記事は、「モバイルのサブスクリプションのアーキテクチャ」の参考記事になります。

Google Play Storeの Realtime Developer Notificationを実装し運用しているので、その中で得た知見を記載していきます。

実現することは、「Androidアプリの最新の定期購読レシート情報を、サービスのサーバーに常に保持しておくということ」です。

Realtime Developer Notificationとは

Realtime Developer Notificationとは、Google Playで管理される定期購入の状態変化をCloud Pub/Subを通して受信することのできるサービスです。

リアルタイム デベロッパー通知を追加する

このRealtime Developer Notificationを用いたレシート情報更新のプロセス図です。

android-realtime-developer-notification

AppleのApp Store Server Notificationと違って、Google Playから直接指定のエンドポイントに最新レシート情報を送信することは出来ません。必ずCloud Pub/Subを経由する必要があります。

Cloud Pub/Subを通して受信することのできるデータは、”purchase_token”のみなので、その情報を元に、最新レシート情報を取得する必要があります。

GCPのプロジェクト作成とCloud Pub/Subの設定

Cloud Pub/Subとは

Pub/Sub は、イベントを処理するサービスとイベントを生成するサービスを切り離す非同期メッセージング サービスです。

Cloud Pub/Subとは

Cloud Pub/Subの設定

Cloud Pub/Subに定期購読の状態変化があったpurchase_tokenの通知が来るので、それをトリガーに最新のレシート情報を取得し、サービスのDBに最新レシート情報を格納するという流れになります。

GCPプロジェクトを作成したら、Coud Pub/Subを開き、「トピック」と「サブスクリプション」を作成します。

「トピック」と「サブスクリプション」の概念がいまいちわかりにくかったです。最終的なイメージとしては、「トピック」はデータの受け皿のニュアンス、「サブスクリプション」は特定のトピックのデータの配信を行うというニュアンス、とぼんやり認識しています。「トピック」に定期購読の更新処理の通知が入り、それを受け取るとサブスクリプションが指定のポイントにデータ連携する、という感じで機能します。

公式サイトに詳細な手順が記載されているので、その記述に従って設定します。
このページは何度も読み返すことになると思います。

リアルタイム デベロッパー通知を追加する

サブスクリプションの作成

サブスクリプションの作成には、以下の点に留意してください。

  • どのトピックデータを配信するのかを指定する
  • 配信タイプは「push」にする
  • pushエンドポイントに、サービスのエンドポイントを指定する。

配信タイプをpushにしてpushエンドポイントを指定することで、Play Consoleから定期購読の状態変化通知を受け取るたびに指定のエンドポイントにjsonデータが配信されるようになります。

Play Console上での設定

Google Play Console上で、リアルタイムデベロッパー通知の欄に、先程作成した「トピック名」を入力します。これにより、定期購読の状態変化があるたびに、指定したトピックに通知されるようになります。

ここまで設定をした上で、Google Play Console上から「テスト送信」を行います。

Play Console -> Topic -> Subscription -> Service Server
と疎通を確認します。

Cloud Pub/Subから受領するデータ形式

Cloud Pub/Subからの「サブスクリプション」で連携されてくるデータはbase64でエンコードされていますが、デコードすると以下のデータが含まれています。

versionstring
packageNamestring
eventTimeMillislong
oneTimeProductNotification
or
subscriptionNotification
or
testNotification
消耗型のプロダクトの場合は、oneTimeProductNotification、
定期購読はsubscriptionNotification、
play consoleからの「テスト送信」の場合は、testNotificationがそれぞれ含まれます。

今回は定期購読の更新を想定していますので、subscriptionNotificationが含まれることになります。

subscriptionNotificationの中には、どのレシートに状態変化があったのかを特定する”purchaseToken”が含まれますので、このデータを元に、そのpurchaseTokenの最新情報を取得するAPIを用いて、サーバー内に保管しているレシート情報を更新することになります。

レシート検証API

定期購読状態に変化があったpurchase_tokenを受け取ったら、次はその情報をもとに、最新のレシート情報を取得し、DB内に保管しているレシート情報を更新します。

[Method: purchases.subscriptions.get  |  Google Play Developer API]

HTTP request

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

packageNameとsubscriptionIdは、GCPやGoogle Play Console上ですでに設定しているもの、tokenは先程Pub/Subから受領したものになります。

問題は、query parameterにはaccess_tokenを含める必要があることです。

access_toekn と refresh_token

最新のレシート情報を取得するには、レシート検証のAPIを使用する必要がありますが、そのAPIを使用するには、access_tokenが必要になります。そして、やっかいなのが、このaccess_tokenは1時間程度の有効期限があり、さらにaccess_tokenを取得するために、refresh_tokenを先に生成しておかなければいけないことです。

[Authorization  |  Google Play Developer API  |  Google Developers]

POSTMANやブラウザでのアクセスを駆使してなんとかクリアします。。

これで定期購読の状態変化をCloud Pub/Subで受け取り、サービスのサーバーにrefresh_toeknのデータとともにPOST、その情報を用いてレシート検証APIで最新レシート情報を取得し、DB内に最新のレシート情報を保存できるようになります。

コードサンプル

公開できる箇所のみに絞っていますが、Pub/Subからの「サブスクリプション」でPush通知されるエンドポイントで動く箇所のコードサンプルを記載します。(Ruby on Rails)

def XXXX
  decoded_data = Base64.decode64(params[:message][:data])
  data = JSON.parse(decoded_data, symbolize_names: true)
  purchase_token = data[:subscriptionNotification][:purchaseToken]

  log = Log.find_by(purchase_token: purchase_token)

  token_json = json_from_api("https://accounts.google.com/o/oauth2/token?grant_type=refresh_token&client_id=XXX&client_secret=XXX&refresh_token=XXX", 'POST', nil)
  access_token = token_json['access_token']
  
  receipt_json = json_from_api("https://www.googleapis.com/androidpublisher/v3/applications/packageName/purchases/subscriptions/subscriptionId/tokens/token?access_token=#{access_token}", 'GET', nil)

  log.update!(よしなにアップデート)

  head :ok
end

private

def json_from_api(url, method, params)
  # Net::HTTPを用いてHTTPレスポンスを返却する処理
end

以上です。参考になりましたら幸いです。