こんにちは、KOUKIです。
NextJSで画面開発を行なっています。
今回は、Checkout処理の実装を行いましょう。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
前回
事前準備
ファイル
1 |
touch next-checkout/constants.ts |
モジュール
1 2 |
cd next-checkout/ npm i axios |
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "name": "next-checkout", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "axios": "^0.21.1", "next": "11.0.1", "react": "17.0.2", "react-dom": "17.0.2" }, "devDependencies": { "@types/react": "^17.0.15", "eslint": "7.31.0", "eslint-config-next": "11.0.1", "typescript": "^4.3.5" } } |
Link Dataの作成
前回、NetJSではURLのパスからコードを取得できることを学習しました。
1 2 3 4 5 6 7 8 9 10 |
// pages/[code].tsx ... export default function Home() { const router = useRouter() const { code } = router.query console.log(code) ... } |
ここで取得したコードからLink Dataを取得し、画面に反映する処理を実装します。
エンドポイント
constants.tsファイルに、APIへのエンドポイントを記述します。
1 2 3 4 |
// constants.ts export default { endpoint: 'http://localhost:8000/api/checkout' } |
このエンドポイントは、以前以下の記事で作成しました。
Fetch処理の実装
APIへCodeに紐づくLink Data情報を取得するための処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// pages/[code].tsx import { useEffect, useState } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' export default function Home() { const router = useRouter() const { code } = router.query const [user, setUser] = useState(null) const [products, setProducts] = useState([]) useEffect(() => { if (code != undefined) { ( async () => { const { data } = await axios.get(`${constants.endpoint}/links/${code}`) setUser(data.user) setProducts(data.products) } )() } }, [code]) ... } |
データの反映
取得したデータを画面に反映させましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// pages/[code].tsx ... export default function Home() { ... return ( <Layout> <main> <div className="py-5 text-center"> <h2>Welcome</h2> <p className="lead">{user?.first_name} {user?.last_name} has invited you to buy these products!</p> </div> <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> <h4 className="d-flex justify-content-between align-items-center mb-3"> <span className="text-primary">Products</span> </h4> <ul className="list-group mb-3"> {products.map(product => { return ( <div key={product.id}> <li className="list-group-item d-flex justify-content-between lh-sm"> <div> <h6 className="my-0">{product.title}</h6> <small className="text-muted">{product.description}</small> </div> <span className="text-muted">${product.price}</span> </li> <li className="list-group-item d-flex justify-content-between"> <div> <h6 className="my-0">Quantity</h6> </div> <input type="number" min="0" className="text-muted form-control" style={{width: '65px'}} defaultValue={0} /> </li> </div> ) })} <li className="list-group-item d-flex justify-content-between"> <span>Total (USD)</span> <strong>$20</strong> </li> </ul> </div> <div className="col-md-7 col-lg-8"> .... </div> </main> </Layout> ) } |
ユーザーネームとプロダクト情報を反映させました。

データが存在しないと反映されないので、お気をつけください。
Total Orderの実装
商品の合計値を出す処理を実装します。
まず、APIからquantityを取得する処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
export default function Home() { ... const [quantities, setQuantities] = useState([]) useEffect(() => { if (code != undefined) { ( async () => { const { data } = await axios.get(`${constants.endpoint}/links/${code}`) setUser(data.user) setProducts(data.products) setQuantities(data.products.map(p => ({ // 追加 product_id: p.id, quantity: 0 }))) } )() } }, [code]) ... } |
次に、商品の数量を変更した時に発火させるchange関数を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// pages/[code].tsx ... export default function Home() { ... const change = (id: number, quantity: number) => { setQuantities(quantities.map(q => { if (q.product_id === id) { return { ...q, quantity } } return q })) } return ( <Layout> <main> ... <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> ... <ul className="list-group mb-3"> {products.map(product => { return ( ... <input type="number" min="0" className="text-muted form-control" style={{width: '65px'}} defaultValue={0} onChange={e => change(product.id, parseInt(e.target.value))} // 追加 /> </li> </div> ) })} ... </ul> ... </div> </main> </Layout> ) } |
Quantityを変更したら発火させたいので、input要素にonChangeを追加し、change関数をセットしています。
最後に、Total Order関数を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// pages/[code].tsx ... export default function Home() { ... const total = () => { return quantities.reduce((s, q) => { const product = products.find(p => p.id === q.product_id) return s + product.price * q.quantity }, 0) } return ( <Layout> <main> ... <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> ... <ul className="list-group mb-3"> ... <li className="list-group-item d-flex justify-content-between"> <span>Total (USD)</span> <strong>${total()}</strong> // 追加 </li> </ul> ... </main> </Layout> ) } |
これで、OKです。
Checkout処理
Checkoutに必要な処理を実装していきます。
フォーム情報の設定
useStateを使って、フォームに入力された情報を保持する処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
// pages/[code].tsx ... export default function Home() { ... const [first_name, setFirstName] = useState(''); const [last_name, setLastName] = useState(''); const [email, setEmail] = useState(''); const [address, setAddress] = useState(''); const [country, setCountry] = useState(''); const [city, setCity] = useState(''); const [zip, setZip] = useState(''); ... const submit = () => {} // この後実装 return ( <Layout> <main> ... <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> ... </div> <div className="col-md-7 col-lg-8"> <h4 className="mb-3">Personal Info</h4> <form className="needs-validation" onSubmit={submit}> <div className="row g-3"> <div className="col-sm-6"> <label htmlFor="firstName" className="form-label">First name</label> <input type="text" className="form-control" id="firstName" placeholder="First Name" required onChange={e => setFirstName(e.target.value)} // 追加 /> </div> <div className="col-sm-6"> <label htmlFor="lastName" className="form-label">Last name</label> <input type="text" className="form-control" id="lastName" placeholder="Last Name" required onChange={e => setLastName(e.target.value)}/>// 追加 </div> <div className="col-12"> <label htmlFor="email" className="form-label">Email</label> <input type="email" className="form-control" id="email" placeholder="you@example.com" required onChange={e => setEmail(e.target.value)}/>// 追加 </div> <div className="col-12"> <label htmlFor="address" className="form-label">Address</label> <input type="text" className="form-control" id="address" placeholder="1234 Main St" required onChange={e => setAddress(e.target.value)}/>// 追加 </div> <div className="col-md-5"> <label htmlFor="country" className="form-label">Country</label> <input className="form-control" id="country" placeholder="Country" onChange={e => setCountry(e.target.value)}// 追加 /> </div> <div className="col-md-4"> <label htmlFor="city" className="form-label">City</label> <input className="form-control" id="city" placeholder="City" onChange={e => setCity(e.target.value)}// 追加 /> </div> <div className="col-md-3"> <label htmlFor="zip" className="form-label">Zip</label> <input type="text" className="form-control" id="zip" placeholder="Zip" onChange={e => setZip(e.target.value)}/>// 追加 </div> </div> <hr className="my-4"/> <button className="w-100 btn btn-primary btn-lg" type="submit">Checkout</button> </form> </div> </div> </main> </Layout> ) } |
useStateで作成したsetXXXXをinput項目のonChangeにセットしました。これで、フォームに入力した値を保持することが可能になります。
Submit処理
次に、Submitの処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const submit = async (e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.post(`${constants.endpoint}/orders`, { first_name, last_name, email, address, country, city, zip, code, products: quantities, }) console.log(data) } |
これで、バックエンド側のAPIにOrder情報を送信できます。
一つだけバグがありました。Quantityを「0」で送信すると400エラーになってしまうのです。

これは、Stripeの以下の処理に原因があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// controllers/orderController.go ... func CreateOrder(ctx *fiber.Ctx) error { ... // stripeアイテムセット lineItems = append(lineItems, &stripe.CheckoutSessionLineItemParams{ Name: stripe.String(product.Title), Description: stripe.String(product.Description), Images: []*string{stripe.String(product.Image)}, Amount: stripe.Int64(100 * int64(product.Price)), Currency: stripe.String("usd"), Quantity: stripe.Int64(int64(requestProduct["quantity"])), // ここ }) } } |
1 2 3 4 5 |
backend_1 | 2021/07/27 18:05:49 Request error from Stripe (status 400): {"code":"parameter_invalid_integer","doc_url":"https://stripe.com/docs/error-codes/ parameter-invalid-integer","status":400,"message":"This value must be greater than or equal to 1.","param":"line_items[0] [quantity]","request_id":"req_XG2MM2Vx9efRD1","type":"invalid_request_error"} |
どうやら1以上でないと送れないようですね。ここではひとまず先に進みますが、余力があれば修正してみてください。
Strap処理
以前、API側でStripeの処理を実装しました。Stripeはオンラインで利用できる決済サービスです。
このStripeを画面から利用するために「公開可能キー」が必要になります。
今回可能キーは、Stripeにログインすると確認することができます。

このキーを定数として定義します。
1 2 3 4 5 |
// constants.ts export default { endpoint: 'http://localhost:8000/api/checkout', stripe_key: 'pk_test_XXX' } |
さらにStripe JSからSDKを読み込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// components/Layout.tsx ... const Layout = (props) => { return ( <> <Head> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossOrigin="anonymous" /> <script src="https://js.stripe.com/v3/"></script> // 追加 </Head> ... </> ) } export default Layout |
これで、Stripeクラスが使えるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// pages/[code].tsx ... declare var Stripe export default function Home() { .... const submit = async (e: SyntheticEvent) => { ... const stripe = new Stripe(constants.stripe_key) stripe.redirectToCheckout({ sessionId: data.id }) } ... } |
適当にフォームに値を入力後、Submitしたら以下の画面に遷移できます。

Checkout後の処理
最後にCheckout後の処理を実装して、完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// pages/success.tsx import { useEffect } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' const Success = () => { const router = useRouter() const {source} = router.query useEffect(() => { if (source !== undefined) { ( async () => { await axios.post(`${constants.endpoint}/orders/confirm`,{ source }) } )() } }) return ( <Layout> <div className="py-5 text-center"> <h2>Success</h2> <p className="lead">Your purchase has been completed!</p> </div> </Layout> ) } export default Success |
おわりに
数ヶ月に及ぶアプリ開発は、この記事にて終了になります。
バックエンドからフロントエンドまで一通り学ぶことができたのではないでしょうか。
ぜひ、オリジナルアプリを作ってみてください!
それでは、また!
記事まとめ
参考書籍
ソースコード
ここまで実装したソースコードを下記に記載します。
[code].tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
// pages/[code].tsx import { SyntheticEvent, useEffect, useState } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' declare var Stripe export default function Home() { const router = useRouter() const { code } = router.query const [user, setUser] = useState(null) const [products, setProducts] = useState([]) const [quantities, setQuantities] = useState([]) const [first_name, setFirstName] = useState(''); const [last_name, setLastName] = useState(''); const [email, setEmail] = useState(''); const [address, setAddress] = useState(''); const [country, setCountry] = useState(''); const [city, setCity] = useState(''); const [zip, setZip] = useState(''); useEffect(() => { if (code != undefined) { ( async () => { const { data } = await axios.get(`${constants.endpoint}/links/${code}`) setUser(data.user) setProducts(data.products) setQuantities(data.products.map(p => ({ product_id: p.id, quantity: 0 }))) } )() } }, [code]) const change = (id: number, quantity: number) => { setQuantities(quantities.map(q => { if (q.product_id === id) { return { ...q, quantity } } return q })) } const total = () => { return quantities.reduce((s, q) => { const product = products.find(p => p.id === q.product_id) return s + product.price * q.quantity }, 0) } const submit = async (e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.post(`${constants.endpoint}/orders`, { first_name, last_name, email, address, country, city, zip, code, products: quantities, }) const stripe = new Stripe(constants.stripe_key) stripe.redirectToCheckout({ sessionId: data.id }) } return ( <Layout> <main> <div className="py-5 text-center"> <h2>Welcome</h2> <p className="lead">{user?.first_name} {user?.last_name} has invited you to buy these products!</p> </div> <div className="row g-5"> <div className="col-md-5 col-lg-4 order-md-last"> <h4 className="d-flex justify-content-between align-items-center mb-3"> <span className="text-primary">Products</span> </h4> <ul className="list-group mb-3"> {products.map(product => { return ( <div key={product.id}> <li className="list-group-item d-flex justify-content-between lh-sm"> <div> <h6 className="my-0">{product.title}</h6> <small className="text-muted">{product.description}</small> </div> <span className="text-muted">${product.price}</span> </li> <li className="list-group-item d-flex justify-content-between"> <div> <h6 className="my-0">Quantity</h6> </div> <input type="number" min="0" className="text-muted form-control" style={{width: '65px'}} defaultValue={0} onChange={e => change(product.id, parseInt(e.target.value))} /> </li> </div> ) })} <li className="list-group-item d-flex justify-content-between"> <span>Total (USD)</span> <strong>${total()}</strong> </li> </ul> </div> <div className="col-md-7 col-lg-8"> <h4 className="mb-3">Personal Info</h4> <form className="needs-validation" onSubmit={submit}> <div className="row g-3"> <div className="col-sm-6"> <label htmlFor="firstName" className="form-label">First name</label> <input type="text" className="form-control" id="firstName" placeholder="First Name" required onChange={e => setFirstName(e.target.value)} /> </div> <div className="col-sm-6"> <label htmlFor="lastName" className="form-label">Last name</label> <input type="text" className="form-control" id="lastName" placeholder="Last Name" required onChange={e => setLastName(e.target.value)}/> </div> <div className="col-12"> <label htmlFor="email" className="form-label">Email</label> <input type="email" className="form-control" id="email" placeholder="you@example.com" required onChange={e => setEmail(e.target.value)}/> </div> <div className="col-12"> <label htmlFor="address" className="form-label">Address</label> <input type="text" className="form-control" id="address" placeholder="1234 Main St" required onChange={e => setAddress(e.target.value)}/> </div> <div className="col-md-5"> <label htmlFor="country" className="form-label">Country</label> <input className="form-control" id="country" placeholder="Country" onChange={e => setCountry(e.target.value)} /> </div> <div className="col-md-4"> <label htmlFor="city" className="form-label">City</label> <input className="form-control" id="city" placeholder="City" onChange={e => setCity(e.target.value)} /> </div> <div className="col-md-3"> <label htmlFor="zip" className="form-label">Zip</label> <input type="text" className="form-control" id="zip" placeholder="Zip" onChange={e => setZip(e.target.value)}/> </div> </div> <hr className="my-4"/> <button className="w-100 btn btn-primary btn-lg" type="submit">Checkout</button> </form> </div> </div> </main> </Layout> ) } |
Layout.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// components/Layout.tsx import Head from 'next/head' const Layout = (props) => { return ( <> <Head> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossOrigin="anonymous" /> <script src="https://js.stripe.com/v3/"></script> </Head> <div className="container"> {props.children} </div> </> ) } export default Layout |
success.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// pages/success.tsx import { useEffect } from 'react' import { useRouter } from 'next/dist/client/router' import Layout from '../components/Layout' import axios from 'axios' import constants from '../constants' const Success = () => { const router = useRouter() const {source} = router.query useEffect(() => { if (source !== undefined) { ( async () => { await axios.post(`${constants.endpoint}/orders/confirm`,{ source }) } )() } }) return ( <Layout> <div className="py-5 text-center"> <h2>Success</h2> <p className="lead">Your purchase has been completed!</p> </div> </Layout> ) } export default Success |
constants.ts
1 2 3 4 5 6 |
// constants.ts export default { endpoint: 'http://localhost:8000/api/checkout', stripe_key: 'pk_test_XXXX' } |
コメントを残す
コメントを投稿するにはログインしてください。