Next.jsとStripe Connectでプラットフォームアプリを作る

現在参画中の案件でStripe Connectを使用して決済機能を実装する機会がありました。

今回、初めてStripeとStripe Connectを触ってみて、実際に動くものを見ながら概要がわかるようなドキュメントがあったらいいなと思ったので簡単なプロトタイプを作成し、記事にしてみました。

他の誰かの理解の助けにあれば幸いです。


*これから解説する以下のアプリケーションはStripeのテストモードを使用しています。クレジットカード番号やそのほかの入力情報はテスト用のデータを使用してください。*
https://stripe.com/docs/testing
https://stripe.com/docs/connect/testing


# やったこと
Stripe ConnectのExpressモードでプラットフォームアプリのプロトタイプを作りました
https://stripe.com/ja-us/connect/use-cases


**コードは[こちら](https://github.com/wish911wish/stripe-connect-app)**


**作成したものは[こちら](https://stripe-connect-app.vercel.app/)**


**イメージ**
![Untitled Diagram (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/d669c3bb-54bc-ddb3-224f-8c0b0636da74.png)

#### 顧客ができること
- クレジットカードをプラットフォームに登録する
- 登録したクレジットカードで複数店舗の支払いを行う


#### 店舗ができること
- 顧客からクレジットカードでの支払いを受け付けられる
- 売上代金を受け取るための銀行口座を登録できる

#### プラットフォームができること
- 店舗ごとの売上をみれる

# 実装の手順
以下の順番で実装しました。

1. Stripeの登録・設定
2. 店舗側の実装
3. 顧客側の実装

# 環境

インフラ：vercel
フロント：Next.js(10.0.0)
APIサーバー：Node.js（Next.jsの[API Routes](https://nextjs.org/docs/api-routes/introduction)）

# 実装
## 1. Stripeの登録・設定

まずはStripeに登録し、Stripe Connectの設定をする。

![設定_–_stripe-connect-app_–_Stripe__テスト_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/f1c31a51-5ad4-2708-ba28-bd2c69fd49c5.png)


上記の設定画面でプラットフォームのサービス名やアイコンなどを指定することで、
店舗のアカウント情報を登録できるようになる。


## 2. 店舗側の実装
Stripe Connectの設定が完了したら店舗用の銀行口座を登録できるようにする。
Stripe ConnectのアカウントタイプはStandard, Express, Customの3種類が存在するが、今回はExpressモードを使用します。

### やりたいこと

店舗側の実装でやりたいことは以下の4つです。

① Stripeが用意した口座登録用のページに遷移できるようにする
② 登録完了ページを表示できる
③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる
④ 店舗管理者用のダッシュボードを表示できる

Express用のStripe Connectのドキュメントを参考に実装しました。
https://stripe.com/docs/connect/express-accounts

### 見た目と実装
① ~ ④の見た目と実装を順に説明していきます。
#### ① Stripeが用意した口座登録用のページに遷移できるようにする

##### 見た目
![80e5b8beec6458d021e8d12a5fb11af8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/d4227d49-bc0a-f2f3-ad22-724b2c385b33.gif)

##### 実装
**APIサーバー側**
フロント側に口座登録用のURLを返すAPIを作成する。

```js:/pages/api/create-connect-account.js
import stripe from '../../lib/stripe'
export default async (req, res) => {
  try {
    // Stripe用の connected accountを作成する
    // このタイミングでアカウントのタイプを選択する（今回は'express'）
    const account = await stripe.accounts.create({
      type: 'express',
      country: 'JP',
    })

    // 作成したconnected accountのidから口座登録用のURLを発行する。
    const origin = process.env.NODE_ENV === 'development' ? `http://${req.headers.host}` : `https://${req.headers.host}`
    const accountLinkURL = await generateAccountLink(account.id, origin)
  
    res.statusCode = 200
    res.json({ url: accountLinkURL })
  } catch (err) {
    res.status(500).send({
      error: err.message
    });
  }
}

function generateAccountLink(accountID, origin) {
  return stripe.accountLinks.create({
    type: "account_onboarding",
    account: accountID,
    refresh_url: `${origin}/onboard-user/refresh`,
    return_url: `${origin}/success`,
  }).then((link) => link.url);
}


```
**フロント側**
上記のAPIサーバーから口座登録用のURL取得し、遷移させる。

```react:pages/owner/register.js
import { useRouter } from 'next/router'
import Layout from '../../component/Layout'
import styles from '../../styles/Home.module.css'
import { POST } from '../../lib/axios'

const RegisterPage = () => {
  const router = useRouter()

  const getSetLink = async () => {
    const result = await POST('/api/create-connect-account', { name: 'test', email: 'test@mail.com'})
    await router.push(result.url)
  }

  return (
    <Layout>
      <main className={styles.main}>
        <h2>店舗オーナー用のメニュー</h2>
        <div className={styles.grid}>	
          <div className={styles.card} onClick={() => getSetLink()}>
            <p>店舗の銀行口座を登録する</p>
          </div>
        </div>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async () => {
  return {
    props: {}
  }
}

export default RegisterPage


```



#### ② 登録完了ページを表示できる

##### 見た目
![2187f433dd6405bfe495b682b241da0d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/e4004dc2-3c28-1ec4-8852-7a03ed9f24db.gif)

##### 実装
①のAPIサーバーで指定したreturn_urlに一致するようにページを作成します。
**フロント側**


```react:pages/success.js
import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Success!!
        </h1>

        <div className={styles.grid}>
          <a href="/" className={styles.card}>
            <h3>stripeの登録が完了しました。</h3>
            <p>Topへ戻る</p>
          </a>
        </div>
      </main>

    </div>
  )
}
```



##### ③ プラットフォーム実装者のStripeダッシュボードで店舗のアカウント情報を確認できる
②まで完了すると、プラットフォーム管理者のダッシュボードで店舗のアカウントを確認できるようになります。
###### 見た目
![Connect_アカウント_–_stripe-connect-app_–_Stripe__テスト_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/b49a1745-6017-e99f-02d6-d6dc14192900.png)

##### ④ 店舗用のダッシュボードを表示できる
②まで完了した店舗担当者は、プラットフォーム管理者とは別のダッシュボードで自身の店舗の口座情報を確認できるようになります。

###### 見た目
![63378a6bea12c14d9c897c7c631c2460.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/ca86cf95-16ad-e54d-5716-e627e56a10be.gif)


###### 実装
店舗担当者用のダッシュボードも口座登録時と同様に、Stripeから発行されたURLでアクセスが可能となります。
なので、[Stripeのライブラリ](https://stripe.com/docs/api/account/login_link)を使用してURLを取得します。

**フロント側**

```react:pages/owner/shop/[id].js
import styles from '../../../styles/Home.module.css'
import stripe from '../../../lib/stripe'
import Layout from '../../../component/Layout'

const RegisterPage = (props) => {
  return (
    <Layout>
      <main className={styles.main}>
        <h2>店舗画面</h2>
        <div className={styles.grid}>
            <a href={props.loginLinkUrl} className={styles.card}>
              <h3>店舗の口座情報を確認する</h3>
            </a>
        </div>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async (ctx) => {
  const accountId = ctx.query.id
  const loginLink = await stripe.accounts.createLoginLink(accountId)

  return {
    props: {
      loginLinkUrl: loginLink.url,
      shopId: ctx.query.id
    }
  }
}

export default RegisterPage

```


## 3. 顧客側の実装
次は顧客用にクレジットカードを登録できるようにして、決済できるようにします。

### やりたいこと

店舗側の実装でやりたいことは以下の２つです。

① クレジットカードを登録できる
② 店舗毎に登録したクレジットカードで決済できる

参考にしたstripeのドキュメントは
① => https://stripe.com/docs/payments/save-and-reuse
② => https://stripe.com/docs/payments/payment-methods/connect#cloning-payment-methods
です

### 見た目と実装
#### ①クレジットカードを登録できる
##### 見た目
Stripeが提供しているinput formを使用してStripe側にクレジットカード情報を登録します。
![84b838ab6fee08f9ac44b64b16c41b87 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/9968d79f-e2e5-3d87-fb5d-b1e882c87516.gif)


##### 実装
**APIサーバー側**
APIサーバー側で行うことは以下の3つです。

- Stripeに顧客のアカウント情報を登録する
- クレジットカード登録用のセットアップを行う
- フロント側にclient_secretを渡す

client_secretとはフロント側でクレジットカードの情報をstripeに送る際に必要となるキーです。

```js:pages/api/register-customer.js
import stripe from '../../lib/stripe'

export default async (req, res) => {
  try {
    const customerName = req.body.customerName

    // Stripeに顧客のアカウント情報を登録する
    const customer = await stripe.customers.create({
      name: customerName
    })
  
    // クレジットカード登録用のセットアップを行う
    const setupIntent = await stripe.setupIntents.create({
      payment_method_types: ['card'],
      customer: customer.id
    });
  
    // フロント側にclient_secretを渡す
    res.statusCode = 201
    res.json({
        id: customer.id,
        name: customer.name,
        client_secret: setupIntent.client_secret
    })
  } catch (err) {
    console.error(err)
    res.status(500).send({
      error: err.message
    });
  }
}
```

**フロント側**
フロント側で行うことは以下の３つです

- Stripe用の入力フォーム('@stripe/react-stripe-js')のセットアップ
- APIサーバー側に問い合わせてclient_secretを取得する
- client_secretを使用して入力フォームで受け取ったクレジットカード情報をStripeへ送付する


```react:pages/customer/register.js　クレジットカード登録画面
import * as React from 'react'
import { Elements } from '@stripe/react-stripe-js'
import stripePromise from '../../lib/loadStripe'
import { CustomerContext } from '../../context/CustomerContext'
import { POST } from '../../lib/axios'
import Layout from '../../component/Layout'
import styles from '../../styles/Home.module.css'
import CardInputForm from '../../component/CardInputForm'

const RegisterPage = () => {
  const { customerState, customerSetter } = React.useContext(CustomerContext)
  const [name, setName] = React.useState('名無しさん')
  const [loading, setLoading] = React.useState(false)

  const registerCustomer = async (e) => {
    e.preventDefault()
    setLoading(true)
    const result = await POST('/api/register-customer', { customerName: name })
    customerSetter({
      name: result.name,
      id: result.id,
      client_secret: result.client_secret
    })
    setLoading(false)
  }

  return (
    <Layout>
      <main className={styles.main}>
        {customerState.client_secret ? (
          <div>
            <h4>こちら↓からクレジットカードを登録してください</h4>
            <p>**テスト用の番号 "4242424242424242" を使用してください**</p>
            {loading ? (
              '登録中...'
            ):(
              <Elements stripe={stripePromise}>
                <CardInputForm clientSecret={customerState.client_secret} customerName={customerState.name}/>
              </Elements >
            )}
          </div>
        ) : (
          <div>
            <h4>お客様のお名前を登録してください</h4>
            <form onSubmit={(e) => registerCustomer(e)}>
              <input type="text" defaultValue={name} onChange={(e) => setName(e.target.value)}></input>
              <button>名前を登録する</button>
            </form>
          </div>
        )}
      </main>
    </Layout>
  )
}

export const getServerSideProps = async () => {
  return {
    props: {
    }
  }
}

export default RegisterPage
```

```react:lib/loadStripe.js
import {loadStripe} from '@stripe/stripe-js';

// Stripeにクレカ情報をPOSTするためのライプラリの設定
const stripePromise = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
);

export default stripePromise
```

```react:component/CardInputForm.js　カード番号入力フォーム
import * as React from 'react'
import {useStripe, useElements, CardElement} from '@stripe/react-stripe-js'

const CardInputForm = (props) => {
    const stripe = useStripe()
    const elements = useElements()
    const [loading, setLoading] = React.useState(false)
    const [message, setMessage] = React.useState('登録する')

    // カードの登録処理
    const handleSubmit = async (event) => {
        event.preventDefault()
        setLoading(true)
        setMessage('登録中。。。')

        if (!stripe || !elements) {
            return;
        }

        // APIサーバー側から受け取ったclient_secretを使用してStripeへカード情報を送付する
        const result = await stripe.confirmCardSetup(props.clientSecret, {
            payment_method: {
              card: elements.getElement(CardElement),
              billing_details: {
                name: props.customerName,
              },
            }
        });

        if (result.error) {
            setMessage('失敗しました')
        } else {
            setMessage('完了しました')
        }

        setLoading(true)
    }
    return (
        <form onSubmit={handleSubmit}>
            <CardElement />
            <button disabled={!stripe || loading}>{message}</button>
        </form>
    )
}

export default CardInputForm

```
上記のフォームで顧客の登録とクレジットカードの登録が完了すると
プラットフォームのStripeアカウントのダッシュボードから顧客のデータが確認できるようになります。

![顧客_–_stripe-connect-app_–_Stripe__テスト__と_Markdown記法_チートシート_-_Qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/a43284a2-af42-c698-0559-51e3a9399022.png)

![顧客_–_stripe-connect-app_–_Stripe__テスト_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/83738199-7047-b5fa-3290-b3d18f2a2d4b.png)




#### ② 店舗毎に登録したクレジットカードで決済できる
①で登録したクレジットカードを使用して決済できるように実装します。

##### 見た目
![3572a7bdeaeb5a4513ef059a24c7cbf6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/b3c120cb-0228-e8b4-95a5-881bed220544.gif)


##### 実装
**APIサーバー側**
上記の`①クレジットカードを登録できる`が完了した状態では、クレジットカードの登録はできた状態ですが、
顧客が店舗に対して支払いを行うためには、[このドキュメント](https://stripe.com/docs/payments/payment-methods/connect#cloning-payment-methods
)でいうと

- connected account（店舗）
- customer（顧客）
- payment method（カード情報）

の３つを紐づける必要があります。

なので、商品の注文に対して
`　店舗 - 顧客 - カード情報　`の紐付けと、
 支払いのセットアップを
APIサーバー側で行います。

```js:pages/api/shop/[id]/buy.js

import stripe from '../../../../lib/stripe'

export default async (req, res) => {
  try {
    // フロントからPOSTされた商品データ
    const item = req.body.item
    const stripeConnectedAccountId = req.query.id
    const customerId = req.body.customer_id

    // 顧客のカードの登録情報を取得（複数のカードが登録されている場合は、複数件のカード情報をする）
    const paymentMethodData = await stripe.paymentMethods.list({
      customer: customerId,
      type: 'card',
    });

    // 店舗毎(stripeConnectedAccountId)にクレジットカード情報(payment_method)を複製
    const clonedPaymentMethod = await stripe.paymentMethods.create({
      customer: customerId,
      payment_method: paymentMethodData.data[0].id,
    }, {
      stripeAccount: stripeConnectedAccountId,
    });

    // 店舗毎(stripeConnectedAccountId)に顧客情報を複製(customer))を複製
    const clonedCustomer = await stripe.customers.create({
      payment_method: clonedPaymentMethod.id,
    }, {
      stripeAccount: stripeConnectedAccountId,
    })

    // 上記の複製したpayment_methodとaccountを使用し、支払いのためのセットアップを行う
    const paymentIntent = await stripe.paymentIntents.create({
      amount: item.price,
      currency: 'jpy',
      payment_method_types: ['card'],
      payment_method: clonedPaymentMethod.id,
      customer: clonedCustomer.id,
      description: `${item.name}の購入代金`,
      metadata: {'name': item.name, 'price': item.price}
    }, {
      stripeAccount: stripeConnectedAccountId,
    });

    // 支払い処理自体はブラウザから行う必要があるため、決済に必要なキー(client_secret)をフロントに渡す
    res.statusCode = 201
    res.json({
      client_secret: paymentIntent.client_secret
    })
  } catch (err) {
    console.error(err)
    res.status(500).send({
      error: err.message
    });
  }
}
```

**フロント側**

- 決済用のstripe.jsを店舗(connected account)用にセットアップする（`loadStripe`）
- 商品のデータをAPIサーバーに渡して, client_secretを受け取る
- client_secretを使用して、Stripeへ決済情報をPOSTする

```react:pages/customer/shop/[id].js　商品一覧ページ
import * as React from 'react'
import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from '@stripe/stripe-js';
import { CustomerContext } from '../../../context/CustomerContext'
import Layout from '../../../component/Layout'
import styles from '../../../styles/Home.module.css'
import CheckoutForm from '../../../component/CheckoutForm'

const RegisterPage = (props) => {
  const { customerState } = React.useContext(CustomerContext)

  
  const stripePromise = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
    { stripeAccount: props.shopId}
  )

  return (
    <Layout>
      <main className={styles.main}>
        <h2>商品一覧</h2>
        <Elements stripe={stripePromise}>
          <div className={styles.grid}>
            {props.itemList.map((item, index) =>
              <div className={styles.card} key={index}>
                <CheckoutForm item={item} customerId={customerState.id} shopId={props.shopId} />
              </div>
            )}
          </div>
        </Elements>
      </main>
    </Layout>
  )
}

export const getServerSideProps = async (ctx) => {
  const itemList = [
    {
      name: 'キノコのかさ',
      price: 100,
    },
    {
      name: 'キノコのスツール',
      price: 200
    },
    {
      name: 'キノコのかべがみ',
      price: 300
    },
  ]

  return {
    props: {
      itemList: itemList,
      shopId: ctx.query.id
    }
  }
}

export default RegisterPage
```

```react:pagescomponent/CheckoutForm.js
import {useStripe} from '@stripe/react-stripe-js';
import { POST } from '../lib/axios'
import * as React from 'react'
import styles from '../styles/Home.module.css'


const CheckoutForm = (props) => {
  const [message, setMessage] = React.useState()

  const stripe = useStripe()
    
  const handleSubmit = async () => {
    setMessage('処理中。。。')
    const result = await POST(`/api/shop/${props.shopId}/buy`, {
      customer_id: props.customerId,
      item: props.item
    })

    const confirm_result = window.confirm('選択した商品を購入します。よろしいですか？');

    if (confirm_result) {
      const paymentResult = await stripe.confirmCardPayment(result.client_secret)
      if (paymentResult.error) {
        setMessage('失敗しました')
      } else {
        setMessage('購入しました')
      }  
    } else {
      setMessage('')
    }
  }

  return (
    <div onClick={() => handleSubmit()}>
      <h3>{props.item.name}</h3>
      <div>¥{props.item.price}</div>
      {message && (
        <div className={styles.title}>{message}</div>
      )}
    </div>
  )
}

export default CheckoutForm
```

ブラウザで`stripe.confirmCardPayment()`の実行が成功すると、
プラットフォーム実装者のダッシュボードで店舗の売り上げが確認できるようになります。

![Connect_アカウント_–_stripe-connect-app_–_Stripe__テスト_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254709/6a109d09-8fea-f85f-95cc-57d2f3f89476.png)