$screenIds
*/
public static function isNoticeScreen(array $screenIds): bool
{
if (!function_exists('get_current_screen')) {
return false;
}
$screen = get_current_screen();
if (!$screen || !isset($screen->id)) {
return false;
}
$id = (string) $screen->id;
if (self::$hookSuffix !== '' && $id === self::$hookSuffix) {
return true;
}
// Network admin screens carry a "-network" suffix (plugins-network).
$baseId = substr($id, -8) === '-network' ? substr($id, 0, -8) : $id;
return in_array($baseId, $screenIds, true);
}
public static function hook(): void
{
add_action('admin_menu', [self::class, 'registerMenu']);
add_action('admin_enqueue_scripts', [self::class, 'maybeEnqueueAssets']);
}
public static function registerMenu(): void
{
if (!function_exists('add_submenu_page')) {
return;
}
$hook = add_submenu_page(
'options-general.php',
__('FluentCart Health', 'sparkcore-commerce'),
__('FluentCart Health', 'sparkcore-commerce'),
'manage_options',
self::PAGE_SLUG,
[self::class, 'render']
);
if (is_string($hook)) {
self::$hookSuffix = $hook;
}
}
public static function maybeEnqueueAssets(string $hookSuffix): void
{
if ($hookSuffix === '' || $hookSuffix !== self::$hookSuffix) {
return;
}
if (!function_exists('wp_enqueue_style')) {
return;
}
wp_enqueue_style(
'fcbd-admin-health',
defined('FCBD_URL') ? FCBD_URL . 'assets/admin-health.css' : '',
[],
defined('FCBD_VERSION') ? FCBD_VERSION : null
);
}
public static function render(): void
{
if (!function_exists('current_user_can') || !current_user_can('manage_options')) {
if (function_exists('wp_die')) {
wp_die(esc_html__('You do not have permission to view this page.', 'sparkcore-commerce'));
}
return;
}
$contractResults = Contracts::verify();
$versionProfile = VersionProfiles::evaluate();
$environment = self::gatherEnvironment();
$cacheAdvice = self::detectCachePlugins();
$reportText = self::buildReportText($contractResults, $versionProfile, $environment, $cacheAdvice);
echo '
';
echo '
' . esc_html__('FluentCart Health', 'sparkcore-commerce') . '
';
self::renderVersionProfileSection($versionProfile);
self::renderContractSection($contractResults);
self::renderEnvironmentSection($environment);
self::renderCacheAdviceSection($cacheAdvice);
self::renderReportSection($reportText);
echo '';
}
// =====================================================================
// (a) contract-status + version-profile section
// =====================================================================
/**
* @param array{version: string|null, status: string, message: string} $versionProfile
*/
private static function renderVersionProfileSection(array $versionProfile): void
{
echo '' . esc_html__('Version profile', 'sparkcore-commerce') . '
';
echo '';
echo '';
echo '| ' . esc_html__('Audited FluentCart profiles', 'sparkcore-commerce') . ' | ';
echo '' . esc_html(self::auditedFluentCartProfilesLabel()) . ' |
';
echo '| ' . esc_html__('Verified Breakdance baseline', 'sparkcore-commerce') . ' | ';
echo '' . esc_html(Contracts::VERIFIED_BREAKDANCE_VERSION) . ' |
';
echo '| ' . esc_html__('Active FluentCart version', 'sparkcore-commerce') . ' | ';
echo '' . esc_html($versionProfile['version'] ?? __('unknown', 'sparkcore-commerce')) . ' |
';
echo '| ' . esc_html__('Verdict', 'sparkcore-commerce') . ' | ';
echo ''
. esc_html(self::profileStatusLabel($versionProfile['status']))
. ' ' . esc_html($versionProfile['message']) . ' |
';
echo '
';
}
private static function profileBadgeClass(string $status): string
{
switch ($status) {
case VersionProfiles::STATUS_KNOWN_GOOD:
return 'ok';
case VersionProfiles::STATUS_UNKNOWN_NEWER:
case VersionProfiles::STATUS_OLDER_UNSUPPORTED:
return 'warn';
default:
return 'unknown';
}
}
private static function profileStatusLabel(string $status): string
{
switch ($status) {
case VersionProfiles::STATUS_KNOWN_GOOD:
return __('Known good', 'sparkcore-commerce');
case VersionProfiles::STATUS_UNKNOWN_NEWER:
return __('Unknown, newer', 'sparkcore-commerce');
case VersionProfiles::STATUS_OLDER_UNSUPPORTED:
return __('Older than supported', 'sparkcore-commerce');
default:
return __('Not detected', 'sparkcore-commerce');
}
}
/**
* @param array $contractResults
*/
private static function renderContractSection(array $contractResults): void
{
$summary = self::summarizeContracts($contractResults);
echo '' . esc_html__('Contract status', 'sparkcore-commerce') . '
';
echo '';
echo esc_html(sprintf(
/* translators: 1: ok count, 2: warn count, 3: uncheckable count, 4: missing count. */
__('%1$d ok, %2$d warning(s), %3$d not checkable, %4$d missing.', 'sparkcore-commerce'),
$summary['ok'],
$summary['warn'],
$summary['uncheckable'],
$summary['missing']
));
echo '
';
echo '';
echo '';
echo '| ' . esc_html__('Interface', 'sparkcore-commerce') . ' | ';
echo '' . esc_html__('Type', 'sparkcore-commerce') . ' | ';
echo '' . esc_html__('Status', 'sparkcore-commerce') . ' | ';
echo '' . esc_html__('Detail', 'sparkcore-commerce') . ' | ';
echo '
';
foreach ($contractResults as $entry) {
echo '';
echo '| ' . esc_html((string) $entry['id']) . ' | ';
echo '' . esc_html((string) $entry['type']) . ' | ';
echo ''
. esc_html(self::contractStatusLabel((string) $entry['status']))
. ' | ';
echo '' . esc_html((string) $entry['detail']) . ' | ';
echo '
';
}
echo '
';
}
private static function contractBadgeClass(string $status): string
{
switch ($status) {
case 'ok':
return 'ok';
case 'warn':
return 'warn';
case 'missing':
return 'missing';
default:
return 'unknown';
}
}
private static function contractStatusLabel(string $status): string
{
switch ($status) {
case 'ok':
return __('Ok', 'sparkcore-commerce');
case 'warn':
return __('Warning', 'sparkcore-commerce');
case 'missing':
return __('Missing', 'sparkcore-commerce');
case 'uncheckable':
return __('Not checkable (E2E)', 'sparkcore-commerce');
default:
return __('Unknown', 'sparkcore-commerce');
}
}
/**
* @param array $contractResults
* @return array{ok: int, warn: int, missing: int, uncheckable: int}
*/
public static function summarizeContracts(array $contractResults): array
{
$summary = ['ok' => 0, 'warn' => 0, 'missing' => 0, 'uncheckable' => 0];
foreach ($contractResults as $entry) {
$status = (string) ($entry['status'] ?? '');
if (array_key_exists($status, $summary)) {
$summary[$status]++;
}
}
return $summary;
}
// =====================================================================
// (b) environment status section
// =====================================================================
/**
* @return array{
* wp_version: string,
* php_version: string,
* fluentcart_version: string,
* fluentcart_pro_version: string,
* breakdance_version: string,
* takeover_enabled: bool,
* atomized_checkout_enabled: bool,
* preview_product_id: int
* }
*/
public static function gatherEnvironment(): array
{
$unknown = __('not active', 'sparkcore-commerce');
return [
'wp_version' => function_exists('get_bloginfo') ? (string) get_bloginfo('version') : $unknown,
'php_version' => PHP_VERSION,
'fluentcart_version' => defined('FLUENTCART_VERSION') ? (string) FLUENTCART_VERSION : $unknown,
'fluentcart_pro_version' => defined('FLUENTCART_PRO_PLUGIN_VERSION') ? (string) FLUENTCART_PRO_PLUGIN_VERSION : $unknown,
'breakdance_version' => defined('__BREAKDANCE_VERSION') ? (string) __BREAKDANCE_VERSION : $unknown,
'takeover_enabled' => function_exists('get_option') && (bool) get_option(TemplateRegistry::TAKEOVER_OPTION, false),
'atomized_checkout_enabled' => function_exists('get_option') && (bool) get_option(self::ATOMIZED_CHECKOUT_OPTION, false),
'preview_product_id' => class_exists(EditorSupport::class) ? EditorSupport::previewProductId() : 0,
];
}
/**
* @param array{
* wp_version: string,
* php_version: string,
* fluentcart_version: string,
* fluentcart_pro_version: string,
* breakdance_version: string,
* takeover_enabled: bool,
* atomized_checkout_enabled: bool,
* preview_product_id: int
* } $environment
*/
private static function renderEnvironmentSection(array $environment): void
{
echo '' . esc_html__('Environment status', 'sparkcore-commerce') . '
';
echo '';
echo '';
$rows = [
__('WordPress', 'sparkcore-commerce') => $environment['wp_version'],
__('PHP', 'sparkcore-commerce') => $environment['php_version'],
__('FluentCart', 'sparkcore-commerce') => $environment['fluentcart_version'],
__('FluentCart Pro', 'sparkcore-commerce') => $environment['fluentcart_pro_version'],
__('Breakdance', 'sparkcore-commerce') => $environment['breakdance_version'],
];
foreach ($rows as $label => $value) {
echo '| ' . esc_html($label) . ' | ' . esc_html($value) . ' |
';
}
echo '| ' . esc_html__('Template takeover (fcbd_takeover)', 'sparkcore-commerce') . ' | '
. esc_html(self::flagLabel($environment['takeover_enabled']))
. ' |
';
echo '| ' . esc_html__('Atomized checkout (fcbd_atomized_checkout)', 'sparkcore-commerce') . ' | '
. esc_html(self::flagLabel($environment['atomized_checkout_enabled']))
. ' ' . esc_html__('The checkout family is built (WP9); this flag is deliberately off until the real purchase gate (wave 3).', 'sparkcore-commerce') . ''
. ' |
';
echo '| ' . esc_html__('Editor preview product', 'sparkcore-commerce') . ' | '
. ($environment['preview_product_id'] > 0
? esc_html(sprintf(
/* translators: %d: WordPress post ID. */
__('Set to product ID %d.', 'sparkcore-commerce'),
$environment['preview_product_id']
))
: esc_html__('Not set.', 'sparkcore-commerce'))
. ' |
';
echo '
';
}
private static function flagLabel(bool $enabled): string
{
return $enabled ? __('on', 'sparkcore-commerce') : __('off', 'sparkcore-commerce');
}
// =====================================================================
// (c) cache advice section
// =====================================================================
/**
* Detects known full-page-cache plugins via class/constant checks (never
* by naming assumption alone; every constant below is verified against
* the real plugin source, see this fork's report for file:line evidence)
* and pairs each with the exact exclusion advice already documented in
* readme.txt's cache FAQ.
*
* @return array
*/
public static function detectCachePlugins(): array
{
return [
[
'id' => 'flyingpress',
'name' => 'FlyingPress',
'active' => defined('FLYING_PRESS_VERSION'),
'evidence' => 'FLYING_PRESS_VERSION',
'advice' => __('Settings > Cache > Excluded URLs: add the cart, checkout, account and receipt page paths (or the FluentCart page setting slugs if you changed them).', 'sparkcore-commerce'),
],
[
'id' => 'litespeed',
'name' => 'LiteSpeed Cache',
'active' => defined('LSCWP_V'),
'evidence' => 'LSCWP_V',
'advice' => __('Cache > Excludes > Do Not Cache URIs: add the same page paths.', 'sparkcore-commerce'),
],
[
'id' => 'wp_rocket',
'name' => 'WP Rocket',
'active' => defined('WP_ROCKET_VERSION'),
'evidence' => 'WP_ROCKET_VERSION',
'advice' => __('Cache > Never Cache URL(s): add the same page paths.', 'sparkcore-commerce'),
],
];
}
/**
* @param array $cacheAdvice
*/
private static function renderCacheAdviceSection(array $cacheAdvice): void
{
echo '' . esc_html__('Cache advice', 'sparkcore-commerce') . '
';
echo '' . esc_html__('This plugin already marks the cart, checkout, account and receipt routes as no-cache. As a second line of defense, also add the same paths to your cache plugin\'s exclusion list:', 'sparkcore-commerce') . '
';
$anyActive = false;
echo '';
foreach ($cacheAdvice as $entry) {
if (!$entry['active']) {
continue;
}
$anyActive = true;
echo '- ' . esc_html($entry['name']) . ': ' . esc_html($entry['advice']) . '
';
}
echo '
';
if (!$anyActive) {
echo '' . esc_html__('No supported full-page cache plugin detected (FlyingPress, LiteSpeed Cache, WP Rocket).', 'sparkcore-commerce') . '
';
}
}
// =====================================================================
// (d) copyable diagnostic report
// =====================================================================
private static function renderReportSection(string $reportText): void
{
echo '' . esc_html__('Copyable diagnostics report', 'sparkcore-commerce') . '
';
echo '' . esc_html__('Contains no email addresses, order data or keys. Safe to share with a support request.', 'sparkcore-commerce') . '
';
echo '';
}
/**
* Pure text-assembly, deliberately free of any WordPress data lookup
* beyond the arguments it is given, so it can be exercised directly by
* tests/admin/run-admin.php with a stubbed contract/version/environment
* fixture. Never includes an email address, order record, or secret: only
* plugin/theme version strings, boolean feature flags, and Contracts::
* verify()'s own capability-level detail strings.
*
* @param array $contractResults
* @param array{version: string|null, status: string, message: string} $versionProfile
* @param array{
* wp_version: string,
* php_version: string,
* fluentcart_version: string,
* fluentcart_pro_version: string,
* breakdance_version: string,
* takeover_enabled: bool,
* atomized_checkout_enabled: bool,
* preview_product_id: int
* } $environment
* @param array $cacheAdvice
*/
public static function buildReportText(
array $contractResults,
array $versionProfile,
array $environment,
array $cacheAdvice
): string {
$summary = self::summarizeContracts($contractResults);
$lines = [];
$lines[] = __('SparkCore Commerce diagnostics report', 'sparkcore-commerce');
$lines[] = str_repeat('=', 48);
$lines[] = '';
$lines[] = __('Environment', 'sparkcore-commerce');
$lines[] = sprintf('WordPress: %s', $environment['wp_version']);
$lines[] = sprintf('PHP: %s', $environment['php_version']);
$lines[] = sprintf('FluentCart: %s', $environment['fluentcart_version']);
$lines[] = sprintf('FluentCart Pro: %s', $environment['fluentcart_pro_version']);
$lines[] = sprintf('Breakdance: %s', $environment['breakdance_version']);
$lines[] = sprintf('fcbd_takeover: %s', self::flagLabel($environment['takeover_enabled']));
$lines[] = sprintf('fcbd_atomized_checkout: %s', self::flagLabel($environment['atomized_checkout_enabled']));
$lines[] = '';
$lines[] = __('Verified baselines', 'sparkcore-commerce');
$lines[] = sprintf('FluentCart: %s', self::auditedFluentCartProfilesLabel());
$lines[] = sprintf('Breakdance: %s', Contracts::VERIFIED_BREAKDANCE_VERSION);
$lines[] = '';
$lines[] = __('Version profile', 'sparkcore-commerce');
$lines[] = sprintf('Status: %s', self::profileStatusLabel($versionProfile['status']));
$lines[] = (string) $versionProfile['message'];
$lines[] = '';
$lines[] = __('Contract status', 'sparkcore-commerce');
$lines[] = sprintf(
'%d ok, %d warn, %d uncheckable, %d missing',
$summary['ok'],
$summary['warn'],
$summary['uncheckable'],
$summary['missing']
);
foreach ($contractResults as $entry) {
$lines[] = sprintf(
'- [%s] %s (%s): %s',
strtoupper((string) $entry['status']),
(string) $entry['id'],
(string) $entry['type'],
(string) $entry['detail']
);
}
$lines[] = '';
$lines[] = __('Cache advice', 'sparkcore-commerce');
$activeCache = false;
foreach ($cacheAdvice as $entry) {
if (!$entry['active']) {
continue;
}
$activeCache = true;
$lines[] = sprintf('- %s: %s', $entry['name'], $entry['advice']);
}
if (!$activeCache) {
$lines[] = __('No supported full-page cache plugin detected.', 'sparkcore-commerce');
}
return implode("\n", $lines);
}
/**
* Every audited (known-good) FluentCart profile, oldest first, so the
* Health page shows the full audited range instead of only the oldest
* baseline constant (which deliberately stays 1.3.27 as the minimum).
*/
private static function auditedFluentCartProfilesLabel(): string
{
$known = [];
foreach (VersionProfiles::profiles() as $version => $profile) {
if (($profile['status'] ?? '') === VersionProfiles::STATUS_KNOWN_GOOD) {
$known[] = (string) $version;
}
}
if ($known === []) {
return Contracts::VERIFIED_VERSION;
}
usort($known, 'version_compare');
return implode(', ', $known);
}
}