Gatsbyで構築したサイトに、Stripeの決済フォーム（Payment Elements）を追加する方法


https://qiita.com/hideokamoto/items/4ffee50ed02a96c8e80c#comment-6d2a00b7277c04934155
## Gatsbyプロジェクトのセットアップ
まずはGatsbyプロジェクトをセットアップしましょう。
今回はStripeの組み込みがメインのため、設定はデフォルトのままにしています。

```bash
$ npm init gatsby
npx: 3個のパッケージを2.389秒でインストールしました。
create-gatsby version 2.17.0



                     Welcome to Gatsby!



This command will generate a new Gatsby site for you in
/Users/hideokamoto/stripe/examples with the setup you
select. Let's answer some questions:


What would you like to call your site?
✔ · My Gatsby Site
What would you like to name the folder where your site will be created?
✔ examples/ stripe-element-gatsby
✔ Will you be using JavaScript or TypeScript?
· TypeScript
✔ Will you be using a CMS?
· No (or I'll add it later)
✔ Would you like to install a styling system?
· No (or I'll add it later)
✔ Would you like to install additional features with other plugins?No items were selected



Thanks! Here's what we'll now do:

    🛠  Create a new Gatsby site in the folder stripe-element-gatsby
  
✔ Shall we do this? (Y/n) · Yes
✔ Created site from template
✔ Installed Gatsby
✔ Installed plugins
✔ Created site in stripe-element-gatsby
🎉  Your new Gatsby site  has been successfully created
at /Users/stripe/examples/stripe-element-gatsby.
Start by going to the directory with

  cd stripe-element-gatsby
  
Start the local development server with

  npm run develop
  
See all commands at

  https://www.gatsbyjs.com/docs/gatsby-cli/
  
```

## Stripe.jsライブラリのインストール

Payment Elementsを導入するために必要なライブラリを追加しましょう。

```bash
$ npm install @stripe/stripe-js @stripe/react-stripe-js
```

## Publishable APIキーを環境変数経由で設定

続いてPayment Elementsを利用するために必要な「公開可能APIキー（Publishable APIキー）」を追加します。

テスト環境用に、`.env.development`ファイルを作成しましょう。

```
GATSBY_STRIPE_PUBLIC_KEY=pk_test_xxx
GATSBY_API_URL=http://localhost:3000
```

Gatbyでは、`GATSBY_`からはじまる環境変数は、フロントエンドのコードからも取得できます。

## 決済フォームを表示したい場所に、Payment Intent作成ボタンを実装

Payment Elementsなど、Stripe.js SDKが提供する要素を表示するためには、Payment IntentsまたはSetup IntentsおよびOrder API(v2)の`client_secret`が必要です。

これはサーバー側の処理で作成しますので、別途以下のようなAPIを用意しましょう。

### Express.jsで簡単に実装したサンプル

```js
if (process.env.NODE_ENV !== 'production') {
    require('dotenv').config({
        path: '.env.development'
    });
}
const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

const stripe = require('stripe')(process.env.STRIPE_SECRET_API_KEY)

//CROS対応（あくまでデモ用）
app.use((req, res, next) => {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "*");
    res.header("Access-Control-Allow-Headers", "*")
    next();
})

app.listen(3000, () => {
    console.log("Start on port 3000.")
})

// Payment Intent作成
app.post('/create_payment_intent', async (req, res) => {
    const amount = req.body.amount || 1099
    const currency = req.body.currency || 'jpy'
    try {
        const paymentIntent = await stripe.paymentIntents.create({
            amount,
            currency,
        })
        res.status(200).json({
            client_secret: paymentIntent.client_secret,
        })
    } catch (e) {
        console.log(e)
        console.log(e.code)
        res.status(e.statusCode || 500).json({
            code: e.code,
            message: e.message,
        })
    }  
})
```

## APIを呼び出して、作成したPaymentIntentを取得する

用意したAPIを呼び出すコンポーネントを用意しましょう。

Client Secretが必要ですので、取得した値は`useState`に格納します。

```tsx

export const StripeElementRoot:FC<PropsWithChildren<{}>> = ({children}) => {
    const [paymentIntentClientSecret, setPaymentIntentClientSecret] = useState<string | undefined>(undefined)
    const createPaymentIntent = async (amount: number, currency: string) => {
        try {
            const result = await fetch(process.env.GATSBY_API_URL as string + 'create_payment_intent', {
                method: 'post',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    amount,
                    currency,
                })
            })
            const response = await result.json()
            if (!result.ok) {
                console.log(response)
                throw new Error(response.message)
            }
            setPaymentIntentClientSecret(response.client_secret)
        } catch (e) {
            console.log(e)
        }
    }
    return (
      <button onClick={createPaymentIntent}>Order</button>
    )
}
```

## Payment Elementsを呼び出す

Client Secretが取得できれば、あとはElementsを呼び出すだけです。

```diff_typescript
+ import { Elements, PaymentElement } from "@stripe/react-stripe-js";
+ import { loadStripe } from "@stripe/stripe-js";
    return (
+     <>
        <button onClick={createPaymentIntent}>Order</button>
+       {paymentIntentClientSecret ? (
+         <Elements
+            stripe={loadStripe(process.env.GATSBY_STRIPE_PUBLIC_KEY as string)}
+            options={{
+                appearance: {
+                  theme: "flat",
+                },
+                clientSecret: paymentIntentClientSecret,
+            }}
+         >
+.         <form onSubmit={async(e) => {
+            e.preventDefault()
+            console.log(true)
+          }}>
+            <PaymentElement />
+            <button type='submit'>Buy</button>
+          </form>
+         </Elements>
+        ): null}
+     </>
    )
```

## 注文確定処理は、別コンポーネントを作成して実装

注文を確定させるための処理などを取得できる`useStripe`フックは、`Elements`の子要素で利用できます。

そのため、実際の組み込みでは`PaymentElement`や`form`は`Elements`とは別のコンポーネントに実装します。

```tsx
const PaymentComponent:React.FC = () => {
  const stripe = useStripe()
  const elements = useElements()
  return (
    <form onSubmit={async(e) => {
      e.preventDefault()
      if (!stripe || !elements) return
      stripe.confirmPayment({
        elements,
        confirmParams: {
          return_url: 'http://localhost:8000'
        }
      })
    }}>
      <PaymentElement />
      <button type='submit'>Buy</button>
    </form>

  )
}

```

## [応用]　`gatsby-browser.tsx`とContextを使った汎用化

`Elements`をPayment IntentsなどのClient Secretの有無で出し分ける必要があるため、コード量が多くなりがちです。

もし汎用化したい場合は、以下のようなProviderを用意しておくのも手かもしれません。

**src/use-stripe-elements.tsx**

```tsx
import { Elements } from "@stripe/react-stripe-js"
import React, { createContext, FC, PropsWithChildren, useContext, useState } from "react"
import { loadStripe } from "@stripe/stripe-js"

const StripeElementContext = createContext<{
    createPaymentIntent: (amount: number, currency: string) => Promise<void>
    hasClientSecret: boolean
}>({} as any)

export const useStripeElement = () => useContext(StripeElementContext)

export const StripeElementProvider:FC<PropsWithChildren<{}>> = ({children}) => {
    const [paymentIntentClientSecret, setPaymentIntentClientSecret] = useState<string | undefined>(undefined)
    const createPaymentIntent = async (amount: number, currency: string) => {
        try {
            const result = await fetch(process.env.GATSBY_API_URL as string + 'create_payment_intent', {
                method: 'post',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    amount,
                    currency,
                })
            })
            const response = await result.json()
            if (!result.ok) {
                console.log(response)
                throw new Error(response.message)
            }
            setPaymentIntentClientSecret(response.client_secret)
        } catch (e) {
            console.log(e)
        }
    }
    const ElementProvider = () => {
        if (!paymentIntentClientSecret) return <>{children}</>
        return (
            <Elements
                stripe={loadStripe(process.env.GATSBY_STRIPE_PUBLIC_KEY as string)}
                options={{
                    appearance: {
                      theme: "flat",
                    },
                    clientSecret: paymentIntentClientSecret,
                }}
            >
                {children}
            </Elements>
        )
    }
    return (
        <StripeElementContext.Provider
            value={{
                createPaymentIntent,
                hasClientSecret: !!paymentIntentClientSecret,
            }}
        >
            <ElementProvider />
        </StripeElementContext.Provider>
    )
}
```

**gatsby-browser.tsx**

```tsx
import React from 'react'
import { WrapRootElementBrowserArgs } from 'gatsby'
import { StripeElementProvider } from './src/use-stripe-elements'

export const wrapRootElement = ({ element }: WrapRootElementBrowserArgs) => {
    return (
        <StripeElementProvider>
            {element}
        </StripeElementProvider>
    )
}
```

**src/payment-demo.tsx**

```tsx

const IndexPage = () => {
  const { createPaymentIntent, hasClientSecret } = useStripeElement()
  if (hasClientSecret) {
    return (
        <StripePaymentElement />
    )
  }
  return (
    <button onClick={() => createPaymentIntent(100, 'jpy')}>Create</button>
  )
}
```

**src/payment-element.tsx**

```tsx

const StripePaymentElement = () => {
  return (
      <form onSubmit={async(e) => {
        e.preventDefault()
        console.log(true)
      }}>
        <PaymentElement />
        <button type='submit'>Buy</button>
      </form>
  )
}
```

## Documents

https://www.gatsbyjs.com/docs/how-to/local-development/environment-variables/

https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/

https://stripe.com/docs/stripe-js/react

https://stripe.com/docs/payments/quickstart

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

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

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

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