Setelah berhasil menghitung biaya ongkos kirim, langkah berikutnya adalah mempelajari cara melakukan proses checkout. Pada tahap ini, kita akan membuat sebuah komponen button yang berfungsi untuk memulai proses checkout ketika button tersebut diklik.
Pada proses checkout, kita akan mengirimkan permintaan (request) ke payment gateway (Midtrans) untuk menghasilkan snap token. Token ini akan digunakan sebagai acuan untuk menampilkan popup pembayaran, sehingga pengguna dapat menyelesaikan transaksi dengan mudah dan aman.
Langkah 1 - Membuat Component Button Checkout
Silahkan teman-teman jalankan perintah berikut ini di dalam terminal/CMD dan pastikan berada di dalam project Laravel-nya.
go
php artisan make:livewire Web/Checkout/BtnCheckout
Jika perintah di atas berhasil dijalankan, maka kita akan mendapatkan 2 file baru, yaitu:
- Class Component:
app/Livewire/Web/Checkout/BtnCheckout.php - View Component:
resources/views/livewire/web/checkout/btn-checkout.blade.php
Langkah 2 - Membuat Fungsi Checkout
Sekarang kita akan menambahkan fungsi checkout di dalam class component, dimana fungsi ini nantinya akan di trigger saat button checkout diklik.
Silahkan buka file app/Livewire/Web/Checkout/BtnCheckout.php, kemudian ubah semua kode-nya menjadi seperti berikut ini.
php
<?php
namespace App\Livewire\Web\Checkout;
use Midtrans\Snap;
use App\Models\Cart;
use Livewire\Component;
use App\Models\Transaction;
use Illuminate\Support\Facades\DB;
class BtnCheckout extends Component
{
public $province_id;
public $city_id;
public $address;
public $selectCourier;
public $selectService;
public $selectCost;
public $totalWeight;
public $grandTotal;
public $response;
public $loading;
/**
* __construct
*
* @return void
*/
public function __construct()
{
// Set midtrans configuration
\Midtrans\Config::$serverKey = config('midtrans.server_key');
\Midtrans\Config::$isProduction = config('midtrans.is_production');
\Midtrans\Config::$isSanitized = config('midtrans.is_sanitized');
\Midtrans\Config::$is3ds = config('midtrans.is_3ds');
}
public function mount($selectCourier = null, $selectService = null, $selectCost = null)
{
$this->selectCourier = $selectCourier;
$this->selectService = $selectService;
$this->selectCost = $selectCost;
}
/**
* storeCheckout
*
* @return void
*/
public function storeCheckout()
{
// Set loading
$this->loading = true;
$customer = auth()->guard('customer')->user();
// Validasi awal
if (!$customer || !$this->province_id || !$this->city_id || !$this->address || !$this->grandTotal) {
session()->flash('error', 'Data tidak lengkap. Silakan periksa kembali.');
return;
}
try {
DB::transaction(function () use ($customer) {
// Buat kode invoice
$invoice = 'INV-' . mt_rand(1000, 9999);
// Buat transaksi
$transaction = Transaction::create([
'customer_id' => $customer->id,
'invoice' => $invoice,
'province_id' => $this->province_id,
'city_id' => $this->city_id,
'address' => $this->address,
'weight' => $this->totalWeight,
'total' => $this->grandTotal,
'status' => 'PENDING',
]);
// Buat data pengiriman
$transaction->shipping()->create([
'shipping_courier' => $this->selectCourier,
'shipping_service' => $this->selectService,
'shipping_cost' => $this->selectCost,
]);
// Detail item
$item_details = [];
$carts = Cart::where('customer_id', $customer->id)->with('product')->get();
foreach ($carts as $cart) {
// Tambahkan detail transaksi
$transaction->transactionDetails()->create([
'product_id' => $cart->product->id,
'qty' => $cart->qty,
'price' => $cart->product->price,
]);
$item_details[] = [
'id' => $cart->product->id,
'price' => $cart->product->price,
'quantity' => $cart->qty,
'name' => $cart->product->title,
];
}
// Tambahkan ongkos kirim ke item details
$item_details[] = [
'id' => 'shipping',
'price' => $this->selectCost,
'quantity' => 1,
'name' => 'Ongkos Kirim: ' . $this->selectCourier . ' - ' . $this->selectService,
];
// Hapus keranjang setelah checkout
Cart::where('customer_id', $customer->id)->delete();
// Payload untuk Midtrans
$payload = [
'transaction_details' => [
'order_id' => $invoice,
'gross_amount' => $this->grandTotal,
],
'customer_details' => [
'first_name' => $customer->name,
'email' => $customer->email,
'shipping_address' => $this->address,
],
'item_details' => $item_details,
];
// Dapatkan token snap Midtrans
$snapToken = Snap::getSnapToken($payload);
// Simpan snap token ke transaksi
$transaction->snap_token = $snapToken;
$transaction->save();
// Simpan respons snap token
$this->response['snap_token'] = $snapToken;
// Set loading
$this->loading = false;
});
// Flash session dan redirect
session()->flash('success', 'Silahkan lakukan pembayaran untuk melanjutkan proses checkout.');
return $this->redirect('/account/my-orders/' . $this->response['snap_token'], navigate: true);
} catch (\Exception $e) {
// Tangani error
session()->flash('error', 'Terjadi kesalahan saat memproses checkout. Silakan coba lagi.');
// Set loading
$this->loading = false;
return;
}
}
public function render()
{
return view('livewire.web.checkout.btn-checkout');
}
}
Dari perubahan kode di atas, pertama kita import Snap dari Midtrans.
perl
use Midtrans\Snap;
Kemudian kita import Model Cart dan Transaction.
perl
use App\Models\Cart;
use App\Models\Transaction;
Dan kita import Facades DB.
perl
use Illuminate\Support\Facades\DB;
Setelah itu, di dalam class kita membuat beberapa properti yang sama seperti yang ada pada component checkout index. Karena kita akan butuh semua data tersebut untuk proses checkout.
php
public $province_id;
public $city_id;
public $address;
public $selectCourier;
public $selectService;
public $selectCost;
public $totalWeight;
public $grandTotal;
public $response;
public $loading;
Kemudian kita membuat method __construct yang berisi konfigurasi dari payment gateway Midtrans, seperti informasi server key, client key, environment, dan lain-lain.
scss
public function __construct()
{
// Set midtrans configuration
\Midtrans\Config::$serverKey = config('midtrans.server_key');
\Midtrans\Config::$isProduction = config('midtrans.is_production');
\Midtrans\Config::$isSanitized = config('midtrans.is_sanitized');
\Midtrans\Config::$is3ds = config('midtrans.is_3ds');
}
Selanjutnya, kita buat method mount dimana di dalamnya kita melakukan assign 3 properti untuk jasa pengiriman.
php
public function mount($selectCourier = null, $selectService = null, $selectCost = null)
{
$this->selectCourier = $selectCourier;
$this->selectService = $selectService;
$this->selectCost = $selectCost;
}
Kemudian kita buat method storeCheckout.
csharp
public function storeCheckout()
{
//...
}
Di dalamnya, pertama kita set properti loading menjadi true.
kotlin
$this->loading = true;
Kemudian kita buat variable $customer yang berisi data customer yang sedang login.
php
$customer = auth()->guard('customer')->user();
Selanjutnya kita membuat pengecekan data, jika data yang dikirimkan masih ada yang kosong, maka kita akan menampilkan sebuah pesan error.
php
if (!$customer || !$this->province_id || !$this->city_id || !$this->address || !$this->grandTotal) {
session()->flash('error', 'Data tidak lengkap. Silakan periksa kembali.');
return;
}
Tapi jika data sudah sesuai semua, maka kita akan lakukan proses checkout di dalam DB Transaction.
php
DB::transaction(function () use ($customer) {
//...
});
Di dalamnya, pertama kita membuat kode invoice.
bash
$invoice = 'INV-' . mt_rand(1000, 9999);
Selanjutnya melakukan insert data transaction menggunakan Model.
php
$transaction = Transaction::create([
'customer_id' => $customer->id,
'invoice' => $invoice,
'province_id' => $this->province_id,
'city_id' => $this->city_id,
'address' => $this->address,
'weight' => $this->totalWeight,
'total' => $this->grandTotal,
'status' => 'PENDING',
]);
Setelah data transaction berhasil diinsert, maka kita lanjutkan melakukan proses insert data pengiriman atau shipping, disini kita memanggil relasi shipping untuk proses create data-nya.
php
$transaction->shipping()->create([
'shipping_courier' => $this->selectCourier,
'shipping_service' => $this->selectService,
'shipping_cost' => $this->selectCost,
]);
Selanjutnya, kita melakukan get data cari charts untuk kita jadikan item details di dalam payment gateway dan termasuk juga data pengiriman (shipping).
php
// Detail item
$item_details = [];
$carts = Cart::where('customer_id', $customer->id)->with('product')->get();
foreach ($carts as $cart) {
// Tambahkan detail transaksi
$transaction->transactionDetails()->create([
'product_id' => $cart->product->id,
'qty' => $cart->qty,
'price' => $cart->product->price,
]);
$item_details[] = [
'id' => $cart->product->id,
'price' => $cart->product->price,
'quantity' => $cart->qty,
'name' => $cart->product->title,
];
}
// Tambahkan ongkos kirim ke item details
$item_details[] = [
'id' => 'shipping',
'price' => $this->selectCost,
'quantity' => 1,
'name' => 'Ongkos Kirim: ' . $this->selectCourier . ' - ' . $this->selectService,
];
Selanjutnya kita hapus data cart berdasarkan data customer.
php
Cart::where('customer_id', $customer->id)->delete();
Kemudian kita masukkan semua data di atas di dalam payload, yang mana nanti akan dikirimkan ke payment gateway Midtrans untuk dibuatkan sebuah snap token.
php
$payload = [
'transaction_details' => [
'order_id' => $invoice,
'gross_amount' => $this->grandTotal,
],
'customer_details' => [
'first_name' => $customer->name,
'email' => $customer->email,
'shipping_address' => $this->address,
],
'item_details' => $item_details,
];
Setelah itu kita lakukan generate snap token berdasarkan data yang ada di dalam payload di atas menggunakan Snap::getSnapToken.
php
// Dapatkan token snap Midtrans
$snapToken = Snap::getSnapToken($payload);
Jika snap token berhasil dibuat, maka kita akan simpan ke dalam data transaction.
php
// Simpan snap token ke transaksi
$transaction->snap_token = $snapToken;
$transaction->save();
Kemudian kita return data snap_token-nya ke dalam properti response.
php
$this->response['snap_token'] = $snapToken;
Dan kita set properti loading menjadi false.
kotlin
$this->loading = false;
Dan terakhir, kita buat session flash yang berisi success melakukan proses checkout dan kita akan diarahkan ke dalam URL /account/my-orders/:snap_token.
kotlin
session()->flash('success', 'Silahkan lakukan pembayaran untuk melanjutkan proses checkout.');
return $this->redirect('/account/my-orders/' . $this->response['snap_token'], navigate: true);
Langkah 2 - Membuat Button Checkout
Sekarang kita akan membuat button checkout yang nanti di dalamnya akan mentrigger method storeCheckout yang sudah kita buat di atas pada class component.
Silahkan buka file resources/views/livewire/web/checkout/btn-checkout.blade.php, kemudian ubah semua kode-nya menjadi seperti berikut ini.
perl
<button wire:click="storeCheckout" class="btn btn-orange-2 rounded border-0 shadow-sm w-100" @if($grandTotal == 0 || $address == '') disabled @endif>@if($loading) Processing @else Process to Payment @endif</button>
Di atas, kita menambahkan event wire:click yang mengarah ke method storeCheckout. Kemudian kita juga membuat kondisi disabled buttonnya jika nilai dari properti $grandTotal adalah 0 dan $address masih kosong.
Langkah 3 - Memanggil Component Button Checkout
Sekarang kita akan panggil component button checkout di atas pada halaman checkout index. Silahkan teman-teman buka file resources/views/livewire/web/checkout/index.blade.php, kemudian ubah semua kode-nya menjadi seperti berikut ini.
xml
@section('title')
Checkout - Eat Your Favorite Foods
@stop
@section('keywords')
Food Store, Eat Your Favorite Foods
@stop
@section('description')
Checkout - Food Store - Eat Your Favorite Foods
@stop
@section('image')
{{ asset('images/logo.png') }}
@stop
<div>
<div class="container">
<div class="row justify-content-center mt-0" style="margin-bottom: 320px;">
<div class="col-md-6">
<div class="bg-white rounded-bottom-custom shadow-sm p-3 sticky-top mb-3">
<div class="d-flex justify-content-start">
<div>
<x-buttons.back />
</div>
</div>
</div>
<div class="card rounded shadow-sm border-0 mb-4">
<div class="card-body">
<h6>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-geo-alt mb-1" viewBox="0 0 16 16">
<path d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A32 32 0 0 1 8 14.58a32 32 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10" />
<path d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4m0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6" />
</svg>
Shipping Information
</h6>
<hr />
<select class="form-select rounded mb-3" wire:model.live="province_id">
<option value="">-- Select Province --</option>
@foreach ($provinces as $province)
<option value="{{ $province->id }}">
{{ $province->name }}
</option>
@endforeach
</select>
<select class="form-select rounded mb-3" wire:model.live="city_id" wire:key="{{ $province_id }}">
<option value="">-- Select City --</option>
@foreach (\App\Models\City::where('province_id', $province_id)->get() as $city)
<option value="{{ $city->id }}">{{ $city->name }}</option>
@endforeach
</select>
<div class="mb-3">
<textarea class="form-control rounded" wire:model.live="address" rows="3" placeholder="Address: Jl. Kebon Jeruk No. 1, Jakarta Barat"></textarea>
</div>
</div>
</div>
@if($city_id)
<div class="card rounded shadow-sm border-0 mb-5">
<div class="card-body">
<h6>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-truck mb-1" viewBox="0 0 16 16">
<path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h9A1.5 1.5 0 0 1 12 3.5V5h1.02a1.5 1.5 0 0 1 1.17.563l1.481 1.85a1.5 1.5 0 0 1 .329.938V10.5a1.5 1.5 0 0 1-1.5 1.5H14a2 2 0 1 1-4 0H5a2 2 0 1 1-3.998-.085A1.5 1.5 0 0 1 0 10.5zm1.294 7.456A2 2 0 0 1 4.732 11h5.536a2 2 0 0 1 .732-.732V3.5a.5.5 0 0 0-.5-.5h-9a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .294.456M12 10a2 2 0 0 1 1.732 1h.768a.5.5 0 0 0 .5-.5V8.35a.5.5 0 0 0-.11-.312l-1.48-1.85A.5.5 0 0 0 13.02 6H12zm-9 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2m9 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2" />
</svg>
Courier Delivery
</h6>
<hr />
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="courier" id="inlineRadio1" wire:click="changeCourier('jne')">
<label class="form-check-label" for="inlineRadio1">JNE</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="courier" id="inlineRadio2" wire:click="changeCourier('pos')">
<label class="form-check-label" for="inlineRadio2">POS</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="courier" id="inlineRadio3" wire:click="changeCourier('tiki')">
<label class="form-check-label" for="inlineRadio3">TIKI</label>
</div>
<div class="justify-content-center mt-3 mb-3 text-center">
<div wire:loading wire:target="changeCourier">
<div class="spinner-border text-orange" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h6 class="mt-2 text-orange">Loading...</h6>
</div>
</div>
{{-- Cost options --}}
@if($showCost)
<hr>
@endif
@if($showCost)
<div class="mb-3">
@foreach($costs ?? [] as $cost)
<div class="form-check form-check-inline">
<label class="form-check-label font-weight-normal me-2">
<input class="form-check-input" type="radio" name="cost" wire:click="getServiceAndCost('{{ $cost['cost'][0]['value'] }}|{{ $cost['service'] }}')" />
<span class="ms-1">{{ $cost['service'] }} - Rp {{ number_format($cost['cost'][0]['value'], 0, ',', '.') }}</span>
</label>
</div>
@endforeach
</div>
@endif
</div>
</div>
@endif
</div>
</div>
</div>
<div class="container fixed-total">
<div class="row justify-content-center">
<div class="col-12 col-md-6">
<div class="card rounded shadow-sm border-0 mb-5">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="mb-0">Total</h6>
</div>
<div class="ms-auto">
<h6 class="mb-0">Rp. {{ number_format($totalPrice) }}</h6>
</div>
</div>
<div class="d-flex justify-content-between mt-3">
<div>
<h6 class="mb-0">Ongkos Kirim</h6>
</div>
<div class="ms-auto">
<h6 class="mb-0">Rp. {{ number_format($selectCost) }}</h6>
</div>
</div>
<div class="d-flex justify-content-between mt-3">
<div>
<h5 class="fw-bold mb-0">Grand Total</h5>
</div>
<div class="ms-auto">
<h5 class="fw-bold mb-0">Rp. {{ number_format($grandTotal) }}</h5>
</div>
</div>
@if($selectCost > 0)
<hr style="border: dotted 1px #e92715;">
<livewire:web.checkout.btn-checkout
key="{{ now() }}"
:province_id="$province_id"
:city_id="$city_id"
:address="$address"
:grandTotal="$grandTotal"
:totalWeight="$totalWeight"
:selectCourier="$selectCourier"
:selectService="$selectService"
:selectCost="$selectCost"
/>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
Dari perubahan kode di atas, kita membuat kondisi untuk menampilkan component button checkout, yaitu jika properti selectCost atau biaya ongkos memiliki nilai di atas 0, maka button checkout akan ditampilkan.
less
@if($selectCost > 0)
//...
@endif
Di dalamnya, kita menampilkan component button checkout dengan memberikan beberapa props.
bash
<livewire:web.checkout.btn-checkout
key="{{ now() }}"
:province_id="$province_id"
:city_id="$city_id"
:address="$address"
:grandTotal="$grandTotal"
:totalWeight="$totalWeight"
:selectCourier="$selectCourier"
:selectService="$selectService"
:selectCost="$selectCost"
/>
Langkah 4 - Uji Coba Checkout
Silahkan buka halaman checkout index kemudian lakukan pengecekan biaya ongkos kirim, kemudian klik button Process to Payment, jika berhasil maka kita akan diarahkan ke dalam halaman detail my order.
