くらげになりたい。

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

StripeのFirebase Extensionsを使ってみた - その4 Nuxtアプリで試してみる編 -

前回の続き。

www.memory-lovers.blog

とりあえず、なんとなくの動きがわかったので、
もう少しコードを書いて試してみる。

試してみたサンプルのソースコードはこちら。
memory-lovers/example-stripe-extensions

サンプルでできること

簡単なサンプルなので、必要最低限のみ。

  • ログインして
  • プランを表示して、
  • 購入して、
  • 購入した情報を表示

サンプルはこんな感じ。

前準備

まずは、FirebaseとStripeが必要なので、
第一回を見ながら、以下を進める。

  • Firebaseプロジェクトの作成
  • Stripe Extensionの有効化
  • Stripeに商品/価格を登録し、カスタマーポータルを設定

https://www.memory-lovers.blog/entry/2021/06/07/050000

その後は、チェックアウトして、
nuxt.config.tsに設定情報を書く。

const config: NuxtConfig = {
  env: {
    // Firebase Config
    API_KEY: "...",
    AUTH_DOMAIN: "...",
    PROJECT_ID: "...",
    STORAGE_BUCKET: "...",
    MESSAGING_SENDER_ID: "...",
    APP_ID: "...",

    // Stripe Public API Key
    STRIPE_API_KEY: "..."
  }
};

最後に、起動すればOK

$ npm install
$ npm run dev
# => http://localhost:3000

ログイン画面(page/index.vue)

ログイン画面はこんな感じ。

画像

ログインするだけ。

マイページ画面(page/mypage.vue)

ログインユーザの情報を表示する画面。
Authのカスタムクレームや有効なサブスクの情報を表示

画像

プラン画面

登録した商品と価格の一覧を表示する画面。
購入もここから。

画像

ハマったポイント

実際に動かしてみてハマったポイントがいくつか。。

拡張機能では購入のみで、サブスクの2重購入もできてしまう

なにか購入したあとに、変更とかキャンセルとかどうするのかな?
と思ったら、カスタマーポータル経由で行ってもらうよう。

拡張機能のソースを見ても購入以外にないっぽい。

さらに、すでにサブスクを購入しているかなどの制限もないため、
定期支払の商品をいくつも買えてしまう

が、カスタマーポータルは定期支払は1つのみしか対応していないので、
ユーザ自身がキャンセルできなくなってしまう。。

なので、一度でも購入していたら、カスタマーポータルで対応してもらうのが良さそう。

カスタムクレームの反映には強制更新が必要

カスタムクレームで購入しているプランがわかるかとおもったけど、
getIdTokenResult(true)で強制更新ないと反映されないよう。

Firebaseのドキュメントにも書いてあった...
カスタム クレームとセキュリティ ルールによるアクセスの制御  |  Firebase

  • カスタム クレームの変更後に、ユーザーがログインまたは再認証する。その結果として発行されたIDトークンには最新のクレームが含まれる。
  • 古いトークンが期限切れになると、既存のユーザー セッションでそのIDトークンが更新される。
  • currentUser.getIdToken(true)を呼び出して IDトークンが強制的に更新される。

拡張機能が用意する各ドキュメントにはIDがない

Firestoreを使うときにはドキュメントだけでパスが分かるように、
IDを持たせていたけど、ドキュメントIDは各ドキュメントに含まれてない。

Stripeの商品IDや価格IDもドキュメントIDになっているため、
DocumentReferenceなどからidを取得しておく必要がある。

購入しているサブスクの商品名などは別途取得が必要

ログインユーザの定期支払は、/customers/{uid}/subscriptions/{id}を見ればいいけど、
商品や価格はDocumentReferenceが設定されている。

単体だと買っているかどうかはわかるけど、商品名などは、
別途、subscription.prodcut.get()などで取得しないといけない。

// /customers/{uid}/subscriptions/{id}
export interface Subscription {
  product: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>;
  price: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData>;
}

型定義はstripe-nodeのを使わないといけない...

第三回で整理した型定義を使おうと思ったら、
stripe-jsだけではだめで、stripe-nodeが必要に..

www.memory-lovers.blog

stripeInterface.tsは、stripe-firebase-extensions/firestore-stripe-subscriptionsを利用してる。

import Stripe from "stripe";
// stripe-firebase-extensionsのinterces.tsをコピー
import interfaces from "./stripeInterface";

// /customers/{uid}/
export type CustomerDoc = {
  email: string;
  stripeId: string;
  stripeLink: string;
};

// /customers/{uid}/checkout_sessions/{id}
export type CheckoutSessionDoc = {
  mode?: "subscription" | "payment" | string; // default: 'subscription'
  price?: string;
  success_url: string;
  cancel_url: string;
  quantity?: number; // default: 1
  payment_method_types?: Stripe.Checkout.SessionCreateParams.PaymentMethodType[]; // default: ['card']
  metadata?: Stripe.Checkout.SessionCreateParams.SubscriptionData; // default: {}
  tax_rates?: Array<string>; // default: []
  allow_promotion_codes?: boolean; // default: false
  trial_from_plan?: boolean; // default: true
  line_items?: Stripe.Checkout.SessionCreateParams.LineItem[];
  billing_address_collection?: Stripe.Checkout.SessionCreateParams.BillingAddressCollection; // default: 'required',
  collect_shipping_address?: boolean; // default: false,
  locale?: Stripe.Checkout.SessionCreateParams.Locale; // default: 'auto',
  promotion_code?: string;
  client_reference_id?: string;
  sessionId?: string;
  error?: {
    message: string;
  };
};

// /customers/{uid}/subscriptions/{id}
export type SubscriptionDoc = interfaces.Subscription;

// /customers/{uid}/payments/{id}
export type PaymentDoc = Stripe.PaymentIntent;

// /customers/{uid}/subscriptions/{id}/invoices/{id}
// /customers/{uid}/payments/{id}/invoices/{id}
export type InvoiceDoc = Stripe.Invoice;

// /products/{id}
export type ProductDoc = interfaces.Product;

// /products/{id}/prices/{id}
export type PriceDoc = interfaces.Price;

// /products/tax_rates/tax_rates/{id}
export type TaxRateDoc = interfaces.TaxRate;

とはいえ、ハマるところも少ない気がする(´ω`)
Stripeすごい(´ω`)

以上!!

Stripe試してみたシリーズ