feat(push notification): add notification

This commit is contained in:
Yoga Pangestu 2025-11-15 13:32:38 +07:00
parent 0ae236d2ca
commit d9df1e369b
11 changed files with 485 additions and 1 deletions

View File

@ -7,7 +7,9 @@
use App\Models\OrderItem;
use App\Models\Perfume;
use App\Models\Purchase;
use App\Models\PushNotification;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -248,6 +250,20 @@ protected function loadStats()
]);
}
#[On('notification:subscribe')]
public function notificationSubscribe(string $subscription)
{
$userId = auth()->id();
PushNotification::updateOrCreate(
[
'user_id' => $userId,
'subscriptions' => $subscription,
],
['subscriptions' => $subscription]
);
}
public function render()
{
return view('livewire.studio.dashboard.overview');

View File

@ -5,6 +5,7 @@
use App\Livewire\Forms\Studio\Master\FormulaForm;
use App\Models\Bottle;
use App\Models\Formula as FormulaModel;
use App\Models\PushNotification;
use App\Traits\WithAuthorization;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
@ -14,6 +15,8 @@
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
use Minishlink\WebPush\Subscription;
use Minishlink\WebPush\WebPush;
#[Title('Rumus')]
class Formula extends Component
@ -93,6 +96,8 @@ public function create()
public function update()
{
$this->pushNotification();
$exists = FormulaModel::where('quality', $this->form->quality)
->where('size', $this->form->size)
->where('id', '!=', $this->form->formula->id)
@ -149,6 +154,32 @@ public function delete(FormulaModel $formula)
Flux::modals()->close();
}
protected function pushNotification()
{
$auth = [
'VAPID' => [
'subject' => config('app.url'),
'publicKey' => config('services.vapid.public_key'),
'privateKey' => config('services.vapid.private_key'),
],
];
$webPush = new WebPush($auth);
$payload = json_encode([
'title' => 'Penggajian',
'body' => 'Penggajian baru telah ditambahkan.',
]);
foreach (PushNotification::all() as $notification) {
$webPush->sendOneNotification(
Subscription::create(json_decode($notification->subscriptions, true)),
$payload,
['TTL' => 5000],
);
}
}
public function render()
{
return view('livewire.studio.master.formulas', [

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PushNotification extends Model
{
protected $guarded = ['id'];
protected function casts(): array
{
return [
'data' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -131,4 +131,9 @@ public function pointRecords(): HasMany
{
return $this->hasMany(PointRecord::class);
}
public function pushNotifications(): HasMany
{
return $this->hasMany(PushNotification::class);
}
}

View File

@ -18,6 +18,7 @@
"livewire/flux": "2.2.5",
"livewire/flux-pro": "2.2.5",
"livewire/livewire": "^3.6",
"minishlink/web-push": "^9.0",
"rappasoft/laravel-livewire-tables": "^3.7",
"spatie/laravel-activitylog": "^4.10",
"spatie/laravel-medialibrary": "^11.15",

330
composer.lock generated
View File

@ -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": "5741a5b81c8f68f6d656467cf2e24651",
"content-hash": "0dcdd0d151296be6bc124bd5e4d44961",
"packages": [
{
"name": "archtechx/enums",
@ -3595,6 +3595,71 @@
],
"time": "2025-01-27T12:07:53+00:00"
},
{
"name": "minishlink/web-push",
"version": "v9.0.3",
"source": {
"type": "git",
"url": "https://github.com/web-push-libs/web-push-php.git",
"reference": "5c185f78ee41f271e2ea7314c80760040465b713"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/5c185f78ee41f271e2ea7314c80760040465b713",
"reference": "5c185f78ee41f271e2ea7314c80760040465b713",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"guzzlehttp/guzzle": "^7.4.5",
"php": ">=8.1",
"spomky-labs/base64url": "^2.0.4",
"web-token/jwt-library": "^3.3.0|^4.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.68.5",
"phpstan/phpstan": "^2.1.2",
"phpunit/phpunit": "^10.5.44|^11.5.6"
},
"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/v9.0.3"
},
"time": "2025-11-13T17:14:30+00:00"
},
{
"name": "monolog/monolog",
"version": "3.9.0",
@ -5779,6 +5844,180 @@
],
"time": "2025-01-13T13:04:43+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/pki-framework",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/Spomky-Labs/pki-framework.git",
"reference": "bf6f55a9d9eb25b7781640221cb54f5c727850d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/bf6f55a9d9eb25b7781640221cb54f5c727850d7",
"reference": "bf6f55a9d9eb25b7781640221cb54f5c727850d7",
"shasum": ""
},
"require": {
"brick/math": "^0.10|^0.11|^0.12|^0.13|^0.14",
"ext-mbstring": "*",
"php": ">=8.1"
},
"require-dev": {
"ekino/phpstan-banned-code": "^1.0|^2.0|^3.0",
"ext-gmp": "*",
"ext-openssl": "*",
"infection/infection": "^0.28|^0.29|^0.31",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpstan/extension-installer": "^1.3|^2.0",
"phpstan/phpstan": "^1.8|^2.0",
"phpstan/phpstan-deprecation-rules": "^1.0|^2.0",
"phpstan/phpstan-phpunit": "^1.1|^2.0",
"phpstan/phpstan-strict-rules": "^1.3|^2.0",
"phpunit/phpunit": "^10.1|^11.0|^12.0",
"rector/rector": "^1.0|^2.0",
"roave/security-advisories": "dev-latest",
"symfony/string": "^6.4|^7.0|^8.0",
"symfony/var-dumper": "^6.4|^7.0|^8.0",
"symplify/easy-coding-standard": "^12.0"
},
"suggest": {
"ext-bcmath": "For better performance (or GMP)",
"ext-gmp": "For better performance (or BCMath)",
"ext-openssl": "For OpenSSL based cyphering"
},
"type": "library",
"autoload": {
"psr-4": {
"SpomkyLabs\\Pki\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joni Eskelinen",
"email": "jonieske@gmail.com",
"role": "Original developer"
},
{
"name": "Florent Morselli",
"email": "florent.morselli@spomky-labs.com",
"role": "Spomky-Labs PKI Framework developer"
}
],
"description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.",
"homepage": "https://github.com/spomky-labs/pki-framework",
"keywords": [
"DER",
"Private Key",
"ac",
"algorithm identifier",
"asn.1",
"asn1",
"attribute certificate",
"certificate",
"certification request",
"cryptography",
"csr",
"decrypt",
"ec",
"encrypt",
"pem",
"pkcs",
"public key",
"rsa",
"sign",
"signature",
"verify",
"x.509",
"x.690",
"x509",
"x690"
],
"support": {
"issues": "https://github.com/Spomky-Labs/pki-framework/issues",
"source": "https://github.com/Spomky-Labs/pki-framework/tree/1.4.0"
},
"funding": [
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"time": "2025-10-22T08:24:34+00:00"
},
{
"name": "symfony/clock",
"version": "v7.3.0",
@ -8618,6 +8857,95 @@
],
"time": "2024-11-21T01:49:47+00:00"
},
{
"name": "web-token/jwt-library",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/web-token/jwt-library.git",
"reference": "b05d01d4138b1e06328e29075a21a6da974935df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/web-token/jwt-library/zipball/b05d01d4138b1e06328e29075a21a6da974935df",
"reference": "b05d01d4138b1e06328e29075a21a6da974935df",
"shasum": ""
},
"require": {
"brick/math": "^0.12|^0.13|^0.14",
"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.0"
},
"funding": [
{
"url": "https://github.com/Spomky",
"type": "github"
},
{
"url": "https://www.patreon.com/FlorentMorselli",
"type": "patreon"
}
],
"time": "2025-10-22T08:01:38+00:00"
},
{
"name": "webmozart/assert",
"version": "1.11.0",

View File

@ -39,4 +39,9 @@
'url' => env('ZAWA_API_URL'),
],
'vapid' => [
'public_key' => env('VAPID_PUBLIC_KEY'),
'private_key' => env('VAPID_PRIVATE_KEY'),
],
];

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('push_notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->json('subscriptions');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('push_notifications');
}
};

20
public/service-worker.js Normal file
View File

@ -0,0 +1,20 @@
self.addEventListener('push', (event) => {
const notification = event.data.json();
event.waitUntil(
self.registration.showNotification(
notification.title, {
body: notification.body,
icon: 'assets/images/dark-logo.png',
data: {
url: notification.url
}
})
);
})
self.addEventListener('notificationclick', (event) => {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
})

View File

@ -23,6 +23,25 @@
{{ $slot }}
@fluxScripts
<script>
document.addEventListener('livewire:navigated', () => {
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
navigator.serviceWorker.ready.then((sw) => {
sw.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: '{{ config('services.vapid.public_key') }}',
}).then((subscription) => {
const json = JSON.stringify(subscription)
Livewire.dispatch('notification:subscribe', {
subscription: json
})
})
})
}
})
})
</script>
</body>
</html>

View File

@ -148,3 +148,9 @@ class="font-bold {{ $item->type->value == \App\Enums\SalaryAdjustmentType::DEDUC
</div>
</flux:modal>
</flux:main>
@script
<script>
navigator.serviceWorker.register('{{ URL::asset('service-worker.js') }}');
</script>
@endscript