status = new Status(); $this->connect = new Connect( $this->status ); $this->settings = new Settings( $this->status, $this->connect ); $this->storefront = new Storefront( $this->status ); $this->blocks = new Blocks( $this->storefront ); $this->elementor = new Elementor( $this->storefront ); $this->attribution = new Attribution(); $this->privacy = new Privacy(); $this->review_prompt = new ReviewPrompt( $this->status ); $this->connector = new Connector(); // Boot each component. $this->status->init(); $this->connect->init(); $this->settings->init(); $this->storefront->init(); $this->blocks->init(); $this->elementor->init(); $this->attribution->init(); $this->privacy->init(); $this->review_prompt->init(); $this->connector->init(); // Plugin action link to the settings page. add_filter( 'plugin_action_links_' . plugin_basename( AISTHETIX_TRYON_FILE ), array( $this, 'plugin_action_links' ) ); } /** * Add a "Settings" link on the Plugins screen. * * @param array $links Existing action links. * @return array */ public function plugin_action_links( $links ) { $settings_link = sprintf( '%s', esc_url( admin_url( 'admin.php?page=aisthetix-tryon' ) ), esc_html__( 'Settings', 'aisthetix-virtual-try-on' ) ); array_unshift( $links, $settings_link ); return $links; } /** * Resolve the brain (central service) base URL, without trailing slash. * * @return string */ public static function brain_url() { $url = AISTHETIX_BRAIN_URL; /** * Filter the aisthetix brain base URL. * * @param string $url Base URL with no trailing slash. */ $url = apply_filters( 'aisthetix_tryon_brain_url', $url ); return untrailingslashit( esc_url_raw( $url ) ); } /** * Get the stored publishable key (safe for the browser). * * @return string */ public static function publishable_key() { return (string) get_option( self::OPTION_PUBLISHABLE_KEY, '' ); } /** * Get the stored secret key. SERVER-ONLY — never enqueue this to the browser. * * @return string */ public static function secret_key() { return (string) get_option( self::OPTION_SECRET_KEY, '' ); } /** * Get the stored store id. * * @return string */ public static function store_id() { return (string) get_option( self::OPTION_STORE_ID, '' ); } /** * Whether the merchant has completed Connect (keys present). * * @return bool */ public static function is_connected() { return '' !== self::publishable_key() && '' !== self::secret_key(); } /** * Read a single plugin setting with a default. * * @param string $key Setting key. * @param mixed $default Default value. * @return mixed */ public static function get_setting( $key, $default = '' ) { $settings = get_option( self::OPTION_SETTINGS, array() ); if ( is_array( $settings ) && array_key_exists( $key, $settings ) ) { return $settings[ $key ]; } return $default; } /** * Whether the merchant has opted into anonymous analytics: the durable * `aisthetix_vid` visitor cookie plus view / add-to-cart / try-on / order * events. Default OFF — nothing tracks passively without this explicit opt-in. * * The shopper-initiated try-on itself is unaffected: it is an explicit action * behind the widget's own consent step, not passive tracking. * * @return bool */ public static function analytics_enabled() { return (bool) self::get_setting( 'enable_analytics', false ); } /** * Session-replay settings handed to the storefront widget. * * Replay is OFF unless BOTH switches are on: the merchant's, in this site's * own settings, and the operator's, which arrives from Core with the * authenticated status call. Two independent conditions on purpose — the * merchant decides for their own store, and Aisthetix has to be able to stop * recording across every merchant site at once, without a plugin release and * a wordpress.org review cycle. * * The operator's half used to be a WordPress filter defaulting to empty. No * production code ever defined it, so replay could not be switched on at * all; and had a site defined it, switching it off again would have meant * editing that site. Served by Core it is a real kill switch, and the * latency is the status cache's TTL — ten minutes, see Status::CACHE_TTL. * * The shopper's own CMP is a third, stricter gate, enforced in the browser — * nothing here can switch replay on for a shopper who has not consented. * * Returns an empty array when replay cannot run, so the widget's config * carries no analytics block at all and the recorder is never even fetched. * * @param array $remote The `replay` block from Core's storefront status. * @return array */ public static function replay_settings( array $remote = array() ) { if ( ! (bool) self::get_setting( 'enable_session_replay', false ) ) { return array(); } // The operator's half. It arrives with the authenticated status call // rather than from a WordPress filter, because a filter is per-site: no // production code defined it, so replay could never have been switched // on at all — and if it had been, switching it OFF again would have // meant editing every merchant's site. Served by Core, it is a real // kill switch, and its latency is the status cache's TTL. if ( empty( $remote['enabled'] ) ) { return array(); } $key = trim( (string) ( isset( $remote['key'] ) ? $remote['key'] : '' ) ); if ( '' === $key ) { return array(); } // A recording that cannot name its store is a recording nobody can find // and nobody can attribute — worse than no recording, because it was // still taken. $store_id = self::store_id(); if ( '' === $store_id ) { return array(); } $host = (string) ( isset( $remote['host'] ) && '' !== $remote['host'] ? $remote['host'] : 'https://ingest.aisthetix.fyi' ); return array( 'analytics' => array( 'replayKey' => $key, 'ingestHost' => untrailingslashit( $host ), 'replayEnabled' => true, 'environment' => (string) ( isset( $remote['environment'] ) ? $remote['environment'] : 'production' ), // From OPTION_STORE_ID, which is where Connect writes it. It was // being read out of the settings array, where it has never been: // every recording registered an empty store. 'storeId' => $store_id, ), ); } }