Google Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について

ユーザ・決済基盤部の宇津(@uzzu)です。

クックパッドでは複数のAndroidアプリでGoogle Play決済(定期購読、消費型商品)を利用しており、 ユーザ・決済基盤部ではそれらのアプリの決済情報を取り扱う共通決済基盤サービスクライアントライブラリを日々開発しています。 直近ではGoogle I/O 2019にて発表されたGoogle Play Billing Client 2.0にも対応し、Cookpad.apk #3のLT枠にてどのように対応していったか発表させて頂きました。

speakerdeck.com

本記事では同発表にて時間が足りず深堀りできなかった、消費型商品における決済の承認(acknowledgement)対応について解説します。 スライドと合わせて読んで頂ければ幸いです。

消費型商品における2.0とそれ以前との違い

2.0以前の消費型商品の購入フローは概ね以下の図のようになっていたかと思います。

f:id:himeatball:20190815060223p:plain
2.0以前の購入フロー

2.0からはこれに加えて、決済の承認が必要になります。 Google Play決済自体は決済処理時に走る(Pending Purchaseを除く)のですが、3日以内に開発者が決済の承認を行わない場合返金されます。 通信断や障害等で購入フローが正常に完了せず商品が付与されなかったユーザが自動的に救済されるようになるのは、サポートコスト削減の面でも非常に良いですね。

f:id:himeatball:20190815060447p:plain
2.0での購入フロー

一見、購入フローに処理が1ステップ追加されただけのように見えます。加えてリリースノートにも

For consumable products, use consumeAsync(), found in the client API.

とあるように、アプリ上でconsumeAsyncを呼び出す事で消費(consume)しつつ決済の承認も行われるので、図に追加した⑤については特にやる事はないのでは?と思われた方もいるかと思います。 しかしながら、商品付与が行われるタイミングにおける決済の承認状態は2.0では未承認、それ以前では承認済という違いがあり、 この違いによってアプリ改ざんに対するリスクを考慮する必要性がでてきます。

consumeAsyncを呼び出さないようにアプリを改ざんされる事を想定した場合、購入処理を実施すると以下のように処理されます。

  1. 消費型商品の購入ボタンを押す
  2. 購入フローに則り商品が付与される
  3. consumeAsyncを呼び出さない為消費が行われないが、商品は既に付与されている(決済が未承認の間、商品は消費されない為、再購入はできない)
  4. 3日後、決済が未承認の為返金される
  5. 返金されると消費型商品が再度購入可能になる
  6. 1に戻る

つまり、2.0以前の購入フローの実装のまま愚直に2.0対応してしまうと、アプリ改ざんによって3日毎に消費型商品を無料で取得する事ができてしまいます。

対策A: サーバサイドで決済を承認する

決済の承認はレシート検証同様にサーバサイドで行いたいという需要に応えるように、Purchases.products: acknowledge
が用意されています。 クックパッドの共通決済基盤サービスではこれを利用して決済を承認しユーザと決済情報の紐付けが正常に行われた上で、各サービスで商品の付与ができる状態とするようにしています。 商品付与後、アプリ上でconsumeAsyncします。

この対策方法はアプリ改ざんに対するリスク、及び決済の承認に関連するアプリ上での購入フローの実装が2.0とそれ以前とで変わらないのが利点です。 ただし、クックパッドのような決済サービスと商品を販売しているサービスが分離されている環境下においては、 決済状態と商品付与状態の整合性の担保ができている前提での対策方法になると考えています。 クックパッドの共通決済基盤サービスにおける整合性についての取り組みは以下の記事を参考にしてください。

https://techlife.cookpad.com/entry/2016/06/01/070000

加えて決済を承認するタイミングについて、商品を付与した上で決済を承認するか、あるいはレシート検証を終えた段階で一旦決済を承認した後に商品を付与するかを検討するかと思います。

クックパッドの共通決済基盤サービスではどうしているかというと、消費型商品においては購入フローの完了処理である所の消費処理がアプリ上でしか実施できない為、購入処理の冪等性を担保できるよう後者を選択しています。 定期購読においてはGoogle Play決済を終えた以降の購入フローをサーバサイドで完結できる為、前者で且つレスポンスタイムを上げる為にJob Queueで非同期に決済の承認を実施しています。

対策B: アプリ上で決済の承認を実施してからサーバにレシートを送信し、サーバ間通信で決済の承認状態を検証する

Billing Client Libraryに決済の承認をするためのメソッド(BillingClient#acknowledgePurchase)が用意されています。 Google Play決済を実施後にこれを呼び出してまず承認してしまい、その上でサーバにレシートを送信し、サーバサイドでPurchases.products: getを呼び出してacknowledgementStateを確認し、 承認済か否かを検証した上で商品を付与した後、アプリ上でconsumeAsyncするような購入フローにします。

この対策方法ではアプリ上に実装されている購入フローはもちろん、通信断等で滞留した決済の再開処理にも手を入れる必要がある為、対策Aよりは手がかかるものの、 サーバ間通信で決済の承認状態を検証する為、対策A同様に介入される余地はないと考えています。

その他の対策方法

例えばdeveloper notificationを頼りに商品を付与する方法があるかと思いますが、developer notificationは現在定期購読のみサポートしているのと、 仮にサポートされるようになったとしても、消費型商品において大半のユーザは購入完了したら遅延なくすぐに商品を使用したい為、 その仕組みを整えるのはそれなりに開発コストがかかりそうです。

決済処理フローはそのままにアプリ改ざん対策に本腰を入れていくとしても、アプリ改ざん対策はいたちごっこになってしまう為、運用コストの増大が予想されます。 素直に前述の対策Aないし対策Bを適用するのが良さそうです。

まとめ

本記事ではGoogle Play Billing Client 2.0における消費型商品の決済の承認(acknowledgement)について解説しました。 弊社において利用していない機能もあり(定期購読のupgrade/downgrade等)、決済の承認に関する網羅的な解説とまではなっていないですが、 Google Play Billing Client 2.0導入の手助けとなれば幸いです。

クックパッドではアプリ内課金をやっていくエンジニアを募集しています

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/