'Connection', ]; private string $page_hook = ''; private string $home_page_hook = ''; private string $connections_page_hook = ''; private string $products_page_hook = ''; private string $orders_page_hook = ''; private string $settings_page_hook = ''; public function __construct() { add_action( 'admin_menu', [ $this, 'register_page' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); add_filter( 'plugin_action_links_' . plugin_basename( MARKET_SYNC_PLUGIN_FILE ), [ $this, 'add_settings_link' ] ); } public function register_page(): void { $hook = add_menu_page( __( 'Market Sync', 'market-sync' ), __( 'Market Sync', 'market-sync' ), 'manage_options', self::MENU_SLUG, [ $this, 'render_admin_app' ], 'dashicons-store', 56 ); if ( ! $hook ) { return; } $this->page_hook = $hook; $home_hook = add_submenu_page( self::MENU_SLUG, __( 'Market Sync Home', 'market-sync' ), __( 'Home', 'market-sync' ), 'manage_options', self::HOME_SLUG, [ $this, 'render_admin_app' ] ); if ( $home_hook ) { $this->home_page_hook = $home_hook; add_action( "load-{$home_hook}", static function (): void { global $title; $title = __( 'Market Sync Home', 'market-sync' ); } ); } $orders_hook = add_submenu_page( self::MENU_SLUG, __( 'Market Sync Orders', 'market-sync' ), __( 'Orders', 'market-sync' ), 'manage_options', self::ORDERS_SLUG, [ $this, 'render_admin_app' ] ); if ( $orders_hook ) { $this->orders_page_hook = $orders_hook; add_action( "load-{$orders_hook}", static function (): void { global $title; $title = __( 'Market Sync Orders', 'market-sync' ); } ); } $products_hook = add_submenu_page( self::MENU_SLUG, __( 'Market Sync Products', 'market-sync' ), __( 'Products', 'market-sync' ), 'manage_options', self::PRODUCTS_SLUG, [ $this, 'render_admin_app' ] ); if ( $products_hook ) { $this->products_page_hook = $products_hook; add_action( "load-{$products_hook}", static function (): void { global $title; $title = __( 'Market Sync Products', 'market-sync' ); } ); } $connections_hook = add_submenu_page( self::MENU_SLUG, __( 'Market Sync Connections', 'market-sync' ), __( 'Connections', 'market-sync' ), 'manage_options', self::CONNECTIONS_SLUG, [ $this, 'render_admin_app' ] ); if ( $connections_hook ) { $this->connections_page_hook = $connections_hook; add_action( "load-{$connections_hook}", static function (): void { global $title; $title = __( 'Market Sync Connections', 'market-sync' ); } ); } remove_submenu_page( self::MENU_SLUG, self::MENU_SLUG ); add_action( "load-{$hook}", static function (): void { global $title; $title = __( 'Market Sync', 'market-sync' ); } ); $settings_hook = add_options_page( __( 'Market Sync Settings', 'market-sync' ), __( 'Market Sync', 'market-sync' ), 'manage_options', self::SETTINGS_SLUG, [ $this, 'render_admin_app' ] ); if ( ! $settings_hook ) { return; } $this->settings_page_hook = $settings_hook; add_action( "load-{$settings_hook}", static function (): void { global $title; $title = __( 'Market Sync Settings', 'market-sync' ); } ); $this->replace_submenu_url( self::MENU_SLUG, self::HOME_SLUG, $this->app_route_url( '/home' ) ); $this->replace_submenu_url( self::MENU_SLUG, self::CONNECTIONS_SLUG, $this->app_route_url( '/connections' ) ); $this->replace_submenu_url( self::MENU_SLUG, self::PRODUCTS_SLUG, $this->app_route_url( '/products' ) ); $this->replace_submenu_url( self::MENU_SLUG, self::ORDERS_SLUG, $this->app_route_url( '/orders' ) ); $this->replace_submenu_url( 'options-general.php', self::SETTINGS_SLUG, $this->app_route_url( '/settings/connection' ) ); } public function add_settings_link( array $links ): array { array_unshift( $links, sprintf( '%s', esc_url( $this->app_route_url( '/settings/connection' ) ), esc_html__( 'Settings', 'market-sync' ) ), sprintf( '%s', esc_url( $this->app_route_url( '/home' ) ), esc_html__( 'Dashboard', 'market-sync' ) ) ); return $links; } public function render_admin_app(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } ?>

page_hook, $this->home_page_hook, $this->connections_page_hook, $this->products_page_hook, $this->orders_page_hook, $this->settings_page_hook ], true ) && ! $this->is_market_sync_admin_page() ) { return; } wp_enqueue_style( 'dashicons' ); wp_enqueue_style( 'wp-components' ); $style_file = MARKET_SYNC_PLUGIN_DIR . 'build/index.css'; if ( file_exists( $style_file ) ) { // Bust the cache per build, not per release: MARKET_SYNC_VERSION stays put across // rebuilds, so browsers kept serving a stale stylesheet against a newer index.js. wp_enqueue_style( 'market-sync-admin-app', MARKET_SYNC_PLUGIN_URL . 'build/index.css', [ 'wp-components' ], (string) filemtime( $style_file ) ); wp_style_add_data( 'market-sync-admin-app', 'rtl', 'replace' ); } $asset_file = MARKET_SYNC_PLUGIN_DIR . 'build/index.asset.php'; $asset = file_exists( $asset_file ) ? require $asset_file : [ 'dependencies' => [ 'react', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-i18n' ], 'version' => MARKET_SYNC_VERSION, ]; wp_enqueue_script( 'market-sync-admin-app', MARKET_SYNC_PLUGIN_URL . 'build/index.js', $asset['dependencies'], $asset['version'], true ); wp_add_inline_script( 'market-sync-admin-app', 'window.marketSyncAdmin = ' . wp_json_encode( $this->script_data() ) . ';', 'before' ); } private function script_data(): array { return [ 'rootId' => 'market-sync-admin-root', 'page' => $this->current_page_slug(), 'settingsTab' => $this->current_settings_tab(), 'restBase' => '/market-sync/v1', // Consent gate (WordPress.org guideline 7): the chat widget's credentials are // inert on their own, so they may ship with the page. The administrator's // identity may not — it is served by Chat_Support_Controller::identity(), which // React calls only after a Contact Support click. 'chatSupport' => [ 'appId' => MARKET_SYNC_CHAT_SUPPORT_APP_ID, 'origin' => MARKET_SYNC_CHAT_SUPPORT_URL, ], 'urls' => [ 'home' => $this->route_hash( '/home' ), 'settings' => $this->route_hash( '/settings/connection' ), 'settingsConnection' => $this->route_hash( '/settings/connection' ), 'connections' => $this->route_hash( '/connections' ), 'products' => $this->route_hash( '/products' ), 'orders' => $this->route_hash( '/orders' ), ], 'settingsTabs' => $this->settings_tabs_with_urls(), ]; } private function current_settings_tab(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = sanitize_key( $_GET['tab'] ?? 'connection' ); return array_key_exists( $tab, self::SETTINGS_TABS ) ? $tab : 'connection'; } private function settings_tabs_with_urls(): array { $tabs = []; foreach ( self::SETTINGS_TABS as $slug => $label ) { $tabs[ $slug ] = [ 'label' => $label, 'url' => $this->route_hash( '/settings/' . $slug ), ]; } return $tabs; } private function replace_submenu_url( string $parent_slug, string $submenu_slug, string $url ): void { global $submenu; if ( empty( $submenu[ $parent_slug ] ) || ! is_array( $submenu[ $parent_slug ] ) ) { return; } foreach ( $submenu[ $parent_slug ] as &$item ) { if ( ! isset( $item[2] ) || $submenu_slug !== $item[2] ) { continue; } $item[2] = $url; break; } unset( $item ); } private function app_route_url( string $route ): string { return admin_url( 'admin.php?page=' . self::MENU_SLUG . $this->route_hash( $route ) ); } private function route_hash( string $route ): string { return '#/' . ltrim( $route, '/' ); } private function is_market_sync_admin_page(): bool { $page = $this->current_page_slug(); return self::MENU_SLUG === $page || self::HOME_SLUG === $page || self::CONNECTIONS_SLUG === $page || self::PRODUCTS_SLUG === $page || self::ORDERS_SLUG === $page || self::SETTINGS_SLUG === $page || str_starts_with( $page, self::MENU_SLUG . '-' ); } private function current_page_slug(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via sanitize_key() before use. $page = wp_unslash( $_GET['page'] ?? '' ); if ( ! is_scalar( $page ) ) { return ''; } return sanitize_key( (string) $page ); } }