くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

StripeのFirebase Extensionsを使ってみた - その2 拡張機能の使い方を見てみる -

前回の続き。

www.memory-lovers.blog

インストールしてサンプルが動かせたけど、
どうなってるかわからないので、もうすこしドキュメント見てみる。

とりあえず、FirebaseコンソールのExtensionsにある
「この拡張機能の動作」を見ていく。

前回までで「Configuring the extension」を見ながら進めたので、
「Using the extension」の続きを確認する。

まとめ

Firestoreの構造はこんな感じ。(コレクション名はデフォルト)

- customers/{uid}          ... 顧客
  - checkout_sessions/{id} ... 顧客のチェックアウトセッション
  - subscriptions/{id}     ... 顧客のサブスク
  - payments/{id}          ... 顧客の一回限り

- products/{id}            ... 商品
  - prices/{id}            ... 価格
- products/tax_rates
  - tax_rates/{id}         ... 税率       

大体の流れ。

1. 決済の流れ
  • 商品の情報はproducts/{id}prices/{id}を取得
  • 決済準備のときに、checkout_sessions/{id}を作成
  • 準備ができたら、Stripe.jsでURLを生成
  • 諸々の設定はcheckout_sessions/{id}作成時にする
2. 購入状態の確認
  • サブスクはsubscriptions/{id}をみる
  • 一回限りはpayments/{id}をみる
  • メタデータを使えば、Firabase Authのカスタムクレームでも可
3. 解約などはカスタマーポータル
  • URLは用意されたFunctionsで生成

「Using the extension」で書かれていること

もろもろの使い方が書いてる感じ。
このあたりを読めば、使い方がわかる。

Sign-up users with Firebase Authentication

Firebase Authenticationを使ったStripeの顧客を作成する方法について。
FirebaseUIライブラリを使うと簡単だよ、とのこと。
拡張機能設定の
「Sync new users to Stripe customers and Cloud Firestore」について書かれてる。

拡張機能設定でSyncにするとAuthのユーザ作成時にStripeの顧客を作成する。
Do not syncにすると最初にStripeのチェックアウトセッション作成時にStripeの顧客を作成する。

List available products and prices

有効な商品や価格の取得方法。

Firestoreでの取得方法が書かれているけど、こんな感じ。

  • products/{productsId}に商品
  • products/{productsId}/prices/{priceId}に価格
  • 商品にはactiveフィールドがあり、有効かどうかのフラグを持つ。
const query = db.collection('products').where('active', '==', true);
const querySnapshot = async query.get();

querySnapshot.forEach(async function (doc) {
  console.log(doc.id, ' => ', doc.data());
  const priceSnap = await doc.ref.collection('prices').get();
  priceSnap.docs.forEach((doc) => {
    console.log(doc.id, ' => ', doc.data());
  });
});

Start a subscription with Stripe Checkout

Stripe Checkoutで決済を開始する方法。

流れとしては、こんな感じ

  • 価格IDを含む、customers/{uid}/checkout_sessionsのDocを作成
  • Fuctionsでセッションを作成し、ドキュメントにsessionIdを保存
  • ドキュメントが更新されたら、sessionIdとStripe.jsを使って、決済ページに移動

Stripe Checkout自体については、Stripeの公式ドキュメントを見ると良い感じ。
Checkout とカスタマーポータルを使用した定期支払い

const docRef = await db
  .collection('customers')
  .doc(currentUser.uid)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });
// Wait for the CheckoutSession to get attached by the extension
docRef.onSnapshot((snap) => {
  const { error, sessionId } = snap.data();
  if (error) {
    // Show an error to your customer and 
    // inspect your Cloud Function logs in the Firebase console.
    alert(`An error occured: ${error.message}`);
  }
  if (sessionId) {
    // We have a session, let's redirect to Checkout
    // Init Stripe
    const stripe = Stripe('pk_test_1234');
    stripe.redirectToCheckout({ sessionId });
  }
});

Importing Stripe.js as ES module

Stripe.jsをES moduleとしてimportする方法。

上の「Start a subscription with Stripe Checkout」の例だと、
こんな感じに書き換えることができる。

import { loadStripe } from '@stripe/stripe-js';

docRef.onSnapshot(async (snap) => {
  const { error, sessionId } = snap.data();
  if (sessionId) {
    const stripe = await loadStripe('pk_test_1234');
    stripe.redirectToCheckout({ sessionId });
  }
});

Handling trials

トライアルについての扱い方。

デフォルトは価格のオプションで設定した「無料トライアル」が適用される。

ただ、再登録などでトライアルを適用したくないユーザ等の場合は、
checkout_sessionsを作成するときにtrial_from_plan: falseを指定すると、
トライアルが適用されなくなる。

const docRef = await db
  .collection('customers')
  .doc(currentUser.uid)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    trial_from_plan: false,
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

Applying discount, coupon, promotion codes

Stripeでは割引クーポンにも対応してる。
顧客向けのプロモーションコードを使用する

決済時にプロモーションコードを入力できるようにするかも、
checkout_sessionsを作成するときに設定する。

allow_promotion_codes: trueを指定すればOK。

const docRef = await db
  .collection('customers')
  .doc(currentUser.uid)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    allow_promotion_codes: true,
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

Applying promotion codes programmatically

プロモーションコードをユーザに入力してもらうのではなく、プログラム側で適用する方法。

これもcheckout_sessionsを作成するときに設定。
promotion_codeにプロモーションコードを指定すればOK。

この方法だと、プロモーションコードにアクセスできる全員が利用できるので注意。
有効期限や利用できる商品/顧客を制限しているかなど確認するといい感じ。

const docRef = await db
  .collection('customers')
  .doc(currentUser.uid)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    allow_promotion_codes: true,
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

Applying tax rates dynamically

動的税率を利用する方法。
Stripeのドキュメントだとこのあたり。
Checkout に税率を追加する | 定期支払いに税を適用する

これもcheckout_sessionsを作成するときに設定。
dynamic_tax_ratesに税率IDを指定する。

const docRef = await db
  .collection("customers")
  .doc(currentUser)
  .collection("checkout_sessions")
  .add({
    line_items: [
      {
        price: "price_1HCUD4HYgolSBA35icTHEXd5",
        quantity: 1,
        dynamic_tax_rates: ["txr_1IJJtvHYgolSBA35ITTBOaew", "txr_1Hlsk0HYgolSBA35rlraUVWO", "txr_1HCshzHYgolSBA35WkPjzOOi"],
      },
    ],
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

Applying static tax rates

固定税率を利用する方法。
Stripeのドキュメントだとこのあたり。
Checkout に税率を追加する | 定期支払いに税を適用する

これもcheckout_sessionsを作成するときに設定。
tax_ratesに税率IDを指定する。最大5つまで。

const docRef = await db
  .collection('customers')
  .doc(currentUser)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'],
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

Collecting a shipping address during checkout

決済時に配送先住所を収集する方法。

これもcheckout_sessionsを作成するときに設定。
collect_shipping_address: trueを指定する。

さらに、この機能を利用するためには、

  1. productsコレクション配下に、shipping_countriesドキュメントを用意して、
  2. allowed_countriesフィールドで国を指定する必要がある

らしい。

サンプルコードには、

const docRef = await db
  .collection("customers")
  .doc(currentUser.uid)
  .collection("checkout_sessions")
  .add({
    collect_shipping_address: true,
    price: "price_1GqIC8HYgolSBA35zoTTN2Zl",
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

と、例が書いてあるが、allowed_countriesフィールドが無い...

Stripeのドキュメントだとこのあたり?
Create a Checkout session with a shipping rate | Add shipping

Setting metadata on the subscription

チェックアウトセッションのメタデータをプログラム側で設定する方法。

メタデータはStripeのDashboardで検索できるので便利らしい。

const docRef = await db
  .collection('customers')
  .doc(currentUser)
  .collection('checkout_sessions')
  .add({
    price: 'price_1GqIC8HYgolSBA35zoTTN2Zl',
    success_url: window.location.origin,
    cancel_url: window.location.origin,
    metadata: {
      item: 'item001',
    },
  });

Adding multiple prices, including one-time setup fees

複数の価格を一度に決済する方法。

サブスク(定期支払)と1回限り(一括)の価格を一緒に使えるらしい。
初回のみのセットアップ費用などで使うっぽい。

checkout_sessionsを作成するときにline_itemsも指定すればOK。

const docRef = await db
    .collection('customers')
    .doc(currentUser)
    .collection('checkout_sessions')
    .add({
      line_items: [
        {
          price: 'price_1HCUD4HYgolSBA35icTHEXd5', // サブスクの価格ID
          quantity: 1,
          tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'],
        },
        {
          price: 'price_1HEtgDHYgolSBA35LMkO3ExX', // 1回限りの価格ID
          quantity: 1,
          tax_rates: ['txr_1HCjzTHYgolSBA35m0e1tJN5'],
        },
      ],
      success_url: window.location.origin,
      cancel_url: window.location.origin,
    });

ただし、複数のサブスク価格を指定する場合は注意が必要

  • 複数のサブスク価格を指定すると、Firestoreですべてが配列に追加される
  • ただ、price属性は、配列の先頭の価格と一緒になる
  • さらに、現在のカスタマーポータルは複数のサブスクには対応してない
  • この場合だとサブスクをキャンセルする機能だけ提供される

なので、複数のサブスクは指定しないほうがよさそう。

Collecting one-time payments without a subscription

1回限りの支払いを利用する方法。

checkout_sessionsを作成するときに1回限りの価格IDを指定するのと合わせて、
mode: "payment"も指定する必要がある。

const docRef = await db
  .collection("customers")
  .doc(currentUser.uid)
  .collection("checkout_sessions")
  .add({
    mode: "payment",
    price: "price_1GqIC8HYgolSBA35zoTTN2Zl", // 1回限りの価格ID
    success_url: window.location.origin,
    cancel_url: window.location.origin,
  });

サブスクの場合はsubscriptionsコレクションに保存されたが、
1回限りの場合はpaymentsコレクションに保存される。
(サブスクと1回限りでは違うコレクションに保存される)

また、Firestoreに同期するために、
Webhookで以下の4つのイベントも送信するように変更が必要

payment_intent.succeeded payment_intent.payment_failed payment_intent.canceled payment_intent.processing

デフォルトがサブスク向けなので、1回限りも使うとなるといろいろ対応が必要そう。

Start a subscription via the Stripe Dashboard or API

v0.1.7からStripeのDashboardAPI経由など、
Stripe Checkout経由で作成されてないサブスクも同期してくれるらしい。
(最新版だとあまり関係ない話。)

同期を有効にするためには、Airebase Authenticationのユーザを
Stripeの顧客&Firebaseの顧客コレクションの同期が必要。

Get the customer’s subscription

サブスクの詳細の保存場所について。

customers/{uid}/subscriptionsに保存されていて、
有効なものはstatusをチェックすればOK。

db.collection('customers')
  .doc(currentUser.uid)
  .collection('subscriptions')
  .where('status', 'in', ['trialing', 'active'])
  .onSnapshot(async (snapshot) => {
    // In this implementation we only expect one active or trialing subscription to exist.
    const doc = snapshot.docs[0];
    console.log(doc.id, ' => ', doc.data());
  });

Redirect to the customer portal

カスタマーポータルのURLを取得する方法。

ext-firestore-stripe-subscriptions-createPortalLinkという関数が
Functionsに追加されているので、それを呼び出せばOK。

const functionRef = firebase
  .app()
  .functions('asia-northeast1')
  .httpsCallable('ext-firestore-stripe-subscriptions-createPortalLink');
const { data } = await functionRef({ returnUrl: window.location.origin });
window.location.assign(data.url);

Delete User Data

ユーザデータの削除について。

拡張機能設定の「Automatically delete Stripe customer objects」を
Auto Deleteにするといいよって書いてある。

Auto Deleteにすると、Firebase Authenticationのユーザが削除されたときに、
Stripe側の顧客オブジェクトも削除され、さらにサブスクもキャンセルになる。

退会時にFirebase Authenticationのユーザを削除すれば、
サブスクも全部解約してくれるので便利。

ただ、Firestoreから顧客ドキュメントを削除されないので、
別の拡張機能(Delete User Data)を使うと便利っぽい。

Monitoring

拡張機能のアクティビティをモニタリングする方法。
Firebaseコンソールで確認できるよう。

Firebaseのドキュメントをみると詳しく書いてあるとのこと。
インストールした Firebase Extensions の管理

まとめ(再掲)

Firestoreの構造はこんな感じ。(コレクション名はデフォルト)

- customers/{uid}          ... 顧客
  - checkout_sessions/{id} ... 顧客のチェックアウトセッション
  - subscriptions/{id}     ... 顧客のサブスク
  - payments/{id}          ... 顧客の一回限り

- products/{id}            ... 商品
  - prices/{id}            ... 価格
- products/tax_rates
  - tax_rates/{id}         ... 税率       

大体の流れ。

1. 決済の流れ
  • 商品の情報はproducts/{id}prices/{id}を取得
  • 決済準備のときに、checkout_sessions/{id}を作成
  • 準備ができたら、Stripe.jsでURLを生成
  • 諸々の設定はcheckout_sessions/{id}作成時にする
2. 購入状態の確認
  • サブスクはsubscriptions/{id}をみる
  • 一回限りはpayments/{id}をみる
  • メタデータを使えば、Firabase Authのカスタムクレームでも可
3. 解約などはカスタマーポータル
  • URLは用意されたFunctionsで生成

こんな感じ。以上!!

なんとなく、雰囲気は掴んできたけど、
ドキュメントの構造やWebhookのイベントがもやもやするので、
次回はそのあたりを見ていく予定(´ω`)

過去のシリーズ