status = $status; $this->connect = $connect; } /** * Register hooks. * * @return void */ public function init() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_notices', array( $this, 'render_notices' ) ); } /** * Add the settings page under the WooCommerce menu. * * @return void */ public function register_menu() { add_submenu_page( 'woocommerce', __( 'aisthetix Try-On', 'aisthetix-virtual-try-on' ), __( 'aisthetix Try-On', 'aisthetix-virtual-try-on' ), 'manage_woocommerce', self::PAGE_SLUG, array( $this, 'render_page' ) ); } /** * Register the settings field group with sanitisation. * * @return void */ public function register_settings() { register_setting( self::OPTION_GROUP, Plugin::OPTION_SETTINGS, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize_settings' ), 'default' => array( 'placement' => 'after_add_to_cart', 'button_text' => '', 'unavailable_title' => '', 'unavailable_message' => '', 'unavailable_button_text' => '', 'unavailable_button_url' => '', 'enable_analytics' => false, 'enable_session_replay' => false, ), ) ); } /** * Sanitise the settings array on save. * * @param array $input Raw input. * @return array Clean settings. */ public function sanitize_settings( $input ) { $clean = array(); $allowed_placements = array( 'after_add_to_cart', 'manual' ); $placement = isset( $input['placement'] ) ? sanitize_text_field( $input['placement'] ) : 'after_add_to_cart'; $clean['placement'] = in_array( $placement, $allowed_placements, true ) ? $placement : 'after_add_to_cart'; $clean['button_text'] = isset( $input['button_text'] ) ? sanitize_text_field( $input['button_text'] ) : ''; $clean['unavailable_title'] = isset( $input['unavailable_title'] ) ? wp_html_excerpt( sanitize_text_field( $input['unavailable_title'] ), 80, '' ) : ''; $clean['unavailable_message'] = isset( $input['unavailable_message'] ) ? wp_html_excerpt( sanitize_textarea_field( $input['unavailable_message'] ), 240, '' ) : ''; $clean['unavailable_button_text'] = isset( $input['unavailable_button_text'] ) ? wp_html_excerpt( sanitize_text_field( $input['unavailable_button_text'] ), 40, '' ) : ''; // The shopper CTA may only remain on this merchant's own site. This prevents // a compromised/admin-pasted setting from turning the widget into an open redirect. $url = isset( $input['unavailable_button_url'] ) ? esc_url_raw( $input['unavailable_button_url'] ) : ''; if ( $url ) { $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); $url_host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! $url_host || strtolower( $url_host ) !== strtolower( $home_host ) ) { $url = ''; } } $clean['unavailable_button_url'] = $url; $clean['enable_analytics'] = ! empty( $input['enable_analytics'] ); $clean['enable_session_replay'] = ! empty( $input['enable_session_replay'] ); return $clean; } /** * Render queued admin notices from the Connect flow. * * @return void */ public function render_notices() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice flag set by our own redirect. $notice = isset( $_GET['aisthetix_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['aisthetix_notice'] ) ) : ''; $map = array( 'connect_success' => array( 'success', __( 'Connected to aisthetix successfully.', 'aisthetix-virtual-try-on' ) ), 'disconnected' => array( 'success', __( 'Disconnected from aisthetix.', 'aisthetix-virtual-try-on' ) ), 'connect_state_error' => array( 'error', __( 'Connect failed: the security check did not match. Please try again.', 'aisthetix-virtual-try-on' ) ), 'connect_exchange_error' => array( 'error', __( 'Connect failed: could not exchange the code for keys. Please try again.', 'aisthetix-virtual-try-on' ) ), ); if ( isset( $map[ $notice ] ) ) { list( $type, $message ) = $map[ $notice ]; printf( '

%2$s

', esc_attr( $type ), esc_html( $message ) ); } if ( ! current_user_can( 'manage_woocommerce' ) || ! Plugin::is_connected() ) { return; } $status = $this->status->get(); if ( empty( $status['trial'] ) || ! is_array( $status['trial'] ) ) { return; } $remaining = isset( $status['trial']['remaining'] ) ? (int) $status['trial']['remaining'] : 0; if ( $remaining > 10 ) { return; } $plan = ! empty( $status['plan'] ) ? $status['plan'] : __( 'paid plan', 'aisthetix-virtual-try-on' ); $included = isset( $status['includedTryOns'] ) ? (int) $status['includedTryOns'] : 0; $url = Plugin::brain_url() . '/dashboard/billing'; if ( $remaining > 0 ) { $message = sprintf( /* translators: 1: remaining trial try-ons, 2: plan name. */ __( 'Your aisthetix trial has %1$d try-ons remaining. Start %2$s now to keep virtual try-on available without interruption.', 'aisthetix-virtual-try-on' ), $remaining, $plan ); $type = 'warning'; } else { $message = sprintf( /* translators: 1: plan name, 2: monthly try-on allowance. */ __( 'Virtual try-on is currently paused. Your 50 trial try-ons have been used. Start %1$s now to restore it and receive %2$d monthly try-ons.', 'aisthetix-virtual-try-on' ), $plan, $included ); $type = 'error'; } printf( '

%2$s %4$s

', esc_attr( $type ), esc_html( $message ), esc_url( $url ), esc_html( sprintf( __( 'Start %s now', 'aisthetix-virtual-try-on' ), $plan ) ) ); } /** * Render the settings page. * * @return void */ public function render_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $connected = Plugin::is_connected(); // On the admin page, force-refresh so plan state is current right after a // plan change; the storefront keeps its cached ~10-min read. $status = $connected ? $this->status->refresh() : $this->status->get(); ?>

render_plan_section( $status ); ?>

%s', esc_html__( 'Not connected', 'aisthetix-virtual-try-on' ) ); return; } if ( ! empty( $status['error'] ) ) { printf( '%s (%s)', esc_html__( 'Connected, but could not reach the service.', 'aisthetix-virtual-try-on' ), esc_html( $status['error'] ) ); return; } $active = ! empty( $status['active'] ) && ! empty( $status['withinQuota'] ); $dot = $active ? '#46b450' : '#dba617'; $label = $active ? __( 'active', 'aisthetix-virtual-try-on' ) : __( 'paused (quota reached)', 'aisthetix-virtual-try-on' ); $resets = ''; if ( ! empty( $status['resetsAt'] ) ) { $ts = strtotime( $status['resetsAt'] ); if ( $ts ) { $resets = date_i18n( get_option( 'date_format' ), $ts ); } } // "Try-on: active — 240/300, resets Jul 1". $line = sprintf( /* translators: 1: state label, 2: remaining count, 3: reset date. */ __( 'Try-on: %1$s — %2$s remaining%3$s', 'aisthetix-virtual-try-on' ), $label, number_format_i18n( (int) $status['remaining'] ), $resets ? sprintf( /* translators: %s: reset date. */ __( ', resets %s', 'aisthetix-virtual-try-on' ), $resets ) : '' ); printf( '%2$s', esc_attr( $dot ), esc_html( $line ) ); if ( ! empty( $status['plan'] ) ) { printf( ' (%s)', esc_html( $status['plan'] ) ); } } }