2018年8月14日火曜日

GooglePlay定期購読の解約方法について

Androidでサブスクリプション(定期購読)を提供しているアプリで退会するには、
  1. Google Play ストア Google Play を開き、
  2. メニュー  メニュー 次へ [定期購入] をタップし、
  3. 解約する定期購入をタップし、
  4. [定期購入を解約] をタップして、
  5. 画面の指示に沿って操作(退会理由のアンケートに答える)
という操作をする必要があって、やや面倒くさい。
詳しくは、以下に書かれています。
https://support.google.com/googleplay/answer/7018481?co=GENIE.Platform%3DAndroid&hl=ja

まず、「アプリ上で退会できないの?」と思うかもしれませんが、それは出来ません。
GooglePlayのライブラリが現状そういう機能を提供していないので。
ただ、これは割と正しい処置かもしれません。

GooglePlayの操作で定期購読を集中管理しているから、その方が不要な契約を整理しやすいと考えられるので。
なお、アプリからの解約は出来ませんが、アプリのサーバからの解約(cancel)なら出来ます。なので、仮にアプリに解約機能を持たせたい場合、サーバへ解約リクエストを出してサーバ側で解約処理をすることなら出来なくはなさそうです。ただし、「定期購読は基本GooglePlayで集中管理する」というスタンスからは外れるので、あまり推奨される方式ではないかも。(それが推奨される方式ならそもそもGooglePlay Billing Libraryに解約機能が入っている訳で)
とあるサブスクリプション課金を提供しているアプリのレビューでこのような意見があったのですが、
アンインストールで自動的に解約という機能はあっても良いんじゃないかと思いました。

まぁ、それをアプリに要望するのはお門違いですが。
GooglePlayが持つべき機能なので、Googleへ文句を言うべき事案だと思います。

あと詐欺云々とか言っちゃってますが、少なくとも詐欺罪は成立しないと思います。
詐欺罪とは、
1、人を欺いて財物を交付させた者
2、前項の方法により、財産上不法の利益を得、又は他人にこれを得させた者

に適用される罪で、つまり「欺いた」証拠を立件しなければ成立しないものなので。この場合、詐欺だと言いたいのなら、契約時の各種同意事項の中に嘘が書かれていたことを立件すべきであって、私が見る限り契約の解除に関する方法は正確に明示されており、当然そこに嘘等は無かったように見受けられたから、詐欺罪は成立しない筈です。(契約者が未成年者か被保佐人で法定代理人の許可無く契約を締結したのであれば取消の余地ならあるかもしれませんが)
ただ、これは法律的な話で、実際普通の成年者でも使用許諾などを完全に理解できない人の方が多い。運用レベルでは徹底的にレベルが低い人を基準にすべきなのだが、こういう仕組みを作っているGoogleの人がそもそもレベルが高い人たちしか居ないから、上手く運用レベルで噛み合っていないのではないだろうか。(つまり、詐欺だと叫びたくなるのも分からないでもない)

2018年8月12日日曜日

GooglePlay IAB(subscription)のレシート検証処理の実装 (Node.js)

AndroidのIn-App-Billing API v3でアプリ内課金(subscription)を実装したアプリ+サーバを作ったのですが、アプリの実装は簡単だった反面サーバを作るのに若干難産したので、そのことをネタに備忘録的なものを書いてみます。なお、サーバ言語はNode.js + TypeScriptです。

【①クライアントの実装】
GooglePlay Billing Libraryを使って実装します。
https://developer.android.com/google/play/billing/billing_library_overview
半日ぐらいで簡単に作れました。ライブラリ自体の使い方が(v3だけあって)かなり洗練されていることに加え、ググれば十分に情報が出てくるので、初心者でも簡単に実装できるかと思います。
つまり、クライアントの実装ネタ自体は十分にあるので本記事では割愛します。

【②サーバ実装】
subscription IABでは、購入時に発行されるreceiptをサーバへ送信し、サーバでGooglePlayへのverification(REST API)を行い、有効な状態ならコンテンツ閲覧などの機能を開放するといった形で実装します。

実装時に注意すべき点としては、APIの実行上限が20万件/日に制限されている(引き上げは出来るけど恐らく有料になる)ので、verificationが完了した状態のreceiptデータをRedisやDB等にキャッシュしておく必要がある(要するに無闇にポンポンGoogleへリクエストを投げてはいけない)事ぐらいでしょうか。

【③verification API】
GooglePlay developer APIのPurchases.proudcts:getを用いて検証します。
https://developer.android.com/google/play/developer-api?hl=ja
https://developers.google.com/android-publisher/api-ref/purchases/products/get

Requires AuthorizationなAPIなので、予めGooglePlay developer consoleでサービスアカウントを作成して、JWT認証できるようにしておく必要があります。

APIは普通にHTTP clientで書いても良いかと思いますが、様々なサーバ言語向けライブラリが用意されていて、少なくともauth系のAPIはライブラリを使用することが(Googleから)推奨されています。
https://developers.google.com/android-publisher/libraries

上記ページには、現時点でJavaとPhytonしかありませんが、ページを辿っていくとNode.jsのライブラリもありました。ただし、Alpha版(この時点で嵐の予感)。
https://github.com/google/google-api-nodejs-client

とりあえず、上記ライブラリでJWT認証+purchases APIを実装してみた。ところが、JWT認証は上手くいったのですが、purchases APIが全然上手く動かない。「Invalid Value」というエラーで失敗するのですが、何処がinvalidなのやら...。

StackOverflow等で調べてもv2以前の情報しか載っていなくて、一応v3の実装自体は入っているけどexampleとか一切無い状態。
https://github.com/google/google-api-nodejs-client/tree/master/src/apis/androidpublisher
やる気が感じられない...せめてどうやって実装するか切り口となる説明ぐらいREADME.mdに書いて欲しい。なので、JWT認証はライブラリで行いつつnode-rest-clientで直にpurchases APIを実行するのが、現時点で最も無難な選択肢だろうと判断しました。

以下、上記の方式で実装したコードを晒します。

【④必要なパッケージ】
npm install --save googleapis
npm install --save node-rest-client
npm install --save xml2js
※xml2jsを入れないとnode-rest-clientのコンストラクタでモジュール不整合が起きる

【⑤実装】
const RestClient = require('node-rest-client');
const { google } = require('googleapis');
const authClient = new google.auth.JWT({
    email: サービスアカウントのEメール,
    key: サービスアカウントのプライベートキー,
    scopes: ['https://www.googleapis.com/auth/androidpublisher']
});

const restClient = new RestClient.Client();

(略)

authClient.authorize((error, tokens) => {
    if (error) {
        略(認証エラー時の処理)
        return;
    }
    const url = 
        "https://www.googleapis.com/androidpublisher/v3/applications/"
        + receipt.packageName + "/purchases/subscriptions/"
        + receipt.productId + "/tokens/" + receipt.purchaseToken
        + "?access_token=" + tokens.access_token;
    restClient.get(url, {
        headers: {
            'Content-Type': 'application/json'
        }
    }, (data, response) => {
        if (200 != response.statusCode) {
            略(APIエラー時の処理)
        } else {
            receiptExpire = data.expiryTimeMillis;
            略(成功時の処理)
        }
    });
});

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...