Debug School

rakesh kumar
rakesh kumar

Posted on

End-to-End Push Notifications in Flutter with Laravel

What Are Push Notifications?
Role of Firebase Cloud Messaging (FCM)
Complete Implementation: Flutter + Laravel + FCM + MySQL

What Are Push Notifications?

Push notifications are messages sent from a server to a user’s device even when the app is not actively open. They are commonly used for:

Booking confirmations

Order status updates

Payment / recharge success messages

Reminders and alerts

Key characteristics:

Server-initiated: The server decides when to send.

Delivered via a push service: Google, Apple, etc.

Can wake the app or show messages in the notification tray.

For your use case (Flutter app + Laravel backend + MySQL bookings), push notifications are ideal for:

“Booking created successfully”

“Booking approved / vehicle ready”

“Booking cancelled”

Role of Firebase Cloud Messaging (FCM)

Firebase Cloud Messaging (FCM) is Google’s push notification service.

FCM’s responsibilities:

Generate device tokens

Each device/app installation gets a unique fcm_token.

You send this token to your backend (Laravel).

Deliver messages from your server to devices

Laravel sends a POST request to FCM with:

Target token(s)

Notification title + body

Optional data (e.g., booking_id)

Handle different app states

App in foreground: You can show a custom in-app notification.

Background / killed: System tray notification appears.

In your architecture:

Flutter app:

Gets FCM token and sends it to Laravel API.

Laravel backend (with MySQL):

Stores token in device_tokens table.

When createBooking() is called and booking is saved, it:

Looks up the user’s token(s).

Sends a request to FCM.

FCM delivers the push notification to the device.

Complete Implementation: Flutter + Laravel + FCM + MySQL

Laravel Setup
STEP 1 — Prerequisite (Service Account JSON)

Place your downloaded JSON here:

storage/app/fcm-service-account.json
Enter fullscreen mode Exit fullscreen mode

STEP 2 — Add in .env

FIREBASE_CREDENTIALS=storage/app/fcm-service-account.json
FIREBASE_PROJECT_ID=your-project-id
Enter fullscreen mode Exit fullscreen mode

Find your project ID in Firebase console.

Migration: device_tokens table
php artisan make:migration create_device_tokens_table

database/migrations/xxxx_xx_xx_create_device_tokens_table.php:
Enter fullscreen mode Exit fullscreen mode
<?php

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

return new class extends Migration {
   public function up(): void
    {
        Schema::create('device_tokens', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('user_id');
            $table->text('fcm_token');
            $table->timestamps();

            $table->foreign('user_id')
                  ->references('id')->on('users')
                  ->onDelete('cascade');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('device_tokens');
    }
};
Enter fullscreen mode Exit fullscreen mode

Run:

php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Model: DeviceToken

php artisan make:model DeviceToken
Enter fullscreen mode Exit fullscreen mode
app/Models/DeviceToken.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class DeviceToken extends Model
{
    protected $fillable = [
        'user_id',
        'fcm_token',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Controller: Save FCM Token from Flutter

Route in routes/api.php:

use App\Http\Controllers\DeviceTokenController;

Route::post('/device-token', [DeviceTokenController::class, 'store']);
Enter fullscreen mode Exit fullscreen mode

Controller app/Http/Controllers/DeviceTokenController.php:

<?php

namespace App\Http\Controllers;

use App\Models\DeviceToken;
use Illuminate\Http\Request;

class DeviceTokenController extends Controller
{
    public function store(Request $request)
    {
        $data = $request->validate([
            'user_id'   => 'required|integer',
            'fcm_token' => 'required|string',
        ]);

        DeviceToken::updateOrCreate(
            ['user_id' => $data['user_id']],
            ['fcm_token' => $data['fcm_token']]
        );

        return response()->json([
            'success' => true,
            'message' => 'FCM token saved',
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

FCM Service: app/Services/FcmService.php

Create the file manually:

use Google\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;

public function sendToToken(string $token, string $title, string $body, array $data = []): bool
{
    try {
        // STEP 1: Load Service Account Credentials
        $client = new Client();
        $client->setAuthConfig(storage_path(env('FIREBASE_CREDENTIALS')));
        $client->addScope('https://www.googleapis.com/auth/firebase.messaging');

        // STEP 2: Fetch OAuth Access Token
        $accessToken = $client->fetchAccessTokenWithAssertion()['access_token'];

        // STEP 3: Build FCM v1 Message Payload
        $payload = [
            'message' => [
                'token' => $token,

                'notification' => [
                    'title' => $title,
                    'body'  => $body,
                ],

                'data' => $data,
            ]
        ];

        $projectId = env('FIREBASE_PROJECT_ID');

        // STEP 4: Send HTTP POST Request
        $response = Http::withToken($accessToken)
            ->post("https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", $payload);

        if ($response->failed()) {
            Log::error('[FCM V1] Send Failed', [
                'token' => $token,
                'response' => $response->body(),
            ]);
            return false;
        }

        Log::info('[FCM V1] Push Sent Successfully', [
            'token' => $token,
            'response' => $response->body(),
        ]);

        return true;

    } catch (\Throwable $e) {
        Log::error('[FCM V1] Exception', [
            'error' => $e->getMessage()
        ]);
        return false;
    }
}


Enter fullscreen mode Exit fullscreen mode

Integrate FCM into your existing createBooking() method

You shared this method earlier. We’ll just add FCM sending at the end.

At top of the file where createBooking lives:

use App\Models\DeviceToken;
use App\Services\FcmService;
use Illuminate\Support\Facades\Log;

Inject FcmService into the class:

protected FcmService $fcm;

public function __construct(FcmService $fcm)
{
    $this->fcm = $fcm;
}
Enter fullscreen mode Exit fullscreen mode

Now your updated createBooking():

public function createBooking($request, $getphone, $getvehicle)
{
    Log::info("inside create booking");
    Log::info($request);

    $endDate = new \DateTime($request->end_date);
    $endDate->modify('-1 day');

    $shop_id     = $getvehicle->shop_id;
    $getshop     = Shop::where('id', $shop_id)->first();
    $destination = $getshop->partner_name;
    $vehicleImage = $getvehicle->vechicle_image;
    $img          = json_decode($vehicleImage, true);
    $payment_type = $getvehicle->payment_type;

    $vehicleprice = $getvehicle->price;
    $vendor       = User::where('id',  $getvehicle->vender_ID)->first();
    $shop         = Shop::where('id', $getvehicle->shop_id)->first();
    $vehicle_id   = $request->has('vehicle_id') ? $request->vehicle_id : $request->vechicle_id;

    $booking = Booking::create([
        'user_name'      => $getphone->name,
        'destination'    => $destination,
        'vechicle_type'  => $getvehicle->vechicle,
        'brand'          => $getvehicle->brand,
        'model'          => $getvehicle->model,
        'price'          => $this->calculateBookingPrice(
            $request->start_date,
            $request->end_date,
            $vehicleprice
        ),
        'start_date'     => $request->start_date,
        'end_date'       => $endDate->format('Y-m-d'),
        'number'         => $getphone->number,
        'myvechical_id'  => $getvehicle->vehical_id,
        'email'          => $getphone->email,
        'user_id'        => $getphone->id,
        'vender_id'      => $getvehicle->vender_ID,
        'vechicle_id'    => $vehicle_id,
        'vechicle_image' => json_encode($img),
        'shop_id'        => $getvehicle->shop_id,
        'status'         => ($payment_type == 1) ? '0' : '3',
        'state'          => ($payment_type == 1) ? 'Pending' : 'Vehicle Ready',
    ]);

    // your existing email & WhatsApp notifications...
    $this->sendEmails(
        $getphone,
        $vendor,
        $booking,
        $getvehicle,
        $shop,
        $request,
        $payment_type
    );

    $this->sendWhatsappMessage(
        $getphone, $getvehicle, $request->total_price, $getvehicle->vechicle,
        $getvehicle->brand, $getvehicle->model, $getshop->partner_name,
        $request->start_date, $endDate, null, $template = 1
    );

    $this->removeDuplicateBookings();

    // 🔔 NEW: Send FCM notification to this user
    $tokens = DeviceToken::where('user_id', $getphone->id)->pluck('fcm_token')->all();

    if (!empty($tokens)) {
        $title = 'Booking Created Successfully';
        $body  = "Hi {$getphone->name}, your booking #{$booking->id} "
               . "for {$getvehicle->brand} {$getvehicle->model} "
               . "from {$request->start_date} to {$endDate->format('Y-m-d')} is created.";

        foreach ($tokens as $token) {
            $this->fcm->sendToToken($token, $title, $body, [
                'type'        => 'booking_created',
                'booking_id'  => $booking->id,
                'price'       => $booking->price,
                'state'       => $booking->state,
                'vehicle'     => $getvehicle->vechicle,
                'brand'       => $getvehicle->brand,
                'model'       => $getvehicle->model,
                'shop_name'   => $getshop->partner_name,
            ]);
        }
    } else {
        Log::warning('[FCM] No device tokens for user', ['user_id' => $getphone->id]);
    }

    return $booking;
}
Enter fullscreen mode Exit fullscreen mode

Flutter Setup

Add dependencies (pubspec.yaml)


step1
android/build.gradle

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.gms:google-services:4.4.2'
    }
}
Enter fullscreen mode Exit fullscreen mode

STEP 2 — Add plugin inside app/build.gradle
android/app/build.gradle

dependencies:
  flutter:
    sdk: flutter

  firebase_core: ^3.6.0
  firebase_messaging: ^15.1.0
  flutter_local_notifications: ^17.2.2
  http: ^1.2.2

Enter fullscreen mode Exit fullscreen mode

STEP 3 — Add Firebase BoM + Firebase Messaging

Inside your dependencies block:


dependencies {
    implementation platform('com.google.firebase:firebase-bom:34.5.0')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-analytics'

    // Your existing dependencies
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'com.google.errorprone:error_prone_annotations:2.15.0'
    implementation 'com.google.crypto.tink:tink-android:1.9.0'
    implementation 'androidx.multidex:multidex:2.0.1'
}
Enter fullscreen mode Exit fullscreen mode

Run:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

Local Notification Service (for foreground notifications)


Create file: lib/local_notification_service.dart

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

class LocalNotificationService {
  static final FlutterLocalNotificationsPlugin notificationsPlugin =
      FlutterLocalNotificationsPlugin();

  static Future<void> initialize() async {
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings("@mipmap/ic_launcher");

    const InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
    );

    await notificationsPlugin.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: (NotificationResponse response) {
        // Handle notification tap while app is in foreground/background
        // You can navigate to a specific screen here using a navigator key
      },
    );
  }

  static Future<void> showNotification(RemoteMessage message) async {
    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'high_importance_channel', // channel ID
      'High Importance Notifications', // channel name
      channelDescription: 'Used for important notifications like bookings.',
      importance: Importance.max,
      priority: Priority.high,
      playSound: true,
    );

    const NotificationDetails platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics);

    await notificationsPlugin.show(
      DateTime.now().millisecondsSinceEpoch ~/ 1000,
      message.notification?.title ?? "New Notification",
      message.notification?.body ?? "",
      platformChannelSpecifics,
      payload: message.data.toString(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Firebase API Wrapper (get token, send to Laravel, handle foreground + background)


Create file: lib/firebase_api.dart

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:http/http.dart' as http;
import 'local_notification_service.dart';

class FirebaseApi {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  Future<void> initNotifications(String userId) async {
    // Request permission (especially required on iOS / Android 13+)
    await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    // Get the token for this device
    final token = await _firebaseMessaging.getToken();
    print('FCM Token: $token');

    if (token != null) {
      await _sendTokenToBackend(userId, token);
    }

    // Listen to token refresh (in case FCM token changes)
    _firebaseMessaging.onTokenRefresh.listen((newToken) {
      print('FCM Token refreshed: $newToken');
      _sendTokenToBackend(userId, newToken);
    });

    // FOREGROUND NOTIFICATIONS
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print(
          'Foreground message received: ${message.notification?.title} - ${message.notification?.body}');
      // Show using local notifications
      LocalNotificationService.showNotification(message);
    });

    // APP OPENED FROM BACKGROUND (user taps notification)
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print(
          'Notification clicked (background): ${message.notification?.title}');
      // Here you can navigate to booking details screen using message.data['booking_id'] etc.
    });
  }

  Future<void> _sendTokenToBackend(String userId, String token) async {
    // Replace with your Laravel domain/IP
    const baseUrl = 'http://YOUR-LARAVEL-DOMAIN'; // e.g. http://192.168.0.100

    await http.post(
      Uri.parse('$baseUrl/api/device-token'),
      body: {
        'user_id': userId,
        'fcm_token': token,
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode


main.dart
   └─ main() 
        └─ LocalNotificationService.initialize()

local_notification_service.dart
   └─ initialize()

Your Login/Home Screen
   └─ FirebaseApi.initNotifications()

firebase_api.dart
   └─ initNotifications()
         └─ onMessage.listen() 
                └─ LocalNotificationService.showNotification()

local_notification_service.dart
   └─ showNotification()
        └─ notificationsPlugin.show()  → (Notification appears)
Enter fullscreen mode Exit fullscreen mode

firebase_notifications

Top comments (0)