[35 | クレジットカード フォーム] << [ホーム] >> [37 | 受注データ実装]
「src/apis.js」ファイルに記述を追加します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/apis.js】115行目〜
import { toast } from 'react-toastify'; function request(path, {data = null, token = null, method = "GET" }) { return fetch(path, { method, headers: { Authorization: token ? `Token ${token}` : "", "Content-Type": "application/json", }, body: method !=="GET" && method !== "DELETE" ? JSON.stringify(data): null, }) .then((response) => { //もし成功したら if (response.ok) { if(method === "DELETE") { return true; } //toast.success("ログイン成功"); return response.json(); } //失敗 return response.json().then((json) => { //JSONエラー if (response.status === 400) { //toast.error("氏名もしくはパスワードに間違いがあります。"); const errors = Object.keys(json).map( (k) => `${(json[k].join(" "))}` ); throw new Error(errors.join(" ")); } throw new Error(JSON.stringify(json)); }) .catch((e) => { if (e.name === "SyntaxError") { throw new Error(response.statusText); } throw new Error(e); }) }) .catch((e) => { //全エラー toast(e.message, { type: "error" }); }) } export function signIn(username, password) { return request("/auth/token/login/", { data: {username, password}, method: "POST", }) } export function register(username, password) { return request("/auth/users/", { data: {username, password}, method: "POST", }) } export function fetchPlaces(token) { return request("/api/places/", {token}); } export function addPlace(data, token) { return request("/api/places/", {data, token, method: "POST" }); } export function uploadImage(image) { const formData = new FormData(); formData.append("file", image); formData.append("upload_preset", "qrmenu_photos"); return fetch("https://api.cloudinary.com/v1_1/dov57gocw/image/upload", { method: "POST", body: formData, }).then((response) => { return response.json(); }); } export function fetchPlace(id, token) { return request(`/api/places/${id}`, { token }); } export function addCategory(data, token) { return request("/api/categories/", {data, token, method: "POST"}); } export function addMenuItems(data, token) { return request("/api/menu_items/", {data, token, method: "POST"}); } export function updateMenuItem(id, data, token) { return request(`/api/menu_items/${id}`, { data, token, method: "PATCH"}); } export function removePlace(id, token) { return request(`/api/places/${id}`, {token, method: "DELETE"}); } export function removeCategory(id, token) { return request(`/api/categories/${id}`, {token, method: "DELETE"}); } export function removeMenuItem(id, token) { return request(`/api/menu_items/${id}`, {token, method: "DELETE"}); } export function updatePlace(id, data, token) { return request(`/api/places/${id}`, { data, token, method: "PATCH"}) } export function createPaymentIntent(data, token) { return request("/api/create_payment_intent/", {data, token, method: "POST"}); }
「src/containers/PaymentForm.js」ファイルを編集します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/containers/PaymentForm.js】
import React, {useContext, useState} from 'react'; import ReactDOM from 'react-dom'; import {loadStripe} from '@stripe/stripe-js'; import { CardElement, Elements, useStripe, useElements, } from '@stripe/react-stripe-js'; import { Form, Button } from 'react-bootstrap'; import { toast } from 'react-toastify'; import { useParams } from 'react-router-dom'; import { createPaymentIntent } from '../apis'; import AuthContext from '../contexts/AuthContext'; const PaymentForm = ({amount, items, onDone}) => { const [loading, setLoading] = useState(false); const stripe = useStripe(); const elements = useElements(); const auth = useContext(AuthContext); const params = useParams(); const onSubmit = async (event) => { event.preventDefault(); const {error, paymentMethod} = await stripe.createPaymentMethod({ type: 'card', card: elements.getElement(CardElement), }); if(!error) { setLoading(true); const json = await createPaymentIntent({ payment_method: paymentMethod, amount, place: params.id, table: params.table, detail: items }, auth.token); if(json?.success) { toast(`注文されました。オーダーNo.${json.order}`, {type: "success"}); onDone(); setLoading(false); } else if (json?.error) { toast(json.error, {type: "error"}); setLoading(false); } } }; return ( <Form onSubmit={onSubmit}> <CardElement options={{ hidePostalCode: true }} /> <Button variant="standard" className="mt-4" block type="submit" disabled={loading}> {loading ? "処理中..." : "支払い"} </Button> </Form> ); }; const stripePromise = loadStripe('pk_test_51Nc5GWJqlrVWXclctY7LiQ0DtBctH1yfgxOdwr65trLpvO6PlEZ4JNKXtYysbRLxF7xrkR7bW8Ojq9xkg6OLkwwb00rwFLWXp9'); const StripeContext = (props) => ( <Elements stripe={stripePromise}> <PaymentForm {...props} /> </Elements> ); export default StripeContext;
「src/pages/Menu.js」ファイルを編集します。
記述編集 【QRMenu/qrmenu_react/src/pages/Menu.js】
import { Container, Row, Col, Button } from 'react-bootstrap'; import { IoCloseOutline } from 'react-icons/io5'; import { useParams } from 'react-router-dom'; import React, { useState, useEffect, useMemo } from 'react'; import { fetchPlace } from '../apis'; import styled from 'styled-components'; import MenuList from '../components/MenuList'; import ShoppingCart from '../components/ShoppingCart'; const OrderButton = styled(Button)` position: fixed; bottom: 20px; right: 20px; border-radius: 50%; box-shadow: 1px 1px 8px rgba(0,0,0,0.2); width: 100px; height: 100px; } `; const Menu = () => { const [ place, setPlace ] = useState({}); const [shoppingCart, setShoppingCart] = useState({}); const [showShoppingCart, setShowShoppingCart] = useState(false); const params = useParams(); const onFetchPlace = async () => { const json = await fetchPlace(params.id); console.log(json); if(json) { setPlace(json); } }; const onAddItemtoShoppingCart = (item) => { setShoppingCart({ ...shoppingCart, [item.id]:{ ...item, quantity: (shoppingCart[item.id]?.quantity || 0) + 1, } }); } const onRemoveItemToShoppingCart = (item) => { if(totalQuantity === 1) { setShowShoppingCart(false); } setShoppingCart({ ...shoppingCart, [item.id]:{ ...item, quantity: (shoppingCart[item.id]?.quantity || 0) - 1, } }); } const onPaymentDone = () => { setShoppingCart({}); setShowShoppingCart(false); } const totalQuantity = useMemo( () => Object.keys(shoppingCart) .map((i) => shoppingCart[i].quantity) .reduce((a,b) => a+ b, 0), [shoppingCart] ); useEffect(() => { onFetchPlace(); }, []); return ( <Container ClassName="mt-5 mb-5"> <Row className="justify-content-center"> <Col lg={8}> {showShoppingCart ? ( <ShoppingCart items={Object.keys(shoppingCart) .map((key) => shoppingCart[key]) .filter((item) => item.quantity > 0) } onAdd={onAddItemtoShoppingCart} onRemove={onRemoveItemToShoppingCart} onPaymentDone={onPaymentDone} /> ) : ( <MenuList place={place} shoppingCart={shoppingCart} onOrder={onAddItemtoShoppingCart} /> )} </Col> </Row> {totalQuantity ? ( <OrderButton variant="standard" onClick={() => setShowShoppingCart(!showShoppingCart)}> {showShoppingCart ? <IoCloseOutline size={25} /> : `${totalQuantity}個注文`} </OrderButton> ) : null} </Container> ) }; export default Menu;
「src/components/ShoppingCart.js」ファイルを編集します。
記述編集 【Desktop/QRMenu/qrmenu_react/src/components/ShoppingCart.js】
import React, {useMemo } from 'react'; import { Card } from 'react-bootstrap'; import OperationButton from './OperationButton'; import { AiFillPlusCircle, AiFillMinusCircle } from 'react-icons/ai'; import PaymentForm from '../containers/PaymentForm'; const ShoppingCart = ({ items, onAdd, onRemove, onPaymentDone }) => { const totalPrice = useMemo( () => items.map((i) => i.quantity * i.price).reduce((a,b) => a+b, 0), [items] ); return ( <> <h3 className="text-center mb-4 mt-5"> <b>注文内容</b> </h3> <Card> <Card.Body> {items.map((item) => ( <div key={item.id} className="d-flex mb-4 align-items-center"> <div className="flex-grow-1"> <p className="mb-0"> <b>{item.name}</b> </p> <span>{item.price}円</span> </div> <div className="d-flex align-items-center"> <OperationButton variant="lightgray" size="sm" onClick={() => onRemove(item)} > <AiFillMinusCircle size={25} color="red" /> </OperationButton> <span> {item.quantity}</span> <OperationButton variant="lightgray" size="sm" onClick={() => onAdd(item)} > <AiFillPlusCircle size={25} color="blue" /> </OperationButton> </div> </div> ))} <hr/> <div className="d-flex justify-content-between"> <h6><b>合計金額</b></h6> <h5><b>{totalPrice.toLocaleString()}円</b></h5> </div> <hr className="mb-4"/> <PaymentForm amount={totalPrice} items={items} onDone={onPaymentDone} /> </Card.Body> </Card> </> ); } export default ShoppingCart;
クレジットカード決済ができるようになりました。
↓↓クリックして頂けると励みになります。
[35 | クレジットカード フォーム] << [ホーム] >> [37 | 受注データ実装]