diff --git a/inc/admin.php b/inc/admin.php index 72fb017f..1c6a00fe 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -23,7 +23,9 @@ class Optml_Admin { const IMAGE_DATA_COLLECTED_BATCH = 100; - const BF_PROMO_DISMISS_KEY = 'optml_bf25_notice_dismiss'; + const BF_PROMO_DISMISS_KEY = 'optml_bf_notice_dismiss'; + + const SDK_BF_DISMISS_META = 'themeisle_sdk_dismissed_notice_black_friday'; const SPC_BANNER_DISMISS_KEY = 'optml_spc_banner_dismiss'; @@ -114,6 +116,7 @@ public function __construct() { } add_filter( 'themeisle-sdk/survey/' . OPTML_PRODUCT_SLUG, [ $this, 'get_survey_metadata' ], 10, 2 ); + add_filter( 'themeisle_sdk_blackfriday_data', [ $this, 'add_black_friday_data' ] ); add_action( 'admin_init', [ $this, 'mark_user_with_offload' ] ); } @@ -1646,6 +1649,63 @@ private function get_spc_status() { return 'not-installed'; } + /** + * Hide the SDK Black Friday notice when Optimole is the only ThemeIsle product. + * + * Optimole shows its Black Friday offer only in its dashboard. Without a config, + * the SDK shows the generic ThemeIsle notice on every admin page. When other + * ThemeIsle products are active, the notice is theirs, so it stays. + * + * @param array $configs The Black Friday configs of the products. + * + * @return array The configs. + */ + public function add_black_friday_data( $configs ) { + if ( ! class_exists( '\ThemeisleSDK\Loader' ) || ! isset( $configs['default'] ) || ! is_array( $configs['default'] ) ) { + return $configs; + } + + if ( [ OPTML_PRODUCT_SLUG ] === array_keys( \ThemeisleSDK\Loader::get_products() ) ) { + $configs['default']['sale_url'] = ''; + } + + return $configs; + } + + /** + * Check if the current user dismissed the SDK Black Friday notice this year. + * + * The SDK uses one dismissal for the notices of all ThemeIsle products. + * + * @param DateTime $now The current date. + * + * @return bool + */ + public static function is_sdk_bf_notice_dismissed( $now ) { + $dismissed_at = get_user_meta( get_current_user_id(), self::SDK_BF_DISMISS_META, true ); + + return ! empty( $dismissed_at ) && wp_date( 'Y', (int) $dismissed_at ) === $now->format( 'Y' ); + } + + /** + * Dismiss the SDK Black Friday notice for the current user. + * + * Called when the user dismisses the Optimole banner. It also hides the SDK + * notice of the other ThemeIsle products. + * + * @return void + */ + public static function dismiss_sdk_bf_notice() { + /** + * The current date. + * + * @var DateTime $now Current date. + */ + $now = apply_filters( 'themeisle_sdk_current_date', new \DateTime( 'now' ) ); + + update_user_meta( get_current_user_id(), self::SDK_BF_DISMISS_META, $now->getTimestamp() ); + } + /** * Get the black friday notices. * @@ -1680,6 +1740,9 @@ public function get_bf_notices( $user_plan ) { */ $now = apply_filters( 'themeisle_sdk_current_date', new \DateTime( 'now' ) ); $current_year = $now->format( 'Y' ); + // Per-year UTM, so each campaign is tracked separately. + $year_suffix = $now->format( 'y' ); + $utm_area = 'bfcm' . $year_suffix; $black_friday_day = new \DateTime( "last Friday of November $current_year" ); $sale_start = clone $black_friday_day; @@ -1705,10 +1768,11 @@ public function get_bf_notices( $user_plan ) { '' . $promo_code . '', '' . __( '25% discount', 'optimole-wp' ) . '' ), - 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( self::get_upgrade_base_link() ), 'bfcm25', 'sidebarnotice' ) ), + 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( self::get_upgrade_base_link() ), $utm_area, 'sidebarnotice' ) ), ]; - if ( get_option( self::BF_PROMO_DISMISS_KEY ) === 'yes' ) { + // The banner shares the SDK dismissal, which resets every year. + if ( self::is_sdk_bf_notice_dismissed( $now ) ) { return $notices; } @@ -1718,7 +1782,7 @@ public function get_bf_notices( $user_plan ) { '' . $promo_code . '', '' . __( '25% discount', 'optimole-wp' ) . '' ); - $cta_link = esc_url_raw( tsdk_utmify( self::get_upgrade_base_link(), 'bfcm25', 'notice' ) ); + $cta_link = esc_url_raw( tsdk_utmify( self::get_upgrade_base_link(), $utm_area, 'notice' ) ); $cta_text = __( 'Claim now', 'optimole-wp' ); $notices['banner'] = [ diff --git a/inc/rest.php b/inc/rest.php index c8e28d9b..c9e14eae 100644 --- a/inc/rest.php +++ b/inc/rest.php @@ -920,6 +920,12 @@ public function dismiss_notice( WP_REST_Request $request ) { return $this->response( [ 'error' => 'Invalid key' ], 'error' ); } + if ( Optml_Admin::BF_PROMO_DISMISS_KEY === $key ) { + Optml_Admin::dismiss_sdk_bf_notice(); + + return $this->response( [ 'success' => 'Notice dismissed' ] ); + } + $result = update_option( $key, 'yes' ); if ( ! $result ) { diff --git a/tests/test-admin.php b/tests/test-admin.php index 6cbd8010..c802aad0 100644 --- a/tests/test-admin.php +++ b/tests/test-admin.php @@ -65,8 +65,8 @@ public function setUp(): void { $this->sale_end->modify( '+7 days' ); $this->sale_end->setTime( 23, 59, 59 ); - // Clean up options before each test - delete_option( Optml_Admin::BF_PROMO_DISMISS_KEY ); + // Each test starts with an admin that has not dismissed the Black Friday notice. + wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) ); // Remove any filters that might be set remove_all_filters( 'themeisle_sdk_is_black_friday_sale' ); @@ -77,7 +77,7 @@ public function setUp(): void { * Clean up after each test. */ public function tearDown(): void { - delete_option( Optml_Admin::BF_PROMO_DISMISS_KEY ); + delete_user_meta( get_current_user_id(), Optml_Admin::SDK_BF_DISMISS_META ); remove_all_filters( 'themeisle_sdk_is_black_friday_sale' ); remove_all_filters( 'themeisle_sdk_current_date' ); parent::tearDown(); @@ -188,20 +188,50 @@ public function test_get_bf_notices_banner_dismissal() { $this->mock_date_to( clone $this->black_friday ); // Test banner is hidden when dismissed - update_option( Optml_Admin::BF_PROMO_DISMISS_KEY, 'yes' ); + Optml_Admin::dismiss_sdk_bf_notice(); $result = $this->admin->get_bf_notices( 'free' ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'sidebar', $result ); $this->assertArrayNotHasKey( 'banner', $result ); // Test banner appears when not dismissed - delete_option( Optml_Admin::BF_PROMO_DISMISS_KEY ); + delete_user_meta( get_current_user_id(), Optml_Admin::SDK_BF_DISMISS_META ); $result = $this->admin->get_bf_notices( 'free' ); $this->assertIsArray( $result ); $this->assertArrayHasKey( 'sidebar', $result ); $this->assertArrayHasKey( 'banner', $result ); } + /** + * Test the dismissal resets and the UTM tag follows the year, and the promo code stays BFCM2525. + */ + public function test_get_bf_notices_uses_yearly_dismissal_and_utm() { + $this->setup_black_friday_sale_period(); + + // A dismissal from 2025 does not hide the 2026 banner. + $this->mock_date_to( new DateTime( '2025-11-28 12:00:00' ) ); + Optml_Admin::dismiss_sdk_bf_notice(); + $this->mock_date_to( new DateTime( '2026-11-27 12:00:00' ) ); + $result = $this->admin->get_bf_notices( 'free' ); + delete_user_meta( get_current_user_id(), Optml_Admin::SDK_BF_DISMISS_META ); + + $this->assertArrayHasKey( 'banner', $result ); + $this->assertSame( Optml_Admin::BF_PROMO_DISMISS_KEY, $result['banner']['dismiss_key'] ); + $this->assertStringContainsString( 'bfcm26', $result['banner']['cta_link'] ); + $this->assertStringContainsString( 'bfcm26', $result['sidebar']['cta_link'] ); + $this->assertStringContainsString( 'BFCM2525', $result['banner']['subtitle'] ); + $this->assertStringContainsString( 'BFCM2525', $result['sidebar']['subtitle'] ); + + // 2025 still uses the legacy UTM tag. + $this->mock_date_to( new DateTime( '2025-11-28 12:00:00' ) ); + $result = $this->admin->get_bf_notices( 'free' ); + + $this->assertStringContainsString( 'bfcm25', $result['banner']['cta_link'] ); + $this->assertStringContainsString( 'bfcm25', $result['sidebar']['cta_link'] ); + $this->assertStringContainsString( 'BFCM2525', $result['banner']['subtitle'] ); + $this->assertStringContainsString( 'BFCM2525', $result['sidebar']['subtitle'] ); + } + /** * Test get_bf_notices contains correct dismiss key in banner. */ @@ -296,23 +326,6 @@ public function test_get_bf_notices_date_scenarios() { $this->assertArrayHasKey( 'sidebar', $result ); } - /** - * Test get_bf_notices banner appears with non-'yes' dismissal values. - */ - public function test_get_bf_notices_banner_dismissal_values() { - $this->setup_black_friday_sale_period(); - $this->mock_date_to( clone $this->black_friday ); - - // Banner only hides when dismissal key is exactly 'yes' - $test_values = [ 'no', '0', '', 'dismissed' ]; - - foreach ( $test_values as $value ) { - update_option( Optml_Admin::BF_PROMO_DISMISS_KEY, $value ); - $result = $this->admin->get_bf_notices( 'free' ); - $this->assertArrayHasKey( 'banner', $result, "Banner should appear when dismiss key is '$value'" ); - } - } - // ------------------------------------------------------------------------- // get_permissions_policy() tests // ------------------------------------------------------------------------- @@ -677,4 +690,64 @@ public function mock_offloaded_svg_download( $preempt, array $args, string $url return $preempt; } + + /** + * Test the SDK Black Friday notice is hidden only when Optimole is the only ThemeIsle product. + */ + public function test_add_black_friday_data_hides_sdk_notice_only_for_optimole_alone() { + // The SDK starts on a later hook, so load its registry class here. + if ( ! class_exists( \ThemeisleSDK\Loader::class ) ) { + require_once OPTML_PATH . 'vendor/codeinwp/themeisle-sdk/src/Loader.php'; + } + + $products = new ReflectionProperty( \ThemeisleSDK\Loader::class, 'products' ); + $products->setAccessible( true ); + $original = $products->getValue(); + + $configs = [ 'default' => [ 'sale_url' => 'https://themeisle.com/blackfriday/' ] ]; + + try { + $products->setValue( null, [ OPTML_PRODUCT_SLUG => 'optimole' ] ); + $result = $this->admin->add_black_friday_data( $configs ); + $this->assertSame( '', $result['default']['sale_url'] ); + + $products->setValue( null, [ OPTML_PRODUCT_SLUG => 'optimole', 'neve' => 'neve' ] ); + $this->assertSame( $configs, $this->admin->add_black_friday_data( $configs ) ); + } finally { + $products->setValue( null, $original ); + } + } + + /** + * Test the banner follows the SDK Black Friday dismissal of the current user. + */ + public function test_get_bf_notices_banner_follows_sdk_dismissal() { + $this->setup_black_friday_sale_period(); + $this->mock_date_to( clone $this->black_friday ); + $last_year = ( clone $this->black_friday )->modify( '-1 year' ); + update_user_meta( get_current_user_id(), Optml_Admin::SDK_BF_DISMISS_META, $last_year->getTimestamp() ); + $result = $this->admin->get_bf_notices( 'free' ); + $this->assertArrayHasKey( 'banner', $result ); + + Optml_Admin::dismiss_sdk_bf_notice(); + $this->assertSame( $this->black_friday->getTimestamp(), (int) get_user_meta( get_current_user_id(), Optml_Admin::SDK_BF_DISMISS_META, true ) ); + + $result = $this->admin->get_bf_notices( 'free' ); + $this->assertArrayHasKey( 'sidebar', $result ); + $this->assertArrayNotHasKey( 'banner', $result ); + } + + /** + * Test the REST dismissal of the banner writes the SDK dismissal, not an option. + */ + public function test_rest_dismiss_bf_notice_uses_sdk_dismissal() { + $this->mock_date_to( clone $this->black_friday ); + + $request = new WP_REST_Request( 'POST' ); + $request->set_param( 'key', Optml_Admin::BF_PROMO_DISMISS_KEY ); + ( new Optml_Rest() )->dismiss_notice( $request ); + + $this->assertFalse( get_option( Optml_Admin::BF_PROMO_DISMISS_KEY ) ); + $this->assertTrue( Optml_Admin::is_sdk_bf_notice_dismissed( clone $this->black_friday ) ); + } }