July 30, 2026 — The unofficial Laracon US Day 3. Get your ticket to The Vibes
Plugin Marketplace

nativephp/mobile-local-notifications

Local notifications for NativePHP Mobile with Laravel Notification channel integration

Local Notifications Plugin for NativePHP Mobile#

Send, schedule, and manage local notifications in NativePHP Mobile apps. Supports immediate and delayed notifications, recurring schedules, action buttons, tap events with URL navigation, custom sounds, badge counts, and silent delivery.

Works on iOS and Android.


Installation#

Copied!
composer require nativephp/mobile-local-notifications

Quick Start#

Copied!
use NativePHP\LocalNotifications\Facades\LocalNotifications;
 
// Request permission first
LocalNotifications::requestPermission();
 
// Send a notification
LocalNotifications::send('hello')
->title('Hello!')
->body('World');
 
// Schedule a daily recurring notification
LocalNotifications::schedule('daily-practice')
->title('Practice Time')
->body('Time for your daily session!')
->dailyAt('09:00');

No terminal method needed — chains auto-fire via __destruct.


Sending Notifications#

Copied!
// Simple
LocalNotifications::send('hello')
->title('Hello!')->body('World');
 
// With URL navigation on tap
LocalNotifications::send('shipped')
->title('Order Shipped')->body('On the way!')
->url('/orders/123');
 
// Delayed (fires after N seconds)
LocalNotifications::send('reminder')
->title('Meeting Soon')->delay(300);
 
// With action buttons (max 3)
LocalNotifications::send('friend-request')
->title('Friend Request')->body('John wants to connect')
->action('Accept', '/friends/accept/42')
->action('Decline', destructive: true)
->action('View Profile', '/users/42');
 
// Silent (no sound or vibration)
LocalNotifications::send('sync')
->title('Synced')->body('Data synced')->silent();
 
// Badge (iOS auto-increments, Android managed by OS)
LocalNotifications::send('msg')
->title('New Message')->body('You have mail')->badge();
 
// Custom sound
LocalNotifications::send('alert')
->title('Alert')->body('Check this out')
->sound('mario');
 
// Custom data payload
LocalNotifications::send('order')
->title('Order Ready')
->data(['order_id' => 55, 'type' => 'pickup']);

Custom Sounds#

Place audio files in your app's resources/sounds/ directory:

Copied!
resources/
sounds/
mario.mp3
chime.wav
alert.caf

Reference by name without extension:

Copied!
LocalNotifications::send('alert')
->title('Alert')
->sound('mario');

Supported formats: .mp3, .wav, .caf, .aiff, .m4a

Files are automatically copied to the native app bundle during build. On iOS, the extension is auto-resolved from the bundle. On Android, a dedicated notification channel is created per sound.


Recurring Schedules#

Copied!
// Daily at a specific time
LocalNotifications::schedule('morning')
->title('Good Morning')->body('Start your day!')
->dailyAt('09:00');
 
// Weekly on specific days
LocalNotifications::schedule('standup')
->title('Standup')->body('Starting soon')
->weeklyOn([1,2,3,4,5], '09:55');
 
// Monthly
LocalNotifications::schedule('rent')
->title('Rent Due')->body('Pay today')
->monthlyOn(1, '10:00');

All Frequency Methods#

Method Description
hourly() Every hour
hourlyAt(15) Every hour at minute 15
daily() Every day at midnight
dailyAt('09:00') Every day at 9:00 AM
weekly() Every week on Sunday at midnight
weeklyOn(1, '09:00') Every Monday at 9:00 AM
weeklyOn([1,3,5], '09:00') Mon, Wed, Fri at 9:00 AM
monthly() First of every month at midnight
monthlyOn(15, '09:00') 15th of every month at 9:00 AM
yearly() January 1st at midnight
yearlyOn(3, 15, '09:00') March 15th at 9:00 AM
everyFifteenMinutes() Every 15 minutes
everyThirtyMinutes() Every 30 minutes
twiceDaily(8, 20) Twice daily at 8:00 AM and 8:00 PM

CRUD for Schedules#

Copied!
$all = LocalNotifications::scheduled(); // List all
$entries = LocalNotifications::get('morning'); // Get by ID
LocalNotifications::edit('morning')->dailyAt('10:00'); // Edit
LocalNotifications::remove('morning'); // Remove
LocalNotifications::cancel('id'); // Cancel specific
LocalNotifications::cancelAll(); // Cancel all
LocalNotifications::clearBadge(); // Clear badge (iOS)

Deploy-Time Schedules#

Define in routes/console.php for notifications that run on a fixed schedule regardless of user input:

Copied!
use Illuminate\Support\Facades\Schedule;
 
Schedule::notification('standup')
->title('Standup')->dailyAt('09:55');
 
Schedule::notification('hydrate')
->title('Drink Water')->everyThirtyMinutes();

Action Buttons#

Copied!
->action(string $label, ?string $url = null, bool $destructive = false, array $data = [])
  • label — Button text. Auto-generates a slug identifier.
  • url — Navigates on tap. Falls back to notification-level url().
  • destructive — Red button on iOS. No visual difference on Android.
  • data — Per-action data merged with notification-level data() in tap events.
  • Max 3 per notification.

Tap Behavior#

Scenario Navigation actionIdentifier
Tap body, no URL Open app null
Tap body, has url() Navigate to URL null
Tap action with URL Navigate to action URL slug of label
Tap action without URL, has url() Navigate to notification URL slug of label
Tap action without URL, no url() Open app slug of label

Cold start: URL navigation works even when the app is killed.


Events#

PHP (Livewire)#

Copied!
use Native\Mobile\Attributes\OnNative;
use NativePHP\LocalNotifications\Events\NotificationTapped;
use NativePHP\LocalNotifications\Events\PermissionGranted;
 
#[OnNative(NotificationTapped::class)]
public function handleTap(string $id, ?string $actionIdentifier = null, array $data = [], ?string $url = null)
{
// Handle notification tap
}
 
#[OnNative(PermissionGranted::class)]
public function handlePermission(bool $granted)
{
// Handle permission result
}

JavaScript#

Copied!
import { LocalNotifications, Events } from '#local-notifications';
import { On, Off } from '#nativephp';
 
On(Events.NotificationTapped, ({ id, actionIdentifier, data, url }) => {
console.log('Tapped:', id);
});
 
On(Events.PermissionGranted, ({ granted }) => {
console.log('Permission:', granted);
});

JavaScript API#

Mirrors the PHP API.

Copied!
import { LocalNotifications } from '#local-notifications';
 
// Send
await LocalNotifications.send('hello').title('Hi').body('World');
await LocalNotifications.send('delayed').title('Soon').delay(10);
await LocalNotifications.send('custom').title('Alert').sound('mario');
 
// Schedule
await LocalNotifications.schedule('daily').title('Practice').dailyAt('09:00');
await LocalNotifications.schedule('mwf').title('Gym').weeklyOn([1,3,5], '08:00');
 
// CRUD
const all = await LocalNotifications.scheduled();
await LocalNotifications.remove('daily');
await LocalNotifications.cancel('id');
await LocalNotifications.cancelAll();
await LocalNotifications.clearBadge();
await LocalNotifications.requestPermission();

Laravel Notification Channel#

Copied!
class OrderShipped extends Notification {
use HasMobileNotification;
public function via($notifiable) { return [...$this->viaMobile()]; }
public function toMobile($notifiable) {
return MobileMessage::create()
->title('Shipped')->body('Your order shipped')
->url('/orders/'.$this->order->id)
->action('Track', '/orders/track/'.$this->order->id);
}
}

Platform Notes#

Feature iOS Android
sound('name') Custom sound from bundle Custom sound via dedicated channel
badge() Increments badge count Badge dots managed by OS
clearBadge() Resets badge to zero No effect
destructive: true Red button text No visual difference
silent() No sound or vibration No sound or vibration
channelId() No effect Sets notification channel
subtitle() Below title Below title
Persistence OS is source of truth SQLite database

Permissions#

Android#

  • POST_NOTIFICATIONS — Required on API 33+
  • SCHEDULE_EXACT_ALARM / USE_EXACT_ALARM — For precise scheduling
  • RECEIVE_BOOT_COMPLETED — Re-register alarms after reboot

iOS#

  • .alert, .sound, .badge via UNUserNotificationCenter

Weekday Reference#

0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday


License#

MIT License. See LICENSE for details.