Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8896

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

$
0
0

現在参画中の案件で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
importstripefrom'../../lib/stripe'exportdefaultasync(req,res)=>{try{// Stripe用の connected accountを作成する// このタイミングでアカウントのタイプを選択する(今回は'express')constaccount=awaitstripe.accounts.create({type:'express',country:'JP',})// 作成したconnected accountのidから口座登録用のURLを発行する。constorigin=process.env.NODE_ENV==='development'?`http://${req.headers.host}`:`https://${req.headers.host}`constaccountLinkURL=awaitgenerateAccountLink(account.id,origin)res.statusCode=200res.json({url:accountLinkURL})}catch(err){res.status(500).send({error:err.message});}}functiongenerateAccountLink(accountID,origin){returnstripe.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'importLayoutfrom'../../component/Layout'importstylesfrom'../../styles/Home.module.css'import{POST}from'../../lib/axios'constRegisterPage=()=>{constrouter=useRouter()constgetSetLink=async()=>{constresult=awaitPOST('/api/create-connect-account',{name:'test',email:'test@mail.com'})awaitrouter.push(result.url)}return(<Layout><mainclassName={styles.main}><h2>店舗オーナー用のメニュー</h2><divclassName={styles.grid}><divclassName={styles.card}onClick={()=>getSetLink()}><p>店舗の銀行口座を登録する</p></div></div></main></Layout>)}exportconstgetServerSideProps=async()=>{return{props:{}}}exportdefaultRegisterPage

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

見た目

2187f433dd6405bfe495b682b241da0d.gif

実装

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

pages/success.js
importHeadfrom'next/head'importstylesfrom'../styles/Home.module.css'exportdefaultfunctionHome(){return(<divclassName={styles.container}><Head><title>Create Next App</title><linkrel="icon"href="/favicon.ico"/></Head><mainclassName={styles.main}><h1className={styles.title}>
          Success!!
        </h1><divclassName={styles.grid}><ahref="/"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
importstylesfrom'../../../styles/Home.module.css'importstripefrom'../../../lib/stripe'importLayoutfrom'../../../component/Layout'constRegisterPage=(props)=>{return(<Layout><mainclassName={styles.main}><h2>店舗画面</h2><divclassName={styles.grid}><ahref={props.loginLinkUrl}className={styles.card}><h3>店舗の口座情報を確認する</h3></a></div></main></Layout>)}exportconstgetServerSideProps=async(ctx)=>{constaccountId=ctx.query.idconstloginLink=awaitstripe.accounts.createLoginLink(accountId)return{props:{loginLinkUrl:loginLink.url,shopId:ctx.query.id}}}exportdefaultRegisterPage

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
importstripefrom'../../lib/stripe'exportdefaultasync(req,res)=>{try{constcustomerName=req.body.customerName// Stripeに顧客のアカウント情報を登録するconstcustomer=awaitstripe.customers.create({name:customerName})// クレジットカード登録用のセットアップを行うconstsetupIntent=awaitstripe.setupIntents.create({payment_method_types:['card'],customer:customer.id});// フロント側にclient_secretを渡すres.statusCode=201res.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*asReactfrom'react'import{Elements}from'@stripe/react-stripe-js'importstripePromisefrom'../../lib/loadStripe'import{CustomerContext}from'../../context/CustomerContext'import{POST}from'../../lib/axios'importLayoutfrom'../../component/Layout'importstylesfrom'../../styles/Home.module.css'importCardInputFormfrom'../../component/CardInputForm'constRegisterPage=()=>{const{customerState,customerSetter}=React.useContext(CustomerContext)const[name,setName]=React.useState('名無しさん')const[loading,setLoading]=React.useState(false)constregisterCustomer=async(e)=>{e.preventDefault()setLoading(true)constresult=awaitPOST('/api/register-customer',{customerName:name})customerSetter({name:result.name,id:result.id,client_secret:result.client_secret})setLoading(false)}return(<Layout><mainclassName={styles.main}>{customerState.client_secret?(<div><h4>こちら↓からクレジットカードを登録してください</h4><p>**テスト用の番号 "4242424242424242" を使用してください**</p>{loading?('登録中...'):(<Elementsstripe={stripePromise}><CardInputFormclientSecret={customerState.client_secret}customerName={customerState.name}/></Elements>)}</div>):(<div><h4>お客様のお名前を登録してください</h4><formonSubmit={(e)=>registerCustomer(e)}><inputtype="text"defaultValue={name}onChange={(e)=>setName(e.target.value)}></input><button>名前を登録する</button></form></div>)}</main></Layout>)}exportconstgetServerSideProps=async()=>{return{props:{}}}exportdefaultRegisterPage
lib/loadStripe.js
import{loadStripe}from'@stripe/stripe-js';// Stripeにクレカ情報をPOSTするためのライプラリの設定conststripePromise=loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);exportdefaultstripePromise
component/CardInputForm.js カード番号入力フォーム
import*asReactfrom'react'import{useStripe,useElements,CardElement}from'@stripe/react-stripe-js'constCardInputForm=(props)=>{conststripe=useStripe()constelements=useElements()const[loading,setLoading]=React.useState(false)const[message,setMessage]=React.useState('登録する')// カードの登録処理consthandleSubmit=async(event)=>{event.preventDefault()setLoading(true)setMessage('登録中。。。')if(!stripe||!elements){return;}// APIサーバー側から受け取ったclient_secretを使用してStripeへカード情報を送付するconstresult=awaitstripe.confirmCardSetup(props.clientSecret,{payment_method:{card:elements.getElement(CardElement),billing_details:{name:props.customerName,},}});if(result.error){setMessage('失敗しました')}else{setMessage('完了しました')}setLoading(true)}return(<formonSubmit={handleSubmit}><CardElement/><buttondisabled={!stripe||loading}>{message}</button></form>)}exportdefaultCardInputForm

上記のフォームで顧客の登録とクレジットカードの登録が完了すると
プラットフォームの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
importstripefrom'../../../../lib/stripe'exportdefaultasync(req,res)=>{try{// フロントからPOSTされた商品データconstitem=req.body.itemconststripeConnectedAccountId=req.query.idconstcustomerId=req.body.customer_id// 顧客のカードの登録情報を取得(複数のカードが登録されている場合は、複数件のカード情報をする)constpaymentMethodData=awaitstripe.paymentMethods.list({customer:customerId,type:'card',});// 店舗毎(stripeConnectedAccountId)にクレジットカード情報(payment_method)を複製constclonedPaymentMethod=awaitstripe.paymentMethods.create({customer:customerId,payment_method:paymentMethodData.data[0].id,},{stripeAccount:stripeConnectedAccountId,});// 店舗毎(stripeConnectedAccountId)に顧客情報を複製(customer))を複製constclonedCustomer=awaitstripe.customers.create({payment_method:clonedPaymentMethod.id,},{stripeAccount:stripeConnectedAccountId,})// 上記の複製したpayment_methodとaccountを使用し、支払いのためのセットアップを行うconstpaymentIntent=awaitstripe.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=201res.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*asReactfrom'react'import{Elements}from'@stripe/react-stripe-js';import{loadStripe}from'@stripe/stripe-js';import{CustomerContext}from'../../../context/CustomerContext'importLayoutfrom'../../../component/Layout'importstylesfrom'../../../styles/Home.module.css'importCheckoutFormfrom'../../../component/CheckoutForm'constRegisterPage=(props)=>{const{customerState}=React.useContext(CustomerContext)conststripePromise=loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,{stripeAccount:props.shopId})return(<Layout><mainclassName={styles.main}><h2>商品一覧</h2><Elementsstripe={stripePromise}><divclassName={styles.grid}>{props.itemList.map((item,index)=><divclassName={styles.card}key={index}><CheckoutFormitem={item}customerId={customerState.id}shopId={props.shopId}/></div>)}</div></Elements></main></Layout>)}exportconstgetServerSideProps=async(ctx)=>{constitemList=[{name:'キノコのかさ',price:100,},{name:'キノコのスツール',price:200},{name:'キノコのかべがみ',price:300},]return{props:{itemList:itemList,shopId:ctx.query.id}}}exportdefaultRegisterPage
pagescomponent/CheckoutForm.js
import{useStripe}from'@stripe/react-stripe-js';import{POST}from'../lib/axios'import*asReactfrom'react'importstylesfrom'../styles/Home.module.css'constCheckoutForm=(props)=>{const[message,setMessage]=React.useState()conststripe=useStripe()consthandleSubmit=async()=>{setMessage('処理中。。。')constresult=awaitPOST(`/api/shop/${props.shopId}/buy`,{customer_id:props.customerId,item:props.item})constconfirm_result=window.confirm('選択した商品を購入します。よろしいですか?');if(confirm_result){constpaymentResult=awaitstripe.confirmCardPayment(result.client_secret)if(paymentResult.error){setMessage('失敗しました')}else{setMessage('購入しました')}}else{setMessage('')}}return(<divonClick={()=>handleSubmit()}><h3>{props.item.name}</h3><div>¥{props.item.price}</div>{message&&(<divclassName={styles.title}>{message}</div>)}</div>)}exportdefaultCheckoutForm

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

Connect_アカウント_–_stripe-connect-app_–_Stripe__テスト_.png


Viewing all articles
Browse latest Browse all 8896

Trending Articles