From d9df1e369b1cf1eebf2c6b7ebfb5ec7c15f1f628 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 15 Nov 2025 13:32:38 +0700 Subject: [PATCH] feat(push notification): add notification --- app/Livewire/Studio/Dashboard/Overview.php | 16 + app/Livewire/Studio/Master/Formula.php | 31 ++ app/Models/PushNotification.php | 23 ++ app/Models/User.php | 5 + composer.json | 1 + composer.lock | 330 +++++++++++++++++- config/services.php | 5 + ...105604_create_push_notifications_table.php | 30 ++ public/service-worker.js | 20 ++ .../views/components/layouts/studio.blade.php | 19 + .../studio/finance/payrolls.blade.php | 6 + 11 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 app/Models/PushNotification.php create mode 100644 database/migrations/2025_11_14_105604_create_push_notifications_table.php create mode 100644 public/service-worker.js diff --git a/app/Livewire/Studio/Dashboard/Overview.php b/app/Livewire/Studio/Dashboard/Overview.php index 8fc4ca2..41a46cd 100644 --- a/app/Livewire/Studio/Dashboard/Overview.php +++ b/app/Livewire/Studio/Dashboard/Overview.php @@ -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'); diff --git a/app/Livewire/Studio/Master/Formula.php b/app/Livewire/Studio/Master/Formula.php index 4acc8e2..cd65e09 100644 --- a/app/Livewire/Studio/Master/Formula.php +++ b/app/Livewire/Studio/Master/Formula.php @@ -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', [ diff --git a/app/Models/PushNotification.php b/app/Models/PushNotification.php new file mode 100644 index 0000000..bea4c70 --- /dev/null +++ b/app/Models/PushNotification.php @@ -0,0 +1,23 @@ + 'array', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 7bdd5ee..3bbe3f2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -131,4 +131,9 @@ public function pointRecords(): HasMany { return $this->hasMany(PointRecord::class); } + + public function pushNotifications(): HasMany + { + return $this->hasMany(PushNotification::class); + } } diff --git a/composer.json b/composer.json index 1f9fb42..5d1c1c8 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 47ff7e5..fc1fa45 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/services.php b/config/services.php index 6740d19..ac32c29 100644 --- a/config/services.php +++ b/config/services.php @@ -39,4 +39,9 @@ 'url' => env('ZAWA_API_URL'), ], + 'vapid' => [ + 'public_key' => env('VAPID_PUBLIC_KEY'), + 'private_key' => env('VAPID_PRIVATE_KEY'), + ], + ]; diff --git a/database/migrations/2025_11_14_105604_create_push_notifications_table.php b/database/migrations/2025_11_14_105604_create_push_notifications_table.php new file mode 100644 index 0000000..342ad48 --- /dev/null +++ b/database/migrations/2025_11_14_105604_create_push_notifications_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..b8609a4 --- /dev/null +++ b/public/service-worker.js @@ -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) + ); +}) diff --git a/resources/views/components/layouts/studio.blade.php b/resources/views/components/layouts/studio.blade.php index ad99cc9..27935e8 100644 --- a/resources/views/components/layouts/studio.blade.php +++ b/resources/views/components/layouts/studio.blade.php @@ -23,6 +23,25 @@ {{ $slot }} @fluxScripts + diff --git a/resources/views/livewire/studio/finance/payrolls.blade.php b/resources/views/livewire/studio/finance/payrolls.blade.php index cdcc541..b91b39b 100644 --- a/resources/views/livewire/studio/finance/payrolls.blade.php +++ b/resources/views/livewire/studio/finance/payrolls.blade.php @@ -148,3 +148,9 @@ class="font-bold {{ $item->type->value == \App\Enums\SalaryAdjustmentType::DEDUC + +@script + +@endscript