iPhoneで Grafanaの グラフを 参照できる アプリ Grafanizer 作ってます。 詳しくは こちらへ

AppStoreでアプリが売れたらFirebaseからIFTTT経由でiPhoneへ通知させる

Dec 25, 2017  
#ifttt #grafanizer #firebase #gas

この記事、本当は Firebase Advent Calendar 2017 に投稿しようとしたものです。

直前になってコードを書き換えたくなってコードに修正を入れてたら間に合わなくなり、「 GoogleAppEngineとFirestoreの機能を利用してGolangとかVue.jsで動く2chみたいな掲示板を作ってGitLabCIで自動デプロイさせて一人DevOptsしてみた 」を投稿しました。が、多分こちらのほうがFirebase色が強いと思います。

はじめに

画面上部にもバナーをつけているのですが、iPhoneでGrafanaのグラフを閲覧できるアプリを作ってます。

AppStoreで公開したりプロダクトページのドメイン代など、アプリを公開するというのはそれなりにお金がかかります。趣味で作ってるので儲けとかまでは全然考えていないのですが、維持費については少しでも回収できたら良いなと思ってアプリ内課金を採用しています。殆ど売れないのですが、おかげさまで2年目にしてようやく回収の見込みがたってきました。

そんなかわいい状況なのですが、数日に一回くらいのペースで売上げがあるとなんとなく嬉しいもので、以前はログインしてからの動線が長いAppStoreの売上げをこまめにチェックしてました。殆ど売れてないのですがw(しつこい

売上に関しては、AppleのiTunesConnectよりもGoogleのFirebaseで確認するほうが楽です。Dashboard画面で広告も含めた収益を確認出来ます。

img

(しかしほとんど売れて(ry

でも、iTunesConnectよりFirebaseのほうが楽になったと言ってもやっぱり面倒なものは面倒なので、どうにか自動化させたいと常々考えていました。

そんな時にFirebaseのFunctionsを使えばアプリ内課金の購入をトリガに出来ることがわかったので、アプリが売れる度に手元のiPhoneに通知を飛ばす方法を検討してみました。

仕様

今回は仕様として以下の二点を考えてみました。

  1. 課金のタイミング in_app_purchase と新規ユーザーの使い始め first_open のログをSpreadsheetに記録する
  2. 課金のタイミングでIFTTTのWebhookを利用して日本円に変換した金額を通知する

通知だけあれば良いのですが、売れた履歴とか新規ユーザーの購入割合とかを後からグラフ化できたらもっとニヤニヤ出来るだろうなと思って、Spreadsheetへの記録も追加しています。

どちらについてもFirebaseFunctionsだけで開発出来ます。しかしSpreadsheetに追加するところでNodeJSなAPIを使うとOAuthなどで面倒なことになるのでHTTP経由にして、GASにて書き込むようにしています。

この方法はURLさえわかれば誰でも書き込めるようになるので、気になる人は↓のURLを参考にしてAPI経由で書き換えてみてください。僕もこの通り実装して一時期動かしてましたが、ソースが長くなって見通しが悪くなったので、今回紹介している認証なしでシンプルな方式を採用しました(ここを書き換えてたら締切日まで間に合わなかった...)。まあ大丈夫でしょう。

firebase/functions-samples

実装

上記の仕様では、Functions側とAppScript側の2つにコードが必要になります。また、IFTTTにも少し設定が必要になります。

Functions

利用する前に2つのURLを書き換えて利用してください。

見ての通り、円ドルの為替レートを取得してSpreadsheetに書き込み、必要に応じてIFTTTへWebhookしてるだけです。

気になる場所といえば、FunctionsはPromiseベースで実装する必要があるという点でしょうか?

あまりPromiseのことを詳しくわからなかったので、直列処理の時に値を引きずり回すところで少し躓いちゃいました。
今回は request-promise を使って Options.Transform で値を再利用を引きずり回すようにして対応したのですが、こういうもんなんでしょうかね?

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const request = require('request-promise')

admin.initializeApp(functions.config().firebase);

const kawaseURL = "http://api.aoikujira.com/kawase/get.php?format=json&code=usd&to=JPY";

const spreadURL = "https://script.google.com/macros/s/XXXXXXXXXXXXXXXXXXXXX/exec"; // 書き込むシートのURL
const iftttURL = "https://maker.ifttt.com/trigger/XXXXXXXXXXXXX/with/key/XXXXXXXXXXXXX"; // WebhookのURL

exports.writePurchase = functions.analytics.event("in_app_purchase").onLog(event => {
    var params = {
        "dataName": event.data.name,
        "purchaseValue": event.data.valueInUSD,
        "country": event.data.user.geoInfo.country
    };

    return Promise.resolve(params)
        .then(toJPY)
        .then(write)
        .then(notify);
});

exports.writeFirstOpen = functions.analytics.event("first_open").onLog(event => {
    var params = {
        "dataName": event.data.name,
        "purchaseValue": 0,
        "country": event.data.user.geoInfo.country
    };

    return Promise.resolve(params)
        .then(toJPY)
        .then(write);
});

exports.testNotify = functions.https.onRequest((req, res) => {
    var params = {
        "dataName": "test",
        "purchaseValue": 4.140365,
        "country": "Japan"
    };
    Promise.resolve(params)
        .then(toJPY)
        .then(write)
        .then(notify);
    res.status(200).send("notify");
});

function toJPY(params) {
    return new Promise((resolve, reject) => {
        var options = {
            uri: kawaseURL,
            transform: function (_body) {
                let body = JSON.parse(_body);
                params.rate = body["JPY"];
                params.jpy = params.purchaseValue * body["JPY"];
                return params;
            }
        };
        request(options)
            .then(resolve);
    });
}

function write(params, cb) {
    return new Promise((resolve, reject) => {
        var options = {
            uri: spreadURL + "?" + Object.keys(params).map(d => {
                return d + "=" + params[d];
            }).join("&"),
            transform: function (body) {
                return params;
            }
        };
        request(options)
            .then(resolve);
    });
}

function notify(params) {
    return new Promise((resolve, reject) => {
        const options = {
            method: "POST",
            uri: iftttURL,
            json: {
                value1: "💰 Grafanizerで " + Math.floor(params.jpy) + "円 の売上げがあったよ!  💰"
            },
            transform: function (body) {
                return params;
            }
        };
        request(options)
            .then(resolve);
    });
}

AppScript

こちらも使う前にsheetIDを指定して利用してください。

こちらも特に難しいところはありません。Webhookがやってきたらタイトル行下に空行をインサートして、その行をやってきた値で埋めているだけです。

var sheetId = "XXXXXXXXXXXXXXXXXXXx";

function doGet(e) {
  Logger.log(e.parameter);
  var spread = SpreadsheetApp.openById(sheetId);
  var sheet = spread.getSheets()[0];
  sheet.insertRowAfter(1);
  var row = new Row(sheet, 2);
  row.val(1, new Date());
  row.val(2, e.parameter.dataName);
  row.val(3, e.parameter.purchaseValue);
  row.val(4, e.parameter.country);
  row.val(5, e.parameter.rate);
  row.val(6, e.parameter.jpy);
  return ContentService.createTextOutput(JSON.stringify(e));
}

Row = function(sheet, i) {
  this.sheet = sheet;
  this.row = i;
};

Row.prototype.val = function(col, value) {
  if (value == undefined) {
    return this.sheet.getRange(this.row, col).getValue();
  } else {
    this.sheet.getRange(this.row, col).setValue(value);
  }
}

IFTTT

IFTTTでWebhookを受信する方法は 以前の記事 や、その他たくさんGoogle検索結果を参考にしてください。

結果

うまく動けば、売上がある度に下記画像のような通知が届きます☺

img

また、Spreadsheetの first_open* の値を使ってこんな感じでグラフ化させることも出来るようになります。さすがドイツ人は仕事真面目ですね!

img

まとめ

  • AppStoreの売上げもFirebaseで取得できる
  • Firebaseをトリガにすれば売上げの度に処理を走らすことができる
  • Spreadsheetにデータを貯めとけば自由にビジュアライズできる
  • IFTTTを利用すればiPohne等にプッシュ通知を送ることが出来る

売上があったと言っても、Appleから30%ひかれるので利益表示にしたほうが現実味があるかもしれない。

「ウザいので通知を切った」という日が訪れるのを待ってる。