Stripeの商品・プラン・価格をGatsbyで取得する

この記事は、「JP_Stripes Advent Calendar 2020」および「Jamstackその2 Advent Calendar 2020」2日目です。

SaaSなどでは、提供しているサービスの価格やプランごとの機能情報などをStripeで一元管理している場合があります。そしてその場合、可能ならばwebsiteで表示する内容についてもStripeのデータを使えるのが理想的です。

Gatsbyを使った場合、source puginという仕組みを使うことでこれが実現できます。

制限キーを取得する

StripeのAPIを利用する場合、sk_test~などからはじまるシークレットキーを利用することが一般的です。しかし、今回のようなケースに使うには、アクセスできるリソースの量があまりにも多く、キーの漏洩リスクなどを考えるとあまりお勧めできません。

利用する用途が限られている場合は、「制限キー」を都度発行し、そちらを利用することが安全です。

Sometimes, we use Stripe to manage the product/plan data master. And we want to show these product/plan data on our own website without posting to another CMS or data source.

When using Gatsby, we can get it by using Source plugin

Prepare: Get a limited API Key

We can get the Stripe data by using the Stripe API key, but the basic API Secret has huge permissions.
In this case, we just read product and plan or price data from API, we don’t have to get any other permission.
So, I recommend creating a Limited Key on the Stripe Dashboard.

今回の場合は、ProductやPlan、Priceなどのリソースの「読み取り」だけを許可します。また、PaymentIntentsの「読み取り」についても許可しましょう。この権限を「なし」にしていると、Stripeからのデータ取得時にエラーが発生しました。

gatbsy-source-stripeプラグインをインストール・設定する

Stripeのデータ取得はGatsbyのプラグインを利用します。

$ npm i -D gatsby-source-stripe

gatsby-config.jsに先ほど発行したキーの情報や、取得したいリソースを定義してやります。

module.exports = {
  plugins: [
    `gatsby-plugin-typescript`,
    {
      resolve: `gatsby-source-stripe`,
      options: {
        objects: ['Product', 'Plan'],
        secretKey: process.env.STRIPE_API_SECRET,
      }
    },
...

上の例では、商品とプランのデータのみ取得するようにしています。

GraphQLのクエリ・スキーマをチェックする

gatsby develop コマンドを実行すると、GatsbyがStripeからデータを取得してくれます。

取得されたデータの確認やクエリのテストは、GraphiQLコンソールを使いましょう。これはhttp://localhost:8000/__graphql のようなURLでアクセスできます。

あとはGraphiQLで取れるデータやクエリの確認を行い、そのクエリを利用してGatsbyのページやコンテンツを実装していくだけです。

商品に関連づけられたプランを取得する

先程のプラグインでは、基本的にそのリソースの全データにアクセスできます。

特定のIDの商品のみ取得したい場合や、商品に関連づいているプランをまとめて取得したい場合には、gatsby-node.jsにStripe SDKを利用したコードを書いてやりましょう。

import { SourceNodesArgs } from "gatsby";
import Stripe from "stripe";

const nodeType = 'CustomProductWithPlans'
export const sourceNodes = async (args: SourceNodesArgs): Promise<void> => {
  const {
    createNodeId,
    actions: {
      createNode,
    },
    createContentDigest,
  } = args
  const stripe = new Stripe(process.env.STRIPE_API_SECRET, {
    apiVersion: '2020-08-27'
  })
  const publishableProductIds = [
    'prod_A',
    'prod_B',
  ]
  const products = await Promise.all(publishableProductIds.map(async productId => {
    try {
      const product = await stripe.products.retrieve(productId)
      return product
    } catch (e) {
      if (e.code === 'resource_missing' && /prod/.test(productId)) {
        return null;
      }
      console.log(e)
      throw e
    }
  })).then(data => data.filter(Boolean))

  const datasets = await Promise.all(products.map(async (product) => {
      if (!product) return null;
    const {data: plans } = await stripe.plans.list({
      product: typeof product === 'string' ? product : product.id,
      active: true
    })
    return {
      ...product,
      plans
    }
  }))
  datasets.forEach(product => {
      if (!product) return;
    const data = {
      ...product,
      id: createNodeId(`${nodeType}${product.id}`),
      productId: product.id,
    }
    createNode({
      ...data,
      internal: {
        type: nodeType,
        contentDigest: createContentDigest(data)
      }
    })
  })
  return undefined;
}

Comment