GASのアドオンにFirebaseとStripeで課金機能を実装した話

GASのアドオンを開発している方は、一度はフリーミアムに一部機能を有料化したいと考えられているのではないでしょうか。Googleは公式に[Restrictions](https://developers.google.com/workspace/add-ons/guides/gsuite-restrictions#charge_users_to_install)で「課金機能は提供してないよ。広告もつけたら駄目ね。けど課金機能を自分で作るのは良いよ。」と言っており、有料化するには自分で一から実装しなければいけません。この記事では実際にどのようにしてアドオンに課金機能をつけたのか紹介します。

## 成果物
- [ガントチャート・ジェネレーター - Google Workspace Marketplace](https://workspace.google.com/marketplace/app/%E3%82%AC%E3%83%B3%E3%83%88%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88%E3%83%BB%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC/597114635116)
- [Ganttchart Generator Premium](https://ganttchart-generator.com/) ※課金ページ

Googleスプレッドシートのアドオン「ガントチャート・ジェネレーター」を開発しました。名前の通り、スプレッドシート上にガントチャートを自動生成してくれるツールで、その内の「ガントチャートの表示単位の変更」と「アサインの空き状況の確認」の機能を課金ユーザーのみ使用できるようにしてあります。 大半の機能は無料で利用できますので、興味のある方はぜひお試しください。
![screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/2302271b-625c-6d7f-ff7b-672eae9dc755.png)



## アーキテクチャ
さて、ここから本題です。まず最初にシステム全体のアーキテクチャについてです。
![GASアドオン_課金機能_アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/7ba9d4fc-1425-2c07-5602-7f5ca38a2cec.png)

- 課金機能 ... Stripe
- データベース ... Firebase (Cloud Firestore)
- 課金ページ ... Firebase (Hosting)

ざっくりとこんな感じです。個人的には課金と言えばPayPalというイメージがあったのですが、 [定額課金時の決済代行サービスを比較](https://qiita.com/kitanote/items/9d260bd649b64c85779a) を読むに、PayPalにするメリットはあまりなさそうで、開発者がしやすそうなStripeを選びました。課金周辺をまるっとStripeに任して、課金ユーザーの管理はFirebase。そこにGoogleスプレッドシートからアクセスし、課金の有無に応じて提供する機能を出し分ける方針としました。

## StripeとFirebaseの連携
### 課金機能
さて、ここでいきなり朗報です。 [Run Subscription Payments with Stripe](https://firebase.google.com/products/extensions/firestore-stripe-subscriptions?hl=ja) なるものが存在しまして、このExtensionsを使うと、いくつかの設定とAPI Keyを登録するだけで、課金機能が完成します。

[Subscription Payments with Stripeを使ってサブスク課金をコードを書かずに実装する](https://qiita.com/mogmet/items/cddc182156028c928ecf) の記事がとても分かりやすく導入方法について解説してくれていますので、基本的にはその通りに設定していけば大丈夫です。強いて言うならば、今回は最終的にGASのアドオンと連携させますので、認証は「Sign in with Google」以外は使えないようにしました。それ以外はほぼそのままです。

### 課金ページ
Run Subscription Payments with Stripe には課金ページもあるのですが、これはそのままプロダクションでは使用できるものではないので、ここはある程度の実装が必要になります。ちなみにデフォルトの課金ページはこんな感じです。

![スクリーンショット 2021-02-21 21.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/3adcc234-1d07-8f81-33d9-d23ee3e32f02.png)

デザインもまあそうなのですが、プロダクトの一覧が全て羅列される仕様なので、特定のアドオンに対しての課金ページとしては使えません。[stripe-samples/firebase-subscription-payments](https://github.com/stripe-samples/firebase-subscription-payments)のコードを基に要件を満たせるように実装していきましょう。※実装後のコードは公開しておりません。

#### メールアドレスの取得と保存
Googleスプレッドシート側で取得するメールアドレスが有料ユーザーかを参照しますので、課金してくれるユーザーをデータベース上に保存しなければいけません。サンプルでは`stripeId`しか保存されないようになっています。

```app.js
firebase.auth().onAuthStateChanged((firebaseUser) => {
  if (firebaseUser) {
    document.querySelector('#loader').style.display = 'none';
    document.querySelector('main').style.display = 'block';
    currentUser = firebaseUser.uid;
    startDataListeners();
  } else {
    document.querySelector('main').style.display = 'none';
    firebaseUI.start('#firebaseui-auth-container', firebaseUiConfig);
  }
});
```

上記で`uid`のみ取得していますが、`firebaseUser`は `email`のプロパティも持っていますので、合わせて取得するように変更しました。


```app.js
// Checkout handler
async function subscribe(event) {
  event.preventDefault();
  document.querySelectorAll('button').forEach((b) => (b.disabled = true));
  const formData = new FormData(event.target);

  const docRef = await db
    .collection('customers')
    .doc(currentUser)
    .collection('checkout_sessions')
    .add({
      price: formData.get('price'),
      allow_promotion_codes: true,
      tax_rates: taxRates,
      success_url: window.location.origin,
      cancel_url: window.location.origin,
      metadata: {
        tax_rate: '10% sales tax exclusive',
      },
    });
  // Wait for the CheckoutSession to get attached by the extension
  docRef.onSnapshot((snap) => {
    const { error, sessionId } = snap.data();
    if (error) {
      // Show an error to your customer and then inspect your function logs.
      alert(`An error occured: ${error.message}`);
      document.querySelectorAll('button').forEach((b) => (b.disabled = false));
    }
    if (sessionId) {
      // We have a session, let's redirect to Checkout
      // Init Stripe
      const stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
      stripe.redirectToCheckout({ sessionId });
    }
  });
}
```
ここで`checkout_sessions`に対して各種の情報を保存していて、この情報が後に`subscriptions`のコレクションに移るようになっているようです。`metadata`オブジェクトのプロパティに取得したemailを保存するようにしました。

#### ログイン認証時のドメインの表示を変更する
![スクリーンショット 2021-02-21 22.38.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147086/80808ba2-010a-24a0-5339-445d67c41721.png)

ログイン認証ダイアログ、何もしなければ表示が`XXXX-1234.firebaseapp.com`のようなプロジェクトIDになっていて、プロダクション環境で使用するにはちょっと辛いんですよね。というわけで、カスタムドメインを取得して表示の変更を行いました。 ドメインの設定についてはググれば参考になる記事がたくさん見つかると思います。例えば、 [お名前.comでドメイン取得からfirebaseに繋ぐまで](https://qiita.com/_pipo_/items/2beb71eb3407775a1bf2)とか。ドメインの設定が終われば、[認証ハンドラのカスタマイズ](https://cloud.google.com/identity-platform/docs/show-custom-domain?hl=ja#customizing_the_authentication_handler)を見ながら設定を進めていきましょう。


#### その他、色々カスタマイズを頑張る
プロダクトの一覧表示を止めるであったり、サンプルコンテンツを消すであったり、細々としたカスタマイズが必要になると思いますが、通常のWeb制作の知識で解決できると思います（急にざっくり）。頑張りましょう！

## GASとFirebase (Cloud Firestore)の連携
Googleスプレッドシート上でのメールアドレスの取得は非常に簡単です。

```index.js
const userEmail = Session.getActiveUser().getEmail()
```
この一行だけ。ここで取得したメールアドレスが課金ユーザーかどうかをCloud Firestoreにアクセスして確認しましょう。GASからのアクセスは [FirestoreGoogleAppsScript](https://github.com/grahamearley/FirestoreGoogleAppsScript) を使うのが便利で良いと思います。READMEは英語ですが頑張って読みます。

>Installation
In the Google online script editor, select the Resources menu item and choose Libraries.... In the "Add a library" input box, enter 1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw and click "Add." Choose the most recent version number.

↑これでインストール。通常のGASのライブラリの使用と同じです。

>Quick start
...中略...
1. Open the Google Service Accounts page by clicking here.
2. Select your Firestore project, and then click "Create Service Account."
3. For your service account's role, choose Datastore > Cloud Datastore Owner.
4. Check the "Furnish a new private key" box and select JSON as your key type.
5. When you press "Create," your browser will download a .json file with your private key (private_key), service account email (client_email), and project ID (project_id). Copy these values into your Google Apps Script — you'll need them to authenticate with Firestore.
6. [Bonus] It is considered best practice to make use of the Properties Service to store this sensitive information.

[README](https://github.com/grahamearley/FirestoreGoogleAppsScript#quick-start)にはリンクがついていますので実際はそこからリンクを踏んでください。簡単に言うと、Run Subscription Payments with Stripeで使用しているプロジェクトからCloud Datastore Owner権限を付与したアカウントを発行し、GASからそのprivate keyを使ってCloud Firestoreへアクセスするということです。尚、private keyはコードにベタ打ちするのではなく、Properties Serviceで保存しておくことが推奨されています。

これでCloud Firestoreへアクセスできるようになりましたので、課金ページで保存したメールアドレスを探索する関数を実装しましょう。

### 課金の有無に応じてコンテンツの出し分け
GASにはUIとして活用できるHTMLサービスが用意されています。このHTMLサービスの中ではあまり高度な操作は出来ないので、大元のHTMLファイルそのものを出し分けるのが一番てっとり早いかなと思いました。

```index.js
const showSidebar = () => {
  return new Promise((resolve) => {
    const userEmail = Session.getActiveUser().getEmail()
    isPremiumUser(userEmail).then((isPremiumUser) => {
      const HTML = isPremiumUser ? 'PremiumPage' : 'Page'
      const HTMLTitle = isPremiumUser ? 'Gantt Chart Generator Premium' : 'Gantt Chart Generator'
      const html = HtmlService.createHtmlOutputFromFile(HTML)
      .setTitle(HTMLTitle)
      .setWidth(300);
      SpreadsheetApp.getUi().showSidebar(html);
      resolve(true)
    })
  })
}
```
こんな感じでサイドバーを出し分けて、そこから操作できる機能に課金の有無で差をつけるイメージです。また、このサイドバーに一般ユーザーには課金の導線を、有料ユーザーには解約の導線をつけると良いでしょう。

## 終わり
そんな訳で、GASのアドレスに課金機能を実装したお話でした。何かの参考になれば幸いです。尚、私はこの後のデプロイで盛大に失敗しましたので、もし2019年4月以前に開発したアドオンをアップデートする場合は[こちらの記事](https://qiita.com/takuya0206/items/ebac4494a8a83aaae170)もぜひご参考ください。私は実開発よりもデプロイの方が数段苦労しました...。皆さんのご健闘をお祈りしております。
おしまい。