Laravel

Shopping Cart Laravel Livewire with Nusagate API

Build shopping cart with laravel livewire and integrate it with Nusagate API.

Installation

First of all we will initialize a Laravel project from scratch. To do this, we execute:

composer create-project --prefer-dist laravel/laravel shopping-cart

Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving the comfort of Laravel (Livewire Docs). We will implement livewire so we need to install livewire package by executed this command :

composer require livewire/livewire

Product System

We will create the product’s entity with its model, migration, factory and data seeder. Factory is how we obtain fake data to run tests with the project. Then, execute the following command:

php artisan make:model -msf Product
  • Update the migration to add several property of Product

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('description');
            $table->float('price');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
};

database/migrations/xxxx_xx_xx_xxxxxx_create_products_table.php

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
 */
class ProductFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'name' => $this->faker->word,
            'description' => $this->faker->text(180),
            'price' => $this->faker->numberBetween(50, 100)
        ];
    }
}

database/factories/ProductFactory.php

Then, Create 10 Product Sample on Seeder

<?php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Product::factory()->count(10)->create();
    }
}

database/seeds/ProductSeeder.php

Next, we need to run the migration and seeder like this :

php artisan migrate:fresh --seed

Cart System

We will create a basic cart system that saved data on Laravel session. In this case, we will create facade Cartthat can do a basic operation to obtain, add, and remove products. With facade we can do the operation with static interface to classes.

  1. Create class App\Helpers\Cart that contains functionality of Cart

<?php

namespace App\Helpers;

use App\Models\Product;

class Cart
{
    public function __construct()
    {
        if ($this->get() === null)
            $this->set($this->empty());
    }

    public function add(Product $product): void
    {
        $cart = $this->get();
        array_push($cart['products'], $product);
        $this->set($cart);
    }

    public function remove(int $productId): void
    {
        $cart = $this->get();
        array_splice($cart['products'], array_search($productId, array_column($cart['products'], 'id')), 1);
        $this->set($cart);
    }

    public function clear(): void
    {
        $this->set($this->empty());
    }

    public function empty(): array
    {
        return [
            'products' => [],
        ];
    }

    public function get(): ?array
    {
        return request()->session()->get('cart');
    }

    private function set($cart): void
    {
        request()->session()->put('cart', $cart);
    }
}

2. Create facade class of Cart on App\Facades\Cart

<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class Cart extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'cart';
    }
}

3. Bind facade and class Cart on AppServiceProvider

public function register() {
    $this->app->bind('cart', function () {
        return new \App\Helpers\Cart;
    });
}

4. Add Facade Cart to list aliases on config/app.php

...
'aliases' => Facade::defaultAliases()->merge([
        // 'ExampleClass' => App\Example\ExampleClass::class,
        'Cart' => App\Facades\Cart::class,
])->toArray(),
...

We need to create navigation bar, in this case we'll create it on header component. So first create Header Component

php artisan make:livewire Header

App\Http\Livewire\Header.php

<?php

namespace App\Http\Livewire;

use App\Facades\Cart;
use Illuminate\View\View;
use Livewire\Component;

class Header extends Component
{
    public $cartTotal = 0;

    protected $listeners = [
        'productAdded' => 'updateCartTotal',
        'productRemoved' => 'updateCartTotal',
        'clearCart' => 'updateCartTotal'
    ];

    public function mount(): void
    {
        $this->cartTotal = count(Cart::get()['products']);
    }

    public function render(): View
    {
        return view('livewire.header');
    }

    public function updateCartTotal(): void
    {
        $this->cartTotal = count(Cart::get()['products']);
    }
}

resources\views\livewire\header.blade.php

<div>
    <nav class="flex items-center justify-between flex-wrap p-6 mb-6 shadow">
        <div class="block lg:hidden">
            <button class="flex items-center px-3 py-2 border rounded text-teal-200 border-teal-400 hover:text-white hover:border-white">
                <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
            </button>
        </div>
        <div class="w-full block flex-grow lg:flex lg:items-center lg:w-auto">
            <div class="text-sm lg:flex-grow">
                <a href="/" data-turbolinks-action="replace" class="block mt-4 lg:inline-block lg:mt-0 mr-4">
                    Home
                </a>
                <a href="/products" data-turbolinks-action="replace" class="block mt-4 lg:inline-block lg:mt-0 mr-4">
                    Products
                </a>
                <a href="/cart" data-turbolinks-action="replace" class="block mt-4 lg:inline-block lg:mt-0 mr-4">
                    Cart({{ $cartTotal }})
                </a>
            </div>
        </div>
    </nav>
</div>

There are 3 menus : Home, Products, and Cart. So, we need to create component for each menu.

php artisan make:livewire Home
php artisan make:livewire Products
php artisan make:livewire Cart

And update the route web.php to this

Route::get('/', App\Http\Livewire\Home::class)->name('home'); 
Route::get('/products', App\Http\Livewire\Products::class)->name('products'); 
Route::get('/cart', App\Http\Livewire\Cart::class)->name('cart');

Run php artisan serve, and check at your localhost. Now, we get our page like this :

Product Page

The product livewire component will pass products data to page with following content like this :

<?php

namespace App\Http\Livewire;

use App\Facades\Cart;
use App\Models\Product;
use Illuminate\View\View;
use Livewire\Component;
use Livewire\WithPagination;

class Products extends Component
{
    use WithPagination;

    public $search;

    protected $updatesQueryString = ['search'];

    public function mount(): void
    {
        $this->search = request()->query('search', $this->search);
    }

    public function render(): View
    {
        return view('livewire.product', [
            'products' => $this->search === null ?
                Product::paginate(12) :
                Product::where('name', 'like', '%' . $this->search . '%')->paginate(12)
        ]);
    }

    public function addToCart(int $productId): void
    {
        Cart::add(Product::where('id', $productId)->first());
        $this->emit('productAdded');
    }
}

and first we need to create our pagination template to load product list, create on views/layout/pagination.blade.php

@if ($paginator->hasPages())
<ul class="pagination" role="navigation">
    @if ($paginator->onFirstPage())
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
        <span class="page-link" aria-hidden="true">
            <span class="d-none d-md-block">&lsaquo;</span>
            <span class="d-block d-md-none">@lang('pagination.previous')</span>
        </span>
    </li>
    @else
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline">
        <button type="button" class="page-link" wire:click="previousPage" rel="prev" aria-label="@lang('pagination.previous')">
            <span class="d-none d-md-block">&lsaquo;</span>
            <span class="d-block d-md-none">@lang('pagination.previous')</span>
        </button>
    </li>
    @endif

    @foreach ($elements as $element)
    @if (is_string($element))
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline disabled d-none d-md-block" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
    @endif

    @if (is_array($element))
    @foreach ($element as $page => $url)
    @if ($page == $paginator->currentPage())
    <li class="text-green-500 inline border-0 border-brand-light px-3 py-2 no-underline active d-none d-md-block" aria-current="page"><span class="page-link">{{ $page }}</span></li>
    @else
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline d-none d-md-block"><button type="button" class="page-link" wire:click="gotoPage({{ $page }})">{{ $page }}</button></li>
    @endif
    @endforeach
    @endif
    @endforeach

    @if ($paginator->hasMorePages())
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline">
        <button type="button" class="page-link" wire:click="nextPage" rel="next" aria-label="@lang('pagination.next')">
            <span class="d-block d-md-none">@lang('pagination.next')</span>
            <span class="d-none d-md-block">&rsaquo;</span>
        </button>
    </li>
    @else
    <li class="inline border-0 border-brand-light px-3 py-2 no-underline disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
        <span class="" aria-hidden="true">
            <span class="d-block d-md-none">@lang('pagination.next')</span>
            <span class="d-none d-md-block">&rsaquo;</span>
        </span>
    </li>
    @endif
</ul>
@endif

The content of product list view is

<div>
    <div class="w-full flex justify-center">
        <input wire:model="search" type="text" class="shadow appearance-none border rounded w-1/2 py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder="Search products by name...">
    </div>


    <div class="w-full flex justify-center">
        <div class="flex flex-col md:flex-wrap md:flex-row p-5">
            @foreach ($products as $product)

            <div class="w-full md:w-1/2 lg:w-1/3 md:px-2 py-2">
                <div class="bg-white rounded shadow p-5 h-full relative">
                    <h5 class="font-black uppercase text-2xl mb-4">
                        {{ $product->name }}
                    </h5>
                    <h6 class="font-bold text-gray-700 text-xl mb-3">{{ $product->price }} USD</h6>
                    <p class="text-gray-900 font-normal mb-12">
                        {{ $product->description }}
                    </p>
                    <div class="flex justify-end mt-5 absolute w-full bottom-0 left-0 pb-5">
                        <button wire:click="addToCart({{ $product->id }})" class="block uppercase font-bold text-blue-600 hover:text-green-500 mr-4">
                            Add to cart
                        </button>
                    </div>
                </div>
            </div>
            @endforeach
        </div>
    </div>

    <div class="w-full flex justify-center pb-6">
        {{ $products->links('layouts.pagination') }}
    </div>
</div>

We have created our page like this :

Cart Page

Cart component handle added product to cart and checkout action. For checkout function we first add functionality to add clear cart before we add integration with Nusagate.

<?php

namespace App\Http\Livewire;

use App\Facades\Cart as CartFacade;
use Carbon\Carbon;
use Livewire\Component;
use Nusagate\Nusagate;


class Cart extends Component
{
    public $cart;
    public $note;
    public $dueDate;
    public $email;

    public function mount(): void
    {
        $this->cart = CartFacade::get();
    }

    public function render()
    {
        return view('livewire.cart');
    }

    public function removeFromCart($productId): void
    {
        CartFacade::remove($productId);
        $this->cart = CartFacade::get();
        $this->totalPrice = 0;
        $this->emit('productRemoved');
    }

    public function checkout()
    {
        CartFacade::clear();
        $this->emit('clearCart');
        $this->cart = CartFacade::get();
    }

}

and the view of cart

<div>
    <div class="w-2/3 mx-auto">
        <div class="bg-white shadow-md rounded my-6">
            @if(count($cart['products']) > 0)
            <table class="text-left w-full border-collapse">
                <thead>
                    <tr>
                        <th class="py-4 px-6 bg-grey-lightest font-bold uppercase text-sm text-grey-dark border-b border-grey-light">Name</th>
                        <th class="py-4 px-6 bg-grey-lightest font-bold uppercase text-sm text-grey-dark border-b border-grey-light">Price</th>
                        <th class="py-4 px-6 bg-grey-lightest font-bold uppercase text-sm text-grey-dark border-b border-grey-light">Actions</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach($cart['products'] as $product)
                    <tr class="hover:bg-grey-lighter">
                        <td class="py-4 px-6 border-b border-grey-light">{{ $product->name }}</td>
                        <td class="py-4 px-6 border-b border-grey-light">{{ $product->price }} USD</td>
                        <td class="py-4 px-6 border-b border-grey-light">
                            <a wire:click="removeFromCart({{ $product->id }})" class="text-green-600 font-bold py-1 px-3 rounded text-xs bg-green hover:bg-green-dark cursor-pointer">Remove</a>
                        </td>
                    </tr>
                    @endforeach
                </tbody>
            </table>

            <div class="m-3">
                <label for="input-group-1" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Your Email</label>
                <div class="relative mb-6">
                    <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
                        <svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                            <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
                            <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
                        </svg>
                    </div>
                    <input wire:model.defer="email" type="text" id="input-group-1" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-3  dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@nusagate.com">
                </div>
            </div>

            <div class="m-3">
                <label for="message" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-400">Order Description</label>
                <textarea wire:model.defer="note" id="message" rows="4" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="..."></textarea>

            </div>

            <div class="text-right w-full p-6">
                <button wire:click="checkout()" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
                    Checkout
                </button>
            </div>


            @else
            <div class="text-center w-full border-collapse p-6">
                <span class="text-lg">Your cart is empty!</span>
            </div>
            @endif


        </div>
    </div>
</div>

The result will be look like this :

Integration with Nusagate API

Before we start to integrate nusagate api, first we need to following requirement steps :

Then, install nusagate php package with run this command

composer require nusagate/nusagate-php

Edit checkout function on Cart Component to following content

 public function checkout()
    {
        $nusagate = new Nusagate(false, 'API_KEY', 'SECRET_KEY');
        $externalId = 'NSGT-' . uniqid();

        $totalPrice = 0;

        foreach ($this->cart['products'] as $product) {
            $totalPrice += $product['price'];
        }

        $payload = array(
            'external_id' => $externalId,
            'price' => $totalPrice,
            'due_date' => Carbon::now()->addDays(1)->format('Y-m-d'),
            'email' => $this->email, //not required
            'description' => $this->note, // not required
            'phone_number' => '6285875517882'
        );

        $response = $nusagate->createInvoice($payload);


        CartFacade::clear();
        $this->emit('clearCart');
        $this->cart = CartFacade::get();

        $url = json_decode($response)->data->paymentLink;

        return redirect()->to($url);
    }

Now, we successufully integrated create invoice on our app. Checkout will be redirect to nusagate page.

Last updated