カートに入れました

サーバー不要Gatsbyプロジェクトで商品を売る方法。stripeの決済機能実装。

環境

  • macOS Big Sur 11.5.2
  • nodejs v16.14.0
  • yarn 1.22.17

事前準備

  • gatsbyプロジェクト作成
  • stripeのアカウント登録
// gatsbyコマンドを使えるようにする
yarn global add gatsby-cli
// gatsby-starter-netlify-cmsというフォルダにgatsbyプロジェクトをクローンする
gatsby new gatsby-starter-netlify-cms https://github.com/netlify-templates/gatsby-starter-netlify-cms

cd gatsby-starter-netlify-cms
// localhost:8000で確認できるようになる
yarn start

stripeアカウントを登録し

https://dashboard.stripe.com/test/dashboardの右下にある

  • 公開可能キー
  • シークレットキー

をgatsby-starter-netlify-cms直下に.env.developmentをいうファイルを作り書き込む

.env.development
GATSBY_STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_SECRET_KEY="sk_test_..."

クライアント側のみの組み込みを有効にしておく

ドメインを入力、保存。最初の商品を作成します

商品情報を入力し、商品を保存をクリック

stripe内に商品が出来ました

  • 次はプロジェクトにstripeと通信するために必要なパッケージを追加
yarn add @stripe/stripe-js gatsby-source-stripe

ここでyarn startでプロジェクトを立ち上げてgraphqlを確認してみます

コード追加をしていくと一番下にstripePriceと真ん中辺りにallStripePriceいう項目が表示されるようになる。ここに先ほどstripe側で作った商品の情報が格納される場所になる

コード追加

gatsby-config.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
  siteMetadata: {
    ...
  },
  plugins: [
    `gatsby-plugin-react-helmet`,
    {
      resolve: `gatsby-source-stripe`,
      options: {
        objects: ["Price"],
        secretKey: process.env.STRIPE_SECRET_KEY,
        downloadFiles: false,
      },
    },
    ...

srcフォルダにutilsフォルダを作る

src/utils/stripe.js
import { loadStripe } from '@stripe/stripe-js'

let stripePromise
// stripeのアカウント情報を取得する
const getStripe = () => {
  if (!stripePromise) {
    stripePromise = loadStripe(process.env.GATSBY_STRIPE_PUBLISHABLE_KEY)
  }
  return stripePromise
}

export default getStripe

商品一覧表示のコンポーネント

src/component/products/Products.js
import React from "react"
import { graphql, useStaticQuery } from "gatsby"
import ProductCard from "./ProductCard"

const containerStyles = {
  display: "flex",
  flexDirection: "row",
  flexWrap: "wrap",
  justifyContent: "space-between",
  padding: "1rem 0 1rem 0",
}

const Products = () => {
  const { prices } = useStaticQuery(
    graphql`
    query ProductPrices {
      prices: allStripePrice(
        filter: { active: { eq: true } }
        sort: { fields: [unit_amount] }
      ) {
        edges {
          node {
            id
            active
            currency
            unit_amount
            product {
              id
              name
            }
          }
        }
      }
    }
    `
  )

  // Group prices by product
  const products = {}
  for (const { node: price } of prices.edges) {
    const product = price.product
    if (!products[product.id]) {
      products[product.id] = product
      products[product.id].prices = []
    }
    products[product.id].prices.push(price)
  }

  return (
    <div style={containerStyles}>
      {Object.keys(products).map(key => (
        <ProductCard key={products[key].id} product={products[key]} />
      ))}
    </div>
  )
}

export default Products

商品表示のコンポーネント(購入ボタンを押すと決済画面になる)

src/component/products/ProductCard.js
import React, { useState } from "react"
import getStripe from "../../utils/stripe"

const cardStyles = {
  display: "flex",
  flexDirection: "column",
  justifyContent: "space-around",
  alignItems: "flex-start",
  padding: "1rem",
  marginBottom: "1rem",
  boxShadow: "5px 5px 25px 0 rgba(46,61,73,.2)",
  backgroundColor: "#fff",
  borderRadius: "6px",
  maxWidth: "300px",
}
const buttonStyles = {
  display: "block",
  fontSize: "13px",
  textAlign: "center",
  color: "#000",
  padding: "12px",
  boxShadow: "2px 5px 10px rgba(0,0,0,.1)",
  backgroundColor: "rgb(255, 178, 56)",
  borderRadius: "6px",
  letterSpacing: "1.5px",
}

const buttonDisabledStyles = {
  opacity: "0.5",
  cursor: "not-allowed",
}

const formatPrice = (amount, currency) => {
  // let price = (amount / 100).toFixed(2)
  let price = amount
  let numberFormat = new Intl.NumberFormat(["ja-JP"], {
    style: "currency",
    currency: currency,
    currencyDisplay: "symbol",
  })
  return numberFormat.format(price)
}

const ProductCard = ({ product }) => {
  const [loading, setLoading] = useState(false)

  const handleSubmit = async event => {
    event.preventDefault()
    setLoading(true)

    const price = new FormData(event.target).get("priceSelect")
    const stripe = await getStripe()
    const { error } = await stripe.redirectToCheckout({
      mode: "payment",
      lineItems: [{ price, quantity: 1 }],
      successUrl: `${window.location.origin}/checkout-success/`,
      cancelUrl: `${window.location.origin}/`,
    })

    if (error) {
      console.warn("Error:", error)
      setLoading(false)
    }
  }

  return (
    <div style={cardStyles}>
      <form onSubmit={handleSubmit}>
        <fieldset style={{ border: "none" }}>
          <legend>
            <h4>{product.name}</h4>
          </legend>
          <label>
            Price{" "}
            <select name="priceSelect">
              {product.prices.map(price => (
                <option key={price.id} value={price.id}>
                  {formatPrice(price.unit_amount, price.currency)}
                </option>
              ))}
            </select>
          </label>
        </fieldset>
        <button
          disabled={loading}
          style={
            loading
              ? { ...buttonStyles, ...buttonDisabledStyles }
              : buttonStyles
          }
        >
          購入する
        </button>
      </form>
    </div>
  )
}

export default ProductCard

購入完了時のリダイレクト先ページ作成

src/pages/checkout-success.js
import React from "react"
import { Link } from "gatsby"

import Layout from "../components/Layout"

const CheckoutSuccessPage = () => (
  <Layout>
    <h1>Success!</h1>
    <Link to="/products">Shop again</Link>
  </Layout>
)

export default CheckoutSuccessPage

src/templates/index-page.jsの適当な場所にProductsコンポーネントを挿入する

src/templates/index-page.js
import React from "react";
import PropTypes from "prop-types";
import { Link, graphql } from "gatsby";
import { getImage } from "gatsby-plugin-image";

import Layout from "../components/Layout";
import Features from "../components/Features";
import BlogRoll from "../components/BlogRoll";
import FullWidthImage from "../components/FullWidthImage";
/* ↓↓↓↓ */
import Products from "../components/products/Products"
/* ↑↑↑↑ */

// eslint-disable-next-line
export const IndexPageTemplate = ({
  image,
  title,
  heading,
  subheading,
  mainpitch,
  description,
  intro,
}) => {
  const heroImage = getImage(image) || image;

  return (
    <div>
      <FullWidthImage img={heroImage} title={title} subheading={subheading} />
      <section className="section section--gradient">
        <div className="container">
          <div className="section">
            <div className="columns">
              <div className="column is-10 is-offset-1">
                <div className="content">
                  <div className="content">
                    <div className="tile">
                      <h1 className="title">{mainpitch.title}</h1>
                    </div>
                    <div className="tile">
                      <h3 className="subtitle">{mainpitch.description}</h3>
                    </div>
                  </div>
                  <div className="columns">
                    <div className="column is-12">
                      <h3 className="has-text-weight-semibold is-size-2">
                        {heading}
                      </h3>
                      <p>{description}</p>
                    </div>
                  </div>
                  /* ↓↓↓↓ */
                  <Products />
                  /* ↑↑↑↑ */
                  <Features gridItems={intro.blurbs} />
                  <div className="columns">
                    <div className="column is-12 has-text-centered">
                      <Link className="btn" to="/products">
                        See all products
                      </Link>
                    </div>
                  </div>
                  <div className="column is-12">
                    <h3 className="has-text-weight-semibold is-size-2">
                      Latest stories
                    </h3>
                    <BlogRoll />

サイト確認

yarn start

でプロジェクトを立ち上げる

商品を表示でき購入ボタンを押すと決済ページに飛ぶ

情報を入力し支払うボタンを押す (テスト時のカード情報について #Stripe でテストのクレジットカードを追加する時の番号は? Visa なら 4242 4242 4242 4242

購入後の画面に遷移し、stripeのダッシュボードにも支払い履歴が反映されました


https://hi1t0.com/products

よかったらご購入よろしくお願いします!