背景
React, TypeScript, Fireabseの個人アプリを作る中で決済機能を実装するために調査をしていたのですが、どうやらFirebaseには便利な拡張機能がいくつか存在し、その中に「Run Payments with Stripe」という拡張機能があることを知りました。
本来Stripeはサーバー側でしか実装することができないのですが「Run Payments with Stripe」を使用することで、サーバー側のロジックをCloud Functionsが自動で生成してくれ、これによってフロントで簡単に決済機能を実装することができます。
「Run Payments with Stripe」の導入に関しては別途わかりやすい記事がありましたので、今回は拡張した後の実装
について触れていきたいと思います。
※名称が実装内容と連想しにくいので公式githubから引用したstripe-firebase-extension
と呼称し進めていきます。
stripe-firebase-extensionの初期設定
stripe-firebase-extensionの初期設定は下記記事を参考にしました。
丸投げで大変恐縮ではありますが手順通り進めていけば問題なく導入することができるので是非試してみてください。
こちらの記事ではstripe-firebase-extensionの導入から実際に継続購入(subscription)の実装まで触れていますが、自分は買い切り(one-time-buy)を実装したかったので該当する方は導入が終わりましたら読み進めてください。
実装したコード(決済画面への遷移まで)
import { loadStripe } from "@stripe/stripe-js";
import { CheckoutSessionDoc } from "../@types/stripe";
import { projectFirestore } from "../firebase/config";
export type line_item = {
price: string;
quantity: number;
};
class StripeClient {
async buy(
uid: string,
line_items: Array<line_item>,
success_url: string,
cancel_url: string
) {
return new Promise(async (resolve, reject) => {
const docRef = await projectFirestore
.collection("customers")
.doc(uid)
.collection("checkout_sessions")
.add({
mode: "payment", // 買い切りで指定する
line_items, // 複数購入に対応
success_url, // 必須パラメータ
cancel_url, // 必須パラメータ
});
docRef.onSnapshot(async (snap) => {
const { error, sessionId } = (await snap.data()) as CheckoutSessionDoc;
if (error) return reject(error);
if (!!sessionId) {
const stripe = await loadStripe(
process.env.REACT_APP_STRIPE_API_KEY || ""
);
if (!stripe) return reject();
stripe.redirectToCheckout({ sessionId });
return resolve(undefined);
}
});
});
}
}
export const stripeClient = new StripeClient();
line_itemsの商品情報に加えて、必須パラメータのsuccess_url, cancel_urlの追加、modeの設定を行いFirestoreに登録するとstripe-firebase-extension(
が用意しているCloud Functions)の関数が走り、checkout_sessionsサブコレクションにsessionIdが追加されます。
生成されたcheckout_sessionsサブコレクションのsessionIdを取得し、それを引数に持たせたstripe.redirectToCheckoutを発火させてStripe決済画面へ遷移することができます。
今回の味噌
購入情報をFirestoreに追加してStripeの決済画面へ遷移するまでのコードです。実装するまでに躓いた箇所をメモしていきます。
■商品を1種類以上購入する場合はline_itemsを使う
stripe-firebase-extensionのデモや他の紹介記事ではpriceで商品の金額を受け取るコードをよく見かけたのですが1個以上の商品を購入する場合はline_itemsを使わないといけません。
もちろんline_itemsで単体購入も出来ますので、問題がなければline_itemsを使うのが良いかと思いました。
mode: "payment", // 買い切りで指定する
price: <金額の文字列が入る>
success_url, // 必須パラメータ
cancel_url, // 必須パラメータ
■price(またはline_items)はStripeが生成した金額の文字列を入れる
上記のpriceで受け取る例で記載しているようにpriceはnumberではなくStripeが生成した金額の文字列を入れます。ドキュメント読んでいなかったのでここで躓きました。
■paymentで指定しないと買い切りとして商品購入できない
modeは必須パラメータではないのですが、何も指定しない場合はsubscriptionで登録されてしまいます。そのため買い切りで商品登録をした場合は、買い切りで決済画面へ遷移するようにpaymentを指定しましょう。
うまく実装したコード(決済画面への遷移まで)
の処理が終わると下記のような情報がFirestoreに格納されているはずです。
実装したコード(呼び出し元)
const onClickBuy = async () => {
if (line_items.length === 0) {
alert("購入する製品を選択してください");
return;
}
const result = await verifyJWT();
console.log(result);
if (!result) {
alert("認証トークンが有効期限切れです。ログインしなおしてください。");
logout();
history.push("/login");
return;
}
try {
setIsPendingBuy(true);
const uid = user.uid;
const seccess_url = `${window.location.origin}/complete`;
const cancel_url = `${window.location.origin}/error`;
await stripeClient.buy(uid, line_items, seccess_url, cancel_url);
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${!!error.message ? error.message : error}`);
}
} finally {
setIsPendingBuy(false);
}
};
呼び出し元のコードも載せておきます。ここでは購入する商品情報をまとめる処理を書いています。
横道に逸れてしまいますがtokenの処置をここに書いているのは後述するStripeの決済画面への遷移がstripe.redirectToCheckout()
というメソッドで遷移する仕様になっており、token認証するタイミングがここしかなかったからです。(ベストプラクティスがあるのでしたら是非知りたい…)
さいごに
stripe.redirectToCheckout()
のようにStripeの購入画面へ遷移するためのAPIも用意されていますが、メインのAPI(stripe-firebase-extensionがやっているような処理)はサーバー側でしか用意されていないので注意してください。
stripe-firebase-extensionを使って購入までの導線はフロントだけで実装することができますが、商品をアプリ画面から登録するのはどうやらできないようなので別途記事を作成しました。商品登録をStripe管理画面から行うのでしたら問題はないのですが、もしお時間がありましたらご覧ください。
改めて実装した内容を振り返ると色々納得できるのですが、0ベースで探っていくと思っていた以上にここまでたどり着くのに時間が掛かったなあという感想です。
Stripe系の記事はサブスクリプションの記事が多かったり、生のStripeの記事が混在していたりと類似のものが多かったので公式のドキュメントを読み込む必要がありました。
この記事でstripe-firebase-extensionを実装したくなりましたら幸いです。
参考URL