'', 'key' => '', 'token' => '', 'claim' => '', 'email' => '', 'connected_at' => '', ) ); } /** * @param array $options Connection state. */ function sevenbe_save_options( $options ) { update_option( SEVENBE_OPTION, $options, false ); } /** * @param array $args Extra query arguments. * @return string The plugin's admin page. */ function sevenbe_page_url( $args = array() ) { return add_query_arg( $args, admin_url( 'admin.php?page=' . SEVENBE_PAGE ) ); } /** * POST JSON to the 7BE service. * * @param string $path API path. * @param array $body Request body. * @return array Decoded response, or array( 'error' => ... ). */ function sevenbe_api( $path, $body ) { $response = wp_remote_post( SEVENBE_API . $path, array( 'timeout' => 45, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { return array( 'error' => 'network' ); } $data = json_decode( wp_remote_retrieve_body( $response ), true ); return is_array( $data ) ? $data : array( 'error' => 'response' ); } // While a connection is pending, 7BE reads this token from the store's own site to confirm the plugin runs here. // Once a day 7BE asks the same route whether the plugin still runs and is still connected: a connected store // answers with a one-way hash of its 7BE id (never the id or the key), a disconnected one says so. add_action( 'rest_api_init', function () { register_rest_route( 'sevenbe/v1', '/verify', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'callback' => function () { $options = sevenbe_options(); if ( '' !== $options['token'] ) { return array( 'store' => $options['store'], 'token' => $options['token'], ); } if ( '' !== $options['key'] ) { return new WP_Error( 'sevenbe_connected', 'Connected to 7BE.', array( 'status' => 404, 'store' => substr( hash( 'sha256', '7be-store:' . $options['store'] ), 0, 16 ), ) ); } return new WP_Error( 'sevenbe_not_connected', 'Not connected to 7BE.', array( 'status' => 404 ) ); }, ) ); } ); /** * Tell 7BE the store disconnects, so it stops scans, order reads and review requests. Signed with the store's key. * A failure is ignored: 7BE also notices within a few days that the plugin no longer answers. * * @param array $options Connection state with store and key. */ function sevenbe_tell_disconnect( $options ) { if ( '' === $options['key'] || '' === $options['store'] ) { return; } $json = wp_json_encode( array( 'sent' => time() * 1000 ) ); wp_remote_post( SEVENBE_API . '/api/apps/disconnect', array( 'timeout' => 8, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-7BE-Store' => $options['store'], 'X-7BE-Signature' => hash_hmac( 'sha256', $json, $options['key'] ), ), 'body' => $json, ) ); } add_action( 'admin_menu', function () { add_submenu_page( 'woocommerce', __( 'AI Shelf', '7be-ai-shelf' ), __( 'AI Shelf', '7be-ai-shelf' ), 'manage_woocommerce', SEVENBE_PAGE, 'sevenbe_render_page' ); }, 60 ); add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) { array_unshift( $links, '' . esc_html__( 'Open AI Shelf', '7be-ai-shelf' ) . '' ); return $links; } ); /** * Ask 7BE to confirm the pending connection and store the signing key. * * @param array $options Connection state with store and claim. * @return string Empty on success, else an error code. */ function sevenbe_finish_connect( $options ) { $result = sevenbe_api( '/api/apps/verify', array( 'store' => $options['store'], 'claim' => $options['claim'], ) ); if ( empty( $result['key'] ) ) { return isset( $result['error'] ) ? sanitize_key( $result['error'] ) : 'verify'; } $options['key'] = sanitize_text_field( $result['key'] ); $options['token'] = ''; $options['claim'] = ''; $options['connected_at'] = gmdate( 'c' ); sevenbe_save_options( $options ); // Send the last 60 days of orders in the background (WP-Cron), so the dashboard has numbers to show. sevenbe_orders_start(); return ''; } add_action( 'admin_post_sevenbe_connect', function () { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', '7be-ai-shelf' ) ); } check_admin_referer( 'sevenbe_connect' ); $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; if ( ! is_email( $email ) ) { wp_safe_redirect( sevenbe_page_url( array( 'sevenbe_error' => 'email' ) ) ); exit; } $claim = wp_generate_password( 40, false, false ); $start = sevenbe_api( '/api/apps/connect', array( 'platform' => 'woocommerce', 'site' => home_url( '/' ), 'name' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), 'email' => $email, 'claim' => $claim, ) ); if ( empty( $start['store'] ) || empty( $start['token'] ) ) { $code = isset( $start['error'] ) ? sanitize_key( $start['error'] ) : 'connect'; wp_safe_redirect( sevenbe_page_url( array( 'sevenbe_error' => $code ) ) ); exit; } $options = array( 'store' => sanitize_key( $start['store'] ), 'key' => '', 'token' => sanitize_key( $start['token'] ), 'claim' => $claim, 'email' => $email, 'connected_at' => '', ); sevenbe_save_options( $options ); $error = sevenbe_finish_connect( $options ); wp_safe_redirect( sevenbe_page_url( $error ? array( 'sevenbe_error' => $error ) : array( 'sevenbe' => 'connected' ) ) ); exit; } ); add_action( 'admin_post_sevenbe_retry', function () { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', '7be-ai-shelf' ) ); } check_admin_referer( 'sevenbe_retry' ); $options = sevenbe_options(); $error = ( '' === $options['claim'] ) ? 'verify' : sevenbe_finish_connect( $options ); wp_safe_redirect( sevenbe_page_url( $error ? array( 'sevenbe_error' => $error ) : array( 'sevenbe' => 'connected' ) ) ); exit; } ); add_action( 'admin_post_sevenbe_disconnect', function () { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', '7be-ai-shelf' ) ); } check_admin_referer( 'sevenbe_disconnect' ); sevenbe_tell_disconnect( sevenbe_options() ); delete_option( SEVENBE_OPTION ); delete_option( SEVENBE_ORDERS_OPTION ); sevenbe_orders_stop(); sevenbe_feeds_stop(); // Everything 7BE shows in the store stops with the connection: the product blocks, the rating, the feed terms. sevenbe_blocks_forget(); sevenbe_site_verification_forget(); delete_transient( 'sevenbe_summary' ); delete_transient( 'sevenbe_ratings' ); delete_transient( 'sevenbe_feed_config' ); wp_safe_redirect( sevenbe_page_url() ); exit; } ); /** * @param string $code Error code from the plugin or the 7BE service. * @return string What went wrong, in plain words. */ function sevenbe_error_text( $code ) { $errors = array( 'email' => __( 'Enter a valid email address.', '7be-ai-shelf' ), 'network' => __( 'Your store could not reach 7BE. Check that your host allows outgoing connections and try again.', '7be-ai-shelf' ), 'site' => __( '7BE connects stores that answer over HTTPS at a public address.', '7be-ai-shelf' ), 'unreachable' => __( '7BE could not reach your store from the internet. A store behind a password, maintenance mode or a firewall that blocks the REST API cannot be connected yet.', '7be-ai-shelf' ), 'token' => __( '7BE reached your store but could not confirm the connection. A caching plugin may be serving an old copy of the REST API: clear the cache and try again.', '7be-ai-shelf' ), 'claim' => __( '7BE could not match this connection; it may have expired. Click Start over, then connect again.', '7be-ai-shelf' ), 'unknown' => __( '7BE could not match this connection; it may have expired. Click Start over, then connect again.', '7be-ai-shelf' ), 'verify' => __( 'The connection was not finished. Click Connect again.', '7be-ai-shelf' ), ); return isset( $errors[ $code ] ) ? $errors[ $code ] : __( 'Something went wrong while connecting. Please try again.', '7be-ai-shelf' ); } /** * A link to 7BE signed with the store's key, valid for two hours: the dashboard, or the summary the admin page shows. * * @param array $options Connection state. * @param string $path Path on 7BE. * @return string */ function sevenbe_signed_url( $options, $path = '/apps/store' ) { $expires = (string) ( ( time() + 2 * HOUR_IN_SECONDS ) * 1000 ); $back = sevenbe_page_url(); $args = array( 's' => $options['store'], 'e' => $expires, 'r' => $back, 'g' => hash_hmac( 'sha256', $options['store'] . '.' . $expires . '.' . $back, $options['key'] ), ); return SEVENBE_API . $path . '?' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 ); } /** * The store's latest numbers from 7BE (GET /api/apps/summary, signed), cached for ten minutes. * * @param array $options Connection state. * @param bool $fresh Read 7BE now; the cached copy is used only when 7BE cannot be reached. * @return array|null Decoded summary, or null when 7BE could not be reached and nothing is cached. */ function sevenbe_summary( $options, $fresh = false ) { $cached = get_transient( 'sevenbe_summary' ); if ( ! $fresh && is_array( $cached ) ) { return $cached; } $response = wp_remote_get( sevenbe_signed_url( $options, '/api/apps/summary' ), array( 'timeout' => 8, 'headers' => array( 'Accept' => 'application/json' ), ) ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return is_array( $cached ) ? $cached : null; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) || empty( $data['store']['name'] ) ) { return is_array( $cached ) ? $cached : null; } set_transient( 'sevenbe_summary', $data, 10 * MINUTE_IN_SECONDS ); return $data; } /** * @param string|null $iso An ISO 8601 time from 7BE. * @return string The date in the site's format, or an empty string. */ function sevenbe_date( $iso ) { $time = $iso ? strtotime( $iso ) : false; return $time ? wp_date( get_option( 'date_format' ), $time ) : ''; } /** * The connected store's panel: what AI assistants brought the store (the dashboard on 7be.io shows the numbers), * the latest scan's numbers and the button that opens the dashboard. The dashboard opens in a new tab; nothing * from 7be.io is embedded in the admin. * * @param array $options Connection state. */ function sevenbe_render_summary( $options ) { $summary = sevenbe_summary( $options ); echo '

' . esc_html__( 'AI Shelf by 7BE', '7be-ai-shelf' ) . '

'; echo '
'; echo '

' . esc_html__( 'See how much ChatGPT, Gemini, Perplexity and Copilot brought you in the last 60 days', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'Open 7BE dashboard', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( '10% of the sales AI brings you. No subscription, no contract. Every order visible; any disputed order removed.', '7be-ai-shelf' ) . '

'; // 7BE's own answer says whether the paid work runs; while it is unknown, the page keeps the free wording. $on = is_array( $summary ) && isset( $summary['plan']['id'] ) && 'free' !== $summary['plan']['id']; if ( $on ) { echo '

' . esc_html__( 'An order counts when its journey started or ended at an AI assistant, and so does any order of that buyer in the 12 months after. 7BE is on for your store: summaries, questions and answers, feeds, reviews and facts run by themselves. Your orders and bills are in the dashboard.', '7be-ai-shelf' ) . '

'; } else { echo '

' . esc_html__( 'An order counts when its journey started or ended at an AI assistant, and so does any order of that buyer in the 12 months after. Seeing what AI brought your store is free. To start the full work (summaries, questions and answers, feeds, reviews, facts), click Start 7BE in the dashboard.', '7be-ai-shelf' ) . '

'; } echo '

' . esc_html__( 'The dashboard opens on 7be.io in a new tab: the orders ChatGPT, Gemini, Perplexity and Copilot brought you, what they answer shoppers about your products, and your billing.', '7be-ai-shelf' ) . '

'; echo '
'; if ( null === $summary ) { echo '

' . esc_html__( 'Your store is connected, but 7BE could not be reached just now to show your latest numbers.', '7be-ai-shelf' ) . '

'; } else { echo '

' . esc_html( $summary['store']['name'] ) . '

'; $money = isset( $summary['money'] ) && is_array( $summary['money'] ) ? $summary['money'] : null; if ( $money && ! empty( $money['ready'] ) ) { $currency = isset( $money['currency'] ) && is_string( $money['currency'] ) ? sanitize_text_field( $money['currency'] ) : 'USD'; $orders = isset( $money['aiOrders'] ) ? (int) $money['aiOrders'] : 0; echo '

' . esc_html( sprintf( /* translators: 1: revenue from AI assistants, with its currency, 2: number of orders */ _n( 'AI assistants brought you %1$s in the last 60 days, from %2$s order.', 'AI assistants brought you %1$s in the last 60 days, from %2$s orders.', $orders, '7be-ai-shelf' ), sevenbe_money_text( isset( $money['aiNet'] ) ? (float) $money['aiNet'] : 0.0, $currency ), number_format_i18n( $orders ) ) ) . '

'; } elseif ( $money ) { echo '

' . esc_html__( '7BE is reading your orders of the last 60 days. Your report appears in the dashboard in a few minutes.', '7be-ai-shelf' ) . '

'; } $latest = isset( $summary['latest'] ) && is_array( $summary['latest'] ) ? $summary['latest'] : null; if ( $latest ) { echo ''; } if ( ! empty( $summary['scan']['running'] ) ) { echo '

' . esc_html__( 'A scan is under way. The dashboard shows the answers as they come in.', '7be-ai-shelf' ) . '

'; } elseif ( ! $latest ) { echo '

' . esc_html__( 'Your first scan has not finished yet. The dashboard shows it as soon as it does.', '7be-ai-shelf' ) . '

'; } $dates = array(); if ( ! empty( $summary['scan']['lastAt'] ) ) { /* translators: %s: date */ $dates[] = sprintf( __( 'Last scan: %s.', '7be-ai-shelf' ), sevenbe_date( $summary['scan']['lastAt'] ) ); } if ( ! empty( $summary['scan']['nextAutoAt'] ) ) { /* translators: %s: date */ $dates[] = sprintf( __( 'Next automatic scan: %s.', '7be-ai-shelf' ), sevenbe_date( $summary['scan']['nextAutoAt'] ) ); } if ( ! empty( $summary['reviews']['count'] ) ) { /* translators: 1: number of reviews, 2: average rating */ $dates[] = sprintf( __( 'Verified reviews: %1$s, average %2$s.', '7be-ai-shelf' ), number_format_i18n( (int) $summary['reviews']['count'] ), number_format_i18n( (float) $summary['reviews']['average'], 1 ) ); } if ( $dates ) { echo '

' . esc_html( implode( ' ', $dates ) ) . '

'; } } echo '
'; } /** * @param float $amount An amount of money. * @param string $currency Its currency code. * @return string For example $1,234.50, or 1.234,50 EUR. */ function sevenbe_money_text( $amount, $currency ) { $currency = '' !== $currency ? $currency : 'USD'; $number = number_format_i18n( (float) $amount, 2 ); return 'USD' === $currency ? '$' . $number : $number . ' ' . $currency; } /** * The step before disconnecting: what AI brought the store through 7BE in the last 3 months, and what stops. * Nothing changes here; the button posts to the disconnect action (nonce, capability check). * * @param array $options Connection state. */ function sevenbe_render_disconnect_confirm( $options ) { // The owner decides here, so the amount is read from 7BE now, not from the 10-minute copy. $summary = sevenbe_summary( $options, true ); $money = is_array( $summary ) && isset( $summary['money'] ) && is_array( $summary['money'] ) ? $summary['money'] : array(); $last90 = isset( $money['last90AiNet'] ) && is_numeric( $money['last90AiNet'] ) ? (float) $money['last90AiNet'] : 0.0; echo '

' . esc_html__( 'Disconnect from 7BE?', '7be-ai-shelf' ) . '

'; echo '
'; echo '

'; if ( $last90 > 0 ) { $currency = isset( $money['currency'] ) && is_string( $money['currency'] ) ? sanitize_text_field( $money['currency'] ) : 'USD'; echo esc_html( /* translators: %s: revenue from AI assistants in the last 3 months, with its currency */ sprintf( __( 'In the last 3 months AI brought you %s through 7BE.', '7be-ai-shelf' ), sevenbe_money_text( $last90, $currency ) ) ) . ' '; } echo esc_html__( 'After disconnecting, the 7BE blocks, rating and feeds stop.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( '7BE stops reading your orders and running scans for this store. Its summaries, questions and answers and its rating leave your product pages, and your feeds stop. Product descriptions you applied from 7BE earlier stay as they are.', '7be-ai-shelf' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_disconnect' ); submit_button( __( 'Disconnect from 7BE', '7be-ai-shelf' ), 'secondary', 'submit', false ); echo '
'; echo '' . esc_html__( 'Keep 7BE connected', '7be-ai-shelf' ) . ''; echo '
'; } /** * The AI Shelf admin page: connect the store, then its summary with a link to the 7BE dashboard. */ function sevenbe_render_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $options = sevenbe_options(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display-only flags from our own redirects. $error = isset( $_GET['sevenbe_error'] ) ? sanitize_key( wp_unslash( $_GET['sevenbe_error'] ) ) : ''; $notice = isset( $_GET['sevenbe'] ) ? sanitize_key( wp_unslash( $_GET['sevenbe'] ) ) : ''; $confirm = isset( $_GET['sevenbe_confirm'] ) ? sanitize_key( wp_unslash( $_GET['sevenbe_confirm'] ) ) : ''; // phpcs:enable echo '
'; if ( '' !== $options['key'] ) { // Disconnecting takes a second step: this page says what stops, and only its button disconnects. if ( 'disconnect' === $confirm ) { sevenbe_render_disconnect_confirm( $options ); echo '
'; return; } if ( 'connected' === $notice ) { echo '

' . esc_html__( 'Your store is connected. 7BE is reading the last 60 days of your orders in the background; your first numbers appear in a few minutes.', '7be-ai-shelf' ) . '

'; } if ( 'fixes' === $notice ) { echo '

' . esc_html__( 'Saved. The next scan checks the result.', '7be-ai-shelf' ) . '

'; } // Start 7BE in the dashboard (Stripe), or plugins before 0.9.2 coming back here from it. $plan_notices = array( 'upgraded' => array( 'success', __( 'Thank you. 7BE is on: the work on your store starts now, and the dashboard shows it.', '7be-ai-shelf' ) ), 'cancelled' => array( 'info', __( 'Nothing changed: Start 7BE was not finished. You can start it from the dashboard at any time.', '7be-ai-shelf' ) ), 'unavailable' => array( 'warning', __( 'Billing is not available right now. Please try again later from the dashboard.', '7be-ai-shelf' ) ), 'changed' => array( 'info', __( '7BE is already on for your store.', '7be-ai-shelf' ) ), ); if ( isset( $plan_notices[ $notice ] ) ) { delete_transient( 'sevenbe_summary' ); echo '

' . esc_html( $plan_notices[ $notice ][1] ) . '

'; } if ( 'feed-sent' === $notice ) { echo '

' . esc_html__( 'Your catalog is on its way to 7BE. A big catalog goes a few pages a minute.', '7be-ai-shelf' ) . '

'; } sevenbe_render_summary( $options ); sevenbe_render_blocks(); sevenbe_render_fixes(); sevenbe_render_reviews(); sevenbe_render_feeds(); echo '

' . esc_html__( 'Disconnect from 7BE', '7be-ai-shelf' ) . '

'; return; } echo '

' . esc_html__( 'AI Shelf by 7BE', '7be-ai-shelf' ) . '

'; if ( $error ) { echo '

' . esc_html( sevenbe_error_text( $error ) ) . '

'; } if ( '' !== $options['claim'] ) { // Connected on 7BE's side but not confirmed yet. echo '

' . esc_html__( 'Finish connecting your store', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( '7BE needs to reach your store once to confirm the connection.', '7be-ai-shelf' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_retry' ); submit_button( __( 'Try again', '7be-ai-shelf' ), 'primary', 'submit', false ); echo '
'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_disconnect' ); submit_button( __( 'Start over', '7be-ai-shelf' ), 'secondary', 'submit', false ); echo '
'; return; } $user = wp_get_current_user(); $email = $options['email'] ? $options['email'] : $user->user_email; echo '
'; echo '

' . esc_html__( 'See how much ChatGPT, Gemini, Perplexity and Copilot send you', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( '7BE brings you shoppers from ChatGPT, Gemini, Perplexity and Copilot. Connect your store, free, and see what they brought you in the last 60 days.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'The 7BE service is 10% of the sales AI brings you. No subscription, no contract. Every order visible; any disputed order removed. Connecting is free, and so is seeing the last 60 days.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'An order counts when its journey started or ended at an AI assistant, and so does any order of that buyer in the 12 months after. Any store can start: once connected, click Start 7BE in the dashboard and 7BE does the full work (summaries, questions and answers, feeds, reviews, facts).', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'What 7BE shows on your product pages is part of the running service. It shows only while this plugin is active and your store is connected, nothing is saved into your products, and it disappears when you disconnect or deactivate the plugin.', '7be-ai-shelf' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_connect' ); echo '


'; echo '

'; submit_button( __( 'Connect store and see the last 60 days', '7be-ai-shelf' ) ); echo '
'; echo '

' . esc_html__( '7BE reads each order\'s total, where its visit came from (WooCommerce order attribution) and a one-way hash of the buyer\'s email, to know a returning buyer. No buyer names, emails or addresses.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'Connecting also sends 7BE your store address, store name and this email. 7BE reads your public catalog and product pages, like a shopper would. Buyer names and emails leave your store only while verified reviews are on: they turn on when you start 7BE, and you can turn them off at any time.', '7be-ai-shelf' ) . ' ' . esc_html__( 'Privacy', '7be-ai-shelf' ) . ' ยท ' . esc_html__( 'Terms', '7be-ai-shelf' ) . '

'; echo '
'; // Fixes keep working without a connection; the panel stays so they can be switched off. if ( in_array( true, sevenbe_fixes(), true ) ) { sevenbe_render_fixes(); } echo ''; }