🥦

【個人開発者向け】Stripeサブスク実装を余計なサーバー代かけずに実装する方法

2023/01/05に公開

個人開発をしている厚木です。
普段、Firebase hosting×firestore で「お金をかけずに」サービス公開しているのですが、決済だけはセキュリティを気にしてバックエンドを作っていました。

たまたま、Firestore と連携して、バックエンド不要でいい感じに実装してくれるという Firebase Extentions 「Run Payments with Stripe」について紹介したいと思います

必要なもの

Firebase
https://firebase.google.com/?hl=ja
nextjs
https://nextjs.org/
Stripe
https://stripe.com/jp

やりたいこと

↓以下のような、(作成中の)サービスでバックエンドなしで決済を実装しました

  • 公開型、エンジニアのキャリア相談Q&Aサイト(作成中)
    • 自分のキャリアの悩みを投稿して、誰かがドバイスをくれるサービス
    • 相談は無料で投稿・閲覧・アドバイス可能
      • アドバイスの閲覧は月額有料会員のみ可能

画面上の流れ

質問は誰でも閲覧可能
image

回答は月額有料会員のみ閲覧可能
image

「有料会員になって読む」ボタンを押すと、Stripeの課金ページに遷移する
image

月額有料会員になると、コンテンツの閲覧が可能になる
image

参考記事

ほとんどこの記事を参考にしてます(バージョンのせいで動きません)
https://qiita.com/mogmet/items/cddc182156028c928ecf

「Run Payments with Stripe」 について

Firebase Extentions の1つで、Firebase と Stripe を繋げてくれるものです。
この Extensions によって、バックエンドなしでサービスに決済を仕込むことができます。

以下のようなことをしてくれます。

  • Stripe のユーザーと Firebase Authentication のユーザーと自動で紐付け
  • Stripe の決済情報を Webhook で Firestore に保存

今回はこれを使って、フロントから Stripe のサブスク決済をして、サブスクユーザーのみ有料コンテンツを見れるみたいな実装の手順を紹介します。

Firebaseプロジェクトに「Run Payments with Stripe」を追加する

「Run Payments with Stripe」をプロジェクトにインストールしてください。

手順の1~3番は指示に従ってポチポチしていくだけでOKです。
※Firebase 料金プランを従量課金の Blaze に変更する必要があります。

image

手順の4番の「Stripe API key with restricted access」 は Stripe の API key を設定します。
Stripe の API KEY は制限付きで発行した方がセキュリティ的に良いです。必要な権限は 「Customers」「Checkout Sessions」「Customer portal」 の書き込み権限と 「Subscriptions」 の読み取りです。

image

発行した API KEY を「Stripe API key with restricted access」に入力して、「シークレットを作成」を押してください。

他はデフォルトのままで大丈夫ですが、入力されている値が Firestore の Collection に保存されるので、気になる方は変更してください。

更新完了まで5分くらいかかります。
image

Firestoreのセキュリティルール修正

「Run Payments with Stripe」では、 Firestore に情報の書き込みを行うので、セキュリティルールの追加が必要です。

追加するセキュリティルールは「Firebase Extensions > Run Payments with Stripe > この拡張機能の動作」の中段くらいにあるので、それを適用してください。
※設定内容によって生成されるルールが異なる場合があるので、ご自身のプロジェクトで確認してください。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /customers/{uid} {
      allow read: if request.auth.uid == uid;

      match /checkout_sessions/{id} {
        allow read, write: if request.auth.uid == uid;
      }
      match /subscriptions/{id} {
        allow read: if request.auth.uid == uid;
      }
      match /payments/{id} {
        allow read: if request.auth.uid == uid;
      }
    }

    match /products/{id} {
      allow read: if true;

      match /prices/{id} {
        allow read: if true;
      }

      match /tax_rates/{id} {
        allow read: if true;
      }
    }
  }
}

この情報を Firestoreのセキュリティルールに追加します。
image

Stripe Webhookに設定追加

次に Stripe の Webhooks 画面を開き、「エンドポイントを追加」を押下します。

image

エンドポイント URLに 「Firebase Extensions > Run Payments with Stripe > この拡張機能の動作」の「Configure Stripe webhooks 」の 2番に書いてある URL を貼り付けます。
image

続いて、「リッスンするイベントの選択」で「Firebase Extensions > Run Payments with Stripe > この拡張機能の動作」の「Configure Stripe webhooks 」の 3番に書いてあるイベントを全て入力します。
image

「イベントを追加」を押して完了するとWebhookが追加されます。
Extention と Webhook を接続するために署名シークレットが必要になるので、「表示」を押して取得します。
image

Webhook の Secret を Extention に設定する

Extentions の「拡張機能を再構成」から、先ほど取得した署名シークレットを設定します。
image

「Stripe webhook secret」に署名シークレット貼り付けて、保存してください。
image

※設定の更新に5分くらいかかります。

これによって、Stripe で商品作成や、誰かが決済を行った際に Extention を介して Firestore に保存されるようになります。

Stripe で商品を作成

ユーザーに購入してほしいサブスク商品を作成します。
サブスクで販売したいので、継続を選択してください。

image

商品作成後と、Firestore の Products にも商品が連携されているはずです。

image

もし作成されていない場合は、Stripe の Webhook からログ確認可能なので、エラー内容を確認してください。
image

カスタマーポータルの有効化

Stripeのカスタマーポータル有効化をしておきましょう。
サブスク解約は実装せず、こちらのリンクを提供するだけで済むようになります。

image

フロント実装

Next との繋ぎこみを行います。
Firestore に必要な Stripe の情報は全て連携されるようになっているので、Firestore と繋ぎこむだけでOKです。

※フロントの実装は雑です。すみません。

購入ページへのリンク生成

Firestore に Checkout Session を作成します。
collection(customers/${uid}/checkout_sessions)に指定フォーマットで情報を書き込むと、Webhookで、Stripe側で処理を行なって、ドキュメントに処理結果が書き込まれます。
まずは、Checkout Session のドキュメントを Firestore に作成します。

※商品が1つという前提で実装しています。

export async function createCheckoutSession({
  userId,
}: {
  userId: string;
}): Promise<string> {
  const db = getFirestore(app);
  // product id を取得
  const products = await getDocs(
    query(collection(db, "products"), where("active", "==", true), limit(1))
  );
  let productId = "";
  products.forEach((doc) => {
    productId = doc.id;
  });
  // products の price を取得
  const prices = await getDocs(
    query(
      collection(db, `products/${productId}/prices`),
      where("active", "==", true),
      limit(1)
    )
  );
  let priceId = "";
  prices.forEach((doc) => {
    priceId = doc.id;
  });

  const checkoutSession = {
    automatic_tax: true,
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: window.location.origin + "/paid", // 決済成功時にリダイレクトする URL
  };

  const collectionRef = collection(db, `customers/${userId}/checkout_sessions`);
  const res = await addDoc(collectionRef, checkoutSession);
  return res.id;
}

image

商品取得URL

Document 作成後、Stripe 側で処理が成功すると Document に商品購入 URL が登録されます。
その URL を取得&ユーザーを対象リンク先に飛ばすと、購入ページが開きます。

export async function getPaymentUrl({
  checkoutSessionsId,
  userId,
}: {
  checkoutSessionsId: string;
  userId: string;
}): Promise<string> {
  const db = getFirestore(app);
  const docRef = doc(
    db,
    `customers/${userId}/checkout_sessions`,
    checkoutSessionsId
  );
  const docSnap = await getDoc(docRef);
  const rslt = docSnap.data();
  return rslt?.url;
}

image

課金ユーザーチェック

対象ユーザーが月額課金ユーザーであれば、customers/${userId}/subscriptionsの Status が active になるようです。初期表示時に取得して出し分けができそうです。

export async function isMembershipUser({
  userId,
}: {
  userId: string;
}): Promise<boolean> {
  const db = getFirestore(app);

  const docs = await getDocs(
    query(
      collection(db, `customers/${userId}/subscriptions`),
      where("status", "in", ["trialing", "active"]),
      limit(1)
    )
  );
  return docs.size > 0 ? true : false;
}

まとめ

Firebase すごい便利ですよね〜。
手順は多いですが、決済も簡単に実装できる。。。。

【宣伝】
「SKILL SET」という、公開型エンジニアのキャリア相談Q&Aサイトを開発しており、近日中にリリース予定です!
Tiwtterでお知らせするので、興味ある方はフォローしていただけると嬉しいです。
https://twitter.com/takuya_web3

GitHubで編集を提案

Discussion