From 81d94a6f7a8f15c4489104ff5a81f686cd062e6b Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Wed, 10 Jun 2026 15:10:10 +0300 Subject: [PATCH 01/13] feat(notifications): add install-plugin action to What's New panel Adds the ability to install and activate a free WP.org plugin directly from a What's New notification card, without leaving the editor. Changes: - whats-new-item.js: InstallPluginButton component with install/activate/ dismiss flow matching the Elementor Angie install pattern (folder_exists error code, POST for activation via wp.apiFetch) - api.php: ALLOWED_INSTALL_SLUGS allowlist + sanitize_install_fields() strips installPlugin/ctaActivate from CDN responses for unknown slugs - options.php: mark_notification_installed() dismisses a single card by ID using the existing _e_notifications_dismissed user_meta - module.php: notifications_mark_installed AJAX action New notification fields: installPlugin (slug), ctaActivate (label) Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 22 ++++ .../assets/js/components/whats-new-item.js | 110 +++++++++++++++++- modules/notifications/module.php | 13 +++ modules/notifications/options.php | 20 ++++ 4 files changed, 164 insertions(+), 1 deletion(-) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 8a3d47eb141f..626ba23bc8fc 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -8,12 +8,21 @@ class API { const NOTIFICATIONS_URL = 'https://assets.elementor.com/notifications/v1/notifications.json'; + /** + * Allowlist of WP.org plugin slugs that may be installed via the What's New panel. + * Any `installPlugin` value not in this list is stripped before the notification + * reaches the browser, protecting against a compromised CDN response. + */ + const ALLOWED_INSTALL_SLUGS = []; + public static function get_notifications_by_conditions( $force_request = false ) { $notifications = static::get_notifications( $force_request ); $filtered_notifications = []; foreach ( $notifications as $notification ) { + $notification = static::sanitize_install_fields( $notification ); + if ( empty( $notification['conditions'] ) ) { $filtered_notifications = static::add_to_array( $filtered_notifications, $notification ); @@ -65,6 +74,19 @@ private static function check_conditions( $groups ) { return false; } + private static function sanitize_install_fields( array $notification ): array { + if ( empty( $notification['installPlugin'] ) ) { + return $notification; + } + + if ( ! in_array( $notification['installPlugin'], static::ALLOWED_INSTALL_SLUGS, true ) ) { + unset( $notification['installPlugin'] ); + unset( $notification['ctaActivate'] ); + } + + return $notification; + } + private static function check_group( $group ) { $is_or_relation = ! empty( $group['relation'] ) && 'OR' === $group['relation']; unset( $group['relation'] ); diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index b98bd8b4653e..24e54cf32c86 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -1,9 +1,108 @@ +import { useState } from 'react'; +import { __ } from '@wordpress/i18n'; import { Box, Button, Divider, Link, Typography } from '@elementor/ui'; import { WhatsNewItemTopicLine } from './whats-new-item-topic-line'; import { WrapperWithLink } from './wrapper-with-link'; import { WhatsNewItemThumbnail } from './whats-new-item-thumbnail'; import { WhatsNewItemChips } from './whats-new-item-chips'; +const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabel } ) => { + const [ status, setStatus ] = useState( 'idle' ); // Idle | installing | activating | done | error + const [ errorMsg, setErrorMsg ] = useState( '' ); + + const dismissCard = () => { + elementorCommon.ajax.addRequest( 'notifications_mark_installed', { + data: { notification_id: notificationId }, + } ); + }; + + const findAndActivate = async () => { + setStatus( 'activating' ); + try { + const plugins = await wp.apiFetch( { path: '/wp/v2/plugins' } ); + const plugin = plugins.find( ( p ) => p.plugin.startsWith( slug + '/' ) ); + + if ( plugin && 'active' === plugin.status ) { + setStatus( 'done' ); + dismissCard(); + return; + } + + if ( plugin ) { + await wp.apiFetch( { + path: `/wp/v2/plugins/${ encodeURIComponent( plugin.plugin ) }`, + method: 'PUT', + data: { status: 'active' }, + } ); + } + + setStatus( 'done' ); + dismissCard(); + } catch ( err ) { + setStatus( 'error' ); + setErrorMsg( err?.message || __( 'Activation failed', 'elementor' ) ); + } + }; + + const handleInstall = async () => { + setStatus( 'installing' ); + try { + await wp.apiFetch( { + path: '/wp/v2/plugins', + method: 'POST', + data: { slug, status: 'active' }, + } ); + setStatus( 'done' ); + dismissCard(); + } catch ( err ) { + // Plugin already installed — find it and activate + if ( 'folder_exists' === err?.code ) { + await findAndActivate(); + } else { + setStatus( 'error' ); + setErrorMsg( err?.message || __( 'Installation failed', 'elementor' ) ); + } + } + }; + + const getButtonLabel = () => { + switch ( status ) { + case 'installing': return __( 'Installing...', 'elementor' ); + case 'activating': return activateLabel || __( 'Activating...', 'elementor' ); + case 'done': return __( 'Installed ✓', 'elementor' ); + default: return installLabel; + } + }; + + const isDisabled = [ 'installing', 'activating', 'done' ].includes( status ); + + return ( + + + { 'error' === status && ( + + { errorMsg } + + ) } + + ); +}; + +InstallPluginButton.propTypes = { + slug: PropTypes.string.isRequired, + notificationId: PropTypes.string.isRequired, + installLabel: PropTypes.string.isRequired, + activateLabel: PropTypes.string, +}; + export const WhatsNewItem = ( { item, itemIndex, itemsLength, setIsOpen } ) => { return ( { ) } ) } - { item.cta && item.ctaLink && ( + { item.installPlugin ? ( + + + + ) : item.cta && item.ctaLink && ( register_ajax_action( 'notifications_get', [ $this, 'ajax_get_notifications' ] ); + $ajax->register_ajax_action( 'notifications_mark_installed', [ $this, 'ajax_mark_notification_installed' ] ); } public function ajax_get_notifications() { @@ -93,4 +94,16 @@ public function ajax_get_notifications() { return $notifications; } + + public function ajax_mark_notification_installed( $data ) { + $notification_id = sanitize_text_field( $data['notification_id'] ?? '' ); + + if ( empty( $notification_id ) ) { + throw new \Exception( 'Missing notification_id' ); + } + + Options::mark_notification_installed( $notification_id ); + + return true; + } } diff --git a/modules/notifications/options.php b/modules/notifications/options.php index 9b81d1055531..e73f2c927314 100644 --- a/modules/notifications/options.php +++ b/modules/notifications/options.php @@ -40,6 +40,26 @@ public static function get_notifications_dismissed() { return $notifications_dismissed; } + public static function mark_notification_installed( string $notification_id ): bool { + $current_user = wp_get_current_user(); + + if ( ! $current_user ) { + return false; + } + + $notifications_dismissed = static::get_notifications_dismissed(); + + if ( ! in_array( $notification_id, $notifications_dismissed, true ) ) { + $notifications_dismissed[] = $notification_id; + } + + update_user_meta( $current_user->ID, '_e_notifications_dismissed', $notifications_dismissed ); + + delete_transient( "elementor_unread_notifications_{$current_user->ID}" ); + + return true; + } + public static function mark_notification_read( $notifications ): bool { $current_user = wp_get_current_user(); From da93d8c00124f433946228d3f481228cb19a1663 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Wed, 10 Jun 2026 15:33:21 +0300 Subject: [PATCH 02/13] fix(notifications): use POST instead of PUT for plugin activation Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/assets/js/components/whats-new-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index 24e54cf32c86..fb50c91c5100 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -31,7 +31,7 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe if ( plugin ) { await wp.apiFetch( { path: `/wp/v2/plugins/${ encodeURIComponent( plugin.plugin ) }`, - method: 'PUT', + method: 'POST', data: { status: 'active' }, } ); } From 6230c99510e8aaa1618cd27a5ab7693a5f25fd81 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Wed, 10 Jun 2026 17:16:49 +0300 Subject: [PATCH 03/13] fix(notifications): polish install-plugin UX and dismiss logic - Use promotion color for install button to match CTA - Track installed notifications separately (_e_notifications_installed) to avoid hiding all read notifications from the panel - Filter out installed notifications server-side on panel open - Invalidate query on panel close so card disappears on next open Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 2 +- .../assets/js/components/whats-new-item.js | 2 +- .../assets/js/components/whats-new.js | 1 + modules/notifications/module.php | 5 +++++ modules/notifications/options.php | 20 +++++++++++++++---- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 626ba23bc8fc..bff22b696d57 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -13,7 +13,7 @@ class API { * Any `installPlugin` value not in this list is stripped before the notification * reaches the browser, protecting against a compromised CDN response. */ - const ALLOWED_INSTALL_SLUGS = []; + const ALLOWED_INSTALL_SLUGS = [ 'angie' ]; public static function get_notifications_by_conditions( $force_request = false ) { $notifications = static::get_notifications( $force_request ); diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index fb50c91c5100..12c0e56780f7 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -81,7 +81,7 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe + { 'done' === status && ( + + + { __( 'Open WP Admin to get started', 'elementor' ) } + + + ) } { 'error' === status && ( { errorMsg } diff --git a/modules/notifications/module.php b/modules/notifications/module.php index 0fcf13399048..036fbb3062c8 100644 --- a/modules/notifications/module.php +++ b/modules/notifications/module.php @@ -79,6 +79,7 @@ public function enqueue_editor_scripts() { private function get_app_js_config(): array { return [ 'is_unread' => Options::has_unread_notifications(), + 'admin_url' => admin_url(), ]; } From a7c10e30ab8b85c92a5decf75dd72091127d595b Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Thu, 11 Jun 2026 07:30:59 +0300 Subject: [PATCH 07/13] fix(notifications): remove angie from install allowlist until capability bug is resolved Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 74e4dfeb09c4..4d0518ce5edb 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -14,7 +14,6 @@ class API { * reaches the browser, protecting against a compromised CDN response. */ const ALLOWED_INSTALL_SLUGS = [ - 'angie', 'site-mailer', 'image-optimization', 'pojo-accessibility', From 797aee51e042de74a4fb7ac9672c81c28ca68189 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Thu, 11 Jun 2026 08:19:00 +0300 Subject: [PATCH 08/13] feat(notifications): support installLabel field for install button Falls back to cta then default 'Install Plugin' for backward compatibility. Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/assets/js/components/whats-new-item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index 2fe452d82a08..83c2eddd9f45 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -178,7 +178,7 @@ export const WhatsNewItem = ( { item, itemIndex, itemsLength, setIsOpen } ) => { From a54373e50f24a830ba6a9123a38af7820dc22af8 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Thu, 11 Jun 2026 08:21:21 +0300 Subject: [PATCH 09/13] feat(notifications): strip installLabel when installPlugin slug is not allowed Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 4d0518ce5edb..42583f911694 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -87,6 +87,7 @@ private static function sanitize_install_fields( array $notification ): array { if ( ! in_array( $notification['installPlugin'], static::ALLOWED_INSTALL_SLUGS, true ) ) { unset( $notification['installPlugin'] ); + unset( $notification['installLabel'] ); unset( $notification['ctaActivate'] ); } From 14ee49e0970f6a97c84811b0540d9445e3c30c67 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Thu, 11 Jun 2026 08:44:27 +0300 Subject: [PATCH 10/13] feat(notifications): add installErrorText and postInstallLink fields - installErrorText: custom error message shown on install/activation failure - postInstallLink: when set, shows a link after successful install - postInstallText: custom label for post-install link (default: "Explore and get started") - All three fields are stripped alongside installPlugin when the slug is not allowed Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 3 +++ .../assets/js/components/whats-new-item.js | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 42583f911694..26aee5237464 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -88,7 +88,10 @@ private static function sanitize_install_fields( array $notification ): array { if ( ! in_array( $notification['installPlugin'], static::ALLOWED_INSTALL_SLUGS, true ) ) { unset( $notification['installPlugin'] ); unset( $notification['installLabel'] ); + unset( $notification['installErrorText'] ); unset( $notification['ctaActivate'] ); + unset( $notification['postInstallLink'] ); + unset( $notification['postInstallText'] ); } return $notification; diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index 83c2eddd9f45..8df2df74e312 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -6,7 +6,7 @@ import { WrapperWithLink } from './wrapper-with-link'; import { WhatsNewItemThumbnail } from './whats-new-item-thumbnail'; import { WhatsNewItemChips } from './whats-new-item-chips'; -const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabel } ) => { +const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabel, errorText, postInstallLink, postInstallText } ) => { const [ status, setStatus ] = useState( 'idle' ); // Idle | installing | activating | done | error const [ errorMsg, setErrorMsg ] = useState( '' ); @@ -40,7 +40,7 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe dismissCard(); } catch ( err ) { setStatus( 'error' ); - setErrorMsg( err?.message || __( 'Activation failed', 'elementor' ) ); + setErrorMsg( errorText || err?.message || __( 'Activation failed', 'elementor' ) ); } }; @@ -60,7 +60,7 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe await findAndActivate(); } else { setStatus( 'error' ); - setErrorMsg( err?.message || __( 'Installation failed', 'elementor' ) ); + setErrorMsg( errorText || err?.message || __( 'Installation failed', 'elementor' ) ); } } }; @@ -87,10 +87,10 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe > { getButtonLabel() } - { 'done' === status && ( + { 'done' === status && postInstallLink && ( - - { __( 'Open WP Admin to get started', 'elementor' ) } + + { postInstallText || __( 'Explore and get started', 'elementor' ) } ) } @@ -108,6 +108,9 @@ InstallPluginButton.propTypes = { notificationId: PropTypes.string.isRequired, installLabel: PropTypes.string.isRequired, activateLabel: PropTypes.string, + errorText: PropTypes.string, + postInstallLink: PropTypes.string, + postInstallText: PropTypes.string, }; export const WhatsNewItem = ( { item, itemIndex, itemsLength, setIsOpen } ) => { @@ -180,6 +183,9 @@ export const WhatsNewItem = ( { item, itemIndex, itemsLength, setIsOpen } ) => { notificationId={ item.id } installLabel={ item.installLabel || item.cta || __( 'Install Plugin', 'elementor' ) } activateLabel={ item.ctaActivate } + errorText={ item.installErrorText } + postInstallLink={ item.postInstallLink } + postInstallText={ item.postInstallText } /> ) : item.cta && item.ctaLink && ( From 63a0e9935bf0859f8e4e0a0bee979509daf29684 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Thu, 11 Jun 2026 11:09:22 +0300 Subject: [PATCH 11/13] fix(notifications): editors see all notifications, admins-only see install ones - ajax_get_notifications: use edit_posts cap (editors can fetch notifications) - install-plugin notifications are filtered out for non-admin users - ajax_mark_notification_installed: keep manage_options (install = admin only) Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/module.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/notifications/module.php b/modules/notifications/module.php index 036fbb3062c8..8936d6bec339 100644 --- a/modules/notifications/module.php +++ b/modules/notifications/module.php @@ -89,14 +89,19 @@ public function register_ajax_actions( $ajax ) { } public function ajax_get_notifications() { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( 'edit_posts' ) ) { throw new \Exception( 'Insufficient permissions' ); } $notifications = API::get_notifications_by_conditions( true ); + $is_admin = current_user_can( 'manage_options' ); + $installed = Options::get_notifications_installed(); - $notifications = array_values( array_filter( $notifications, function( $n ) use ( $installed ) { + $notifications = array_values( array_filter( $notifications, function( $n ) use ( $installed, $is_admin ) { + if ( ! empty( $n['installPlugin'] ) && ! $is_admin ) { + return false; + } return ! in_array( $n['id'], $installed, true ); } ) ); From 222d56f4aa95714b397e879560656c3a63307891 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Sat, 20 Jun 2026 08:43:17 +0300 Subject: [PATCH 12/13] feat(notifications): add angie to install plugin allowlist Adds 'angie' to ALLOWED_INSTALL_SLUGS so What's New notifications can trigger Angie installation via the installPlugin field. Co-Authored-By: Claude Sonnet 4.6 --- modules/notifications/api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/notifications/api.php b/modules/notifications/api.php index 26aee5237464..e17c4a617802 100644 --- a/modules/notifications/api.php +++ b/modules/notifications/api.php @@ -19,6 +19,7 @@ class API { 'pojo-accessibility', 'cookiez', 'manage', + 'angie', ]; public static function get_notifications_by_conditions( $force_request = false ) { From 56708d94d5d5434e0367a81bcffd2cb165765256 Mon Sep 17 00:00:00 2001 From: Aviv Umflat Date: Sat, 20 Jun 2026 08:59:01 +0300 Subject: [PATCH 13/13] fix(notifications): address standards and edge-case issues in install-plugin feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix wp_set_script_translations using wrong handle (e-editor-notifications → e-admin-notifications) in admin closure - Remove dead `admin_url` key from get_app_js_config() — was unused by JS - Fix wp_get_current_user() guards: use exists() instead of falsy check (get_current_user always returns WP_User) - Fix has_unread_notifications() to also subtract installed IDs so bell dot clears after plugin install - Fix findAndActivate() to set error state when plugin not found in REST list instead of silently succeeding Co-Authored-By: Claude Sonnet 4.6 --- .../notifications/assets/js/components/whats-new-item.js | 8 ++++---- modules/notifications/module.php | 3 +-- modules/notifications/options.php | 6 +++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/modules/notifications/assets/js/components/whats-new-item.js b/modules/notifications/assets/js/components/whats-new-item.js index 8df2df74e312..f041e4d24fda 100644 --- a/modules/notifications/assets/js/components/whats-new-item.js +++ b/modules/notifications/assets/js/components/whats-new-item.js @@ -22,13 +22,13 @@ const InstallPluginButton = ( { slug, notificationId, installLabel, activateLabe const plugins = await wp.apiFetch( { path: '/wp/v2/plugins' } ); const plugin = plugins.find( ( p ) => p.plugin.startsWith( slug + '/' ) ); - if ( plugin && 'active' === plugin.status ) { - setStatus( 'done' ); - dismissCard(); + if ( ! plugin ) { + setStatus( 'error' ); + setErrorMsg( errorText || __( 'Plugin not found.', 'elementor' ) ); return; } - if ( plugin ) { + if ( 'active' !== plugin.status ) { await wp.apiFetch( { path: `/wp/v2/plugins/${ encodeURIComponent( plugin.plugin ) }`, method: 'POST', diff --git a/modules/notifications/module.php b/modules/notifications/module.php index 8936d6bec339..3b9b177590b8 100644 --- a/modules/notifications/module.php +++ b/modules/notifications/module.php @@ -40,7 +40,7 @@ public function __construct() { $this->get_app_js_config() ); - wp_set_script_translations( 'e-editor-notifications', 'elementor' ); + wp_set_script_translations( 'e-admin-notifications', 'elementor' ); }, 5 /* Before Elementor's admin enqueue scripts */ ); add_action( 'elementor/editor/v2/scripts/enqueue', [ $this, 'enqueue_editor_scripts' ] ); @@ -79,7 +79,6 @@ public function enqueue_editor_scripts() { private function get_app_js_config(): array { return [ 'is_unread' => Options::has_unread_notifications(), - 'admin_url' => admin_url(), ]; } diff --git a/modules/notifications/options.php b/modules/notifications/options.php index 263e7c3b770c..604eae544120 100644 --- a/modules/notifications/options.php +++ b/modules/notifications/options.php @@ -16,7 +16,7 @@ public static function has_unread_notifications(): bool { $notifications = API::get_notifications_by_conditions(); $notifications_ids = wp_list_pluck( $notifications, 'id' ); - $unread_notifications = array_diff( $notifications_ids, static::get_notifications_dismissed() ); + $unread_notifications = array_diff( $notifications_ids, static::get_notifications_dismissed(), static::get_notifications_installed() ); set_transient( "elementor_unread_notifications_{$current_user->ID}", $unread_notifications, HOUR_IN_SECONDS ); } @@ -43,7 +43,7 @@ public static function get_notifications_dismissed() { public static function get_notifications_installed(): array { $current_user = wp_get_current_user(); - if ( ! $current_user ) { + if ( ! $current_user->exists() ) { return []; } @@ -55,7 +55,7 @@ public static function get_notifications_installed(): array { public static function mark_notification_installed( string $notification_id ): bool { $current_user = wp_get_current_user(); - if ( ! $current_user ) { + if ( ! $current_user->exists() ) { return false; }