🐡

Firebase ExtensionsでStripeによるサブスク実装

2023/03/29に公開

自身で構築中のPWAでサブスクリプションを実装したいと思っており、Stripeを使うのが便利らしいということで勉強しながら手順をまとめてみました。

以下、自前のWebサイトにサブスクリプションを搭載するまでの手順です。

Stripe

Stripeはオンライン決済のプラットフォーム。
今や知らない人はいないAmazonでも導入されているとのこと。
決済履歴は当然、顧客情報も管理でき、サブスクリプションの場合は顧客自身でその中断もできる優れものです。

決済手段の組み込み方法は3つ

Stripeによる決済手段の組み込み方法は以下の3種類。

方法 内容
Stripe Payment Links Stripeで発行される支払い処理用のリンクを利用する方法。商品選択のページも含めてStripeが用意してくれるものを利用する。リンクを共有するだけで良いので、この方法だとWebサイトすらなくてもオンライン決済が実現可能。
Stripe Checkout 自身のWebサイトからリダイレクトしてStripeのチェックアウトページで決済を行う方法。決済後は設定した自身のWebサイトへリダイレクトされるので、ユーザーは支払い時のみStripeのサイトを利用することになる。
Stripe Elements Stripeで用意されているコンポーネントを自身のWebサイトで利用して支払処理を実装する方法。すべて自身のWebサイト内で完結させることになる。

顧客管理も含めてStripeだけで完結できる場合はStripe Payment Linksを利用するのが一番簡単ですが、会員専用サイトなどのように、ログインユーザーが会員か非会員かでサイト内の動きを変えなければならない場合は使えません。

私が想定しているのは会員専用サイトのサブスクなので、Stripe Checkoutを利用してみることにします。
※Elementsを使いこなせる気がしません。

FirebaseをBaasとして実装する場合、いろいろ調べてみると便利なExtensionがありました。
Run Payments with Stripe
Cloud FunctionsとWebhookを利用して、商品情報やユーザー情報をFirestoreと同期してくれるとのこと。ユーザーの認証にはAuthenticationを使います。

ということで、以下の環境で頑張って実装してみます。

Nuxt2
Vuetify2
Firebase

Firebaseプロジェクトの用意

プロジェクトの作成方法は割愛しますが、Cloud functionsを利用するのでBlazeプランに変更しておく必要があります。

Authenticationの有効化

Firebase Authenticationを有効化しメールアドレス認証を有効化します。

Firestoreの有効化

Run Payments with StripeではFirestoreを利用しますので、これを有効化しておきます。

  1. 「データベースの作成」をクリック
  2. テストモードを選択して「次へ」をクリック
  3. ロケーションを「asia-northeast1(Tokyo)」にして「有効にする」をクリック

Hostingの有効化

多分ですが、ローカルのhttp環境ではStripeとの連携がうまくいかないと思うので、Hostingも有効化し、https環境で動作確認できるようにします。

デモプロジェクトの用意

Nuxt2での実装を試してみます。UI FrameworkにはVuetifyを使用します。

npxでNuxtプロジェクトを作成

npx create-nuxt-app stripe-demo

npxを使ってプロジェクトを作成してみたところ、npm run devするとvuetifyのcssについてmodule warningが出力されました。
解消の仕方は別の記事にしましたので、気になる方は参考にしてみてください。

https://zenn.dev/shisyamo4131/articles/eb7330f5a21cd0

Firebaseをインストールして初期化

npm install firebase
firebase init

firebaes initではFirestore、Hostingの2つを利用する設定でOKです。
プロジェクトは先ほど作成したものを使います。

プライバシーポリシーと利用規約、特定商取引法に基づく表記のページを作ります

後ほどStripe側の設定で必要になりますので、プライバシーポリシーと利用規約、特定商取引法に基づく表記のページを作っておきます。内容は皆無で構いません。ここではprivacy.vue、terms.vue、specifiedCommerce.vueをpagesディレクトリに作成しました。

page file
プライバシーポリシー pages/privacy.vue
利用規約 pages/terms.vue
特定商取引法に基づく表記 pages/specifiedCommerce.vue

Hostingにdeploy

ここまでの動作確認をするためにも、一旦デプロイしておきましょう。

npm run generate
firebase deploy

FirebaseのHostingから発行されたURLにアクセスして上記画面が表示されればOKです。

Stripeでプロダクトを用意

  1. Stripeにアクセスしてアカウントを作成します。

  2. デモ用のプロダクトを用意してテスト環境のまま、「開発者」メニューから「APIキー」を選択します。

  3. 「制限付きのキーを作成」をクリックします。

  4. キーの名前に適当な名前を入力し、以下の権限を設定して「キーを作成」をクリックします。

項目 権限
Customers 書き込み
Checkout sessions 書き込み
Customer portal 書き込み
Subscriptions 読み取り
  1. 戻った画面で「テストキーを表示」させて「rk_XXXXXX」コピーして控えておきます。

Run Payments with Stripeのインストール

Run Payments with Stripeにアクセスし、「Install in Firebase console」をクリックしてRun Payments with Stripeをプロジェクトにインストールします。

  1. 「Install in Firebase console」をクリック
  2. 用意しておいたデモプロジェクトを選択

Run Payments with Stripeの設定

インストール先のプロジェクトを選択すると、Run Payments with Stripeの設定画面が開きます。

  1. 「Firebaseだけでなく、Stripeでの課金が発生する可能性があります。」というお知らせです。そのまま「次へ」をクリック。
  2. Firebaseプロジェクト内で有効化されるAPIや実装されるCloud Functionsなどの情報が表示されます。


一通り表示された後、有効化されていない必要なサービスがあればここから有効化できます。表示されていればすべて「有効にする」をクリックします。

すべて有効にできたら「次へ」をクリックします。

  1. アクセス権
    Run Payments with Stripeを利用することによって、表示された権限を持つアカウントがプロジェクト内に生成されます。そのまま「次へ」をクリックします。

  2. 構成を設定
    Run Payments with Stripeの各種設定を行います。

項目 説明
Cloud Functions deployment location Run Payments with Stripeで実装されるCloud Functionsのロケーションを選択します。ここでは「Tokyo(asia-northeast1)」を設定しました。
Products and pricing plans collection Stripeのプロダクトと課金プランの情報を格納するFirestoreのCollection名です。「products」のままにします。
Customer details and subscriptions collection Stripeの顧客情報と課金情報を格納するFirestoreのCollection名です。「customers」のままにします。
Stripe configuration collection Stripeの設定情報を格納するコレクションです。「configuration」のままにします。
Sync new users to Stripe customers and Cloud Firestore Stripe上の顧客情報をどのタイミングで作成するかを選択します。「Sync」にすると、Authenticationでユーザーが作成されたタイミングで、「Do not sync」にした場合は、Checkoutセッションが作成されたタイミングで作成されます。Authenticationの匿名認証を有効にした状態で「Sync」にすると無駄に顧客情報が増えていきますので、提供するサービスの内容に応じて設定します。ここでは「Do not sync」のままにします。
Automatically delete Stripe customer objects AuthenticationまたはFirestoreでユーザーが削除された際に、Stripe上の顧客情報を一緒に削除するかどうかを設定します。Stripe上の顧客情報が削除された場合、当該顧客のサブスクリプションはすべてキャンセルされます。プロダクトにも寄りますが、ユーザー情報が削除されれば当然、サブスクリプションもキャンセルして欲しいので「Auto delete」を選択します。
Stripe API key with restricted access Stripeで発行されるもので、Cloud FunctionsからStripeのリソースにアクセスするための制限付きキーです。上記で控えておいたStripeの「制限付きのキー」を貼り付けて「シークレットを作成」をクリックします。
Stripe webhook secret FirebaseからStripeに各種情報を送信するためのWebhookの設定です。一旦空のままで進めます。
Minimum instances for createCheckoutSession function CheckoutセッションのためのCloud Functionsインスタンスを最低いくつ用意しておくかを設定します。Cloud Functionsがコールドスタート(インスタンス0の状態からスタート)するとレスポンスまでに時間がかかることがあります。逆にインスタンスが生成された状態からスタートすると高速でレスポンスが返ってきます。お試しなので今回は0のままで進めます。
イベントを有効にする よく分からないので一旦、チェックを入れずに進めます。

最後に「拡張機能をインストール」をクリックします。

Firebaseのコンソール「拡張機能」が表示され、インストールが処理されているのを確認できます。完了するまで時間がかかるようです。

Firestore rulesの設定

Run Payments with Stripeのインストールが終わったら、追加の設定を行います。

  1. Run Payments with Stripeの管理画面にアクセス
    FirebaseコンソールからExtensionsにアクセスすると以下のようになっていると思います。

    初めての場合は「始める」、それ以降は「→」をクリックし、管理画面に行きます。
  2. 「この拡張機能の動作」を選択すると、以下のように表示されます。

    スクロールすると、Firestoreに設定すべきrulesと上記で一旦保留にしておいたWebhookの設定について記されています。
  3. Firestore rulesの設定
    指定された内容でFirestore rulesを設定します。

Webhookの設定

Run Payments with Stripeの管理画面に表示されているとおりにWebhookを設定します。

  1. Configure Stripe webhooksに記載されているURLをコピーします。
  2. Stripeのダッシュボード(URLのすぐ上にリンクがあります)へ行き、「エンドポイントを追加」をクリックします。
  3. エンドポイントURLに先ほどコピーしたURLを貼り付けます。
  4. リッスンするイベントに以下を設定します。
product.created
product.updated
product.deleted
price.created
price.updated
price.deleted
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
payment_intent.processing
payment_intent.succeeded
payment_intent.canceled
payment_intent.payment_failed
tax_rate.created (optional)
tax_rate.updated (optional)
invoice.paid (optional, will sync invoices to Cloud Firestore)
invoice.payment_succeeded (optional, will sync invoices to Cloud Firestore)
invoice.payment_failed (optional, will sync invoices to Cloud Firestore)
invoice.upcoming (optional, will sync invoices to Cloud Firestore)
invoice.marked_uncollectible (optional, will sync invoices to Cloud Firestore)
invoice.payment_action_required (optional, will sync invoices to Cloud Firestore)

「イベントを追加」をクリックします。
5. 次の画面が表示されるので、「署名シークレット」を表示させ、内容をコピーします。

  1. Firebaseコンソールに戻り、Run Payments with Stripeの管理画面から「拡張機能の構成」→「拡張機能の再構成」を選択し、コピーした「署名シークレット」を「Stripe webhook secret」に貼り付けます。貼り付けたら「シークレットを作成」をクリックし、最後に「保存」をクリックします。

商品を登録

サブスクの対象となる商品をStripeに登録します。

  1. Stripeのダッシュボードから「商品」のページに移動します。
  2. 「+商品を追加」をクリックします。
  3. 商品情報を入力していきましょう。
項目 説明
名前 商品名です。適当でOKです。
料金体系モデル いろいろな料金体系が用意されていますが、ここでは「標準の料金体系」を選択します。
価格 商品の価格を入力します。
請求期間 サブスクリプションで毎月課金したいので、月次を選択します。すぐ上にある「継続」、「一括」は「継続」を選択しておきます。

他にもいろいろと設定できるようですが、そのあたりは別の機会に試すこととします。
無料トライアルの設定などは興味ありです。

「保存」をクリックし、商品登録を完了させると、登録された商品の詳細ページに移動します。

商品の登録が完了すると、WebhookによってCloud Functionsが実行され、Firestoreにも商品が登録されます。

Stripeで公開情報の設定

  1. Stripeのダッシュボードから歯車をクリックし、「ビジネス設定」の中にある「公開情報」をクリックします。

  1. 表示された画面で各種項目を埋めていきます。
項目 説明
公開ビジネス名 基本的にはサービスの名前でOKだと思います。ここではstripe-demoとしました。
サポートのメールアドレス ユーザーがサポートを求める先のメールアドレスを入力します。
サポート電話番号 領収書と請求書に表示される電話番号を入力します。今回の試行時ではOFFにすることができませんでした。とりあえず入力しておきます。
サポートの住所 サービス提供者の住所を入力します。適当に入力しておきます。
明細書表記(英文字) 基本的には公開ビジネス名(を英語にしたもの)と一緒でOKだと思います。
明細書表記 こちらも公開ビジネス名と一緒で良いと思います。略称も入力しなければなりません。
明細書表記(カナ) こちらも公開ビジネス名と一緒で良いでしょう。略称も入力しなければなりません。
ビジネスのWebサイト Firebase Hostingから発行されたURLを入力します。
サポートのWebサイト 同上でOKです。
プライバシーポリシー 事前に用意したページのURLを入力します。
利用規約 事前に用意したページのURLを入力します。
特定商取引法に基づく表記 事前に用意したページのURLを入力します。

ここまで入力できたら「保存」をクリックします。

Run Payments with Stripeの使い方

FirebaseのExtensionsからRun Payments with Stripeの管理画面に移動し、「この拡張機能の動作」を見てみると、サンプルコードが記載されています。
いくつか試してみます。
サンプルのコードがVersion8の書き方だったので、Version9の書き方に修正しつつ、デモプロジェクトで実際に動かせるようにvueファイルにしていきます。

List available products and prices

現在有効となっている商品および価格の情報を取得して表示します。

products.vue
<template>
  <v-card>
    <v-card-text> 商品の取得テストを実行します。 </v-card-text>
    <v-card-actions>
      <v-btn @click="test">test</v-btn>
    </v-card-actions>
  </v-card>
</template>

<script>
import { collection, getDocs, query, where } from 'firebase/firestore'
export default {
  methods: {
    test() {
      const colRef = collection(this.$firestore, 'products')
      const q = query(colRef, where('active', '==', true))
      getDocs(q).then(function (querySnapshot) {
        querySnapshot.forEach(async function (doc) {
          console.log(doc.id, '=>', doc.data())
          const priceColRef = collection(doc.ref, 'prices')
          const priceSnap = await getDocs(priceColRef)
          priceSnap.docs.forEach((doc) => {
            console.log(doc.id, '=>', doc.data())
          })
        })
      })
    },
  },
}
</script>

<style></style>

動かしてみると、Firestoreに登録されている商品情報をコンソールに出力することができました。

Stripe関係なく、Firestoreに登録されいているドキュメントを読み込んだだけなので当然なのですが・・・
要は「商品ページを作っておけば、Stripeで商品を登録するだけで反映される」ということです。

Subscription payments (web only)

サブスクリプションの登録です。

checkout.vue
<template>
  <v-card>
    <v-card-title> Checkoutのテストを実行します。 </v-card-title>
    <v-card-text>
      <div>ログイン状態: {{ isAuthenticated }}</div>
      <div>ユーザー情報: {{ user }}</div>
    </v-card-text>
    <v-card-actions>
      <v-btn @click="login">login</v-btn>
      <v-btn @click="checkout">checkout</v-btn>
      <v-btn @click="logout">logout</v-btn>
    </v-card-actions>
  </v-card>
</template>

<script>
import { addDoc, collection, doc, onSnapshot } from 'firebase/firestore'
import { signInWithEmailAndPassword, signOut } from 'firebase/auth'
export default {
  data() {
    return {
      user: null,
    }
  },
  computed: {
    isAuthenticated() {
      return !!this.user
    },
  },
  methods: {
    login() {
      signInWithEmailAndPassword(this.$auth, 'hoge@sample.com', 'fugafuga')
        .then((userCredential) => {
          this.user = userCredential.user
          console.log('login success.')
        })
        .catch((error) => {
          console.log(error)
        })
    },
    logout() {
      signOut(this.$auth)
        .then(() => {
          this.user = null
          console.log('logout success.')
        })
        .catch((error) => {
          console.log(error)
        })
    },
    async checkout() {
      const customerRef = doc(
        collection(this.$firestore, 'customers'),
        this.user.uid
      )
      const checkoutColRef = collection(customerRef, 'checkout_sessions')
      const docRef = await addDoc(checkoutColRef, {
        automatic_tax: true,
        price: 'price_1MqpEYKdOqi8OlZoZT8MEXVP',
        success_url: window.location.origin,
        cancel_url: window.location.origin,
      })
      onSnapshot(docRef, (snapshot) => {
        const { error, url } = snapshot.data()
        if (error) {
          alert(`An error occured: ${error.message}`)
        }
        if (url) {
          window.location.assign(url)
        }
      })
    },
  },
}
</script>

<style></style>

automatic_tax: true は消費税の自動計算をStripe側で行うようにする設定です。
priceのIDは適宜、自身が登録した商品のものに書き換えてください。

簡易的ではありますが、Authenticationでユーザーを作成してログインできるようにし、ログイン後にCheckoutボタンをクリックすると見事にStripeの支払ページに遷移しました。

支払いが完了すると、Stripe上で設定したリダイレクト先に遷移することも確認できました。

ユーザーが登録したサブスクリプションのキャンセルなどはStripe側でカスタマーポータルが用意されているので、そちらに促せばOKのようです。

ノーコードというわけにはいきませんでしたし、StripeとFirebaseを連携させるための設定が多く、一度に覚えるのは大変ですが、クライアントからはFirestoreを操作することで決済処理が完結してしまう手軽さには驚きです。

記事としては中途半端、かつ長文すぎて読みづらくなってしまったのでいずれ整理するかもしれませんが、Nuxt2、Vuetify、Firebase、Stripeを利用したサブスクリプションを実装するまでの流れでした。

参考記事

公式手順
「これは革命」とまでは言わないけれど、Webサービスへのサブスク導入がめちゃめちゃ楽なのでぜひ知って欲しいStripeのFirebase Extensions

Firebase ExtensionsのRun Subscription Payments with Stripeを使ってサブスク課金をコードを書かずに実装する

Discussion