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 のものを使うように。
また拡張機能の詳細ページにはさらっとしか書いてないが、専用の Web SDK を使って stripe のセッションを作成しないと正常に機能しない。
具体的には session 作成時に Firestore 側で customer ドキュメントを作っていないので、 StripeからWebhook送信したとき 200 OK にはなるものの「User Not Found」というレスポンスが返ってくる。そして customers
配下に checkout_sessions
などが作られない。
もちろんこれも invertase のリポジトリのものをインストールするように注意。npm i @invertase/firestore-stripe-payments
で行けたが、これだと余計なものまでインストールしてしまっているかもしれない。
依存関係
そしてなにより、この Web SDK が firebase のバージョンへの依存関係がシビアで、以下のように package.json
で overrides
を使って各サービスのバージョンを 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 などからご相談ください。
Discussion