web push notifications
This commit is contained in:
parent
1e0782385b
commit
98dc8659ba
@ -63,3 +63,7 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
- 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
|
||||
|
||||
- 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.
|
||||
- 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 ===
|
||||
|
||||
|
||||
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\PasskeyAuthenticatable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use NotificationChannels\WebPush\HasPushSubscriptions;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@ -32,7 +33,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
class User extends Authenticatable implements PasskeyUser
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable, HasPushSubscriptions;
|
||||
|
||||
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\Str;
|
||||
|
||||
use App\Notifications\NewActivityNotification;
|
||||
|
||||
class ActivityService
|
||||
{
|
||||
/**
|
||||
@ -54,6 +56,8 @@ class ActivityService
|
||||
'subject_type' => $subject ? get_class($subject) : null,
|
||||
]);
|
||||
|
||||
$this->notify($message);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
@ -70,6 +74,21 @@ class ActivityService
|
||||
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.
|
||||
*/
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"inertiajs/inertia-laravel": "^3.0",
|
||||
"laravel-notification-channels/webpush": "^11.0",
|
||||
"laravel/chisel": "^0.1.0",
|
||||
"laravel/fortify": "^1.37.2",
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4f6fe33dc680e6446bd6318d5bdd9ec9",
|
||||
"content-hash": "f4c79dcf0b7f9f54487404715d1085c1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@ -1461,6 +1461,72 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v0.1.1",
|
||||
@ -2759,6 +2825,77 @@
|
||||
],
|
||||
"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",
|
||||
"version": "3.10.0",
|
||||
@ -5027,6 +5164,71 @@
|
||||
],
|
||||
"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",
|
||||
"version": "3.2.3",
|
||||
@ -6702,6 +6904,86 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v1.38.1",
|
||||
@ -8475,6 +8757,95 @@
|
||||
],
|
||||
"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",
|
||||
"version": "2.4.0",
|
||||
@ -12251,5 +12622,5 @@
|
||||
"php": "^8.4"
|
||||
},
|
||||
"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...
|
||||
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">
|
||||
import AppLayout from '@/layouts/app/AppSidebarLayout.vue';
|
||||
import type { BreadcrumbItem } from '@/types';
|
||||
import { usePushNotifications } from '@/composables/usePushNotifications';
|
||||
|
||||
const { breadcrumbs = [] } = defineProps<{
|
||||
breadcrumbs?: BreadcrumbItem[];
|
||||
}>();
|
||||
|
||||
const { isSubscribed, subscribe, unsubscribe } = usePushNotifications();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<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>
|
||||
</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::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::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
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,
|
||||
fonts: [
|
||||
bunny('Instrument Sans', {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user