WhatsApp Integration - luckydeva03/barbershop_app GitHub Wiki
Panduan lengkap integrasi WhatsApp untuk booking appointment dan komunikasi customer dengan barbershop.
WhatsApp integration memungkinkan customer untuk:
- Direct Booking: Klik tombol untuk langsung WhatsApp barbershop
- Template Messages: Pesan booking yang sudah diformat
- Store Contact: Kontak langsung ke nomor barbershop
- Automated Response: (Optional) Auto-reply untuk booking
- Click-to-Chat: Simple WhatsApp URL redirection
- WhatsApp Business API: (Advanced) untuk automated messaging
- QR Code: (Optional) untuk quick contact
- Web Widget: (Optional) floating WhatsApp button
<?php
namespace App\Helper;
class WhatsAppHelper
{
/**
* Format phone number for WhatsApp
*/
public static function formatPhoneNumber($phone)
{
if (!$phone) return null;
// Remove all non-numeric characters
$phone = preg_replace('/[^0-9]/', '', $phone);
// Handle Indonesian numbers
if (substr($phone, 0, 1) === '0') {
$phone = '62' . substr($phone, 1);
} elseif (substr($phone, 0, 2) !== '62') {
$phone = '62' . $phone;
}
return $phone;
}
/**
* Generate WhatsApp chat URL
*/
public static function generateChatUrl($phone, $message = '')
{
$formattedPhone = self::formatPhoneNumber($phone);
if (!$formattedPhone) {
return null;
}
$encodedMessage = urlencode($message);
return "https://wa.me/{$formattedPhone}?text={$encodedMessage}";
}
/**
* Generate booking message template
*/
public static function generateBookingMessage($storeName, $customerName = '')
{
$greeting = $customerName ? "Hi, my name is {$customerName}." : "Hi!";
return "{$greeting} I would like to book an appointment at {$storeName}. " .
"Could you please let me know your available times? Thank you!";
}
/**
* Generate QR code URL for WhatsApp
*/
public static function generateQRCode($phone, $message = '', $size = 200)
{
$chatUrl = self::generateChatUrl($phone, $message);
if (!$chatUrl) return null;
return "https://api.qrserver.com/v1/create-qr-code/?size={$size}x{$size}&data=" .
urlencode($chatUrl);
}
/**
* Check if phone number is valid for WhatsApp
*/
public static function isValidWhatsAppNumber($phone)
{
$formatted = self::formatPhoneNumber($phone);
return $formatted && strlen($formatted) >= 10 && strlen($formatted) <= 15;
}
/**
* Generate different message templates
*/
public static function getMessageTemplates()
{
return [
'booking' => "Hi! I would like to book an appointment at {store_name}. Could you please let me know your available times? Thank you!",
'inquiry' => "Hi! I would like to know more about the services at {store_name}. Could you provide me with more information?",
'complaint' => "Hi! I have a concern regarding my recent visit to {store_name}. Could we discuss this please?",
'feedback' => "Hi! I wanted to share some feedback about my experience at {store_name}.",
'general' => "Hi! I have a question about {store_name}.",
];
}
/**
* Replace template variables
*/
public static function processTemplate($template, $variables = [])
{
foreach ($variables as $key => $value) {
$template = str_replace('{' . $key . '}', $value, $template);
}
return $template;
}
}
Add to existing Store
model (app/Models/Store.php
):
// Add to existing Store model
/**
* Get formatted WhatsApp number
*/
public function getWhatsappNumberAttribute()
{
return WhatsAppHelper::formatPhoneNumber($this->phone);
}
/**
* Get WhatsApp chat URL
*/
public function getWhatsappUrlAttribute()
{
if (!$this->phone) return null;
$message = WhatsAppHelper::generateBookingMessage($this->name);
return WhatsAppHelper::generateChatUrl($this->phone, $message);
}
/**
* Check if store has WhatsApp
*/
public function getHasWhatsappAttribute()
{
return WhatsAppHelper::isValidWhatsAppNumber($this->phone);
}
/**
* Get WhatsApp QR code URL
*/
public function getWhatsappQrCodeAttribute()
{
if (!$this->phone) return null;
$message = WhatsAppHelper::generateBookingMessage($this->name);
return WhatsAppHelper::generateQRCode($this->phone, $message);
}
/**
* Generate custom WhatsApp message
*/
public function getWhatsAppUrl($messageType = 'booking', $customerName = '', $customMessage = '')
{
if (!$this->phone) return null;
if ($customMessage) {
$message = $customMessage;
} else {
$templates = WhatsAppHelper::getMessageTemplates();
$template = $templates[$messageType] ?? $templates['booking'];
$message = WhatsAppHelper::processTemplate($template, [
'store_name' => $this->name,
'customer_name' => $customerName,
]);
}
return WhatsAppHelper::generateChatUrl($this->phone, $message);
}
@props([
'store',
'messageType' => 'booking',
'customerName' => '',
'customMessage' => '',
'size' => 'md',
'variant' => 'success',
'showIcon' => true,
'showText' => true,
'target' => '_blank'
])
@php
$whatsappUrl = $store->getWhatsAppUrl($messageType, $customerName, $customMessage);
$sizeClasses = [
'sm' => 'btn-sm',
'md' => '',
'lg' => 'btn-lg'
];
$sizeClass = $sizeClasses[$size] ?? '';
@endphp
@if($whatsappUrl)
<a href="{{ $whatsappUrl }}"
class="btn btn-{{ $variant }} {{ $sizeClass }} whatsapp-btn"
target="{{ $target }}"
data-store="{{ $store->name }}"
data-phone="{{ $store->phone }}"
{{ $attributes }}>
@if($showIcon)
<i class="fab fa-whatsapp"></i>
@endif
@if($showText)
@if($showIcon) @endif
{{ $slot ?? 'WhatsApp' }}
@endif
</a>
@endif
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Track WhatsApp button clicks
document.querySelectorAll('.whatsapp-btn').forEach(button => {
button.addEventListener('click', function() {
const storeName = this.dataset.store;
const phone = this.dataset.phone;
// Analytics tracking (optional)
if (typeof gtag !== 'undefined') {
gtag('event', 'whatsapp_click', {
event_category: 'engagement',
event_label: storeName,
value: 1
});
}
console.log(`WhatsApp clicked for ${storeName} (${phone})`);
});
});
});
</script>
@endpush
<!-- resources/views/components/store-card.blade.php -->
<div class="card store-card h-100">
<div class="card-img-container">
<img src="{{ $store->image_url ?: asset('images/default-store.jpg') }}"
class="card-img-top"
alt="{{ $store->name }}"
style="height: 200px; object-fit: cover;">
</div>
<div class="card-body">
<h5 class="card-title">{{ $store->name }}</h5>
<!-- Rating -->
<div class="rating mb-2">
@for($i = 1; $i <= 5; $i++)
<i class="fas fa-star {{ $i <= $store->average_rating ? 'text-warning' : 'text-muted' }}"></i>
@endfor
<span class="ms-1 text-muted">({{ $store->reviews_count }})</span>
</div>
<!-- Address -->
<p class="card-text">
<i class="fas fa-map-marker-alt text-muted me-1"></i>
<small>{{ Str::limit($store->address, 50) }}</small>
</p>
<!-- Operating Status -->
<div class="mb-2">
@if($store->isOpenNow())
<span class="badge bg-success">Open Now</span>
@else
<span class="badge bg-secondary">Closed</span>
@endif
</div>
<!-- Description -->
<p class="card-text text-muted">
{{ Str::limit($store->description, 80) }}
</p>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex gap-2">
<!-- View Details Button -->
<a href="{{ route('stores.show', $store) }}" class="btn btn-outline-primary flex-fill">
<i class="fas fa-eye"></i> Details
</a>
<!-- WhatsApp Booking Button -->
@if($store->has_whatsapp)
<x-whatsapp-button
:store="$store"
message-type="booking"
:customer-name="auth()->user()?->name ?? ''"
class="flex-fill">
<i class="fab fa-whatsapp"></i> Book Now
</x-whatsapp-button>
@else
<button class="btn btn-secondary flex-fill" disabled>
<i class="fas fa-phone-slash"></i> No WhatsApp
</button>
@endif
</div>
</div>
</div>
@extends('layouts.app')
@section('title', $store->name)
@section('content')
<div class="container">
<!-- Store Header -->
<div class="row mb-4">
<div class="col-md-8">
<h1>{{ $store->name }}</h1>
<div class="d-flex align-items-center mb-3">
<div class="rating me-3">
@for($i = 1; $i <= 5; $i++)
<i class="fas fa-star {{ $i <= $store->average_rating ? 'text-warning' : 'text-muted' }}"></i>
@endfor
<span class="ms-1">{{ $store->average_rating }}/5 ({{ $store->reviews_count }} reviews)</span>
</div>
@if($store->isOpenNow())
<span class="badge bg-success">Open Now</span>
@else
<span class="badge bg-secondary">Closed</span>
@endif
</div>
</div>
<div class="col-md-4 text-md-end">
<!-- WhatsApp Actions -->
@if($store->has_whatsapp)
<div class="whatsapp-actions">
<h6 class="text-muted mb-2">Book via WhatsApp:</h6>
<div class="d-flex flex-column gap-2">
<!-- Quick Booking -->
<x-whatsapp-button
:store="$store"
message-type="booking"
:customer-name="auth()->user()?->name ?? ''"
size="lg"
class="w-100">
<i class="fab fa-whatsapp"></i> Book Appointment
</x-whatsapp-button>
<!-- Service Inquiry -->
<x-whatsapp-button
:store="$store"
message-type="inquiry"
variant="outline-success"
class="w-100">
<i class="fas fa-question-circle"></i> Ask About Services
</x-whatsapp-button>
</div>
<!-- Custom Message Modal Trigger -->
<button type="button"
class="btn btn-link text-decoration-none p-0 mt-2"
data-bs-toggle="modal"
data-bs-target="#customMessageModal">
<i class="fas fa-edit"></i> Send Custom Message
</button>
</div>
@endif
</div>
</div>
<!-- Store Info Tabs -->
<div class="row">
<div class="col-md-8">
<ul class="nav nav-tabs" id="storeTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="info-tab" data-bs-toggle="tab" data-bs-target="#info" type="button" role="tab">
<i class="fas fa-info-circle"></i> Information
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="services-tab" data-bs-toggle="tab" data-bs-target="#services" type="button" role="tab">
<i class="fas fa-cut"></i> Services
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab">
<i class="fas fa-phone"></i> Contact
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reviews-tab" data-bs-toggle="tab" data-bs-target="#reviews" type="button" role="tab">
<i class="fas fa-star"></i> Reviews
</button>
</li>
</ul>
<div class="tab-content" id="storeTabContent">
<!-- Information Tab -->
<div class="tab-pane fade show active" id="info" role="tabpanel">
<div class="p-3">
<p class="lead">{{ $store->description }}</p>
<h6>Operating Hours:</h6>
<div class="operating-hours">
{!! nl2br(e($store->operating_hours_formatted)) !!}
</div>
</div>
</div>
<!-- Services Tab -->
<div class="tab-pane fade" id="services" role="tabpanel">
<div class="p-3">
@if($store->services)
<div class="row">
@foreach($store->services as $service)
<div class="col-md-6 mb-2">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle text-success me-2"></i>
{{ $service }}
</div>
</div>
@endforeach
</div>
@else
<p class="text-muted">No services listed.</p>
@endif
</div>
</div>
<!-- Contact Tab -->
<div class="tab-pane fade" id="contact" role="tabpanel">
<div class="p-3">
<div class="contact-info">
<!-- Address -->
<div class="contact-item mb-3">
<h6><i class="fas fa-map-marker-alt text-primary me-2"></i>Address</h6>
<p class="ms-4">{{ $store->address }}</p>
</div>
<!-- Phone & WhatsApp -->
@if($store->phone)
<div class="contact-item mb-3">
<h6><i class="fas fa-phone text-primary me-2"></i>Phone</h6>
<div class="ms-4">
<p class="mb-1">{{ $store->phone }}</p>
@if($store->has_whatsapp)
<div class="whatsapp-options">
<small class="text-muted d-block mb-2">Available on WhatsApp:</small>
<div class="btn-group" role="group">
<x-whatsapp-button
:store="$store"
message-type="booking"
size="sm">
Book
</x-whatsapp-button>
<x-whatsapp-button
:store="$store"
message-type="inquiry"
variant="outline-success"
size="sm">
Inquire
</x-whatsapp-button>
</div>
</div>
@endif
</div>
</div>
@endif
<!-- Email -->
@if($store->email)
<div class="contact-item mb-3">
<h6><i class="fas fa-envelope text-primary me-2"></i>Email</h6>
<p class="ms-4">
<a href="mailto:{{ $store->email }}">{{ $store->email }}</a>
</p>
</div>
@endif
</div>
</div>
</div>
<!-- Reviews Tab -->
<div class="tab-pane fade" id="reviews" role="tabpanel">
<!-- Reviews content here -->
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-md-4">
<!-- WhatsApp QR Code -->
@if($store->has_whatsapp)
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fab fa-whatsapp text-success"></i>
Quick WhatsApp Contact
</h6>
</div>
<div class="card-body text-center">
<img src="{{ $store->whatsapp_qr_code }}"
alt="WhatsApp QR Code"
class="img-fluid mb-3"
style="max-width: 150px;">
<p class="small text-muted">Scan to chat directly</p>
<x-whatsapp-button
:store="$store"
message-type="booking"
class="w-100">
<i class="fab fa-whatsapp"></i> Open WhatsApp
</x-whatsapp-button>
</div>
</div>
@endif
<!-- Location Map -->
@if($store->latitude && $store->longitude)
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-map-marker-alt"></i>
Location
</h6>
</div>
<div class="card-body p-0">
<div id="store-map" style="height: 300px;"></div>
</div>
</div>
@endif
</div>
</div>
</div>
<!-- Custom Message Modal -->
<div class="modal fade" id="customMessageModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fab fa-whatsapp text-success"></i>
Send Custom WhatsApp Message
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="customMessageForm">
<div class="modal-body">
<div class="mb-3">
<label for="customerName" class="form-label">Your Name (Optional)</label>
<input type="text"
class="form-control"
id="customerName"
value="{{ auth()->user()?->name ?? '' }}"
placeholder="Enter your name">
</div>
<div class="mb-3">
<label for="messageType" class="form-label">Message Type</label>
<select class="form-select" id="messageType">
<option value="booking">Booking Appointment</option>
<option value="inquiry">Service Inquiry</option>
<option value="complaint">Complaint/Issue</option>
<option value="feedback">Feedback</option>
<option value="custom">Custom Message</option>
</select>
</div>
<div class="mb-3">
<label for="customMessage" class="form-label">Message</label>
<textarea class="form-control"
id="customMessage"
rows="4"
placeholder="Your message will appear here..."></textarea>
<div class="form-text">Preview of your WhatsApp message</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
</button>
<button type="button" class="btn btn-success" id="sendWhatsAppBtn">
<i class="fab fa-whatsapp"></i> Send via WhatsApp
</button>
</div>
</form>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const store = @json($store->only(['name', 'phone']));
const templates = @json(WhatsAppHelper::getMessageTemplates());
const customerNameInput = document.getElementById('customerName');
const messageTypeSelect = document.getElementById('messageType');
const customMessageTextarea = document.getElementById('customMessage');
const sendWhatsAppBtn = document.getElementById('sendWhatsAppBtn');
// Update message preview
function updateMessagePreview() {
const customerName = customerNameInput.value.trim();
const messageType = messageTypeSelect.value;
if (messageType === 'custom') {
// Don't auto-update for custom messages
return;
}
let template = templates[messageType] || templates.booking;
// Replace placeholders
template = template.replace('{store_name}', store.name);
if (customerName) {
template = `Hi, my name is ${customerName}. ` + template.replace(/^Hi!\s*/, '');
}
customMessageTextarea.value = template;
}
// Event listeners
customerNameInput.addEventListener('input', updateMessagePreview);
messageTypeSelect.addEventListener('change', function() {
if (this.value === 'custom') {
customMessageTextarea.value = '';
customMessageTextarea.focus();
} else {
updateMessagePreview();
}
});
// Send WhatsApp message
sendWhatsAppBtn.addEventListener('click', function() {
const message = customMessageTextarea.value.trim();
if (!message) {
alert('Please enter a message');
return;
}
const whatsappUrl = `https://wa.me/${store.phone.replace(/\D/g, '')}?text=${encodeURIComponent(message)}`;
window.open(whatsappUrl, '_blank');
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('customMessageModal'));
modal.hide();
});
// Initialize with default message
updateMessagePreview();
});
// Google Maps (if available)
@if($store->latitude && $store->longitude)
function initMap() {
const storeLocation = { lat: {{ $store->latitude }}, lng: {{ $store->longitude }} };
const map = new google.maps.Map(document.getElementById('store-map'), {
zoom: 15,
center: storeLocation,
});
const marker = new google.maps.Marker({
position: storeLocation,
map: map,
title: '{{ $store->name }}',
});
const infoWindow = new google.maps.InfoWindow({
content: `
<div>
<h6>{{ $store->name }}</h6>
<p>{{ $store->address }}</p>
@if($store->has_whatsapp)
<a href="{{ $store->whatsapp_url }}" target="_blank" class="btn btn-success btn-sm">
<i class="fab fa-whatsapp"></i> WhatsApp
</a>
@endif
</div>
`
});
marker.addListener('click', () => {
infoWindow.open(map, marker);
});
}
@endif
</script>
@if($store->latitude && $store->longitude)
<script async defer
src="https://maps.googleapis.com/maps/api/js?key={{ config('services.google.maps_api_key') }}&callback=initMap">
</script>
@endif
@endpush
@endsection
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class WhatsAppBusinessService
{
protected $apiUrl;
protected $accessToken;
protected $phoneNumberId;
public function __construct()
{
$this->apiUrl = config('services.whatsapp.api_url');
$this->accessToken = config('services.whatsapp.access_token');
$this->phoneNumberId = config('services.whatsapp.phone_number_id');
}
/**
* Send a text message
*/
public function sendTextMessage($to, $message)
{
if (!$this->isConfigured()) {
return $this->logError('WhatsApp Business API not configured');
}
try {
$response = Http::withToken($this->accessToken)
->post("{$this->apiUrl}/{$this->phoneNumberId}/messages", [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => ['body' => $message]
]);
if ($response->successful()) {
Log::info('WhatsApp message sent successfully', [
'to' => $to,
'message_id' => $response->json('messages.0.id')
]);
return $response->json();
} else {
$this->logError('Failed to send WhatsApp message', [
'response' => $response->json(),
'status' => $response->status()
]);
return false;
}
} catch (\Exception $e) {
$this->logError('WhatsApp API exception', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Send a template message
*/
public function sendTemplateMessage($to, $templateName, $languageCode = 'en', $parameters = [])
{
if (!$this->isConfigured()) {
return $this->logError('WhatsApp Business API not configured');
}
try {
$template = [
'name' => $templateName,
'language' => ['code' => $languageCode]
];
if (!empty($parameters)) {
$template['components'] = [
[
'type' => 'body',
'parameters' => array_map(function($param) {
return ['type' => 'text', 'text' => $param];
}, $parameters)
]
];
}
$response = Http::withToken($this->accessToken)
->post("{$this->apiUrl}/{$this->phoneNumberId}/messages", [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'template',
'template' => $template
]);
if ($response->successful()) {
Log::info('WhatsApp template sent successfully', [
'to' => $to,
'template' => $templateName,
'message_id' => $response->json('messages.0.id')
]);
return $response->json();
} else {
$this->logError('Failed to send WhatsApp template', [
'response' => $response->json(),
'status' => $response->status()
]);
return false;
}
} catch (\Exception $e) {
$this->logError('WhatsApp template API exception', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Handle incoming webhook
*/
public function handleWebhook($payload)
{
try {
if (isset($payload['entry'][0]['changes'][0]['value']['messages'])) {
$messages = $payload['entry'][0]['changes'][0]['value']['messages'];
foreach ($messages as $message) {
$this->processIncomingMessage($message);
}
}
return true;
} catch (\Exception $e) {
$this->logError('Webhook processing error', ['error' => $e->getMessage()]);
return false;
}
}
/**
* Process incoming message
*/
protected function processIncomingMessage($message)
{
$from = $message['from'];
$messageType = $message['type'];
Log::info('Incoming WhatsApp message', [
'from' => $from,
'type' => $messageType,
'message_id' => $message['id']
]);
// Handle different message types
switch ($messageType) {
case 'text':
$text = $message['text']['body'];
$this->handleTextMessage($from, $text);
break;
case 'button':
$buttonText = $message['button']['text'];
$this->handleButtonMessage($from, $buttonText);
break;
default:
Log::info('Unhandled message type', ['type' => $messageType]);
}
}
/**
* Handle incoming text message
*/
protected function handleTextMessage($from, $text)
{
// Auto-responder logic
$lowercaseText = strtolower($text);
if (str_contains($lowercaseText, 'booking') || str_contains($lowercaseText, 'appointment')) {
$this->sendAutoReply($from, 'booking');
} elseif (str_contains($lowercaseText, 'price') || str_contains($lowercaseText, 'cost')) {
$this->sendAutoReply($from, 'pricing');
} elseif (str_contains($lowercaseText, 'hour') || str_contains($lowercaseText, 'open')) {
$this->sendAutoReply($from, 'hours');
} else {
$this->sendAutoReply($from, 'general');
}
}
/**
* Send auto-reply based on context
*/
protected function sendAutoReply($to, $context)
{
$replies = [
'booking' => "Thank you for your interest in booking! Please let us know your preferred date and time, and we'll confirm your appointment.",
'pricing' => "Thank you for your inquiry! Our service prices vary depending on the treatment. Please visit our store or call us for detailed pricing information.",
'hours' => "Our operating hours are Monday-Friday 9 AM - 9 PM, Saturday-Sunday 8 AM - 10 PM. We look forward to serving you!",
'general' => "Thank you for contacting us! We'll get back to you as soon as possible during business hours."
];
$message = $replies[$context] ?? $replies['general'];
$this->sendTextMessage($to, $message);
}
/**
* Check if API is properly configured
*/
protected function isConfigured()
{
return $this->apiUrl && $this->accessToken && $this->phoneNumberId;
}
/**
* Log error with context
*/
protected function logError($message, $context = [])
{
Log::error("WhatsApp Business API: {$message}", $context);
return false;
}
}
Add to existing services config:
// Add to config/services.php
'whatsapp' => [
'api_url' => env('WHATSAPP_API_URL', 'https://graph.facebook.com/v17.0'),
'access_token' => env('WHATSAPP_ACCESS_TOKEN'),
'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'),
'verify_token' => env('WHATSAPP_VERIFY_TOKEN'),
'webhook_url' => env('WHATSAPP_WEBHOOK_URL'),
],
# WhatsApp Business API (Optional - for advanced features)
WHATSAPP_API_URL=https://graph.facebook.com/v17.0
WHATSAPP_ACCESS_TOKEN=your_access_token_here
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id_here
WHATSAPP_VERIFY_TOKEN=your_verify_token_here
WHATSAPP_WEBHOOK_URL=https://yourdomain.com/api/whatsapp/webhook
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\WhatsAppBusinessService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class WhatsAppWebhookController extends Controller
{
protected $whatsappService;
public function __construct(WhatsAppBusinessService $whatsappService)
{
$this->whatsappService = $whatsappService;
}
/**
* Verify webhook (GET request)
*/
public function verify(Request $request)
{
$verifyToken = config('services.whatsapp.verify_token');
if ($request->get('hub_verify_token') === $verifyToken) {
return response($request->get('hub_challenge'));
}
return response('Forbidden', 403);
}
/**
* Handle webhook (POST request)
*/
public function handle(Request $request)
{
Log::info('WhatsApp webhook received', $request->all());
$payload = $request->all();
if ($this->whatsappService->handleWebhook($payload)) {
return response('OK', 200);
}
return response('Error', 500);
}
}
// Add to routes/api.php
use App\Http\Controllers\Api\WhatsAppWebhookController;
Route::get('/whatsapp/webhook', [WhatsAppWebhookController::class, 'verify']);
Route::post('/whatsapp/webhook', [WhatsAppWebhookController::class, 'handle']);
@props([
'store' => null,
'position' => 'bottom-right',
'size' => 'md'
])
@php
$positions = [
'bottom-right' => 'bottom: 20px; right: 20px;',
'bottom-left' => 'bottom: 20px; left: 20px;',
'top-right' => 'top: 20px; right: 20px;',
'top-left' => 'top: 20px; left: 20px;'
];
$sizes = [
'sm' => '50px',
'md' => '60px',
'lg' => '70px'
];
$positionStyle = $positions[$position] ?? $positions['bottom-right'];
$buttonSize = $sizes[$size] ?? $sizes['md'];
@endphp
@if($store && $store->has_whatsapp)
<div id="whatsapp-float"
class="whatsapp-float"
style="position: fixed; {{ $positionStyle }} z-index: 1000;">
<a href="{{ $store->whatsapp_url }}"
target="_blank"
class="btn btn-success rounded-circle shadow-lg d-flex align-items-center justify-content-center"
style="width: {{ $buttonSize }}; height: {{ $buttonSize }}; text-decoration: none;"
title="Chat on WhatsApp"
data-bs-toggle="tooltip">
<i class="fab fa-whatsapp" style="font-size: {{ $size === 'lg' ? '24px' : ($size === 'sm' ? '18px' : '20px') }};"></i>
</a>
<!-- Pulse animation -->
<div class="whatsapp-pulse"
style="position: absolute; top: 0; left: 0; width: {{ $buttonSize }}; height: {{ $buttonSize }}; border-radius: 50%; background-color: rgba(37, 211, 102, 0.3); animation: whatsapp-pulse 2s infinite;"></div>
</div>
<style>
@keyframes whatsapp-pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.7;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.whatsapp-float:hover .btn {
transform: scale(1.1);
transition: transform 0.2s ease;
}
</style>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltip
const floatButton = document.querySelector('#whatsapp-float [data-bs-toggle="tooltip"]');
if (floatButton) {
new bootstrap.Tooltip(floatButton);
}
});
</script>
@endpush
@endif
Add to your main layout (resources/views/layouts/app.blade.php
):
<!-- Add before closing body tag -->
@if(isset($store))
<x-whatsapp-float :store="$store" />
@endif
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class WhatsAppAnalyticsService
{
/**
* Track WhatsApp click
*/
public static function trackClick($storeId, $messageType = 'general', $userId = null)
{
DB::table('whatsapp_analytics')->insert([
'store_id' => $storeId,
'user_id' => $userId,
'message_type' => $messageType,
'action' => 'click',
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'created_at' => now(),
]);
}
/**
* Get click statistics
*/
public static function getClickStats($storeId = null, $days = 30)
{
$query = DB::table('whatsapp_analytics')
->where('action', 'click')
->where('created_at', '>=', now()->subDays($days));
if ($storeId) {
$query->where('store_id', $storeId);
}
return [
'total_clicks' => $query->count(),
'unique_users' => $query->whereNotNull('user_id')->distinct('user_id')->count(),
'by_message_type' => $query->select('message_type', DB::raw('count(*) as count'))
->groupBy('message_type')
->pluck('count', 'message_type')
->toArray(),
'daily_clicks' => $query->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count'))
->groupBy('date')
->orderBy('date')
->pluck('count', 'date')
->toArray(),
];
}
}
<?php
// database/migrations/xxxx_create_whatsapp_analytics_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('whatsapp_analytics', function (Blueprint $table) {
$table->id();
$table->foreignId('store_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('message_type')->default('general');
$table->string('action'); // click, sent, delivered, read
$table->string('ip_address')->nullable();
$table->text('user_agent')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['store_id', 'created_at']);
$table->index(['action', 'created_at']);
});
}
public function down()
{
Schema::dropIfExists('whatsapp_analytics');
}
};
<!-- Simple WhatsApp button -->
<x-whatsapp-button :store="$store">
Book Appointment
</x-whatsapp-button>
<!-- Custom message type -->
<x-whatsapp-button
:store="$store"
message-type="inquiry"
variant="outline-success">
Ask Questions
</x-whatsapp-button>
<!-- With customer name -->
<x-whatsapp-button
:store="$store"
:customer-name="auth()->user()->name"
custom-message="I'd like to book for tomorrow">
Custom Booking
</x-whatsapp-button>
// Generate WhatsApp URL
$url = $store->getWhatsAppUrl('booking', 'John Doe');
// Check if store has WhatsApp
if ($store->has_whatsapp) {
// Show WhatsApp options
}
// Format phone number
$formatted = WhatsAppHelper::formatPhoneNumber('081234567890');
// Result: 6281234567890
// Generate custom message
$message = WhatsAppHelper::processTemplate(
'Hi {customer_name}! I want to book at {store_name}',
['customer_name' => 'John', 'store_name' => 'Barbershop Central']
);
Next: Security Features untuk implementasi keamanan sistem.