💲

Next.js x Firestore x Run Payments with Stripe Extension

に公開

前提

  • Next.js Page Router

Run Payments with Stripe Extension

Firestore を使っていてStripeを導入する場合、現在は Invertase 社によってメンテナンスされている「Run Payments with Stripe」拡張機能を使うことが多いと思う。ちなみに Stripe 社のものは警告もでるが現在非推奨なので Invertase のものを使うように。

https://extensions.dev/extensions/invertase/firestore-stripe-payments

また拡張機能の詳細ページにはさらっとしか書いてないが、専用の Web SDK を使って stripe のセッションを作成しないと正常に機能しない。

https://github.com/invertase/stripe-firebase-extensions/tree/next/firestore-stripe-web-sdk

具体的には session 作成時に Firestore 側で customer ドキュメントを作っていないので、 StripeからWebhook送信したとき 200 OK にはなるものの「User Not Found」というレスポンスが返ってくる。そして customers 配下に checkout_sessions などが作られない。

もちろんこれも invertase のリポジトリのものをインストールするように注意。npm i @invertase/firestore-stripe-payments で行けたが、これだと余計なものまでインストールしてしまっているかもしれない。

依存関係

そしてなにより、この Web SDK が firebase のバージョンへの依存関係がシビアで、以下のように package.jsonoverrides を使って各サービスのバージョンを Web SDK が使用しているバージョンに合わせないと FireStore のエラーがでる。また Firebase JavaScript SDK v10 に対応していない模様。

  // この組み合わせ固定でないとエラーが出る
  "dependencies": {
    "firebase": "^9.23.0",
  },
  "overrides": {
    "@firebase/app": "^0.11.4",
    "@firebase/auth": "^1.10.0",
    "@firebase/firestore": "^4.7.10"
  },

決済やユーザーのプレミアム機能の利用資格(Entitlements)など重要な部分であるため、ここをこういったライブラリに頼るのは怖く、将来的に自前で
ちなみに firebase-admin SDK では使えない。

Code

firebase の初期化については簡易版を記載するが、Next.jsにおいてはクライアントサイドでの初期化(app: FirebaseApp)とサーバーサイドで firebase-admin を使った初期化(app: admin.App)があり、これら綺麗なファイル構成がいまいちピンと来ていない。

stripe.ts

// lib/stripe.ts
import { getFirestore } from '@firebase/firestore'; // 先頭に '@' が付く方がモダン
import { getStripePayments } from '@invertase/firestore-stripe-payments';
import { app } from '@/lib/firebase.ts';

getFirestore(app); // firestore が初期化されてないといけない。firebase.ts でやってればここでは不要なはず

export const payments = getStripePayments(app, {
  productsCollection: 'products',
  customersCollection: 'customers', // 拡張機能の構成で 'users' を指定してれば 'users' に
});

page/pricing/index.tsx

import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { createCheckoutSession } from '@invertase/firestore-stripe-payments';
import { payments } from '@/lib/stripe';

const PricingPage = () => {
  const router = useRouter();
  const [loading, setLoading] = useState<boolean>(false);

  const handleSubscription = async (priceId: string) => {
    setLoading(priceId);

    try {
      const session = await createCheckoutSession(payments, {
        price: priceId,
        success_url: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: `${window.location.origin}/pricing`,
      });
      router.push(session.url); // window.location.href より router を使うのがいいらしい
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(null);
    }
  };

  return (
        <>
          <Button
            onClick={() => handleSubscription('your_product_price_id')}
            disabled={loading}
          >
            {loading ? '処理中...' : '登録する'}
          </Button>
       </>
    )
}

解決しない場合

お困りでしたら X のDM、もしくは Lancers などからご相談ください。
https://www.lancers.jp/profile/takuya108817

Discussion