学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Django3.2 | 36 | QRオーダーシステムの構築 | 支払い処理実装

[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>&nbsp;&nbsp;{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;



クレジットカード決済ができるようになりました。

クレジットカードを入力して注文する
クレジットカードを入力して注文する


注文完了
注文完了


Stripe確認
Stripe確認


posticoでOrderテーブル確認
posticoでOrderテーブル確認


管理画面確認
管理画面確認



↓↓クリックして頂けると励みになります。


[35 | クレジットカード フォーム] << [ホーム] >> [37 | 受注データ実装]