53
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GAOGAOAdvent Calendar 2020

Day 24

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

Last updated at Posted at 2020-12-23

現在参画中の案件で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

コードはこちら

作成したものはこちら

イメージ
Untitled Diagram (1).png

顧客ができること

  • クレジットカードをプラットフォームに登録する
  • 登録したクレジットカードで複数店舗の支払いを行う

店舗ができること

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

プラットフォームができること

  • 店舗ごとの売上をみれる

実装の手順

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

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

環境

インフラ:vercel
フロント:Next.js(10.0.0)
APIサーバー:Node.js(Next.jsのAPI Routes

実装

1. Stripeの登録・設定

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

設定_–stripe-connect-app–Stripe__テスト.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

実装

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

/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取得し、遷移させる。

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

実装

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

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

④ 店舗用のダッシュボードを表示できる

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

見た目

63378a6bea12c14d9c897c7c631c2460.gif

実装

店舗担当者用のダッシュボードも口座登録時と同様に、Stripeから発行されたURLでアクセスが可能となります。
なので、Stripeのライブラリを使用してURLを取得します。

フロント側

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. 顧客側の実装

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

やりたいこと

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

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

参考にした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

実装

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

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

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

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
    });
  }
}

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

  • Stripe用の入力フォーム('@stripe/react-stripe-js')のセットアップ
  • APIサーバー側に問い合わせてclient_secretを取得する
  • client_secretを使用して入力フォームで受け取ったクレジットカード情報をStripeへ送付する
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
lib/loadStripe.js
import {loadStripe} from '@stripe/stripe-js';

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

export default stripePromise
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

顧客_–stripe-connect-app–Stripe__テスト.png

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

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

見た目

3572a7bdeaeb5a4513ef059a24c7cbf6.gif

実装

APIサーバー側
上記の①クレジットカードを登録できるが完了した状態では、クレジットカードの登録はできた状態ですが、
顧客が店舗に対して支払いを行うためには、このドキュメントでいうと

  • connected account(店舗)
  • customer(顧客)
  • payment method(カード情報)

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

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

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する
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
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

53
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?