diff --git a/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php b/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php new file mode 100644 index 0000000..a676222 --- /dev/null +++ b/database/migrations/2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table.php @@ -0,0 +1,36 @@ +string('pixel_meta_id', 100)->nullable(); + $table->string('pixel_google_id', 100)->nullable(); + $table->string('pixel_linkedin_id', 100)->nullable(); + $table->string('webhook_url', 2048)->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('short_urls', function (Blueprint $table) { + $table->dropColumn([ + 'pixel_meta_id', + 'pixel_google_id', + 'pixel_linkedin_id', + 'webhook_url', + ]); + }); + } +}; diff --git a/resources/lang/en/default.php b/resources/lang/en/default.php index 7b98e6e..c5b92de 100644 --- a/resources/lang/en/default.php +++ b/resources/lang/en/default.php @@ -338,4 +338,33 @@ return [ 'warning_description' => 'You are leaving this secure portal and being redirected to an external target link. Please ensure you trust the address below:', 'warning_btn_continue' => 'Continue to Destination', 'warning_btn_back' => 'Go Back', + + // Marketing & API Tab + 'tab_marketing' => 'Marketing & API', + 'marketing_pixels_title' => 'Retargeting Pixels (Client-Side)', + 'marketing_pixels_desc' => 'Add ad tracking scripts to capture visitor cookies and build remarketing lists.', + 'pixel_meta' => 'Meta Pixel ID', + 'pixel_google' => 'Google Tag / GA4 ID', + 'pixel_linkedin' => 'LinkedIn Partner ID', + 'marketing_webhooks_title' => 'Link Webhook Integration', + 'marketing_webhooks_desc' => 'Configure a custom destination URL to send a real-time HTTP POST notification on each click.', + 'webhook_url' => 'Dedicated Webhook URL', + + // Settings tab additions + 'settings_tab_developer' => 'API & Webhooks', + 'settings_section_global_webhook' => 'Global Webhook Configuration', + 'settings_global_webhook_url' => 'Global Webhook URL', + 'settings_global_webhook_url_helper' => 'Destination URL to dispatch event payloads asynchronously in the background for all links.', + 'settings_webhook_events' => 'Monitored Webhook Events', + 'settings_webhook_events_helper' => 'Select which events should trigger a POST notification.', + 'webhook_event_visited' => 'Link Visited (Click)', + 'webhook_event_created' => 'Link Created', + 'webhook_event_expired' => 'Link Expired (Date)', + 'webhook_event_limit_reached' => 'Link Click Limit Reached', + 'settings_section_api_keys' => 'Developer API Keys', + 'settings_api_keys_description' => 'Manage API keys to integrate with external systems (e.g. CRM, Zapier, Make). Authenticate requests using the X-Api-Key header.', + 'settings_api_keys' => 'Active API Keys', + 'api_key_name' => 'Key Name (Description)', + 'api_key' => 'API Key', + 'active' => 'Active', ]; diff --git a/resources/lang/pl/default.php b/resources/lang/pl/default.php index 28f8061..d0cb416 100644 --- a/resources/lang/pl/default.php +++ b/resources/lang/pl/default.php @@ -339,4 +339,33 @@ return [ 'warning_description' => 'Opuszczasz ten bezpieczny portal i zostajesz przekierowany na zewnętrzny link docelowy. Upewnij się, że ufasz poniższemu adresowi:', 'warning_btn_continue' => 'Kontynuuj do celu', 'warning_btn_back' => 'Wróć', + + // Marketing & API Tab + 'tab_marketing' => 'Marketing i API', + 'marketing_pixels_title' => 'Piksele retargetingowe (Client-Side)', + 'marketing_pixels_desc' => 'Dodaj skrypty śledzące, aby dołączać ciasteczka odwiedzających i budować grupy remarketingowe reklam.', + 'pixel_meta' => 'Meta Pixel ID', + 'pixel_google' => 'Google Tag / GA4 ID', + 'pixel_linkedin' => 'LinkedIn Partner ID', + 'marketing_webhooks_title' => 'Integracja Webhook linku', + 'marketing_webhooks_desc' => 'Skonfiguruj niestandardowy adres URL, pod który wysłane zostanie natychmiastowe powiadomienie HTTP POST po kliknięciu.', + 'webhook_url' => 'Dedykowany Webhook URL', + + // Settings tab additions + 'settings_tab_developer' => 'API i Webhooki', + 'settings_section_global_webhook' => 'Konfiguracja globalnego Webhooka', + 'settings_global_webhook_url' => 'Globalny Webhook URL', + 'settings_global_webhook_url_helper' => 'Adres URL, pod który wysyłane będą w tle powiadomienia o zdarzeniach ze wszystkich linków.', + 'settings_webhook_events' => 'Monitorowane zdarzenia Webhooka', + 'settings_webhook_events_helper' => 'Wybierz zdarzenia, które powinny wyzwalać wysłanie powiadomienia POST.', + 'webhook_event_visited' => 'Odwiedziny linku (Wizyta)', + 'webhook_event_created' => 'Utworzenie linku', + 'webhook_event_expired' => 'Wygaśnięcie linku (Data)', + 'webhook_event_limit_reached' => 'Osiągnięcie limitu kliknięć', + 'settings_section_api_keys' => 'Klucze API Dewelopera', + 'settings_api_keys_description' => 'Zarządzaj kluczami API do integracji z zewnętrznymi systemami (np. CRM, Zapier, Make). Autoryzacja odbywa się za pomocą nagłówka X-Api-Key.', + 'settings_api_keys' => 'Aktywne klucze API', + 'api_key_name' => 'Nazwa klucza (Opis)', + 'api_key' => 'Klucz API', + 'active' => 'Aktywny', ]; diff --git a/resources/views/pixel-loading.blade.php b/resources/views/pixel-loading.blade.php new file mode 100644 index 0000000..7d8561f --- /dev/null +++ b/resources/views/pixel-loading.blade.php @@ -0,0 +1,160 @@ + + + + + + Connecting... + + + + + + + + @if(!empty($pixelMetaId)) + + + @endif + + + @if(!empty($pixelGoogleId)) + + + @endif + + + @if(!empty($pixelLinkedinId)) + + + @endif + + + + + +
+
+
+
+
+
+

Connecting Safely

+

Securing connection & forwarding you now...

+
+ + + + + diff --git a/routes/web.php b/routes/web.php index 6563d82..f502a40 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,3 +11,11 @@ Route::match( ->name('short-url.redirect') ->where('key', '[a-zA-Z0-9_-]+') ->middleware(config('filament-short-url.middleware', ['web', 'throttle:120,1'])); + +Route::prefix('api/short-url') + ->middleware([\Bjanczak\FilamentShortUrl\Http\Middleware\AuthenticateShortUrlApi::class]) + ->group(function () { + Route::get('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'index']); + Route::post('links', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'store']); + Route::delete('links/{id}', [\Bjanczak\FilamentShortUrl\Http\Controllers\ShortUrlApiController::class, 'destroy']); + }); diff --git a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php index 9075fec..3c3b475 100644 --- a/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php +++ b/src/Filament/Resources/ShortUrlResource/Pages/ShortUrlSettingsPage.php @@ -74,6 +74,9 @@ class ShortUrlSettingsPage extends Page implements HasForms 'qr_gradient_from' => $mgr->get('qr_gradient_from', '#4f46e5'), 'qr_gradient_to' => $mgr->get('qr_gradient_to', '#06b6d4'), 'qr_gradient_type' => $mgr->get('qr_gradient_type', 'linear'), + 'global_webhook_url' => $mgr->get('global_webhook_url'), + 'webhook_events' => $mgr->get('webhook_events', ['visited']), + 'api_keys' => $mgr->get('api_keys', []), ]); } @@ -537,6 +540,56 @@ class ShortUrlSettingsPage extends Page implements HasForms ->visible(fn (Get $get): bool => (bool) $get('qr_gradient_enabled')), ]), ]), + + // ── Developer API & Webhooks ──────────────────────── + Tab::make(__('filament-short-url::default.settings_tab_developer')) + ->icon('heroicon-o-cpu-chip') + ->schema([ + Section::make(__('filament-short-url::default.settings_section_global_webhook')) + ->schema([ + TextInput::make('global_webhook_url') + ->label(__('filament-short-url::default.settings_global_webhook_url')) + ->helperText(__('filament-short-url::default.settings_global_webhook_url_helper')) + ->url() + ->nullable() + ->columnSpanFull(), + + Select::make('webhook_events') + ->label(__('filament-short-url::default.settings_webhook_events')) + ->helperText(__('filament-short-url::default.settings_webhook_events_helper')) + ->multiple() + ->options([ + 'visited' => __('filament-short-url::default.webhook_event_visited'), + 'created' => __('filament-short-url::default.webhook_event_created'), + 'expired' => __('filament-short-url::default.webhook_event_expired'), + 'limit_reached' => __('filament-short-url::default.webhook_event_limit_reached'), + ]) + ->default(['visited']) + ->columnSpanFull(), + ]), + + Section::make(__('filament-short-url::default.settings_section_api_keys')) + ->description(__('filament-short-url::default.settings_api_keys_description')) + ->schema([ + \Filament\Forms\Components\Repeater::make('api_keys') + ->label(__('filament-short-url::default.settings_api_keys')) + ->schema([ + TextInput::make('name') + ->label(__('filament-short-url::default.api_key_name')) + ->required(), + TextInput::make('key') + ->label(__('filament-short-url::default.api_key')) + ->disabled() + ->dehydrated() + ->default(fn () => 'sh_key_'.bin2hex(random_bytes(16))), + Toggle::make('is_active') + ->label(__('filament-short-url::default.active')) + ->default(true), + ]) + ->columns(3) + ->default([]), + ]), + ]), ]), ]) ->statePath('data'); diff --git a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php index 508db89..c3cb944 100644 --- a/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php +++ b/src/Filament/Resources/ShortUrlResource/Schemas/ShortUrlForm.php @@ -27,6 +27,7 @@ class ShortUrlForm static::linkTab(), static::targetingTab(), static::trackingTab(), + static::marketingTab(), static::qrDesignTab(), ])->columnSpanFull(), ]); @@ -477,4 +478,43 @@ class ShortUrlForm ]), ]); } + + private static function marketingTab(): Tab + { + return Tab::make(__('filament-short-url::default.tab_marketing')) + ->icon('heroicon-o-megaphone') + ->schema([ + Section::make(__('filament-short-url::default.marketing_pixels_title')) + ->description(__('filament-short-url::default.marketing_pixels_desc')) + ->schema([ + TextInput::make('pixel_meta_id') + ->label(__('filament-short-url::default.pixel_meta')) + ->placeholder('e.g., 1234567890') + ->maxLength(100) + ->nullable(), + TextInput::make('pixel_google_id') + ->label(__('filament-short-url::default.pixel_google')) + ->placeholder('e.g., G-XXXXXXXXXX or AW-XXXXXXXXXX') + ->maxLength(100) + ->nullable(), + TextInput::make('pixel_linkedin_id') + ->label(__('filament-short-url::default.pixel_linkedin')) + ->placeholder('e.g., 1234567') + ->maxLength(100) + ->nullable(), + ])->columns(3), + + Section::make(__('filament-short-url::default.marketing_webhooks_title')) + ->description(__('filament-short-url::default.marketing_webhooks_desc')) + ->schema([ + TextInput::make('webhook_url') + ->label(__('filament-short-url::default.webhook_url')) + ->placeholder('https://api.yourcrm.com/webhooks/clicks') + ->url() + ->maxLength(2048) + ->nullable() + ->columnSpanFull(), + ]), + ]); + } } diff --git a/src/FilamentShortUrlServiceProvider.php b/src/FilamentShortUrlServiceProvider.php index 6908043..45925dd 100644 --- a/src/FilamentShortUrlServiceProvider.php +++ b/src/FilamentShortUrlServiceProvider.php @@ -32,6 +32,7 @@ class FilamentShortUrlServiceProvider extends PackageServiceProvider '2026_06_01_000004_add_targeting_and_security_to_short_urls_table', '2026_06_01_000005_create_short_url_daily_stats_table', '2026_06_02_000006_add_max_visits_and_expiration_redirect_to_short_urls_table', + '2026_06_02_000007_add_retargeting_pixels_and_webhooks_to_short_urls_table', ]) ->hasCommands([ SyncBufferedCountersCommand::class, diff --git a/src/Http/Controllers/ShortUrlApiController.php b/src/Http/Controllers/ShortUrlApiController.php new file mode 100644 index 0000000..dc06871 --- /dev/null +++ b/src/Http/Controllers/ShortUrlApiController.php @@ -0,0 +1,137 @@ +paginate(30); + + $transformed = $links->getCollection()->map(fn ($link) => $this->transformLink($link)); + + return response()->json([ + 'data' => $transformed, + 'meta' => [ + 'current_page' => $links->currentPage(), + 'last_page' => $links->lastPage(), + 'per_page' => $links->perPage(), + 'total' => $links->total(), + ], + ]); + } + + /** + * Store a newly created short URL. + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'destination_url' => 'required|url|max:2048', + 'url_key' => 'nullable|string|alpha_dash|max:50|unique:short_urls,url_key', + 'notes' => 'nullable|string|max:1000', + 'is_enabled' => 'nullable|boolean', + 'redirect_status_code' => 'nullable|integer|in:301,302', + 'single_use' => 'nullable|boolean', + 'forward_query_params' => 'nullable|boolean', + 'max_visits' => 'nullable|integer|min:1', + 'expiration_redirect_url' => 'nullable|url|max:2048', + 'activated_at' => 'nullable|date', + 'expires_at' => 'nullable|date', + 'pixel_meta_id' => 'nullable|string|max:100', + 'pixel_google_id' => 'nullable|string|max:100', + 'pixel_linkedin_id' => 'nullable|string|max:100', + 'webhook_url' => 'nullable|url|max:2048', + ]); + + $shortUrl = $this->service->create($validated); + + // Fire 'created' webhook if active + $this->dispatchCreatedWebhook($shortUrl); + + return response()->json([ + 'message' => 'Short URL created successfully.', + 'data' => $this->transformLink($shortUrl), + ], 201); + } + + /** + * Remove the specified short URL. + */ + public function destroy(int $id): JsonResponse + { + $shortUrl = ShortUrl::findOrFail($id); + $shortUrl->delete(); + + return response()->json([ + 'message' => 'Short URL deleted successfully.', + ]); + } + + /** + * Transform a ShortUrl model to API response array. + */ + private function transformLink(ShortUrl $link): array + { + return [ + 'id' => $link->id, + 'destination_url' => $link->destination_url, + 'url_key' => $link->url_key, + 'short_url' => $link->getShortUrl(), + 'is_enabled' => (bool) $link->is_enabled, + 'redirect_status_code' => (int) $link->redirect_status_code, + 'total_visits' => (int) $link->total_visits, + 'unique_visits' => (int) $link->unique_visits, + 'max_visits' => $link->max_visits ? (int) $link->max_visits : null, + 'activated_at' => $link->activated_at ? $link->activated_at->toIso8601String() : null, + 'expires_at' => $link->expires_at ? $link->expires_at->toIso8601String() : null, + 'pixel_meta_id' => $link->pixel_meta_id, + 'pixel_google_id' => $link->pixel_google_id, + 'pixel_linkedin_id' => $link->pixel_linkedin_id, + 'webhook_url' => $link->webhook_url, + 'notes' => $link->notes, + 'created_at' => $link->created_at->toIso8601String(), + ]; + } + + /** + * Dispatch webhook if global or per-link webhook is active for 'created' event. + */ + private function dispatchCreatedWebhook(ShortUrl $shortUrl): void + { + $targetUrl = $shortUrl->webhook_url; + $globalUrl = config('filament-short-url.global_webhook_url'); + $events = config('filament-short-url.webhook_events', []); + + if (empty($targetUrl) && !empty($globalUrl) && in_array('created', $events)) { + $targetUrl = $globalUrl; + } + + if (!empty($targetUrl)) { + $connection = config('filament-short-url.queue_connection', 'sync'); + dispatch(new SendWebhookJob( + url: $targetUrl, + event: 'created', + payload: [ + 'event' => 'created', + 'timestamp' => now()->toIso8601String(), + 'short_url' => $this->transformLink($shortUrl), + ] + )->onConnection($connection ?: 'sync')); + } + } +} diff --git a/src/Http/Controllers/ShortUrlRedirectController.php b/src/Http/Controllers/ShortUrlRedirectController.php index 384b55a..f5ac180 100644 --- a/src/Http/Controllers/ShortUrlRedirectController.php +++ b/src/Http/Controllers/ShortUrlRedirectController.php @@ -141,6 +141,15 @@ class ShortUrlRedirectController extends Controller cache()->forget("filament-short-url:{$shortUrl->url_key}"); } + if (! empty($shortUrl->pixel_meta_id) || ! empty($shortUrl->pixel_google_id) || ! empty($shortUrl->pixel_linkedin_id)) { + return response(view('filament-short-url::pixel-loading', [ + 'destination' => $destination, + 'pixelMetaId' => $shortUrl->pixel_meta_id, + 'pixelGoogleId' => $shortUrl->pixel_google_id, + 'pixelLinkedinId' => $shortUrl->pixel_linkedin_id, + ]))->header('Content-Type', 'text/html'); + } + return redirect()->away($destination, $shortUrl->redirect_status_code); } } diff --git a/src/Http/Middleware/AuthenticateShortUrlApi.php b/src/Http/Middleware/AuthenticateShortUrlApi.php new file mode 100644 index 0000000..6a24880 --- /dev/null +++ b/src/Http/Middleware/AuthenticateShortUrlApi.php @@ -0,0 +1,50 @@ +header('X-Api-Key'); + + if (!$apiKey && $auth = $request->header('Authorization')) { + if (str_starts_with(strtolower($auth), 'bearer ')) { + $apiKey = substr($auth, 7); + } + } + + if (empty($apiKey)) { + return response()->json([ + 'error' => 'Unauthorized. API Key is missing.', + ], 401); + } + + $mgr = app(ShortUrlSettingsManager::class); + $keys = $mgr->get('api_keys', []); + + $valid = false; + foreach ($keys as $keyObj) { + if (($keyObj['key'] ?? '') === $apiKey && (bool) ($keyObj['is_active'] ?? false)) { + $valid = true; + break; + } + } + + if (!$valid) { + return response()->json([ + 'error' => 'Unauthorized. Invalid or inactive API Key.', + ], 401); + } + + return $next($request); + } +} diff --git a/src/Jobs/SendWebhookJob.php b/src/Jobs/SendWebhookJob.php new file mode 100644 index 0000000..de7b362 --- /dev/null +++ b/src/Jobs/SendWebhookJob.php @@ -0,0 +1,70 @@ +onQueue(config('filament-short-url.queue_name', 'default')); + } + + /** + * Execute the job. + */ + public function handle(): void + { + try { + $response = Http::timeout(10) + ->withHeaders([ + 'Content-Type' => 'application/json', + 'User-Agent' => 'wYachts-ShortUrl-Webhook/1.5', + ]) + ->post($this->url, $this->payload); + + if ($response->failed()) { + Log::warning("[FilamentShortUrl] Webhook delivery returned client/server error", [ + 'url' => $this->url, + 'event' => $this->event, + 'status' => $response->status(), + ]); + + // Throw exception to trigger queue retry + throw new \RuntimeException("Webhook failed with status " . $response->status()); + } + } catch (\Throwable $e) { + Log::warning("[FilamentShortUrl] Webhook delivery failed", [ + 'url' => $this->url, + 'event' => $this->event, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } +} diff --git a/src/Jobs/TrackShortUrlVisitJob.php b/src/Jobs/TrackShortUrlVisitJob.php index a477a43..bcdf4e8 100644 --- a/src/Jobs/TrackShortUrlVisitJob.php +++ b/src/Jobs/TrackShortUrlVisitJob.php @@ -90,6 +90,53 @@ class TrackShortUrlVisitJob implements ShouldQueue // Fire event for user listeners ShortUrlVisited::dispatch($shortUrl, $visit); + // Trigger Webhook if active + $targetUrl = $shortUrl->webhook_url; + $globalUrl = config('filament-short-url.global_webhook_url'); + $events = config('filament-short-url.webhook_events', []); + + if (empty($targetUrl) && ! empty($globalUrl) && in_array('visited', $events)) { + $targetUrl = $globalUrl; + } + + if (! empty($targetUrl)) { + dispatch(new \Bjanczak\FilamentShortUrl\Jobs\SendWebhookJob( + url: $targetUrl, + event: 'visited', + payload: [ + 'event' => 'visited', + 'timestamp' => now()->toIso8601String(), + 'short_url' => [ + 'id' => $shortUrl->id, + 'destination_url' => $shortUrl->destination_url, + 'url_key' => $shortUrl->url_key, + 'short_url' => $shortUrl->getShortUrl(), + 'total_visits' => (int) $shortUrl->getRealTimeTotalVisits(), + 'unique_visits' => (int) $shortUrl->unique_visits, + ], + 'visit' => [ + 'id' => $visit->id, + 'visited_at' => $visit->visited_at->toIso8601String(), + 'device_type' => $visit->device_type, + 'browser' => $visit->browser, + 'browser_version' => $visit->browser_version, + 'operating_system' => $visit->operating_system, + 'operating_system_version' => $visit->operating_system_version, + 'country' => $visit->country, + 'country_code' => $visit->country_code, + 'city' => $visit->city, + 'referer_url' => $visit->referer_url, + 'referer_host' => $visit->referer_host, + 'utm_source' => $visit->utm_source, + 'utm_medium' => $visit->utm_medium, + 'utm_campaign' => $visit->utm_campaign, + 'utm_term' => $visit->utm_term, + 'utm_content' => $visit->utm_content, + ], + ] + )->onConnection($this->connection ?: 'sync')); + } + // Optional GA4 Measurement Protocol integration if ($shortUrl->ga_tracking_id && config('filament-short-url.ga4.api_secret')) { $this->sendGa4Hit($shortUrl, $visit); diff --git a/src/Models/ShortUrl.php b/src/Models/ShortUrl.php index b81c7f1..0dcc19e 100644 --- a/src/Models/ShortUrl.php +++ b/src/Models/ShortUrl.php @@ -80,6 +80,10 @@ class ShortUrl extends Model 'unique_visits', 'max_visits', 'expiration_redirect_url', + 'pixel_meta_id', + 'pixel_google_id', + 'pixel_linkedin_id', + 'webhook_url', ]; /** @var array */ @@ -186,6 +190,19 @@ class ShortUrl extends Model if ($m->activated_at === null && $m->expires_at === null) { $m->expiration_redirect_url = null; } + + if (empty($m->pixel_meta_id)) { + $m->pixel_meta_id = null; + } + if (empty($m->pixel_google_id)) { + $m->pixel_google_id = null; + } + if (empty($m->pixel_linkedin_id)) { + $m->pixel_linkedin_id = null; + } + if (empty($m->webhook_url)) { + $m->webhook_url = null; + } }); static::saved(function (self $m) { diff --git a/src/Services/ShortUrlSettingsManager.php b/src/Services/ShortUrlSettingsManager.php index 6a4e078..f009770 100644 --- a/src/Services/ShortUrlSettingsManager.php +++ b/src/Services/ShortUrlSettingsManager.php @@ -76,6 +76,9 @@ class ShortUrlSettingsManager 'qr_gradient_from' => config('filament-short-url.qr_defaults.gradient_from', '#4f46e5'), 'qr_gradient_to' => config('filament-short-url.qr_defaults.gradient_to', '#06b6d4'), 'qr_gradient_type' => config('filament-short-url.qr_defaults.gradient_type', 'linear'), + 'global_webhook_url' => null, + 'webhook_events' => ['visited'], + 'api_keys' => [], ], $stored); return $this->cache; @@ -142,6 +145,9 @@ class ShortUrlSettingsManager 'qr_gradient_from', 'qr_gradient_to', 'qr_gradient_type', + 'global_webhook_url', + 'webhook_events', + 'api_keys', ]; $filtered = array_intersect_key($data, array_flip($keys)); @@ -292,6 +298,8 @@ class ShortUrlSettingsManager 'filament-short-url.qr_defaults.gradient_from' => $settings['qr_gradient_from'], 'filament-short-url.qr_defaults.gradient_to' => $settings['qr_gradient_to'], 'filament-short-url.qr_defaults.gradient_type' => $settings['qr_gradient_type'], + 'filament-short-url.global_webhook_url' => $settings['global_webhook_url'] ?? null, + 'filament-short-url.webhook_events' => $settings['webhook_events'] ?? ['visited'], ]); } }