save_features( $extra['features'] ); } } /** * Forget the connection (keeps automation prefs). */ public function clear_connection() { delete_option( self::OPT_API_KEY ); delete_option( self::OPT_STORE_ID ); delete_option( self::OPT_WEBHOOK_SECRET ); delete_option( self::OPT_SYNC_CONFIG ); delete_option( self::OPT_PROJECT_ID ); delete_option( self::OPT_PLATFORM ); delete_option( self::OPT_FEATURES ); delete_transient( Dashboard::CREDITS_TRANSIENT ); delete_transient( Dashboard::CREDITS_BACKOFF ); Dashboard::reset_memo(); self::reset_connect_notice(); } /* --------------------------------------------------------------------- */ /* Connect notice (per-user, so it is never a site-wide permanent nag) */ /* --------------------------------------------------------------------- */ /** * Has the current user dismissed the "connect an account" notice? * * @return bool */ public static function connect_notice_dismissed() { $user_id = get_current_user_id(); return $user_id > 0 && '1' === (string) get_user_meta( $user_id, self::META_CONNECT_NOTICE, true ); } /** * Hide the notice for the current user. */ public static function dismiss_connect_notice() { $user_id = get_current_user_id(); if ( $user_id > 0 ) { update_user_meta( $user_id, self::META_CONNECT_NOTICE, '1' ); } } /** * Show it again for everyone. Called on connect and disconnect so the * notice reflects the current state rather than a stale dismissal. */ public static function reset_connect_notice() { delete_metadata( 'user', 0, self::META_CONNECT_NOTICE, '', true ); } /** * Has the current user dismissed the Dashboard "Start here" card? * * @return bool */ public static function start_card_dismissed() { $user_id = get_current_user_id(); return $user_id > 0 && '1' === (string) get_user_meta( $user_id, self::META_START_CARD, true ); } /** * Hide the "Start here" card for the current user. */ public static function dismiss_start_card() { $user_id = get_current_user_id(); if ( $user_id > 0 ) { update_user_meta( $user_id, self::META_START_CARD, '1' ); } } /** * ACE project id linked to this site (from register/bootstrap). * * @return string */ public function get_project_id() { return (string) get_option( self::OPT_PROJECT_ID, '' ); } /** * Server-declared feature flags (contentHub, ecomAeo, botTraffic, ...). * * @return array */ public function get_features() { $features = get_option( self::OPT_FEATURES, array() ); return is_array( $features ) ? array_map( 'boolval', $features ) : array(); } /** * @param string $feature Feature key. * @return bool True when the server did not say otherwise. */ public function feature_enabled( $feature ) { $features = $this->get_features(); return ! array_key_exists( $feature, $features ) || $features[ $feature ]; } /** * @param array $features Feature map from register/heartbeat. */ public function save_features( $features ) { update_option( self::OPT_FEATURES, array_map( 'boolval', (array) $features ) ); } /** * Connected = we have both a key and a registered store id. * * @return bool */ public function is_connected() { return '' !== $this->get_api_key() && '' !== $this->get_store_id(); } /** * Server-declared sync configuration (maxBatchSize, supportedFields). * * @return array */ public function get_sync_config() { $config = get_option( self::OPT_SYNC_CONFIG, array() ); return is_array( $config ) ? $config : array(); } /** * Server batch ceiling; defaults to 100. * * @return int */ public function get_max_batch_size() { $config = $this->get_sync_config(); $max = isset( $config['maxBatchSize'] ) ? (int) $config['maxBatchSize'] : 100; return $max > 0 ? min( $max, 100 ) : 100; } /** * Selected content types (defaults to the recommended set). * * @return string[] */ public function get_content_types() { $saved = get_option( self::OPT_CONTENT_TYPES, null ); if ( ! is_array( $saved ) || empty( $saved ) ) { return Content_Types::recommended(); } return Content_Types::sanitize_selection( $saved ); } /** * @param array $types Content type keys. */ public function set_content_types( $types ) { update_option( self::OPT_CONTENT_TYPES, Content_Types::sanitize_selection( $types ) ); } /** * Whether to auto-append IRL cards + internal links to the storefront page. * * @return bool */ public function is_auto_append() { return 'yes' === get_option( self::OPT_AUTO_APPEND, 'yes' ); } /* --------------------------------------------------------------------- */ /* Automation prefs */ /* --------------------------------------------------------------------- */ /** * @return bool */ public function is_auto_sync() { return 'yes' === get_option( self::OPT_AUTO_SYNC, 'no' ); } /** * @return string daily|weekly */ public function get_sync_frequency() { $freq = (string) get_option( self::OPT_SYNC_FREQUENCY, 'daily' ); return in_array( $freq, array( 'daily', 'weekly' ), true ) ? $freq : 'daily'; } /** * @param string $freq daily|weekly. */ public function set_sync_frequency( $freq ) { $freq = in_array( $freq, array( 'daily', 'weekly' ), true ) ? $freq : 'daily'; update_option( self::OPT_SYNC_FREQUENCY, $freq ); } /** * @return bool */ public function is_auto_apply() { return 'yes' === get_option( self::OPT_AUTO_APPLY, 'no' ); } /** * @return bool */ public function is_schema_enabled() { return 'yes' === get_option( self::OPT_SCHEMA_ENABLED, 'yes' ); } /** * @return bool */ public function is_debug_log() { return 'yes' === get_option( self::OPT_DEBUG_LOG, 'no' ); } /* --------------------------------------------------------------------- */ /* Sync bookkeeping */ /* --------------------------------------------------------------------- */ /** * @return string MySQL datetime or empty. */ public function get_last_sync() { return (string) get_option( self::OPT_LAST_SYNC, '' ); } /** * Stamp the last sync time (UTC). */ public function touch_last_sync() { update_option( self::OPT_LAST_SYNC, gmdate( 'Y-m-d H:i:s' ) ); } /** * @return int */ public function get_sync_cursor() { return (int) get_option( self::OPT_SYNC_CURSOR, 0 ); } /** * @param int $page Page index. */ public function set_sync_cursor( $page ) { update_option( self::OPT_SYNC_CURSOR, max( 0, (int) $page ) ); } /* --------------------------------------------------------------------- */ /* Content publishing prefs (4.0) */ /* --------------------------------------------------------------------- */ /** * @return string draft|publish — default status for published articles. */ public function get_content_status() { $status = (string) get_option( self::OPT_CONTENT_STATUS, 'draft' ); return in_array( $status, array( 'draft', 'publish' ), true ) ? $status : 'draft'; } /** * @return string blocks|classic — post_content format. */ public function get_content_format() { $format = (string) get_option( self::OPT_CONTENT_FORMAT, 'blocks' ); return in_array( $format, array( 'blocks', 'classic' ), true ) ? $format : 'blocks'; } /** * @return int Default author user id (0 = the acting user). */ public function get_content_author() { return max( 0, (int) get_option( self::OPT_CONTENT_AUTHOR, 0 ) ); } /** * @return bool Whether to emit Article JSON-LD for published articles. */ public function is_article_schema() { return 'yes' === get_option( self::OPT_ARTICLE_SCHEMA, 'yes' ); } /* --------------------------------------------------------------------- */ /* Crawler tracking prefs (4.0) */ /* --------------------------------------------------------------------- */ /** * @return bool Master toggle for the native AI crawler tracker. */ public function is_bots_enabled() { return 'yes' === get_option( self::OPT_BOTS_ENABLED, 'yes' ); } /** * @return bool Whether per-page paths are stored (else one row per bot per day). */ public function is_bots_paths() { return 'yes' === get_option( self::OPT_BOTS_PATHS, 'yes' ); } /** * @return int Retention window in days (30|90|180). */ public function get_bots_retention() { $days = (int) get_option( self::OPT_BOTS_RETENTION, 90 ); return in_array( $days, array( 30, 90, 180 ), true ) ? $days : 90; } /** * @return bool Whether daily aggregates are reported back to ACE (opt-in). */ public function is_bots_report() { return 'yes' === get_option( self::OPT_BOTS_REPORT, 'no' ); } /** * Generic boolean writer used by the settings form. * * @param string $option Option key. * @param mixed $value Raw value (truthy or 'yes'). */ public function set_bool( $option, $value ) { $yes = ( 'yes' === $value || true === $value || '1' === (string) $value || 'on' === $value ); update_option( $option, $yes ? 'yes' : 'no' ); } }