Next.jsで作る静的サイトに、use-shopping-cartを使ってStripe Checkoutを利用したECカート機能を実装する

以前「カート機能を簡単に実装できるフックライブラリ」としてuse-shopping-cartを紹介しました。

https://qiita.com/hideokamoto/items/e7ee06e5084d853b7d7d

今回は、このuse-shopping-cartをNext.jsで作成した静的サイトで利用する方法を紹介します。


# 事前準備

今回紹介する方法では、REST APIなどを使用せず、クライアント側の組み込みのみでCheckoutのセッションを開始します。

そのため、事前にStripeダッシュボードの設定ページで、「クライアント側のみの組み込み」を有効にしましょう。
直接飛ぶ場合: https://dashboard.stripe.com/settings/checkout

**無効化されている状態**

![スクリーンショット 0004-03-10 14.45.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/367b6099-e605-c6b4-441d-d7e70fcae674.png)


**有効になっている状態**
![スクリーンショット 0004-03-10 14.43.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/a086dfc0-008d-634e-3f21-0b46351c6c37.png)



# Next.jsプロジェクトのセットアップ

まずはNext.jsプロジェクトをセットアップしましょう。
今回はuse-shopping-cart組み込みがメインのため、設定はデフォルトのままにしています。

```bash
$ npx create-next-app@latest
npx: 1個のパッケージを1.696秒でインストールしました。
✔ What is your project named? … demo-nextjs-use-shopping-cart
Creating a new Next.js app in /Users/sandbox/demo-nextjs-use-shopping-cart.

```

セットアップが終わったら、プロジェクトのディレクトリへ移動します。

```bash
% cd demo-nextjs-use-shopping-cart
```

また、環境変数でStripeの公開可能キーとシークレットAPIキーを保存しておきましょう。

**env.development**
```diff
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_xxx
STRIPE_SECRET_API_KEY=sk_test_xxx
```

# use-shopping-cartライブラリを追加する

続いてuse-shopping-cartをインストールしましょう。

```bash
% npm i use-shopping-cart stripe
```

# use-shopping-cartのProviderを設定する

use-shopping-cartを利用するには、Providerコンポーネントを配置する必要があります。

Next.jsの場合、`pages/_app.js`を以下のように変更することで、設定できます。

```diff_jsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
+import { CartProvider } from 'use-shopping-cart'

function MyApp({ Component, pageProps }: AppProps) {
-  return <Component {...pageProps} />
+  return (
+    <CartProvider
+      mode={"payment"}
+      cartMode={"client-only"}
+      stripe={process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY}
+      successUrl={'https://example.com/success'}
+      cancelUrl={'https://example.com/cancel'}
+      currency={'jpy'}
+    >
+      <Component {...pageProps} />
+    </CartProvider>
+  )
}

export default MyApp
```

これでセットアップは完了です。


# use-shopping-cartでサイトを実装する

続いて、useShoppingCartフックを利用して、カート操作や注文のボタンを実装しましょう。

## 「今すぐ注文」ボタンを作成する

まずは１つの商品をすぐに注文できるボタンを作りましょう。
今すぐ注文ボタンを作るには、`checkoutSingleItem`を利用します。

```jsx
import * as React from "react"
import { useShoppingCart } from 'use-shopping-cart'

const Home = () => {
  const { checkoutSingleItem } = useShoppingCart()
  return (
    <main>
      <div>
        <button onClick={() => {
          checkoutSingleItem('price_xxxx')
        }}>今すぐ注文</button>
      </div>
  </main>
)

export default Home
```

これで、`今すぐ注文`をクリックするとStripeの注文ページにリダイレクトされるようになります。

## カート機能を実装する

続いて、商品を追加・変更できるカート機能を作りましょう。

### カートに商品を追加する

カートへの追加は、`addItem`を利用します。

```jsx
import * as React from "react"
import { useShoppingCart } from 'use-shopping-cart'

const IndexPage = () => {
  const { addItem } = useShoppingCart()
  return (
    <main>
      <div>
        <button onClick={() => {
          addItem({
            name: 'バナナ',
            price_id: 'price_xxxxx',
            price: 400
          })
        }}>バナナを追加する</button>
        <button onClick={() => {
          addItem({
            name: 'パン',
            price_id: 'price_xxxxx',
            price: 400
          })
        }}>パンを追加する</button>
      </div>
  </main>
)

export default IndexPage
```

これで、use-shopping-cart内部にあるstoreに商品のデータを追加できるようになりm最多。

### カートの中身を表示する

追加したカートの中身をみるには、`cartDetails`を利用します。
また、`formattedTotalPrice`を利用することで、合計金額も取得できます。

```jsx

import * as React from "react"
import { useShoppingCart } from 'use-shopping-cart'

const IndexPage = () => {
  const { formattedTotalPrice, cartDetails } = useShoppingCart()
  return (
    <main>
      <div>
        <ul>
          {Object.values(cartDetails).map((cart: any) => {
            return (
              <li key={cart.id}>
                {cart.name} - {cart.formattedPrice} * {cart.quantity} = {cart.formattedValue}
              </li>
            )
          })}
          <li>合計: {formattedTotalPrice}</li>
        </ul>
      </div>
  </main>
)

export default IndexPage
```

これで、カートの中身をユーザーに表示できるようになりました。

![スクリーンショット 0004-03-10 15.09.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/5b285167-94fd-7525-477c-dfa5b59bd140.png)

### カートの商品数を変更できるようにする

カートの数量を変更する関数もフックに用意されています。

```diff_jsx

import * as React from "react"
import { useShoppingCart } from 'use-shopping-cart'

const IndexPage = () => {
-  const { formattedTotalPrice, cartDetails } = useShoppingCart()
+  const { formattedTotalPrice, cartDetails, decrementItem, incrementItem, removeItem } = useShoppingCart()
  return (
    <main>
      <div>
        <ul>
          {Object.values(cartDetails).map((cart: any) => {
            return (
              <li key={cart.id}>
                {cart.name} - {cart.formattedPrice} * {cart.quantity} = {cart.formattedValue}
+                <button onClick={() => decrementItem(cart.price_id)}>１つ減らす</button>
+                <button onClick={() => incrementItem(cart.price_id)}>１つ増やす</button>
+                <button onClick={() => removeItem(cart.price_id)}>削除</button>
              </li>
            )
          })}
          <li>合計: {formattedTotalPrice}</li>
        </ul>
      </div>
  </main>
)

export default IndexPage
```

これらを組み合わせることで、とても簡素ですが通販に欠かせないカートシステムを実装できました。

![スクリーンショット 0004-03-10 15.13.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/6fe2bee8-25fe-e51b-6960-d2e516ae2b39.png)


### 注文ページ(Stripe Checkout)へ移動する

最後にStripe Checkoutへのリダイレクトするボタンを追加しましょう。

注文ページへのリンクは`redirectToCheckout`を利用します。

 
```diff_jsx

import * as React from "react"
import { useShoppingCart } from 'use-shopping-cart'

const IndexPage = () => {
-  const { formattedTotalPrice, cartDetails, decrementItem, incrementItem, removeItem } = useShoppingCart()
+  const { formattedTotalPrice, cartDetails, decrementItem, incrementItem, removeItem, redirectToCheckout } = useShoppingCart()
  return (
    <main>
      <div>
        <ul>
          {Object.values(cartDetails).map((cart: any) => {
            return (
              <li key={cart.id}>
                {cart.name} - {cart.formattedPrice} * {cart.quantity} = {cart.formattedValue}
+                <button onClick={() => decrementItem(cart.price_id)}>１つ減らす</button>
+                <button onClick={() => incrementItem(cart.price_id)}>１つ増やす</button>
+                <button onClick={() => removeItem(cart.price_id)}>削除</button>
              </li>
            )
          })}
          <li>合計: {formattedTotalPrice}</li>
        </ul>
        <button onClick={() => redirectToCheckout()}>注文する</button>
      </div>
  </main>
)

export default IndexPage
```

カート内の商品情報などは、use-shopping-cart側が内部的に処理してくれるため、とてもシンプルな実装で対応できます。

# Next.jsでStripeの料金データを取り込む

ここまでは、料金・商品データをベタ打ちしていました。

Next.jsのSSGモードでサイトを構築している場合、`getStaticProps`関数内でStripe APIを呼び出すことで、料金データを自動で取り込みできるようになります。

```js
import Stripe from 'stripe'

export const getStaticProps = async () => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_API_KEY, {
    apiVersion: '2020-08-27'
  })
  const prices = await stripe.prices.list()
  return {
    props: {
      prices
    }
  }
}
```

また、use-shopping-cartの`addItem`で使える形にするには、以下のような整形を行いましょう。

```diff_javascript

export const getStaticProps = async () => {
  const stripe = new Stripe(process.env.STRIPE_SECRET_API_KEY, {
    apiVersion: '2020-08-27'
  })
  const prices = await stripe.prices.list()
  return {
    props: {
-      prices
+      prices: prices.data.map(price => {
+        if (!price.active) return null;
+        return {
+          price_id: price.id,
+          name: price.nickname,
+          price: price.unit_amount,
+          currency: price.currency,
+        }
+      })
    }
  }
}
```

# ビルド時の注意

`next build`を実行する場合、環境変数を`.env.local`や`.env.development`のみの保存しているとエラーになります。

> Error: You did not provide an API key. You need to provide your API key in the Authorization header, using Bearer auth (e.g. 'Authorization: Bearer YOUR_SECRET_KEY'). See https://stripe.com/docs/api#authentication for details, or we can help at https://support.stripe.com/.

これは、ビルド時に読まれる環境変数ファイルが異なるためですので、[Next.jsのドキュメント](https://nextjs.org/docs/basic-features/environment-variables#environment-variable-load-order)を参考にファイルを配置するようにしましょう。

# 終わりに

サーバー側の処理でCheckoutセッションを開始していないため、機能に制限があることに注意が必要です。
ですが、Next.jsのSSGモードのみで完結するサイトにEC機能を組み込みたい場合、use-shopping-cartとの組み合わせがとても便利です。

Stripeを使ったオンライン決済の第一歩として、ぜひお試しください。

# [PR] Stripe開発者向け情報をQiitaにて配信中！

- [Stripe Updates]：開発者向けStripeアップデート紹介・解説
- ユースケース別のStripe製品や実装サンプルの紹介
- Stripeと外部サービス・OSSとの連携方法やTipsの紹介
- 初心者向けのチュートリアル(予定)

など、Stripeを利用してオンラインビジネスを始める方法について週に２〜３本ペースで更新中です。

-> [Stripe Organizationsをフォローして最新情報をQiitaで受け取る](https://qiita.com/organizations/stripe)