diff --git a/assets/css/page-widgets/todo.css b/assets/css/page-widgets/todo.css
index cdd6898d96..1c7745fa5b 100644
--- a/assets/css/page-widgets/todo.css
+++ b/assets/css/page-widgets/todo.css
@@ -39,7 +39,7 @@
display: inline-block;
width: 24px;
height: 24px;
- background-image: url("../../images/icon_progress_planner.svg");
+ background-image: var(--prpl-icon-url);
background-size: contain;
background-repeat: no-repeat;
}
@@ -136,7 +136,7 @@
display: inline-block;
width: 24px;
height: 24px;
- background-image: url("../../images/icon_progress_planner.svg");
+ background-image: var(--prpl-icon-url);
background-size: contain;
background-repeat: no-repeat;
}
diff --git a/assets/js/focus-element.js b/assets/js/focus-element.js
index 09b56d2e02..174b38b77f 100644
--- a/assets/js/focus-element.js
+++ b/assets/js/focus-element.js
@@ -8,9 +8,7 @@
const prplGetIndicatorElement = ( content, taskId, points ) => {
// Create an element.
const imgEl = document.createElement( 'img' );
- imgEl.src =
- progressPlannerFocusElement.base_url +
- '/assets/images/icon_progress_planner.svg';
+ imgEl.src = progressPlannerFocusElement.iconUrl;
imgEl.alt = points
? prplL10n( 'fixThisIssue' ).replace( '%d', points )
: '';
diff --git a/assets/js/yoast-focus-element.js b/assets/js/yoast-focus-element.js
index e35cda92e7..f5b1b01c13 100644
--- a/assets/js/yoast-focus-element.js
+++ b/assets/js/yoast-focus-element.js
@@ -14,7 +14,7 @@ class ProgressPlannerYoastFocus {
constructor() {
this.container = document.querySelector( '#yoast-seo-settings' );
this.tasks = progressPlannerYoastFocusElement.tasks;
- this.baseUrl = progressPlannerYoastFocusElement.base_url;
+ this.iconUrl = progressPlannerYoastFocusElement.iconUrl;
if ( this.container ) {
this.init();
@@ -219,7 +219,7 @@ class ProgressPlannerYoastFocus {
// Create an icon image.
const iconImg = document.createElement( 'img' );
- iconImg.src = this.baseUrl + '/assets/images/icon_progress_planner.svg';
+ iconImg.src = this.iconUrl;
iconImg.alt = 'Ravi';
iconImg.width = 16;
iconImg.height = 16;
diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php
index 8959089943..5908b9850f 100644
--- a/classes/admin/class-enqueue.php
+++ b/classes/admin/class-enqueue.php
@@ -301,7 +301,7 @@ public function localize_script( $handle, $localize_data = [] ) {
$localize_data = [
'name' => 'prplCelebrate',
'data' => [
- 'raviIconUrl' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg',
+ 'raviIconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(),
'confettiOptions' => $confetti_options,
],
];
diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php
index 21c58a8647..73d38c170f 100644
--- a/classes/admin/class-page.php
+++ b/classes/admin/class-page.php
@@ -261,7 +261,7 @@ public function maybe_enqueue_focus_el_script( $hook ) {
'tasks' => $tasks_details,
'totalPoints' => $total_points,
'completedPoints' => $completed_points,
- 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ),
+ 'iconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(),
'l10n' => [
/* translators: %d: The number of points. */
'fixThisIssue' => \esc_html__( 'Fix this issue to get %d point(s) in Progress Planner', 'progress-planner' ),
diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php
index 171abb3d0a..bfc2903d2a 100644
--- a/classes/admin/widgets/class-todo.php
+++ b/classes/admin/widgets/class-todo.php
@@ -26,6 +26,19 @@ final class ToDo extends Widget {
*/
protected $width = 2;
+ /**
+ * Enqueue styles.
+ *
+ * @return void
+ */
+ public function enqueue_styles() {
+ parent::enqueue_styles();
+ \wp_add_inline_style(
+ "progress-planner/page-widgets/{$this->id}",
+ ':root { --prpl-icon-url: url("' . \progress_planner()->get_ui__branding()->get_admin_menu_icon() . '"); }'
+ );
+ }
+
/**
* Print the widget content.
*
diff --git a/classes/class-badges.php b/classes/class-badges.php
index 904c94f0b2..2c751ba636 100644
--- a/classes/class-badges.php
+++ b/classes/class-badges.php
@@ -83,7 +83,19 @@ public function __construct() {
* @return \Progress_Planner\Badges\Badge[]
*/
public function get_badges( $context ) {
- return isset( $this->$context ) ? $this->$context : [];
+ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline @var for PHPStan.
+ /** @var \Progress_Planner\Badges\Badge[] $badges */
+ $badges = isset( $this->$context ) ? $this->$context : [];
+
+ /**
+ * Filter the badges for a context.
+ *
+ * @param \Progress_Planner\Badges\Badge[] $badges The badges.
+ * @param string $context The badges context.
+ *
+ * @return \Progress_Planner\Badges\Badge[]
+ */
+ return \apply_filters( 'progress_planner_badges', $badges, $context );
}
/**
@@ -101,7 +113,18 @@ public function get_badge( $badge_id ) {
}
}
}
- return null;
+
+ /**
+ * Filter for retrieving a single badge by ID.
+ *
+ * Allows external plugins to provide custom badges.
+ *
+ * @param \Progress_Planner\Badges\Badge|null $badge The badge object or null.
+ * @param string $badge_id The badge ID.
+ *
+ * @return \Progress_Planner\Badges\Badge|null
+ */
+ return \apply_filters( 'progress_planner_get_badge', null, $badge_id );
}
/**
@@ -183,6 +206,8 @@ public function get_latest_completed_badge() {
// Get the settings for badges (stores completion dates).
$settings = \progress_planner()->get_settings()->get( 'badges', [] );
+ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline @var for PHPStan.
+ /** @var string|null $latest_date */
$latest_date = null;
// Loop through all badge contexts to find the most recently completed badge.
@@ -204,20 +229,30 @@ public function get_latest_completed_badge() {
if ( null === $latest_date ) {
$this->latest_completed_badge = $badge;
if ( isset( $settings[ $badge->get_id() ]['date'] ) ) {
- $latest_date = $settings[ $badge->get_id() ]['date'];
+ $latest_date = (string) $settings[ $badge->get_id() ]['date'];
}
continue;
}
// Compare completion dates as Unix timestamps to find the most recent.
// Using >= ensures that if multiple badges complete simultaneously, the last one processed wins.
- if ( \DateTime::createFromFormat( 'Y-m-d H:i:s', $settings[ $badge->get_id() ]['date'] )->format( 'U' ) >= \DateTime::createFromFormat( 'Y-m-d H:i:s', $latest_date )->format( 'U' ) ) {
- $latest_date = $settings[ $badge->get_id() ]['date'];
+ if ( \DateTime::createFromFormat( 'Y-m-d H:i:s', (string) $settings[ $badge->get_id() ]['date'] )->format( 'U' ) >= \DateTime::createFromFormat( 'Y-m-d H:i:s', $latest_date )->format( 'U' ) ) {
+ $latest_date = (string) $settings[ $badge->get_id() ]['date'];
$this->latest_completed_badge = $badge;
}
}
}
+ /**
+ * Filter the latest completed badge.
+ *
+ * @param \Progress_Planner\Badges\Badge|null $badge The latest completed badge.
+ * @param string|null $latest_date The latest completion date.
+ *
+ * @return \Progress_Planner\Badges\Badge|null
+ */
+ $this->latest_completed_badge = \apply_filters( 'progress_planner_latest_completed_badge', $this->latest_completed_badge, $latest_date );
+
return $this->latest_completed_badge;
}
}
diff --git a/classes/class-lessons.php b/classes/class-lessons.php
index c9efdecae4..2373395d0c 100644
--- a/classes/class-lessons.php
+++ b/classes/class-lessons.php
@@ -58,7 +58,7 @@ public function get_remote_api_items() {
[
'site' => \get_site_url(),
'license_key' => \progress_planner()->get_license_key(),
- 'locale' => apply_filters( 'prpl_lesson_locale', \get_locale() ),
+ 'locale' => \apply_filters( 'prpl_lesson_locale', \get_locale() ),
],
$url
);
diff --git a/classes/class-plugin-upgrade-tasks.php b/classes/class-plugin-upgrade-tasks.php
index a111e79065..f1762c9d68 100644
--- a/classes/class-plugin-upgrade-tasks.php
+++ b/classes/class-plugin-upgrade-tasks.php
@@ -104,14 +104,26 @@ protected function add_initial_onboarding_tasks() {
public function maybe_add_onboarding_tasks() {
$onboard_task_provider_ids = \apply_filters( 'prpl_onboarding_task_providers', [] );
- // Privacy policy is not accepted, so it's a fresh install.
- $fresh_install = ! \progress_planner()->is_privacy_policy_accepted();
-
- // Check if task providers option exists, it will not on fresh installs and v1.0.4 and older.
- $old_task_providers = \get_option( 'progress_planner_previous_version_task_providers', [] );
+ // Check if task providers option exists. If not, it's either a fresh install or upgrading from v1.0.4 or older.
+ $old_task_providers = \get_option( 'progress_planner_previous_version_task_providers', [] );
+ $task_providers_option_set = false !== \get_option( 'progress_planner_previous_version_task_providers', false );
+
+ // Fresh install detection:
+ // - Option doesn't exist AND privacy policy not yet accepted (standalone fresh install)
+ // - Option doesn't exist AND running as branded/hosted version (pp-hosts fresh install, privacy auto-accepted)
+ // In these cases, save current providers as baseline without showing upgrade popover.
+ if ( ! $task_providers_option_set ) {
+ $is_branded_version = \defined( 'PROGRESS_PLANNER_BRANDING_ID' );
+ $is_privacy_policy_pending = ! \progress_planner()->is_privacy_policy_accepted();
+
+ if ( $is_branded_version || $is_privacy_policy_pending ) {
+ // Fresh install - save current providers as baseline and skip upgrade popover.
+ \update_option( 'progress_planner_previous_version_task_providers', \array_unique( $onboard_task_provider_ids ), SORT_REGULAR );
+ return;
+ }
- // We're upgrading from v1.0.4 or older, set the old task providers to what we had before the upgrade.
- if ( ! $fresh_install && empty( $old_task_providers ) ) {
+ // Upgrading from v1.0.4 or older (standalone, privacy accepted but no task providers option).
+ // Set baseline to what existed before the upgrade.
$old_task_providers = [
'core-blogdescription',
'wp-debug-display',
diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php
index e25fc60367..d88ff5dc7b 100644
--- a/classes/class-suggested-tasks.php
+++ b/classes/class-suggested-tasks.php
@@ -100,12 +100,25 @@ public function init(): void {
* @return void
*/
public function insert_activity( string $task_id ): void {
+ /**
+ * Filter the activity category for a completed task.
+ *
+ * Allows customizing the category used when recording task completion activities.
+ * For example, onboarding tasks may use 'onboarding_task' instead of 'suggested_task'
+ * to exclude them from monthly badge calculations.
+ *
+ * @param string $category The activity category (default: 'suggested_task').
+ * @param string $task_id The task ID being completed.
+ */
+ $category = \apply_filters( 'progress_planner_task_activity_category', 'suggested_task', $task_id );
+
// Insert an activity.
- $activity = new Suggested_Task_Activity();
- $activity->type = 'completed';
- $activity->data_id = (string) $task_id;
- $activity->date = new \DateTime();
- $activity->user_id = \get_current_user_id();
+ $activity = new Suggested_Task_Activity();
+ $activity->category = $category;
+ $activity->type = 'completed';
+ $activity->data_id = (string) $task_id;
+ $activity->date = new \DateTime();
+ $activity->user_id = \get_current_user_id();
$activity->save();
// Allow other classes to react to the completion of a suggested task.
diff --git a/classes/suggested-tasks/providers/class-core-update.php b/classes/suggested-tasks/providers/class-core-update.php
index 0a681b4373..63f00960eb 100644
--- a/classes/suggested-tasks/providers/class-core-update.php
+++ b/classes/suggested-tasks/providers/class-core-update.php
@@ -89,7 +89,7 @@ public function add_core_update_link( $update_actions ) {
foreach ( \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ) as $task ) {
if ( $this->get_task_id() === \progress_planner()->get_suggested_tasks()->get_task_id_from_slug( $task->post_name ) ) {
$update_actions['prpl_core_update'] =
- '
' .
+ '
' .
'' . \esc_html__( 'Click here to celebrate your completed task!', 'progress-planner' ) . '';
break;
}
diff --git a/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php b/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php
index 6df0b6f0c1..efc0f5ed1d 100644
--- a/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php
+++ b/classes/suggested-tasks/providers/class-reduce-autoloaded-options.php
@@ -151,8 +151,8 @@ protected function is_plugin_active() {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
- $plugins = get_plugins();
- $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && is_plugin_active( $this->plugin_path );
+ $plugins = \get_plugins();
+ $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && \is_plugin_active( $this->plugin_path );
}
return $this->is_plugin_active;
@@ -168,7 +168,7 @@ protected function get_autoloaded_options_count() {
if ( null === $this->autoloaded_options_count ) {
$autoload_values = \wp_autoload_values_to_autoload();
- $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) );
+ $placeholders = \implode( ',', \array_fill( 0, \count( $autoload_values ), '%s' ) );
// phpcs:disable WordPress.DB
$this->autoloaded_options_count = $wpdb->get_var(
diff --git a/classes/suggested-tasks/providers/class-set-valuable-post-types.php b/classes/suggested-tasks/providers/class-set-valuable-post-types.php
index 35d4f30ae4..dc522fb43a 100644
--- a/classes/suggested-tasks/providers/class-set-valuable-post-types.php
+++ b/classes/suggested-tasks/providers/class-set-valuable-post-types.php
@@ -81,7 +81,7 @@ public function check_public_post_types() {
\update_option( 'progress_planner_public_post_types', $public_post_types );
// Exit if post type was removed, or it is not public anymore, since the user will not to able to make different selection.
- if ( count( $public_post_types ) < count( $previosly_set_public_post_types ) ) {
+ if ( \count( $public_post_types ) < \count( $previosly_set_public_post_types ) ) {
return;
}
@@ -180,8 +180,8 @@ public function handle_interactive_task_specific_submit() {
}
$post_types = \wp_unslash( $_POST['prpl-post-types-include'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- array elements are sanitized below.
- $post_types = explode( ',', $post_types );
- $post_types = array_map( 'sanitize_text_field', $post_types );
+ $post_types = \explode( ',', $post_types );
+ $post_types = \array_map( 'sanitize_text_field', $post_types );
\progress_planner()->get_admin__page_settings()->save_post_types( $post_types );
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
index c4a96db060..1eeb1f91e5 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
@@ -66,8 +66,8 @@ public function enqueue_assets( $hook ) {
[
'name' => 'progressPlannerYoastFocusElement',
'data' => [
- 'tasks' => $focus_tasks,
- 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ),
+ 'tasks' => $focus_tasks,
+ 'iconUrl' => \progress_planner()->get_ui__branding()->get_admin_menu_icon(),
],
]
);
diff --git a/progress-planner.php b/progress-planner.php
index 49218aba2f..5fa2c26a6a 100644
--- a/progress-planner.php
+++ b/progress-planner.php
@@ -28,7 +28,7 @@
require_once PROGRESS_PLANNER_DIR . '/autoload.php';
-if ( ! function_exists( 'progress_planner' ) ) {
+if ( ! \function_exists( 'progress_planner' ) ) {
/**
* Get the progress planner instance.
*
diff --git a/tests/phpunit/test-class-base.php b/tests/phpunit/test-class-base.php
index 1dfc8f5ae3..a7c9d01051 100644
--- a/tests/phpunit/test-class-base.php
+++ b/tests/phpunit/test-class-base.php
@@ -215,7 +215,7 @@ public function test_is_debug_mode_enabled() {
$this->assertFalse( $result );
// Set the current user to a user with the manage_options capability.
- wp_set_current_user( 1 );
+ \wp_set_current_user( 1 );
// Option is set, and the user has the manage_options capability.
$result = $this->base_instance->is_debug_mode_enabled();
@@ -228,7 +228,7 @@ public function test_is_debug_mode_enabled() {
$this->assertIsBool( $result );
// Unset the current user.
- wp_set_current_user( 0 );
+ \wp_set_current_user( 0 );
}
}
diff --git a/tests/phpunit/test-class-page-types.php b/tests/phpunit/test-class-page-types.php
index eac50f96d0..becd2c9625 100644
--- a/tests/phpunit/test-class-page-types.php
+++ b/tests/phpunit/test-class-page-types.php
@@ -74,7 +74,7 @@ public static function set_lessons_cache() {
[
'site' => \get_site_url(),
'license_key' => \progress_planner()->get_license_key(),
- 'locale' => apply_filters( 'prpl_lesson_locale', \get_locale() ),
+ 'locale' => \apply_filters( 'prpl_lesson_locale', \get_locale() ),
],
$url
);
diff --git a/tests/phpunit/traits/test-class-ajax-security-aioseo.php b/tests/phpunit/traits/test-class-ajax-security-aioseo.php
index fb37ca13c2..b6a27d5db4 100644
--- a/tests/phpunit/traits/test-class-ajax-security-aioseo.php
+++ b/tests/phpunit/traits/test-class-ajax-security-aioseo.php
@@ -82,7 +82,7 @@ public function test_verify_aioseo_active_or_fail_not_active() {
} catch ( \WPAjaxDieContinueException $e ) {
// WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer.
// Get the response.
- $response = json_decode( $this->_last_response, true );
+ $response = \json_decode( $this->_last_response, true );
$this->assertFalse( $response['success'] );
$this->assertArrayHasKey( 'data', $response );
$this->assertArrayHasKey( 'message', $response['data'] );
diff --git a/tests/phpunit/traits/test-class-ajax-security-base.php b/tests/phpunit/traits/test-class-ajax-security-base.php
index 554a308cc0..d7b91f5045 100644
--- a/tests/phpunit/traits/test-class-ajax-security-base.php
+++ b/tests/phpunit/traits/test-class-ajax-security-base.php
@@ -115,7 +115,7 @@ public function test_verify_nonce_or_fail_invalid() {
} catch ( \WPAjaxDieContinueException $e ) {
// WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer.
// Get the response.
- $response = json_decode( $this->_last_response, true );
+ $response = \json_decode( $this->_last_response, true );
$this->assertFalse( $response['success'] );
$this->assertArrayHasKey( 'data', $response );
$this->assertArrayHasKey( 'message', $response['data'] );
@@ -168,7 +168,7 @@ public function test_verify_capability_or_fail_non_admin() {
} catch ( \WPAjaxDieContinueException $e ) {
// WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer.
// Get the response.
- $response = json_decode( $this->_last_response, true );
+ $response = \json_decode( $this->_last_response, true );
$this->assertFalse( $response['success'] );
$this->assertArrayHasKey( 'data', $response );
$this->assertArrayHasKey( 'message', $response['data'] );
diff --git a/tests/phpunit/traits/test-class-ajax-security-yoast.php b/tests/phpunit/traits/test-class-ajax-security-yoast.php
index 07c775a132..9bcda9724e 100644
--- a/tests/phpunit/traits/test-class-ajax-security-yoast.php
+++ b/tests/phpunit/traits/test-class-ajax-security-yoast.php
@@ -82,7 +82,7 @@ public function test_verify_yoast_active_or_fail_not_active() {
} catch ( \WPAjaxDieContinueException $e ) {
// WordPress Core's dieHandler() calls ob_get_clean() which gets output and cleans buffer.
// Get the response.
- $response = json_decode( $this->_last_response, true );
+ $response = \json_decode( $this->_last_response, true );
$this->assertFalse( $response['success'] );
$this->assertArrayHasKey( 'data', $response );
$this->assertArrayHasKey( 'message', $response['data'] );
diff --git a/views/dashboard-widgets/score.php b/views/dashboard-widgets/score.php
index 5908b4951d..da80254c82 100644
--- a/views/dashboard-widgets/score.php
+++ b/views/dashboard-widgets/score.php
@@ -69,7 +69,7 @@