'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 );
}
}