array( 'label' => __( 'What do you sell, and to whom?', 'ai-eshop-optimizer' ), 'options' => array( 'retail' => __( 'Retail: products, to the public', 'ai-eshop-optimizer' ), 'trade' => __( 'Trade or wholesale: to businesses, often at their own prices', 'ai-eshop-optimizer' ), 'catalogue' => __( 'A catalogue with enquiries: no prices, or no checkout', 'ai-eshop-optimizer' ), 'services' => __( 'Services: bookings, appointments, quotes', 'ai-eshop-optimizer' ), ), ), 'where' => array( 'label' => __( 'Where do you sell, besides here?', 'ai-eshop-optimizer' ), 'options' => array( 'here' => __( 'Only here', 'ai-eshop-optimizer' ), 'marketplaces' => __( 'Marketplaces too: Skroutz, Shopflix, Amazon, eBay', 'ai-eshop-optimizer' ), 'physical' => __( 'A physical shop too', 'ai-eshop-optimizer' ), 'both' => __( 'Marketplaces and a physical shop', 'ai-eshop-optimizer' ), ), ), 'first' => array( 'label' => __( 'What brought you here first?', 'ai-eshop-optimizer' ), 'options' => array( 'get_found' => __( 'Get found: search engines and product feeds', 'ai-eshop-optimizer' ), 'ads' => __( 'Know what my ads actually earn', 'ai-eshop-optimizer' ), 'better_pages' => __( 'Better shop pages', 'ai-eshop-optimizer' ), 'bring_back' => __( 'Bring people back: email, push, recovered carts', 'ai-eshop-optimizer' ), 'data' => __( 'Get data in and out: ERP, feeds, exports', 'ai-eshop-optimizer' ), 'looking' => __( 'Just looking', 'ai-eshop-optimizer' ), ), 'optional' => true, ), ); } public static function label( $question, $key ) { $q = self::questions(); if ( ! isset( $q[ $question ]['options'][ $key ] ) ) { return ''; } $full = (string) $q[ $question ]['options'][ $key ]; $p = strpos( $full, ':' ); return trim( false === $p ? $full : substr( $full, 0, $p ) ); } public static function get() { $v = get_option( self::OPTION ); if ( ! is_array( $v ) ) { return null; } $q = self::questions(); $out = array( 'sell' => '', 'where' => '', 'first' => '', 'at' => 0, 'by' => 0 ); foreach ( array( 'sell', 'where', 'first' ) as $k ) { $val = isset( $v[ $k ] ) ? sanitize_key( (string) $v[ $k ] ) : ''; $out[ $k ] = isset( $q[ $k ]['options'][ $val ] ) ? $val : ''; } $out['at'] = isset( $v['at'] ) ? (int) $v['at'] : 0; $out['by'] = isset( $v['by'] ) ? (int) $v['by'] : 0; if ( '' === $out['sell'] || '' === $out['where'] ) { return null; } return $out; } public static function is_answered() { return null !== self::get(); } public static function save( array $in, $uid ) { $q = self::questions(); $row = array(); foreach ( array( 'sell', 'where', 'first' ) as $k ) { $val = isset( $in[ $k ] ) ? sanitize_key( (string) $in[ $k ] ) : ''; $row[ $k ] = isset( $q[ $k ]['options'][ $val ] ) ? $val : ''; } if ( '' === $row['sell'] || '' === $row['where'] ) { return false; } $row['at'] = time(); $row['by'] = (int) $uid; update_option( self::OPTION, $row, false ); return true; } public static function clear() { delete_option( self::OPTION ); } public static function summary() { $s = self::get(); if ( ! $s ) { return ''; } $bits = array(); foreach ( array( 'sell', 'where', 'first' ) as $k ) { $l = self::label( $k, $s[ $k ] ); if ( '' !== $l ) { $bits[] = $l; } } return implode( ' · ', $bits ); } public static function facts_line( array $facts, $cogs = null ) { $bits = array(); if ( ! empty( $facts['wc'] ) ) { $bits[] = '' !== (string) $facts['wc_version'] ? sprintf( __( 'WooCommerce %s', 'ai-eshop-optimizer' ), $facts['wc_version'] ) : __( 'WooCommerce', 'ai-eshop-optimizer' ); } else { $bits[] = __( 'WooCommerce is not installed', 'ai-eshop-optimizer' ); } $bits[] = sprintf( _n( '%s product', '%s products', (int) $facts['products'], 'ai-eshop-optimizer' ), number_format_i18n( (int) $facts['products'] ) ); $bits[] = sprintf( _n( '%s completed order', '%s completed orders', (int) $facts['orders_completed'], 'ai-eshop-optimizer' ), number_format_i18n( (int) $facts['orders_completed'] ) ); if ( '' !== (string) $facts['country_name'] ) { $bits[] = sprintf( __( 'store based in %s', 'ai-eshop-optimizer' ), $facts['country_name'] ); } if ( '' !== (string) $facts['currency'] ) { $bits[] = (string) $facts['currency']; } if ( '' !== (string) $facts['competitor'] ) { $bits[] = sprintf( __( '%s active', 'ai-eshop-optimizer' ), $facts['competitor'] ); } if ( '' !== (string) $facts['cmp'] ) { $bits[] = sprintf( __( '%s active', 'ai-eshop-optimizer' ), $facts['cmp'] ); } if ( is_array( $cogs ) && (int) $cogs['products'] > 0 ) { $bits[] = sprintf( __( 'cost known for %1$s of %2$s products', 'ai-eshop-optimizer' ), number_format_i18n( (int) $cogs['with_cost'] ), number_format_i18n( (int) $cogs['products'] ) ); } return implode( ' · ', $bits ); } public static function facts() { static $memo = null; if ( null !== $memo ) { return $memo; } $wc = class_exists( 'WooCommerce' ); $products = 0; if ( post_type_exists( 'product' ) ) { $c = wp_count_posts( 'product' ); $products = ( $c && isset( $c->publish ) ) ? (int) $c->publish : 0; } $orders = 0; if ( $wc && function_exists( 'wc_orders_count' ) ) { $orders = (int) wc_orders_count( 'completed' ); } $country = ''; $cname = ''; $curr = ''; $wcv = ''; if ( $wc ) { $wcv = defined( 'WC_VERSION' ) ? (string) WC_VERSION : ''; $raw = (string) get_option( 'woocommerce_default_country', '' ); $bits = explode( ':', $raw ); $country = strtoupper( trim( (string) $bits[0] ) ); if ( '' !== $country && function_exists( 'WC' ) && WC()->countries && isset( WC()->countries->countries[ $country ] ) ) { $cname = (string) WC()->countries->countries[ $country ]; } $curr = function_exists( 'get_woocommerce_currency' ) ? (string) get_woocommerce_currency() : (string) get_option( 'woocommerce_currency', '' ); } $competitor = ''; if ( ! class_exists( 'AIEO_SEO_Store' ) && defined( 'AIEO_DIR' ) && file_exists( AIEO_DIR . 'includes/seo/class-aieo-seo-store.php' ) ) { require_once AIEO_DIR . 'includes/seo/class-aieo-seo-store.php'; } if ( class_exists( 'AIEO_SEO_Store' ) && method_exists( 'AIEO_SEO_Store', 'competitor_seo_plugin' ) ) { $competitor = (string) AIEO_SEO_Store::competitor_seo_plugin(); } $cmp = ''; if ( class_exists( 'AIEO_Consent' ) && method_exists( 'AIEO_Consent', 'external_cmp_active' ) ) { $cmp = (string) AIEO_Consent::external_cmp_active(); } $profile = ''; $plabel = ''; if ( ! class_exists( 'AIEO_SEO_Score' ) && defined( 'AIEO_DIR' ) && file_exists( AIEO_DIR . 'includes/seo/class-aieo-seo-score.php' ) ) { require_once AIEO_DIR . 'includes/seo/class-aieo-seo-score.php'; } if ( class_exists( 'AIEO_SEO_Score' ) && method_exists( 'AIEO_SEO_Score', 'profile_status' ) ) { $ps = AIEO_SEO_Score::profile_status(); $profile = isset( $ps['profile'] ) ? (string) $ps['profile'] : ''; $plabel = isset( $ps['label'] ) ? (string) $ps['label'] : ''; } $first = (int) get_option( 'aieo_first_activated_at', 0 ); $days = $first > 0 ? (int) floor( ( time() - $first ) / DAY_IN_SECONDS ) : -1; $facts = array( 'wc' => $wc, 'wc_version' => $wcv, 'products' => $products, 'orders_completed' => $orders, 'country' => $country, 'country_name' => $cname, 'currency' => $curr, 'competitor' => $competitor, 'cmp' => $cmp, 'profile' => $profile, 'profile_label' => $plabel, 'first_activated_at' => $first, 'days_since_activation' => $days, 'locale' => (string) get_locale(), ); $filtered = apply_filters( 'aieo_shop_shape_facts', $facts ); if ( is_array( $filtered ) ) { foreach ( $facts as $k => $v ) { if ( array_key_exists( $k, $filtered ) ) { $facts[ $k ] = is_int( $v ) ? (int) $filtered[ $k ] : ( is_bool( $v ) ? (bool) $filtered[ $k ] : (string) $filtered[ $k ] ); } } } $memo = $facts; return $memo; } public static function screen() { $f = self::facts(); if ( $f['products'] < self::BLANK_PRODUCT_CEILING && 0 === $f['orders_completed'] ) { return 'blank'; } return self::is_answered() ? 'checklist' : 'confirm'; } public static function is_fresh() { $f = self::facts(); return $f['first_activated_at'] > 0 && $f['days_since_activation'] >= 0 && $f['days_since_activation'] < self::FRESH_DAYS; } public static function install_source() { if ( ! class_exists( 'AIEO_Rooster_Signup' ) || ! method_exists( 'AIEO_Rooster_Signup', 'source' ) ) { return ''; } $ev = (array) AIEO_Rooster_Signup::source(); if ( ( isset( $ev['root'] ) && 'cloudways' === $ev['root'] ) || ( isset( $ev['host'] ) && 'cloudwaysapps' === $ev['host'] ) ) { return 'cloudways'; } return ''; } public static function proposed() { $f = self::facts(); return array( 'sell' => in_array( $f['profile'], array( 'catalog', 'elearning' ), true ) && ! ( class_exists( 'AIEO_SEO_Score' ) && AIEO_SEO_Score::has_sales() ) ? 'catalogue' : 'retail', 'where' => 'here', 'first' => '', ); } public static function first_win( $first ) { switch ( (string) $first ) { case 'better_pages': return array( 'tab' => 'swatches', 'why' => __( 'The first proof that this plugin can do things, not only see them: colour and size swatches on a real product, with Undo beside the switch.', 'ai-eshop-optimizer' ), 'needs_products' => true, ); case 'ads': return array( 'tab' => 'conversions', 'why' => __( 'Nothing about ad earnings can be known until a sale is recorded as a conversion. This is where that starts, and it costs nothing to switch on.', 'ai-eshop-optimizer' ), 'needs_products' => false, ); case 'bring_back': return array( 'tab' => 'workflows', 'why' => __( 'Recovering an abandoned cart is the message with the best return in the plugin. It waits for the first carts, and the rule can be written today.', 'ai-eshop-optimizer' ), 'needs_products' => true, ); case 'data': return array( 'tab' => 'stock_import', 'why' => __( 'The catalogue is what everything else reads. A stock and price file is the fastest way to bring it in, and the same screen re-imports it every day.', 'ai-eshop-optimizer' ), 'needs_products' => false, ); case 'get_found': case 'looking': default: return array( 'tab' => 'seo_health', 'why' => __( 'Free, read-only, no key: one number for your pages and three named fixes. A mirror, which proves competence without asking for anything.', 'ai-eshop-optimizer' ), 'needs_products' => false, ); } } public static function suggested_pins( $shape = null ) { $s = is_array( $shape ) ? $shape : self::get(); if ( ! $s ) { return array(); } $ids = array(); $by_first = array( 'get_found' => array( 'seo_health', 'seo_general' ), 'ads' => array( 'conversions', 'attribution' ), 'better_pages' => array( 'swatches', 'badges' ), 'bring_back' => array( 'workflows', 'web-push' ), 'data' => array( 'stock_import', 'orders_export' ), 'looking' => array( 'seo_health' ), ); if ( isset( $by_first[ $s['first'] ] ) ) { $ids = array_merge( $ids, $by_first[ $s['first'] ] ); } $by_sell = array( 'retail' => array( 'badges', 'side_cart', 'swatches' ), 'trade' => array( 'role_pricing', 'orders_export' ), 'catalogue' => array( 'product_requests', 'seo_health' ), 'services' => array( 'seo_health', 'faq' ), ); if ( isset( $by_sell[ $s['sell'] ] ) ) { $ids = array_merge( $ids, $by_sell[ $s['sell'] ] ); } $by_where = array( 'marketplaces' => array( 'marketplace_feeds' ), 'physical' => array( 'store_pages' ), 'both' => array( 'marketplace_feeds', 'store_pages' ), ); if ( isset( $by_where[ $s['where'] ] ) ) { $ids = array_merge( $ids, $by_where[ $s['where'] ] ); } $ids[] = 'connections'; $ids = array_values( array_unique( array_filter( array_map( 'sanitize_key', $ids ) ) ) ); $ids = array_values( array_filter( $ids, array( __CLASS__, 'destination_exists' ) ) ); return array_slice( $ids, 0, self::MAX_SEEDED ); } public static function destination_exists( $id ) { if ( ! class_exists( 'AIEO_DMM' ) ) { return false; } static $known = null; if ( null === $known ) { $known = array(); foreach ( (array) AIEO_DMM::instance()->get_subtabs() as $t ) { if ( isset( $t['id'] ) ) { $known[ (string) $t['id'] ] = true; } } } return isset( $known[ (string) $id ] ); } public static function seed_pins( $uid ) { $uid = (int) $uid; if ( $uid < 1 || ! class_exists( 'AIEO_DMM' ) ) { return array(); } $pins = get_user_meta( $uid, AIEO_DMM::PINS_META, true ); $pins = is_array( $pins ) ? array_values( array_filter( array_map( 'sanitize_key', $pins ) ) ) : array(); $cap = defined( 'AIEO_DMM::MAX_PINS' ) ? (int) AIEO_DMM::MAX_PINS : 32; $room = max( 0, $cap - count( $pins ) ); $add = array_slice( array_values( array_diff( self::suggested_pins(), $pins ) ), 0, $room ); if ( empty( $add ) ) { return array(); } update_user_meta( $uid, AIEO_DMM::PINS_META, array_merge( $pins, $add ) ); $seeded = get_user_meta( $uid, self::SEEDED_PINS_META, true ); $seeded = is_array( $seeded ) ? array_map( 'sanitize_key', $seeded ) : array(); update_user_meta( $uid, self::SEEDED_PINS_META, array_values( array_unique( array_merge( $seeded, $add ) ) ) ); return $add; } public static function seeded_pins( $uid ) { $uid = (int) $uid; if ( $uid < 1 || ! class_exists( 'AIEO_DMM' ) ) { return array(); } $seeded = get_user_meta( $uid, self::SEEDED_PINS_META, true ); if ( ! is_array( $seeded ) || empty( $seeded ) ) { return array(); } $pins = get_user_meta( $uid, AIEO_DMM::PINS_META, true ); $pins = is_array( $pins ) ? array_map( 'sanitize_key', $pins ) : array(); return array_values( array_intersect( array_map( 'sanitize_key', $seeded ), $pins ) ); } public static function touch_pins( $uid, $ids = null ) { $uid = (int) $uid; if ( $uid < 1 ) { return; } if ( null === $ids ) { delete_user_meta( $uid, self::SEEDED_PINS_META ); return; } $seeded = get_user_meta( $uid, self::SEEDED_PINS_META, true ); if ( ! is_array( $seeded ) || empty( $seeded ) ) { return; } $left = array_values( array_diff( array_map( 'sanitize_key', $seeded ), array_map( 'sanitize_key', (array) $ids ) ) ); if ( empty( $left ) ) { delete_user_meta( $uid, self::SEEDED_PINS_META ); } else { update_user_meta( $uid, self::SEEDED_PINS_META, $left ); } } const REDUCED_META = 'aieo_reduced_mode'; const KEEP_META = 'aieo_reduced_keep'; const REL_CORE = 'core'; const REL_LIKELY = 'likely'; const REL_UNLIKELY = 'unlikely'; const REL_NOT_FOR_YOU = 'not_for_you'; public static function reduced_on( $uid ) { $uid = (int) $uid; return $uid > 0 && '1' === (string) get_user_meta( $uid, self::REDUCED_META, true ) && self::is_answered(); } public static function set_reduced( $uid, $on ) { $uid = (int) $uid; if ( $uid < 1 ) { return; } if ( $on ) { update_user_meta( $uid, self::REDUCED_META, '1' ); } else { delete_user_meta( $uid, self::REDUCED_META ); } } public static function kept( $uid ) { $v = get_user_meta( (int) $uid, self::KEEP_META, true ); return is_array( $v ) ? array_values( array_filter( array_map( 'sanitize_key', $v ) ) ) : array(); } public static function keep( $uid, $id, $keep = true ) { $uid = (int) $uid; $id = sanitize_key( (string) $id ); if ( $uid < 1 || '' === $id ) { return; } $list = array_values( array_diff( self::kept( $uid ), array( $id ) ) ); if ( $keep ) { $list[] = $id; } if ( empty( $list ) ) { delete_user_meta( $uid, self::KEEP_META ); } else { update_user_meta( $uid, self::KEEP_META, array_values( array_unique( $list ) ) ); } } public static function relevance( $id, $group, $section = '', $shape = null, $personas = array() ) { $rel = self::shape_relevance( $id, $group, $section, $shape ); if ( empty( $personas ) || ! is_array( $personas ) || ! class_exists( 'AIEO_Personas' ) ) { return $rel; } return AIEO_Personas::combine( $rel, AIEO_Personas::rank( $id, $group, $section, $personas ) ); } private static function shape_relevance( $id, $group, $section = '', $shape = null ) { $s = is_array( $shape ) ? $shape : self::get(); if ( ! $s ) { return self::REL_LIKELY; } $id = (string) $id; $group = (string) $group; $section = (string) $section; $sell = (string) $s['sell']; $where = (string) $s['where']; $first = (string) $s['first']; if ( in_array( $group, array( 'home', 'tools', 'setup' ), true ) ) { return self::REL_LIKELY; } $physical = in_array( $where, array( 'physical', 'both' ), true ); $marketplace = in_array( $where, array( 'marketplaces', 'both' ), true ); $rel = self::REL_LIKELY; if ( ! $marketplace && ( 'marketplaces' === $section || in_array( $id, array( 'marketplace_feeds', 'marketplace_order_importer', 'marketplace_taxonomy', 'product_exchanger' ), true ) ) ) { $rel = self::REL_NOT_FOR_YOU; } if ( ! $physical && in_array( $id, array( 'branch_scanner', 'stock_recovery', 'missing_hot_sellers', 'missing_hot_sellers_priority', 'store_pages', 'supplier_orders', 'insights_unit_sales', 'insights_reorder_summary' ), true ) ) { $rel = self::REL_UNLIKELY; } if ( 'services' === $sell ) { if ( 'storefront' === $group && in_array( $section, array( 'product_cards', 'pdp' ), true ) ) { $rel = self::REL_UNLIKELY; } if ( ( 'catalog_sync' === $group && 'stock_management' === $section ) || ( 'eshop_insights' === $group && in_array( $section, array( 'stock_management', 'stock_reordering' ), true ) ) ) { $rel = self::REL_NOT_FOR_YOU; } if ( 'pricing_offers' === $group ) { $rel = self::REL_UNLIKELY; } } if ( 'catalogue' === $sell ) { if ( 'storefront' === $group && 'checkout_handling' === $section ) { $rel = self::REL_NOT_FOR_YOU; } if ( 'pricing_offers' === $group || ( 'attribution' === $group && 'costs_roi' === $section ) ) { $rel = self::REL_UNLIKELY; } } if ( 'retail' === $sell && 'role_pricing' === $id ) { $rel = self::REL_UNLIKELY; } if ( 'trade' === $sell && in_array( $id, array( 'brand_gifts', 'loyalty_classes' ), true ) ) { $rel = self::REL_UNLIKELY; } $core = false; switch ( $first ) { case 'get_found': $core = ( 'seo' === $group ) || ( 'promote' === $group && in_array( $section, array( 'health', 'linking', 'tools', 'general' ), true ) ); break; case 'ads': $core = ( 'attribution' === $group ) || in_array( $id, array( 'conversions', 'consent', 'ad_connectors', 'ads', 'conversion_goals' ), true ); break; case 'better_pages': $core = ( 'storefront' === $group ); break; case 'bring_back': $core = ( 'promote' === $group ) || ( 'customer_profiling' === $group && 'reach' === $section ); break; case 'data': $core = ( 'catalog_sync' === $group ) || ( 'tools' === $group && 'external' === $section ); break; } if ( $core && self::REL_LIKELY === $rel ) { $rel = self::REL_CORE; } return (string) apply_filters( 'aieo_shop_shape_relevance', $rel, $id, $group, $section, $s ); } public static function never_hidden( $uid, $active_id = '' ) { static $running = null; if ( null === $running ) { $running = array(); if ( class_exists( 'AIEO_Setup_Checklist' ) ) { foreach ( AIEO_Setup_Checklist::running_now() as $r ) { if ( ! empty( $r['on'] ) && ! empty( $r['tab'] ) ) { $running[ (string) $r['tab'] ] = true; } } } } $out = $running; foreach ( self::kept( $uid ) as $k ) { $out[ $k ] = true; } if ( '' !== (string) $active_id ) { $out[ (string) $active_id ] = true; } return $out; } public static function is_hidden( $id, $group, $section, $uid ) { if ( ! self::reduced_on( $uid ) ) { return false; } $never = self::never_hidden( $uid ); if ( isset( $never[ (string) $id ] ) ) { return false; } return in_array( self::relevance( $id, $group, $section ), array( self::REL_UNLIKELY, self::REL_NOT_FOR_YOU ), true ); } public static function reduce_tree( array $tree, $active_id, $uid ) { $total = 0; $shown = 0; $hidden = array(); if ( ! self::reduced_on( $uid ) ) { foreach ( $tree as $e ) { $total += ( isset( $e['kind'] ) && 'group' === $e['kind'] ) ? count( (array) $e['children'] ) : 1; } return array( 'tree' => $tree, 'shown' => $total, 'total' => $total, 'hidden' => array() ); } $never = self::never_hidden( $uid, $active_id ); $out = array(); foreach ( $tree as $e ) { if ( ! isset( $e['kind'] ) || 'group' !== $e['kind'] ) { $total++; $shown++; $out[] = $e; continue; } $gid = (string) $e['id']; $keep = static function ( $child ) use ( $gid, $never, &$total, &$shown, &$hidden ) { $total++; $cid = (string) $child['id']; $sec = isset( $child['section'] ) ? (string) $child['section'] : ''; $ok = isset( $never[ $cid ] ) || ! in_array( self::relevance( $cid, $gid, $sec ), array( self::REL_UNLIKELY, self::REL_NOT_FOR_YOU ), true ); if ( $ok ) { $shown++; } else { $hidden[] = $cid; } return $ok; }; $e['children'] = array_values( array_filter( (array) $e['children'], $keep ) ); if ( ! empty( $e['sections'] ) ) { $secs = array(); foreach ( (array) $e['sections'] as $sec ) { $sec['children'] = array_values( array_filter( (array) $sec['children'], static function ( $c ) use ( $e ) { foreach ( $e['children'] as $k ) { if ( $k['id'] === $c['id'] ) { return true; } } return false; } ) ); if ( ! empty( $sec['children'] ) ) { $secs[] = $sec; } } $e['sections'] = $secs; if ( empty( $secs ) ) { unset( $e['sections'] ); } } if ( ! empty( $e['children'] ) ) { $out[] = $e; } } return array( 'tree' => $out, 'shown' => $shown, 'total' => $total, 'hidden' => $hidden ); } }