web push notifications
This commit is contained in:
parent
1e0782385b
commit
98dc8659ba
@ -63,3 +63,7 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
VAPID_PUBLIC_KEY=
|
||||||
|
VAPID_PRIVATE_KEY=
|
||||||
|
VITE_VAPID_PUBLIC_KEY="${VAPID_PUBLIC_KEY}"
|
||||||
|
|||||||
12
AGENTS.md
12
AGENTS.md
@ -5,11 +5,6 @@
|
|||||||
|
|
||||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||||
|
|
||||||
## Persistent Project Context (IMPORTANT)
|
|
||||||
|
|
||||||
- You MUST read, understand, and strictly follow the underlying business logic and feature requests documented in `IDEA.md` in every session.
|
|
||||||
- You MUST adhere to all style architecture, backend transaction, and integration decisions recorded in `DECISIONS.md`. Update `DECISIONS.md` when any major design decisions are made during a session.
|
|
||||||
|
|
||||||
## Foundational Context
|
## Foundational Context
|
||||||
|
|
||||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||||
@ -58,12 +53,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
|
|||||||
- Stick to existing directory structure; don't create new base folders without approval.
|
- Stick to existing directory structure; don't create new base folders without approval.
|
||||||
- Do not change the application's dependencies without approval.
|
- Do not change the application's dependencies without approval.
|
||||||
|
|
||||||
## Activity Service
|
|
||||||
|
|
||||||
- The `app/Services/ActivityService.php` class is used to create system messages and activities.
|
|
||||||
- To create a system message, use the `createMessage` method. The `$user` parameter should be `null` to indicate a system message.
|
|
||||||
- The `createMutation` method can be used to create a mutation and its associated system message.
|
|
||||||
|
|
||||||
## Frontend Bundling
|
## Frontend Bundling
|
||||||
|
|
||||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||||
@ -137,7 +126,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac
|
|||||||
|
|
||||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||||
- **Environment Isolation during Test Runs (IMPORTANT)**: The test runner environment (e.g. `vendor/bin/pest` or `php artisan test`) can be polluted by the main project's local `.env` file settings if run directly in the active shell. This pollution can override test-specific configurations defined in `phpunit.xml` and lead to unexpected failures, such as CSRF (`419 Page Expired`) errors or database connection issues. Always prefix test commands with `env -i PATH=$PATH HOME=$HOME TERM=$TERM` (e.g., `env -i PATH=$PATH HOME=$HOME TERM=$TERM vendor/bin/pest`) to enforce a clean, isolated environment run.
|
|
||||||
|
|
||||||
=== inertia-laravel/core rules ===
|
=== inertia-laravel/core rules ===
|
||||||
|
|
||||||
|
|||||||
37
app/Http/Controllers/WebPushController.php
Normal file
37
app/Http/Controllers/WebPushController.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class WebPushController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'endpoint' => 'required',
|
||||||
|
'keys.p256dh' => 'required',
|
||||||
|
'keys.auth' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$endpoint = $request->endpoint;
|
||||||
|
$token = $request->keys['auth'];
|
||||||
|
$key = $request->keys['p256dh'];
|
||||||
|
|
||||||
|
Auth::user()->updatePushSubscription($endpoint, $key, $token);
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'endpoint' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::user()->deletePushSubscription($request->endpoint);
|
||||||
|
|
||||||
|
return response()->json(['success' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ use Illuminate\Support\Carbon;
|
|||||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
|
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $id
|
* @property int $id
|
||||||
@ -32,7 +33,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
|||||||
class User extends Authenticatable implements PasskeyUser
|
class User extends Authenticatable implements PasskeyUser
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable, HasPushSubscriptions;
|
||||||
|
|
||||||
public function dynamics()
|
public function dynamics()
|
||||||
{
|
{
|
||||||
|
|||||||
61
app/Notifications/NewActivityNotification.php
Normal file
61
app/Notifications/NewActivityNotification.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
use NotificationChannels\WebPush\WebPushChannel;
|
||||||
|
use NotificationChannels\WebPush\WebPushMessage;
|
||||||
|
|
||||||
|
class NewActivityNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public $activity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new notification instance.
|
||||||
|
*/
|
||||||
|
public function __construct($activity)
|
||||||
|
{
|
||||||
|
$this->activity = $activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification's delivery channels.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [WebPushChannel::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the web push representation of the notification.
|
||||||
|
*/
|
||||||
|
public function toWebPush(object $notifiable): WebPushMessage
|
||||||
|
{
|
||||||
|
return (new WebPushMessage)
|
||||||
|
->title('New Activity')
|
||||||
|
->icon('/apple-touch-icon.png')
|
||||||
|
->body($this->activity['content'])
|
||||||
|
->action('View', 'view')
|
||||||
|
->data(['url' => $this->activity['url']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ use App\Models\ReadCursor;
|
|||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
use App\Notifications\NewActivityNotification;
|
||||||
|
|
||||||
class ActivityService
|
class ActivityService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@ -54,6 +56,8 @@ class ActivityService
|
|||||||
'subject_type' => $subject ? get_class($subject) : null,
|
'subject_type' => $subject ? get_class($subject) : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->notify($message);
|
||||||
|
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +74,21 @@ class ActivityService
|
|||||||
return $mutation;
|
return $mutation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function notify(Message $message)
|
||||||
|
{
|
||||||
|
$dynamic = $message->chat->chatable;
|
||||||
|
|
||||||
|
if ($dynamic instanceof Dynamic) {
|
||||||
|
$participants = $dynamic->participants;
|
||||||
|
|
||||||
|
foreach ($participants as $participant) {
|
||||||
|
if ($message->user_id !== $participant->id) {
|
||||||
|
$participant->notify(new NewActivityNotification($message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all activities for a given entity.
|
* Retrieve all activities for a given entity.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.4",
|
||||||
"inertiajs/inertia-laravel": "^3.0",
|
"inertiajs/inertia-laravel": "^3.0",
|
||||||
|
"laravel-notification-channels/webpush": "^11.0",
|
||||||
"laravel/chisel": "^0.1.0",
|
"laravel/chisel": "^0.1.0",
|
||||||
"laravel/fortify": "^1.37.2",
|
"laravel/fortify": "^1.37.2",
|
||||||
"laravel/framework": "^13.7",
|
"laravel/framework": "^13.7",
|
||||||
|
|||||||
375
composer.lock
generated
375
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "4f6fe33dc680e6446bd6318d5bdd9ec9",
|
"content-hash": "f4c79dcf0b7f9f54487404715d1085c1",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@ -1461,6 +1461,72 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-04-30T15:30:29+00:00"
|
"time": "2026-04-30T15:30:29+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel-notification-channels/webpush",
|
||||||
|
"version": "11.0.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel-notification-channels/webpush.git",
|
||||||
|
"reference": "85b577e64459a9df06a24062e2b300abbaa99fa9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel-notification-channels/webpush/zipball/85b577e64459a9df06a24062e2b300abbaa99fa9",
|
||||||
|
"reference": "85b577e64459a9df06a24062e2b300abbaa99fa9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/notifications": "^12.0|^13.0",
|
||||||
|
"illuminate/support": "^12.0|^13.0",
|
||||||
|
"minishlink/web-push": "^10.0.1",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^3.1",
|
||||||
|
"laravel/pint": "^1.25",
|
||||||
|
"mockery/mockery": "^1.0",
|
||||||
|
"orchestra/testbench": "^9.2|^10.0|^11.0",
|
||||||
|
"phpunit/phpunit": "^11.5.3|^12.5.12|^13.1.11",
|
||||||
|
"rector/rector": "^2.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"NotificationChannels\\WebPush\\WebPushServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"NotificationChannels\\WebPush\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Cretu Eusebiu",
|
||||||
|
"email": "me@cretueusebiu.com",
|
||||||
|
"homepage": "http://cretueusebiu.com",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Joost de Bruijn",
|
||||||
|
"email": "joost@aqualabs.nl",
|
||||||
|
"role": "Maintainer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Web Push Notifications driver for Laravel.",
|
||||||
|
"homepage": "https://github.com/laravel-notification-channels/webpush",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel-notification-channels/webpush/issues",
|
||||||
|
"source": "https://github.com/laravel-notification-channels/webpush/tree/11.0.0"
|
||||||
|
},
|
||||||
|
"time": "2026-05-24T13:22:27+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/chisel",
|
"name": "laravel/chisel",
|
||||||
"version": "v0.1.1",
|
"version": "v0.1.1",
|
||||||
@ -2759,6 +2825,77 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-03-08T20:05:35+00:00"
|
"time": "2026-03-08T20:05:35+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "minishlink/web-push",
|
||||||
|
"version": "v10.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/web-push-libs/web-push-php.git",
|
||||||
|
"reference": "c922021b4ed1a61e6604d8dc33a2e0378b4382e3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/c922021b4ed1a61e6604d8dc33a2e0378b4382e3",
|
||||||
|
"reference": "c922021b4ed1a61e6604d8dc33a2e0378b4382e3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"guzzlehttp/guzzle": "^7.9.2",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^2.0|^3.0",
|
||||||
|
"spomky-labs/base64url": "^2.0.4",
|
||||||
|
"symfony/polyfill-php83": "^1.33",
|
||||||
|
"web-token/jwt-library": "^3.4.9|^4.0.6"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^v3.92.2",
|
||||||
|
"phpstan/phpstan": "^2.1.33",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||||
|
"phpstan/phpstan-phpunit": "^2.0",
|
||||||
|
"phpstan/phpstan-strict-rules": "^2.0",
|
||||||
|
"phpunit/phpunit": "^11.5.46|^12.5.2",
|
||||||
|
"symfony/polyfill-iconv": "^1.33"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Optional for performance.",
|
||||||
|
"ext-gmp": "Optional for performance."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Minishlink\\WebPush\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Louis Lagrange",
|
||||||
|
"email": "lagrange.louis@gmail.com",
|
||||||
|
"homepage": "https://github.com/Minishlink"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Web Push library for PHP",
|
||||||
|
"homepage": "https://github.com/web-push-libs/web-push-php",
|
||||||
|
"keywords": [
|
||||||
|
"Push API",
|
||||||
|
"WebPush",
|
||||||
|
"notifications",
|
||||||
|
"push",
|
||||||
|
"web"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/web-push-libs/web-push-php/issues",
|
||||||
|
"source": "https://github.com/web-push-libs/web-push-php/tree/v10.1.0"
|
||||||
|
},
|
||||||
|
"time": "2026-05-28T09:37:37+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "monolog/monolog",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@ -5027,6 +5164,71 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-06-11T12:45:25+00:00"
|
"time": "2024-06-11T12:45:25+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spomky-labs/base64url",
|
||||||
|
"version": "v2.0.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Spomky-Labs/base64url.git",
|
||||||
|
"reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d",
|
||||||
|
"reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/extension-installer": "^1.0",
|
||||||
|
"phpstan/phpstan": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-beberlei-assert": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-deprecation-rules": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-phpunit": "^0.11|^0.12",
|
||||||
|
"phpstan/phpstan-strict-rules": "^0.11|^0.12"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Base64Url\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/base64url/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Base 64 URL Safe Encoding/Decoding PHP Library",
|
||||||
|
"homepage": "https://github.com/Spomky-Labs/base64url",
|
||||||
|
"keywords": [
|
||||||
|
"base64",
|
||||||
|
"rfc4648",
|
||||||
|
"safe",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Spomky-Labs/base64url/issues",
|
||||||
|
"source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2020-11-03T09:10:25+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spomky-labs/cbor-php",
|
"name": "spomky-labs/cbor-php",
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
@ -6702,6 +6904,86 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-04-10T16:19:22+00:00"
|
"time": "2026-04-10T16:19:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/polyfill-php83",
|
||||||
|
"version": "v1.38.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/polyfill-php83.git",
|
||||||
|
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8",
|
||||||
|
"reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/polyfill",
|
||||||
|
"name": "symfony/polyfill"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"bootstrap.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Polyfill\\Php83\\": ""
|
||||||
|
},
|
||||||
|
"classmap": [
|
||||||
|
"Resources/stubs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"compatibility",
|
||||||
|
"polyfill",
|
||||||
|
"portable",
|
||||||
|
"shim"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-27T06:51:48+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php84",
|
"name": "symfony/polyfill-php84",
|
||||||
"version": "v1.38.1",
|
"version": "v1.38.1",
|
||||||
@ -8475,6 +8757,95 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-05-31T15:00:08+00:00"
|
"time": "2026-05-31T15:00:08+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "web-token/jwt-library",
|
||||||
|
"version": "4.1.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/web-token/jwt-library.git",
|
||||||
|
"reference": "fbcbf2c276d04d8b056f5c2957815abd5dfb704d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/web-token/jwt-library/zipball/fbcbf2c276d04d8b056f5c2957815abd5dfb704d",
|
||||||
|
"reference": "fbcbf2c276d04d8b056f5c2957815abd5dfb704d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"brick/math": "^0.12|^0.13|^0.14|^0.15|^0.16|^0.17",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/clock": "^1.0",
|
||||||
|
"spomky-labs/pki-framework": "^1.2.1"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"spomky-labs/jose": "*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance",
|
||||||
|
"ext-gmp": "GMP or BCMath is highly recommended to improve the library performance",
|
||||||
|
"ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)",
|
||||||
|
"ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
|
||||||
|
"paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
|
||||||
|
"spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (AxxxKW, AxxxGCMKW, PBES2-HSxxx+AyyyKW...)",
|
||||||
|
"symfony/console": "Needed to use console commands",
|
||||||
|
"symfony/http-client": "To enable JKU/X5U support."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Jose\\Component\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Florent Morselli",
|
||||||
|
"homepage": "https://github.com/Spomky"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "All contributors",
|
||||||
|
"homepage": "https://github.com/web-token/jwt-framework/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "JWT library",
|
||||||
|
"homepage": "https://github.com/web-token",
|
||||||
|
"keywords": [
|
||||||
|
"JOSE",
|
||||||
|
"JWE",
|
||||||
|
"JWK",
|
||||||
|
"JWKSet",
|
||||||
|
"JWS",
|
||||||
|
"Jot",
|
||||||
|
"RFC7515",
|
||||||
|
"RFC7516",
|
||||||
|
"RFC7517",
|
||||||
|
"RFC7518",
|
||||||
|
"RFC7519",
|
||||||
|
"RFC7520",
|
||||||
|
"bundle",
|
||||||
|
"jwa",
|
||||||
|
"jwt",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/web-token/jwt-library/issues",
|
||||||
|
"source": "https://github.com/web-token/jwt-library/tree/4.1.7"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Spomky",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.patreon.com/FlorentMorselli",
|
||||||
|
"type": "patreon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-06-06T18:12:39+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "webmozart/assert",
|
"name": "webmozart/assert",
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
@ -12251,5 +12622,5 @@
|
|||||||
"php": "^8.4"
|
"php": "^8.4"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::connection(config('webpush.database_connection'))->create(config('webpush.table_name'), function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->morphs('subscribable', 'push_subscriptions_subscribable_morph_idx');
|
||||||
|
$table->string('endpoint', 500)->unique();
|
||||||
|
$table->string('public_key')->nullable();
|
||||||
|
$table->string('auth_token')->nullable();
|
||||||
|
$table->string('content_encoding')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::connection(config('webpush.database_connection'))->dropIfExists(config('webpush.table_name'));
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -49,3 +49,7 @@ initializeTheme();
|
|||||||
|
|
||||||
// This will listen for flash toast data from the server...
|
// This will listen for flash toast data from the server...
|
||||||
initializeFlashToast();
|
initializeFlashToast();
|
||||||
|
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js');
|
||||||
|
}
|
||||||
|
|||||||
54
resources/js/composables/usePushNotifications.ts
Normal file
54
resources/js/composables/usePushNotifications.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export function usePushNotifications() {
|
||||||
|
const isSubscribed = ref(false);
|
||||||
|
|
||||||
|
async function subscribe() {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(import.meta.env.VITE_VAPID_PUBLIC_KEY),
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.post('/subscriptions', subscription);
|
||||||
|
|
||||||
|
isSubscribed.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsubscribe() {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registration = await navigator.serviceWorker.ready;
|
||||||
|
const subscription = await registration.pushManager.getSubscription();
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
await axios.post('/subscriptions/delete', { endpoint: subscription.endpoint });
|
||||||
|
await subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubscribed.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array(base64String: string) {
|
||||||
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||||
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSubscribed,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,14 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
||||||
import type { BreadcrumbItem } from '@/types';
|
import type { BreadcrumbItem } from '@/types';
|
||||||
|
import { usePushNotifications } from '@/composables/usePushNotifications';
|
||||||
|
|
||||||
const { breadcrumbs = [] } = defineProps<{
|
const { breadcrumbs = [] } = defineProps<{
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { isSubscribed, subscribe, unsubscribe } = usePushNotifications();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<slot />
|
<slot />
|
||||||
|
<div class="fixed bottom-4 right-4">
|
||||||
|
<button
|
||||||
|
@click="isSubscribed ? unsubscribe() : subscribe()"
|
||||||
|
class="rounded-full bg-blue-500 px-4 py-2 text-white"
|
||||||
|
>
|
||||||
|
{{ isSubscribed ? 'Unsubscribe' : 'Subscribe' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
40
resources/js/sw.js
Normal file
40
resources/js/sw.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// This file is based on the one found in the laravel-notification-channels/webpush package.
|
||||||
|
|
||||||
|
// We need this to get the VAPID_PUBLIC_KEY from the .env file
|
||||||
|
const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY;
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
const payload = event.data ? event.data.json() : {};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
body: payload.body,
|
||||||
|
icon: payload.icon,
|
||||||
|
badge: payload.badge,
|
||||||
|
data: {
|
||||||
|
url: payload.url,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(self.registration.showNotification(payload.title, options));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
clients.openWindow(event.notification.data.url)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vapidPublicKey) {
|
||||||
|
self.addEventListener('load', async () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||||
|
console.log('Service worker registered.', registration);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error registering service worker:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -9,6 +9,9 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
Route::inertia('/', 'Welcome')->name('home');
|
Route::inertia('/', 'Welcome')->name('home');
|
||||||
|
|
||||||
|
Route::post('subscriptions', [\App\Http\Controllers\WebPushController::class, 'store'])->name('subscriptions.store');
|
||||||
|
Route::post('subscriptions/delete', [\App\Http\Controllers\WebPushController::class, 'destroy'])->name('subscriptions.destroy');
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
input: ['resources/css/app.css', 'resources/js/app.ts'],
|
input: ['resources/css/app.css', 'resources/js/app.ts', 'resources/js/sw.js'],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
fonts: [
|
fonts: [
|
||||||
bunny('Instrument Sans', {
|
bunny('Instrument Sans', {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user