6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FirebaseとStripeでサブスクを実装してみた感想とざっくりと実装の紹介

Last updated at Posted at 2020-10-30

TL, DR

  • 実装自体はかなり楽でした(まだ解約などを実装していないので途中まで実装した人の肌感です)
  • Stripeが提供してくれている購入フォームがとても優秀!!!
  • 商品の追加はCloud Functions経由でしかできなかった(間違っていたらごめんなさい)
  • checkout_sessionsサブコレクションを作成してからsubscriptionサブコレクションが作成されるらしい

調べながら実装しているので、間違った点などございましたらコメントなどで指摘して頂けるとありがたいです。

参考にした記事

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

stripe-samples/firebase-subscription-payments

実装方針

商品の追加編

基本的に1つ目の記事が優秀過ぎてそのままなぞっていくだけで出来ました。

だた、今回はユーザが商品の追加を出来るようにする必要があったのでそこがハマり所でした。

少し調べたところこちらのライブラリなどを使うのが一般的のようなのですが

stripe/stripe-node

これはサーバサイドでしか使えません。

商品の追加のためだけにExpressでサーバを立てるのも面倒だったのでCloud Functions経由で商品を追加することにしました。

ソースコードをそのまま載せることは出来ないのでStripeの公式ドキュメントに沿って流れだけ説明しますとCloud Functions内で

const stripe = new Stripe(functions.config().stripe.apikey, { apiVersion: '2020-08-27' });

こんな感じで初期化してから、

~~~何か処理する~~~
const product = await stripe.products.create({
  name: '商品名',
  description: '商品の説明文',
});
await stripe.prices.create({
  unit_amount: 100,
  currency: 'jpy',
  recurring: { interval: 'month' },
  product: product.id,
});

と書いてあげると、FirebaseにStripeの拡張機能をインストールした時Products and pricing plans collectionで指定したルートコレクションにドキュメントが追加されていると思います。

決済編

さて、苦労したのは決済です。

実際に自分で手を動かしてどのドキュメントが作成されなんのフィールドが追加されるのかを追うだけで一日かかってしまいました(僕の実力の無さによるものです。反省。)

結論から申し上げますと、先ほど紹介したリポジトリの中のapp.jsを見れば終わりです。

流れとしては

  1. FirebaseにStripeの拡張機能をインストールする時にCustomer details and subscriptions collectionで指定したコレクションのドキュメントのサブコレクションにcheckout_sessionsドキュメントを作成する
  2. 1が実行されるとCustomer details and subscriptions collectionで指定したコレクションのドキュメントにStripeIdStripeLinkが追加される
  3. エラーがなければStripeの決済フォームに飛ばす

こんな感じです。

文字で説明しても伝わりにくいと思うので少しコードも交えながら説明していきます。

ユーザがボタンを押してhandleClick関数が実行されたとしましょう。

  const stripePromise = loadStripe(process.env.STRIPE_API_KEY);
 
  const handleClick = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    e.preventDefault();
    const docRef = await firestore()
      .collection('customers')
      .doc(uid)
      .collection('checkout_sessions')
      .add({
        price: 'price_xxxxxxxxxxxxxxxxxxxxx',
        allow_promotion_codes: true,
        tax_rates: [process.env.STRIPE_TAX_RATE],
        success_url: 'http://localhost:3000/',
        cancel_url: 'http://localhost:3000/',
        metadata: { tax_rate: '10% sales tax exclusive' },
      });
    docRef.onSnapshot(async (snap) => {
      const { error, sessionId } = snap.data();
      error && alert(error.message);
      if (sessionId) {
        const stripe = await stripePromise;
        stripe.redirectToCheckout({ sessionId });
      }
    });
  };

customersというのがCustomer details and subscriptions collectionで指定したコレクション名です。

stripe.redirectToCheckout({ sessionId })部分で実際にStripeの決済フォームに飛びます。

僕がハマったのは

  1. priceの値は価格ではなくStripeの商品のAPI IDであること
  2. tax_ratesにはarrayを渡す必要がある

この2点でした。

1は最初リポジトリのサンプルコードを読んだ時に

priceだから価格を入れればいいのかな〜

とか考えて動かず悩みました。

素直にドキュメントのIDを渡してやれば良かったのですね。

2は最初そのまま文字列で渡してエラーを吐かれてまたサンプルリポジトリを確認しに行って

const taxRates = ['txr_1HCshzHYgolSBA35WkPjzOOi'];

と書いてあるのに気がつき、そこでようやく

文字列じゃなくて配列で渡すのか

と気付きました。

みなさんが同じ轍を踏まないことを願います。

最後に

思いつきで記事を書いたのですが、後半がだいぶ雑になってしまったので分からないとこがありましたらコメントを頂けるとお答えできる範囲で対応させて頂きます。

ではでは、良いFirebase Lifeを。

6
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?