Settings -> Products and are linked here. */ final class AdminPage { const SLUG = 'strikewise'; /** Right below WooCommerce and Products. */ const POSITION = '55.8'; /** User meta: the premium banner was hidden. */ const META_PROMO_HIDDEN = 'strikewise_promo_hidden'; /** Action of admin-post.php that hides the premium banner. */ const ACTION_HIDE_PROMO = 'strikewise_hide_promo'; /** @var string Hook suffix of the overview page. */ private static $hook = ''; public function register(): void { add_action( 'admin_menu', array( $this, 'add_page' ), 60 ); add_action( 'admin_menu', array( $this, 'add_settings_link' ), 70 ); add_filter( 'submenu_file', array( $this, 'highlight_settings' ), 99 ); add_action( 'admin_post_' . self::ACTION_HIDE_PROMO, array( $this, 'hide_promo' ) ); } /** * Hides the premium banner for the current user. */ public function hide_promo(): void { check_admin_referer( self::ACTION_HIDE_PROMO ); if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'strikewise' ), '', array( 'response' => 403 ) ); } update_user_meta( get_current_user_id(), self::META_PROMO_HIDDEN, time() ); wp_safe_redirect( admin_url( 'admin.php?page=' . self::SLUG ) ); exit; } public function add_page(): void { self::$hook = (string) add_menu_page( __( 'Strikewise', 'strikewise' ), __( 'Strikewise', 'strikewise' ), 'manage_woocommerce', self::SLUG, array( $this, 'render' ), self::menu_icon(), self::POSITION ); add_submenu_page( self::SLUG, __( 'Strikewise', 'strikewise' ), __( 'Overview', 'strikewise' ), 'manage_woocommerce', self::SLUG, array( $this, 'render' ) ); } /** * Hook suffix of the overview page, for screen checks. */ public static function hook(): string { return '' !== self::$hook ? self::$hook : 'toplevel_page_' . self::SLUG; } public function add_settings_link(): void { add_submenu_page( self::SLUG, __( 'Settings', 'strikewise' ), __( 'Settings', 'strikewise' ), 'manage_woocommerce', self::settings_path() ); } /** * Marks "Settings" in the Strikewise menu while its section in WooCommerce is shown. * WordPress keeps the WooCommerce menu open on that page; assets/settings.js moves it. * * @param string|null $submenu_file * @return string|null */ public function highlight_settings( $submenu_file ) { return self::on_settings() ? self::settings_path() : $submenu_file; } private static function on_settings(): bool { // phpcs:ignore WordPress.Security.NonceVerification -- only decides which menu entry is highlighted. return isset( $_GET['page'], $_GET['section'] ) && 'wc-settings' === $_GET['page'] && Settings::SECTION === $_GET['section']; } private static function settings_path(): string { return 'admin.php?page=wc-settings&tab=products§ion=' . Settings::SECTION; } /** * Price tag as menu icon; WordPress colours it like the other menu icons. */ private static function menu_icon(): string { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- data URI of the menu icon. } public function render(): void { $tracked = Stats::collect()['tracked']; $premium = Licensing::can_use_premium(); $settings = self::settings_url(); echo '
'; echo '
'; echo ''; echo '

' . esc_html__( 'Strikewise', 'strikewise' ) . ' '; if ( $premium ) { echo '' . esc_html__( 'Premium', 'strikewise' ) . ''; } else { echo '' . esc_html__( 'Free', 'strikewise' ) . ''; } echo '

' . esc_html__( 'Show discounts the way the law requires, automatically.', 'strikewise' ) . '

'; echo ''; echo '
'; echo '
'; $issues = Health::issues(); $protection = Health::protection(); $this->render_hero( $issues, $protection ); echo '
'; $this->tile( 'tag', number_format_i18n( $protection['reduced'] ), __( 'reduced right now', 'strikewise' ), __( 'with the lowest price of the 30 days before', 'strikewise' ), '' ); $this->tile( 'shield', number_format_i18n( $protection['corrected'] ), __( 'strike-through prices corrected', 'strikewise' ), __( 'set to the lowest price, automatically', 'strikewise' ), 'ok' ); $this->tile( 'hidden', number_format_i18n( $protection['hidden'] ), __( 'not shown as a reduction', 'strikewise' ), __( 'sham discounts and reductions without an earlier price', 'strikewise' ), 'muted' ); $this->tile( 'pulse', number_format_i18n( $tracked ), __( 'products monitored', 'strikewise' ), __( 'every price change is recorded', 'strikewise' ), '' ); echo '
'; /** * Fires below the key figures on the Strikewise overview (premium: scanner, sale planner). */ do_action( 'strikewise_admin_page_sections' ); $this->render_checklist(); if ( Licensing::show_upsell() && ! get_user_meta( get_current_user_id(), self::META_PROMO_HIDDEN, true ) ) { $this->render_upsell( $protection['reduced'] ); } echo '
'; } /** * The answer to "is my shop safe?" at the top of the page. * * @param array $issues * @param array{reduced:int,corrected:int,hidden:int,exempt:int,unknown:int} $protection */ private function render_hero( array $issues, array $protection ): void { $tone = Health::tone( $issues ); echo '
'; echo '
' . wp_kses( Icons::svg( 'ok' === $tone ? 'shield' : 'alert' ), Html::allowed() ) . '
'; echo '

' . esc_html( Health::headline( $issues ) ) . '

'; if ( ! $issues ) { echo '

' . esc_html( $protection['reduced'] > 0 /* translators: %d: number of products */ ? sprintf( _n( 'Strikewise shows the lowest price of the 30 days before for %d reduced product and corrects strike-through prices automatically.', 'Strikewise shows the lowest price of the 30 days before for %d reduced products and corrects strike-through prices automatically.', $protection['reduced'], 'strikewise' ), $protection['reduced'] ) : __( 'As soon as you reduce a product, Strikewise shows the lowest price of the 30 days before and corrects the strike-through price.', 'strikewise' ) ) . '

'; } else { echo '
    '; foreach ( array_slice( $issues, 0, 4 ) as $issue ) { echo '
  • ' . esc_html( $issue['title'] ) . '' . esc_html( $issue['text'] ) . '
    '; if ( '' !== $issue['url'] ) { echo '' . esc_html( $issue['link'] ) . ''; } echo '
  • '; } echo '
'; } echo '
'; } private function tile( string $icon, string $value, string $label, string $hint, string $tone ): void { echo '
'; echo '' . wp_kses( Icons::svg( $icon ), Html::allowed() ) . ''; echo '

' . esc_html( $value ) . '

'; echo '

' . esc_html( $label ) . '

'; echo '

' . esc_html( $hint ) . '

'; echo '
'; } /** * What Strikewise takes care of, in plain words; open points are marked. */ private function render_checklist(): void { $display = Settings::show_single() && Settings::show_loop(); $cjeu = CjeuMode::is_enabled(); $hide = $cjeu && CjeuMode::hides_unknown(); $daily = null !== self::next_daily_run(); $import = OmnibusImporter::state(); $items = array( array( $display, __( 'Lowest price shown', 'strikewise' ), __( 'Below every reduced price, on product pages and in lists.', 'strikewise' ) ), array( $cjeu, __( 'Strike-through prices corrected', 'strikewise' ), __( 'They show the lowest price of the 30 days before (CJEU, C-330/23).', 'strikewise' ) ), array( $hide, __( 'No sham discounts', 'strikewise' ), __( 'A reduction without a lower price before is not shown as one.', 'strikewise' ) ), array( $daily, __( 'Imports included', 'strikewise' ), __( 'A daily sync also records prices changed by imports.', 'strikewise' ) ), array( true, __( 'Evidence kept', 'strikewise' ), __( 'Every price change stays on record for 400 days.', 'strikewise' ) ), ); if ( OmnibusImporter::STATUS_DONE === $import['status'] && (int) $import['products'] > 0 ) { /* translators: %d: number of products */ $items[] = array( true, __( 'History taken over', 'strikewise' ), sprintf( __( 'Price history of %d products imported from Omnibus.', 'strikewise' ), (int) $import['products'] ) ); } /** * Filters the checklist on the overview (premium adds UVP mode and agency access). * * @param array $items */ $items = (array) apply_filters( 'strikewise_checklist', $items ); echo '

' . esc_html__( 'How Strikewise protects your shop', 'strikewise' ) . '

'; echo '
'; echo '
    '; foreach ( $items as $item ) { echo '
  • ' . wp_kses( Icons::svg( $item[0] ? 'check' : 'alert' ), Html::allowed() ); echo '
    ' . esc_html( $item[1] ) . '' . esc_html( $item[2] ) . '
  • '; } echo '
'; } /** * Premium banner for shops without a license. */ private function render_upsell( int $reduced ): void { $features = array( array( 'shield', __( 'Compliance scanner', 'strikewise' ), __( 'Checks every reduced product each week and reports problems by e-mail.', 'strikewise' ) ), array( 'calendar-alt', __( 'Sale planner', 'strikewise' ), __( 'Check Black Friday discounts before they go live and schedule them in one click.', 'strikewise' ) ), array( 'editor-strikethrough', __( 'UVP mode', 'strikewise' ), __( 'Comparisons with the manufacturer price labelled correctly as UVP.', 'strikewise' ) ), array( 'tag', __( 'Percentage badge', 'strikewise' ), __( 'The sale badge shows the real reduction, e.g. "−20 %", calculated from the lowest price.', 'strikewise' ) ), ); echo '
'; $hide = wp_nonce_url( admin_url( 'admin-post.php?action=' . self::ACTION_HIDE_PROMO ), self::ACTION_HIDE_PROMO ); echo '' . esc_html__( 'Hide this banner', 'strikewise' ) . ''; echo '
'; echo '' . esc_html__( 'Strikewise Premium', 'strikewise' ) . ''; echo '

' . esc_html__( 'Checked every week, planned before the sale starts', 'strikewise' ) . '

'; echo '

' . esc_html( $reduced > 0 /* translators: %d: number of products */ ? sprintf( _n( '%d product in your shop is reduced right now. Premium checks it every week and reports problems by e-mail, before a warning letter does.', '%d products in your shop are reduced right now. Premium checks them every week and reports problems by e-mail, before a warning letter does.', $reduced, 'strikewise' ), $reduced ) : __( 'Premium checks every reduced product every week and reports problems by e-mail.', 'strikewise' ) ) . '

'; echo '
    '; foreach ( $features as $feature ) { echo '
  • ' . esc_html( $feature[1] ) . '' . esc_html( $feature[2] ) . '
  • '; } echo '
'; echo '

' . esc_html__( 'Also included: price log per product as CSV, as evidence for your lawyer, and the agency cockpit for licenses with 5 or more shops.', 'strikewise' ) . '

'; echo '
'; echo '
'; $trial = Licensing::trial_url(); if ( '' !== $trial ) { echo '' . esc_html__( 'Try 14 days for free', 'strikewise' ) . ''; echo '' . esc_html__( 'No credit card needed', 'strikewise' ) . ''; } echo '' . esc_html__( 'See plans', 'strikewise' ) . ''; echo '
'; echo '
'; } public static function settings_url(): string { return admin_url( 'admin.php?page=wc-settings&tab=products§ion=' . Settings::SECTION ); } public static function format_time( int $timestamp ): string { return wp_date( get_option( 'date_format' ) . ', ' . get_option( 'time_format' ), $timestamp ); } public static function next_daily_run(): ?int { if ( ! function_exists( 'as_next_scheduled_action' ) ) { return null; } $next = as_next_scheduled_action( Reconciler::HOOK_DAILY, array(), Reconciler::GROUP ); if ( true === $next ) { return time(); // Running right now. } return is_int( $next ) ? $next : null; } }