cordovaでsubscriptionを導入

現在も試行錯誤中ですが、ある程度理解できてきたので備忘録も兼ねて掲載します。

私の環境は、ionicです。ionicには、ngcordovaというプラグイン郡がありますが、課金に関してはngcordovaに存在しないので、cordovaで動くものを使います。

使用するプラグインはj3k0さんのcordova-plugin-purchaseです。

基本的な導入方法については、以前記事を書いていたのでCordova 5 + cordova-plugin-purchaseの記事を参考にして下さい。

課金の種類

Androidで使える課金アイテムの種類は次のとおりです。
ios版も同じです

Non-consumable :
非消費型アイテム。一度購入したら永続的に購入者のものになります。
例えばレースゲームでおまけコースを利用可能にするなど
consumable :
消費型アイテム。使う度に無くなるアイテムです。
今流行のガチャチケみたいなのはこれに分類されます。
subscription:
今回のメインテーマ。毎月、毎年など、周期を決めて課金されます。
携帯の電話料金(定額)みたいなイメージ

まずsubscriptionですが、購読という意味です。
月額定額制で、毎月や毎年など、サイクルを決めて、周期的に課金を実行してくれる仕組みです。
売り切りであるアプリ内課金(Non-ConsumableやConsumable)と異なり、毎周期毎に収益が発生します。

subscriptionの処理は、結構日本語で解説しているサイトが少ないので、ちょっとトラブルになると英語のサイトをさまよう事になります

subscription処理(Android)

ハイブリッドアプリなのでiOSでも同じようにできるはずですが、まだ試していないのでAndroid限定ということにします。
課金処理の仕組みについてはAndroidのサイトを見てください。ここでは触れません。
使用するプラグインの説明書きが書かれています(英語)
10回くらい読み返して、試行錯誤しながらやっと想定した動きになってきた。語学という壁は厄介です。

cordova-plugin-purchaseで月額課金

Step1 商品の登録

consumableやnon-consumableも同じですが、商品の登録が必要です。
Google play developer consoleから追加して下さい。

Step2 商品の登録(src)

ここから本番です。
Step1で追加した商品を、JavaScriptに追記していきます

// デバイスの準備が出来てからじゃないとだめなので、必ず$ionicPlatform.ready()の中で書きます
$ionicPlatform.ready(function() {
    // アプリ内課金
    store.register({
      id:     "hogehoge",   // Step1で追加したidを指定。jp.xxxといった頭文字は不要です。
      alias: "mysubscription", //別名をつけることもできる。今回は使いませんので好きにどうぞ
      type:  store.PAID_SUBSCRIPTION //月額定額制の場合は、←のように記載します。
    });
    store.ready();   //準備完了
    store.refresh();
})

これ以降$ionicPlatform.readyは省略しますが、
以下のソースは全て$ionicPlatform.redayの中に書いて下さい。

Step3 課金処理のウインドウ表示

    $scope.subscription = function(){
      store.order("hogehoge");
    }

ボタンとかにイベント紐付ければよし。課金ボタンを押したら、store.orderが呼ばれます。カッコ内の引数には、Step2でかいたidを指定します。

Step4 課金処理開始直後の処理 approved

ここからsubscription独特の処理が入ってきます。

    store.when("hogehoge").approved(function(product) {
      //alert(JSON.stringify(product));  //例えば、receiptの中身を確認したいときは左記のように書くとよい
      product.verify();  //この処理は必須。receiptが本物か確認する処理。詳しくはstep5で
    });

課金処理に成功すると、store.when().approvedが実行されます。
この処理は購入完了後と、アプリの起動直後に呼ばれます。
課金には、receiptという購入情報が記載されますが、cordovaではproductという引数がそれを表しています。

Step5 store.validatorでチェックする

store.validate この処理が一番難しかった。解読にまる2日くらい使ったんじゃなかろうか。

store.validateは、receiptが本物かどうかを確認する処理です。
receiptはユーザ(スマホ)の中に保存されているので、改ざんできてしまいます。なのでreceiptを自分のサーバに転送させて、そこで認証が正しいかどうか、チェックする流れになります。チェックの結果をサーバから受け取り、正しければ
store.verified処理が実行され、証明書が不正やチェック失敗であれば
store.unverified処理が実行されます。
※store.verifiedは次のステップで紹介

とりあえず面倒なら次の通りにかけばOKです

store.validator = function(product, callback) {
   callback(true, product);
}

store.validatorの処理を通さないと、次のstore.verifiedが呼び出されません。なのでとりあえずチェックはスルーして次のstore.verifiedを呼びたい時には、callback(true , product)とかくだけでも良いのです。

Step5-1 store.validatorで真面目にチェックする

ちゃんとreceiptをチェックしたいときは、次のようになります。
スマホのreceiptをサーバに転送する。
サーバで、receiptの整合性をチェックする。
サーバで結果を返す。(1なら成功 0なら失敗)
スマホでサーバの結果を元に処理を分岐する

スマホのreceiptをサーバに転送する方法ですが、次のようになります

    store.validator = function(product, callback) {
      $http({
        method : 'POST',
        url:url+'validate.php',
        data: product
      }).success(function(data, status, headers, config) {
        if(data['res'] == "1"){
          alert("認証OK...");
          callback(true, product);
        }else{
          //認証失敗時は、何度か自動でリトライするらしい
          alert("認証失敗");
          callback(false , "Impossible to proceed with validation");
        }
      }).error(function(data, status, headers, config) {
        callback(false , "Impossible to proceed with validation");
      });
    };

まぁ特に特別なことはやってないです。
サーバー側はPHPの例ですが、次の通り

<?php
header("Access-Control-Allow-Origin: *");
header('Content-type:application/json; charset=UTF-8');
@$data = json_decode(file_get_contents('php://input'),true);
$res = array("res"=>0);

//証明の確認書類準備
$signed_data = $data['transaction']['receipt'];
$signature = $data['transaction']['signature'];
$public_key_base64 = "MIIB******"; //MIIBで始まる長い文字列。google developer consoleからどうぞ
$key =  "-----BEGIN PUBLIC KEY-----\n".chunk_split($public_key_base64, 64,"\n").'-----END PUBLIC KEY-----';

$key = openssl_get_publickey($key);
$signature = base64_decode($signature);

$result = openssl_verify($signed_data, $signature, $key);
if (0 === $result) {
  $res = array("res"=>0);
}else if (1 !== $result){
  $res = array("res"=>0);
}else{
  $res = array("res"=>1);
}

die(json_encode($res));
?>

暗号の知識はあまりないのでよくわからないのですが、このように記述すれば動きました。
参考にさせていただいたサイト1
参考にさせていただいたサイト2

Step 6 verified / unverified

store.validatorの処理後に、callbackでtrueを返すと
store.validateが実行されます。逆にcallbackでfalseを返すと
store.unvalidateが実行されます。

store.when("hogehoge").unverified(function(p) {
  //store.validatorで callback(false)時に実行される。認証の失敗
  alert("認証に失敗しました");
  //認証に失敗した時の処理を書く
});
store.when("hogehoge").verified(function(product) {
  product.finish();// storeの購入情報をfinishedにする
});

Step7 update処理

Storeの情報が変わると実行されるstore.updatedが用意されているので、こちらを使います。
注意:store.updatedは変わった時以外にも、アプリ起動直後にも何度か呼ばれます。
購入情報が課金済みになっていれば、ロックを解除する などの処理を書く

store.when("hogehoge").updated(function(product) {
    if(product.owned){
        //課金が継続されています
    }else{
        //課金が確認されていません
    }
});

アプリ起動直後はproduct.ownedがfalseの状態です。
その後approvedやvalidateやらを経て、store.finishが実行されるとownedがtureに変わるようです。
この処理(store.updated)はアプリ起動直後にも実行されるので、最初は無課金、のちに課金ユーザ という流れになるようです。

 おまけ 期限切れ expired

最初すごく混同して考えていたのですが、
「アプリの継続課金をやめて、期限が切れる」ときと
「アプリの継続課金を継続しているが、支払に失敗して期限が切れる」とき

違うみたいですね(当然といえば当然ですが)
「アプリの継続課金をやめて、期限が切れる」ときは、
approvedが呼ばれなくなるみたいです。実際に期限切れを起こすには、デバッグ用端末で課金すると期限が1日になるので、これで確認するしかないようです。しかし、updatedは呼ばれるみたい。なので課金を脱退した人の権限を剥奪するには、updatedでやらないとだめみたいですね。
(unverifiedではダメ。unverifiedはapproved>validator>失敗時に呼ばれるが、approved自体が動かないので)

最後の
「アプリの継続課金を継続しているが、支払に失敗して期限が切れる」とき
ですが、確認が出来ません。ただ、おそらくですが、マニュアルによると

store.when("hogehoge").expired(function(p){

という命令があるようなので、おそらくこれではないのかなぁと。
公式マニュアルのstore.validateはこちら