ECサイトやWebサービスに欠かせないのが決済システムです。世の中に数ある決済サービスの中から今回は「Square」という決済サービスをシステムへ導入していきます。

バージョン
Laravel 8.60.0
Square API Version 2021-09-15

前回はSquare決済を導入する準備をしました。今回はWebサイトに決済登録フォームを作っていきます。

決済フォームの作成

早速、決済フォームページを作っていきます。

適当なbladeファイルを作成し、以下のように記述してください。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Laravel × Square</title>

        <!-- CSRF Token -->
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
        <!-- Styles -->
        <link href="{{ asset('css/app.css') }}" rel="stylesheet">
        <!-- Script -->
        <script src="{{ asset('js/app.js') }}"></script>
        <!-- Square -->
        <style>
            * {
                box-sizing: border-box;
            }

            body {
                font-family: Arial, sans-serif;
            }

            #payment-form {
                max-width: 550px;
                min-width: 300px;
                margin: 150px auto;
            }

            .buyer-inputs {
                display: flex;
                gap: 20px;
                justify-content: space-between;
                border: none;
                margin: 0;
                padding: 0;
            }

            #card-container {
                margin-top: 45px;
                /* this height depends on the size of the container element */
                /* We transition from a single row to double row at 485px */
                /* Settting this min-height minimizes the impact of the card form loading */
                min-height: 90px;
            }

            #gift-card-container {
                margin-top: 45px;
                min-height: 90px;
            }

            @media screen and (max-width: 500px) {
                #card-container {
                    min-height: 140px;
                }
            }

            #ach-button {
                margin-top: 20px;
            }

            #landing-page-layout {
                width: 80%;
                margin: 150px auto;
                max-width: 1000px;
            }

            #its-working {
                color: #737373;
            }

            #example-container {
                width: 100%;
                border: 1px solid #b3b3b3;
                padding: 48px;
                margin: 32px 0;
                border-radius: 12px;
            }

            #example-list {
                display: flex;
                flex-direction: column;
                gap: 15px;
            }

            h3 {
                margin: 0;
            }

            p {
                line-height: 24px;
            }

            label {
                font-size: 12px;
                width: 100%;
            }

            input {
                padding: 12px;
                width: 100%;
                border-radius: 5px;
                border-width: 1px;
                margin-top: 20px;
                font-size: 16px;
                border: 1px solid rgba(0, 0, 0, 0.15);
            }

            input:focus {
                border: 1px solid #006aff;
            }

            button {
                color: #ffffff;
                background-color: #006aff;
                border-radius: 5px;
                cursor: pointer;
                border-style: none;
                user-select: none;
                outline: none;
                font-size: 16px;
                font-weight: 500;
                line-height: 24px;
                padding: 12px;
                width: 100%;
                box-shadow: 1px;
            }

            button:active {
                background-color: rgb(0, 85, 204);
            }

            button:disabled {
                background-color: rgba(0, 0, 0, 0.05);
                color: rgba(0, 0, 0, 0.3);
            }

            #payment-status-container {
                display: flex;
                align-items: center;
                justify-content: center;
                border: 1px solid rgba(0, 0, 0, 0.05);
                box-sizing: border-box;
                border-radius: 50px;
                margin: 0 auto;
                width: 225px;
                height: 48px;
                visibility: hidden;
            }

            #payment-status-container.missing-credentials {
                width: 350px;
            }

            #payment-status-container.is-success:before {
                content: '';
                background-color: #00b23b;
                width: 16px;
                height: 16px;
                margin-right: 16px;
                -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM11.7071 6.70711C12.0968 6.31744 12.0978 5.68597 11.7093 5.29509C11.3208 4.90422 10.6894 4.90128 10.2973 5.28852L11 6C10.2973 5.28852 10.2973 5.28853 10.2973 5.28856L10.2971 5.28866L10.2967 5.28908L10.2951 5.29071L10.2886 5.29714L10.2632 5.32224L10.166 5.41826L9.81199 5.76861C9.51475 6.06294 9.10795 6.46627 8.66977 6.90213C8.11075 7.4582 7.49643 8.07141 6.99329 8.57908L5.70711 7.29289C5.31658 6.90237 4.68342 6.90237 4.29289 7.29289C3.90237 7.68342 3.90237 8.31658 4.29289 8.70711L6.29289 10.7071C6.68342 11.0976 7.31658 11.0976 7.70711 10.7071L11.7071 6.70711Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E");
                mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM11.7071 6.70711C12.0968 6.31744 12.0978 5.68597 11.7093 5.29509C11.3208 4.90422 10.6894 4.90128 10.2973 5.28852L11 6C10.2973 5.28852 10.2973 5.28853 10.2973 5.28856L10.2971 5.28866L10.2967 5.28908L10.2951 5.29071L10.2886 5.29714L10.2632 5.32224L10.166 5.41826L9.81199 5.76861C9.51475 6.06294 9.10795 6.46627 8.66977 6.90213C8.11075 7.4582 7.49643 8.07141 6.99329 8.57908L5.70711 7.29289C5.31658 6.90237 4.68342 6.90237 4.29289 7.29289C3.90237 7.68342 3.90237 8.31658 4.29289 8.70711L6.29289 10.7071C6.68342 11.0976 7.31658 11.0976 7.70711 10.7071L11.7071 6.70711Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E");
            }

            #payment-status-container.is-success:after {
                content: 'Payment successful';
                font-size: 14px;
                line-height: 16px;
            }

            #payment-status-container.is-failure:before {
                content: '';
                background-color: #cc0023;
                width: 16px;
                height: 16px;
                margin-right: 16px;
                -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM5.70711 4.29289C5.31658 3.90237 4.68342 3.90237 4.29289 4.29289C3.90237 4.68342 3.90237 5.31658 4.29289 5.70711L6.58579 8L4.29289 10.2929C3.90237 10.6834 3.90237 11.3166 4.29289 11.7071C4.68342 12.0976 5.31658 12.0976 5.70711 11.7071L8 9.41421L10.2929 11.7071C10.6834 12.0976 11.3166 12.0976 11.7071 11.7071C12.0976 11.3166 12.0976 10.6834 11.7071 10.2929L9.41421 8L11.7071 5.70711C12.0976 5.31658 12.0976 4.68342 11.7071 4.29289C11.3166 3.90237 10.6834 3.90237 10.2929 4.29289L8 6.58579L5.70711 4.29289Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E%0A");
                mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM5.70711 4.29289C5.31658 3.90237 4.68342 3.90237 4.29289 4.29289C3.90237 4.68342 3.90237 5.31658 4.29289 5.70711L6.58579 8L4.29289 10.2929C3.90237 10.6834 3.90237 11.3166 4.29289 11.7071C4.68342 12.0976 5.31658 12.0976 5.70711 11.7071L8 9.41421L10.2929 11.7071C10.6834 12.0976 11.3166 12.0976 11.7071 11.7071C12.0976 11.3166 12.0976 10.6834 11.7071 10.2929L9.41421 8L11.7071 5.70711C12.0976 5.31658 12.0976 4.68342 11.7071 4.29289C11.3166 3.90237 10.6834 3.90237 10.2929 4.29289L8 6.58579L5.70711 4.29289Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E%0A");
            }

            #payment-status-container.is-failure:after {
                content: 'Payment failed';
                font-size: 14px;
                line-height: 16px;
            }

            #payment-status-container.missing-credentials:before {
                content: '';
                background-color: #cc0023;
                width: 16px;
                height: 16px;
                margin-right: 16px;
                -webkit-mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM5.70711 4.29289C5.31658 3.90237 4.68342 3.90237 4.29289 4.29289C3.90237 4.68342 3.90237 5.31658 4.29289 5.70711L6.58579 8L4.29289 10.2929C3.90237 10.6834 3.90237 11.3166 4.29289 11.7071C4.68342 12.0976 5.31658 12.0976 5.70711 11.7071L8 9.41421L10.2929 11.7071C10.6834 12.0976 11.3166 12.0976 11.7071 11.7071C12.0976 11.3166 12.0976 10.6834 11.7071 10.2929L9.41421 8L11.7071 5.70711C12.0976 5.31658 12.0976 4.68342 11.7071 4.29289C11.3166 3.90237 10.6834 3.90237 10.2929 4.29289L8 6.58579L5.70711 4.29289Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E%0A");
                mask: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM5.70711 4.29289C5.31658 3.90237 4.68342 3.90237 4.29289 4.29289C3.90237 4.68342 3.90237 5.31658 4.29289 5.70711L6.58579 8L4.29289 10.2929C3.90237 10.6834 3.90237 11.3166 4.29289 11.7071C4.68342 12.0976 5.31658 12.0976 5.70711 11.7071L8 9.41421L10.2929 11.7071C10.6834 12.0976 11.3166 12.0976 11.7071 11.7071C12.0976 11.3166 12.0976 10.6834 11.7071 10.2929L9.41421 8L11.7071 5.70711C12.0976 5.31658 12.0976 4.68342 11.7071 4.29289C11.3166 3.90237 10.6834 3.90237 10.2929 4.29289L8 6.58579L5.70711 4.29289Z' fill='black' fill-opacity='0.9'/%3E%3C/svg%3E%0A");
            }

            #payment-status-container.missing-credentials:after {
                content: 'applicationId and/or locationId is incorrect';
                font-size: 14px;
                line-height: 16px;
            }
        </style>
        <script
            type="text/javascript"
            src="https://sandbox.web.squarecdn.com/v1/square.js"
        ></script>
        <script>
            const appId = "{{ config('square.application_id') }}";
            const locationId = "{{ config('square.location_id') }}";

            async function initializeCard(payments) {
                const card = await payments.card();
                await card.attach('#card-container');
                return card;
            }

            // Call this function to send a payment token, buyer name, and other details
            // to the project server code so that a payment can be created with
            // Payments API
            async function createPayment(token) {
                const body = JSON.stringify({
                    locationId,
                    sourceId: token,
                });
                const paymentResponse = await fetch('/square/createPayment', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                    },
                    body,
                });
                if (paymentResponse.ok) {
                    return paymentResponse.json();
                }
                const errorBody = await paymentResponse.text();
                throw new Error(errorBody);
            }

            // This function tokenizes a payment method.
            // The ‘error’ thrown from this async function denotes a failed tokenization,
            // which is due to buyer error (such as an expired card). It is up to the
            // developer to handle the error and provide the buyer the chance to fix
            // their mistakes.
            async function tokenize(paymentMethod) {
                const tokenResult = await paymentMethod.tokenize();
                if (tokenResult.status === 'OK') {
                    return tokenResult.token;
                } else {
                    let errorMessage = `Tokenization failed-status: ${tokenResult.status}`;
                    if (tokenResult.errors) {
                        errorMessage += ` and errors: ${JSON.stringify(
                            tokenResult.errors
                        )}`;
                    }
                    throw new Error(errorMessage);
                }
            }

            // Helper method for displaying the Payment Status on the screen.
            // status is either SUCCESS or FAILURE;
            function displayPaymentResults(status) {
                const statusContainer = document.getElementById(
                    'payment-status-container'
                );
                if (status === 'SUCCESS') {
                    statusContainer.classList.remove('is-failure');
                    statusContainer.classList.add('is-success');
                } else {
                    statusContainer.classList.remove('is-success');
                    statusContainer.classList.add('is-failure');
                }

                statusContainer.style.visibility = 'visible';
            }

            document.addEventListener('DOMContentLoaded', async function () {
                if (!window.Square) {
                    throw new Error('Square.js failed to load properly');
                }

                const payments = window.Square.payments(appId, locationId);
                let card;
                try {
                    card = await initializeCard(payments);
                } catch (e) {
                    console.error('Initializing Card failed', e);
                    return;
                }

                // Checkpoint 2.
                async function handlePaymentMethodSubmission(event, paymentMethod) {
                    event.preventDefault();

                    try {
                        // disable the submit button as we await tokenization and make a
                        // payment request.
                        cardButton.disabled = true;
                        const token = await tokenize(paymentMethod);
                        const paymentResults = await createPayment(token);
                        displayPaymentResults('SUCCESS');

                        console.debug('Payment Success', paymentResults);
                    } catch (e) {
                        cardButton.disabled = false;
                        displayPaymentResults('FAILURE');
                        console.error(e.message);
                    }
                }

                const cardButton = document.getElementById(
                    'card-button'
                );
                cardButton.addEventListener('click', async function (event) {
                    await handlePaymentMethodSubmission(event, card);
                });
            });
        </script>
    </head>
    <body>
        <div id="app">
            <div class="m-auto w-50 m-5 p-5">
                <form id="payment-form">
                    <div id="card-container"></div>
                    <button id="card-button" type="button">1,000円支払う</button>
                </form>
                <div id="payment-status-container"></div>
            </div>
        </div>
    </body>
</html>

これでページを表示してみてください。
下図のような決済フォームができます。

実はこれでもうフロント側は完了です!簡単ですよね!

次にサーバーサイドを実装します。
web.phpにpostルートを設定します。

Route::post('square/createPayment', [SquareController::class, 'createPayment'])->name('square.createPayment');
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class SquareController extends Controller
{
    public function createPayment(Request $request)
    {
        return true;
    }
}

今回は決済フォームの実装なので単にtrueを返すだけにします。これでサーバー側も完了です。

実際にsandbox用のカード番号で試してみましょう!
カード番号に4111 1111 1111 1111、MM/YY は当月以降の年月を適当に入力し、CVVも適当な数字を入力します。カード番号を入力するとZIPという項目が表示されますがこちらも適当な数字を入力してください。ZIPとは郵便番号だそうですが、日本のクレジットカード番号を入力した時はZIP項目は表示されないかと思います(実際試してないので本番環境で試してみてください)。全て入力できたらボタンを押下してください。

下図のように支払い完了の表示がされます。

現段階では実際に決済は実行されておらずフロントの表示のみとなります。

表示はいろいろ変えられるみたいなので下記のドキュメントも覗いてみてください。

https://square.github.io/web-payments-showcase

次回はサーバーサイドで決済処理を実装していきましょう!

関連記事

参考URL

Square Developer
Square公式ドキュメント クレカ決済フォーム
SquareAPIリファレンス

この記事をシェアする

Laravel開発のお悩み相談・学習サポートはこちら

じゅのきちメンターサービス

じゅのきちメンターサービス