... ). */ function sevenbe_signed_api( $body, $blocking = true, $path = '/api/reviews/orders' ) { $options = sevenbe_options(); if ( '' === $options['key'] ) { return array( 'error' => 'connect' ); } $body['sent'] = time() * 1000; $json = wp_json_encode( $body ); $response = wp_remote_post( SEVENBE_API . $path, array( 'timeout' => $blocking ? 45 : 5, 'blocking' => $blocking, '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, ) ); if ( ! $blocking ) { return array(); } 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' ); } /** * The orders completed in the last 90 days (up to 500, newest first). * * @return WC_Order[] */ function sevenbe_backfill_orders() { return wc_get_orders( array( 'status' => 'completed', 'date_completed' => '>' . ( time() - SEVENBE_BACKFILL_DAYS * DAY_IN_SECONDS ), 'limit' => 500, 'orderby' => 'date', 'order' => 'DESC', ) ); } /** * How many buyers turning reviews on would ask: one per email in the last 90 days' orders. * * @return int[] array( orders, buyers ). */ function sevenbe_backfill_count() { $emails = array(); $orders = sevenbe_backfill_orders(); foreach ( $orders as $order ) { $email = strtolower( (string) $order->get_billing_email() ); if ( is_email( $email ) ) { $emails[ $email ] = true; } } return array( count( $orders ), count( $emails ) ); } /** * What 7BE needs to ask the buyer of one order for a review. * * @param WC_Order $order The order. * @return array|null Null when the order has no buyer email. */ function sevenbe_order_payload( $order ) { $email = $order->get_billing_email(); if ( ! is_email( $email ) ) { return null; } $date = $order->get_date_completed() ? $order->get_date_completed() : $order->get_date_created(); $products = array(); foreach ( $order->get_items() as $item ) { if ( count( $products ) >= 6 ) { break; } $product = is_callable( array( $item, 'get_product' ) ) ? $item->get_product() : null; $row = array( 'name' => wp_strip_all_tags( $item->get_name() ) ); if ( $product ) { $row['url'] = get_permalink( $product->get_parent_id() ? $product->get_parent_id() : $product->get_id() ); if ( $product->get_sku() ) { $row['sku'] = $product->get_sku(); } // WooCommerce 9.2+ keeps the GTIN, UPC, EAN or ISBN in its own field. if ( is_callable( array( $product, 'get_global_unique_id' ) ) && $product->get_global_unique_id() ) { $row['gtin'] = $product->get_global_unique_id(); } } $products[] = $row; } return array( 'order' => (string) $order->get_order_number(), 'orderedAt' => $date ? $date->date( 'c' ) : gmdate( 'c' ), 'email' => $email, 'name' => trim( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ), 'products' => $products, ); } // A completed order goes to 7BE without slowing the admin down (the request does not wait). add_action( 'woocommerce_order_status_completed', function ( $order_id ) { if ( ! sevenbe_reviews_on() ) { return; } $order = wc_get_order( $order_id ); $payload = $order ? sevenbe_order_payload( $order ) : null; if ( $payload ) { sevenbe_signed_api( array( 'orders' => array( $payload ) ), false ); } } ); /** * Follow what 7BE says about verified reviews (the blocks answer's `reviews`, see sevenbe_follow_server()). On: the * plugin turns them on the way the button does, with the orders completed in the last 90 days, in the background. * Off: the plugin stops sending orders. The owner's button keeps working either way; 7BE records its choice. * * @param bool $on Whether 7BE runs review requests for this store. */ function sevenbe_reviews_follow( $on ) { if ( $on && ! sevenbe_reviews_on() ) { if ( ! wp_next_scheduled( 'sevenbe_reviews_auto_on' ) ) { wp_schedule_single_event( time() + 5, 'sevenbe_reviews_auto_on' ); } } elseif ( ! $on && sevenbe_reviews_on() ) { update_option( SEVENBE_REVIEWS_OPTION, array( 'on' => false ), false ); } } /** * WP-Cron: 7BE turned verified reviews on when the owner started it. The same request as the button: on, with * the last 90 days of completed orders (7BE asks each buyer once). If 7BE cannot be reached, the next read of * the blocks schedules it again. */ function sevenbe_reviews_auto_on() { if ( sevenbe_reviews_on() || '' === sevenbe_options()['key'] || ! function_exists( 'wc_get_orders' ) ) { return; } $result = sevenbe_signed_api( array( 'enable' => true, 'orders' => array_values( array_filter( array_map( 'sevenbe_order_payload', sevenbe_backfill_orders() ) ) ), ) ); if ( ! empty( $result['on'] ) ) { update_option( SEVENBE_REVIEWS_OPTION, array( 'on' => true, 'auto' => true, ), false ); } } add_action( 'sevenbe_reviews_auto_on', 'sevenbe_reviews_auto_on' ); add_action( 'admin_post_sevenbe_reviews', 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_reviews' ); $on = ! empty( $_POST['on'] ); if ( $on && empty( $_POST['rules'] ) ) { wp_safe_redirect( sevenbe_page_url( array( 'sevenbe' => 'reviews-rules' ) ) . '#sevenbe-reviews' ); exit; } $body = array( 'enable' => $on ); if ( $on ) { // The orders completed in the last 90 days, so the first reviews do not wait for new orders. $body['orders'] = array_values( array_filter( array_map( 'sevenbe_order_payload', sevenbe_backfill_orders() ) ) ); } $result = sevenbe_signed_api( $body ); $saved = isset( $result['on'] ) && (bool) $result['on'] === $on; if ( $saved ) { update_option( SEVENBE_REVIEWS_OPTION, array( 'on' => $on ), false ); } $notice = $saved ? ( $on ? 'reviews-on' : 'reviews-off' ) : 'reviews-error'; wp_safe_redirect( sevenbe_page_url( array( 'sevenbe' => $notice ) ) . '#sevenbe-reviews' ); exit; } ); /** * @return bool Whether the merchant chose to show the store's own product reviews on 7BE. */ function sevenbe_import_on() { $state = get_option( SEVENBE_IMPORT_OPTION, array() ); return ! empty( $state['on'] ); } /** * One WooCommerce product review as 7BE imports it: no email, no IP, the name as the shop shows it. * * @param WP_Comment $comment An approved review. * @return array|null Null when the review has no 1–5 rating. */ function sevenbe_review_payload( $comment ) { $rating = (int) get_comment_meta( $comment->comment_ID, 'rating', true ); if ( $rating < 1 || $rating > 5 ) { return null; } $product = wc_get_product( $comment->comment_post_ID ); $row = array( 'rating' => $rating, 'text' => wp_strip_all_tags( $comment->comment_content ), 'name' => wp_strip_all_tags( $comment->comment_author ), 'date' => mysql2date( 'c', $comment->comment_date_gmt ? $comment->comment_date_gmt . ' +0000' : $comment->comment_date ), 'verifiedBuyer' => (bool) get_comment_meta( $comment->comment_ID, 'verified', true ), ); if ( $product ) { $row['product'] = array( 'name' => wp_strip_all_tags( $product->get_name() ), 'url' => get_permalink( $product->get_id() ), ); if ( $product->get_sku() ) { $row['product']['sku'] = $product->get_sku(); } if ( is_callable( array( $product, 'get_global_unique_id' ) ) && $product->get_global_unique_id() ) { $row['product']['gtin'] = $product->get_global_unique_id(); } } return $row; } /** * Send every approved product review, 200 at a time; the first batch replaces what 7BE had. * * @return bool Whether every batch went through. */ function sevenbe_send_all_reviews() { $page = 0; $start = true; do { $comments = get_comments( array( 'type' => 'review', 'status' => 'approve', 'number' => 200, 'offset' => $page * 200, 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ) ); $fetched = count( $comments ); $rows = array_values( array_filter( array_map( 'sevenbe_review_payload', $comments ) ) ); $body = array( 'reviews' => $rows ); if ( $start ) { $body['start'] = true; } $result = sevenbe_signed_api( $body, true, '/api/reviews/import' ); if ( isset( $result['error'] ) ) { return false; } $start = false; ++$page; } while ( 200 === $fetched && $page < 25 ); return true; } // A review approved later goes to 7BE as it is approved (the request does not wait). add_action( 'wp_set_comment_status', function ( $comment_id, $status ) { if ( 'approve' !== $status || ! sevenbe_import_on() ) { return; } $comment = get_comment( $comment_id ); $row = ( $comment && 'review' === $comment->comment_type ) ? sevenbe_review_payload( $comment ) : null; if ( $row ) { sevenbe_signed_api( array( 'reviews' => array( $row ) ), false, '/api/reviews/import' ); } }, 10, 2 ); // Once a day the whole set again, so edited and deleted reviews follow on 7BE. add_action( 'sevenbe_reviews_resync', function () { if ( sevenbe_import_on() ) { sevenbe_send_all_reviews(); } } ); add_action( 'admin_post_sevenbe_import', 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_import' ); $on = ! empty( $_POST['import'] ); if ( $on ) { $saved = sevenbe_send_all_reviews(); if ( $saved && ! wp_next_scheduled( 'sevenbe_reviews_resync' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'sevenbe_reviews_resync' ); } } else { $result = sevenbe_signed_api( array( 'off' => true ), true, '/api/reviews/import' ); $saved = ! isset( $result['error'] ); wp_clear_scheduled_hook( 'sevenbe_reviews_resync' ); } if ( $saved ) { update_option( SEVENBE_IMPORT_OPTION, array( 'on' => $on ), false ); } $notice = $saved ? ( $on ? 'import-on' : 'import-off' ) : 'reviews-error'; wp_safe_redirect( sevenbe_page_url( array( 'sevenbe' => $notice ) ) . '#sevenbe-reviews' ); exit; } ); /** * The verified reviews panel on the plugin page (connected stores only). */ function sevenbe_render_reviews() { $on = sevenbe_reviews_on(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only flag from our own redirect. $notice = isset( $_GET['sevenbe'] ) ? sanitize_key( wp_unslash( $_GET['sevenbe'] ) ) : ''; $messages = array( 'reviews-on' => __( 'Verified reviews are on. Your orders from the last 90 days were sent; each buyer is asked once, and new orders a few days after each order.', '7be-ai-shelf' ), 'import-on' => __( 'Your product reviews are on your 7BE profile, marked as imported from WooCommerce.', '7be-ai-shelf' ), 'import-off' => __( 'Your product reviews are taken off your 7BE profile.', '7be-ai-shelf' ), 'reviews-off' => __( 'Verified reviews are off. No more orders are sent to 7BE.', '7be-ai-shelf' ), 'reviews-rules' => __( 'Please accept the review rules to turn reviews on.', '7be-ai-shelf' ), 'reviews-error' => __( '7BE could not be reached. Nothing was changed; please try again.', '7be-ai-shelf' ), ); echo '
'; echo '

' . esc_html__( 'Verified reviews', '7be-ai-shelf' ) . ' '; echo $on ? '' . esc_html__( 'On', '7be-ai-shelf' ) . '' : '' . esc_html__( 'Off', '7be-ai-shelf' ) . ''; echo '

'; if ( isset( $messages[ $notice ] ) ) { echo '

' . esc_html( $messages[ $notice ] ) . '

'; } $state = get_option( SEVENBE_REVIEWS_OPTION, array() ); if ( $on && ! empty( $state['auto'] ) ) { echo '

' . esc_html__( '7BE turned them on when you started 7BE. You can turn them off here at any time.', '7be-ai-shelf' ) . '

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

' . esc_html__( 'They turn on by themselves when you start 7BE in the dashboard, or now with the button below.', '7be-ai-shelf' ) . '

'; } echo '

' . esc_html__( 'Reviews only from real orders. When an order is completed, 7BE emails the buyer one review request a few days later, and one reminder. Reviews go on your public profile on 7BE, where shoppers and AI assistants read them, and you can show them on your site.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'When on, the plugin sends 7BE each completed order: its number, date and products, and the buyer\'s email and name. 7BE uses them only for the review request and deletes the email and name 120 days later.', '7be-ai-shelf' ) . ' ' . esc_html__( 'How it works', '7be-ai-shelf' ) . ' · ' . esc_html__( 'Privacy', '7be-ai-shelf' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_reviews' ); if ( ! $on ) { list( $order_count, $buyer_count ) = sevenbe_backfill_count(); echo ''; if ( $buyer_count > 0 ) { /* translators: 1: number of buyers, 2: number of orders, 3: number of days. */ echo '

' . esc_html( sprintf( _n( 'Turning on also asks %1$d buyer from %2$d orders completed in the last %3$d days.', 'Turning on also asks %1$d buyers from %2$d orders completed in the last %3$d days.', $buyer_count, '7be-ai-shelf' ), $buyer_count, $order_count, SEVENBE_BACKFILL_DAYS ) ) . ' ' . esc_html__( 'Each buyer gets one request and one reminder, at most 100 requests a day.', '7be-ai-shelf' ) . '

'; } echo '

'; } submit_button( $on ? __( 'Turn off verified reviews', '7be-ai-shelf' ) : __( 'Turn on verified reviews', '7be-ai-shelf' ), $on ? 'secondary' : 'primary', 'submit', false ); echo '
'; // The store's own product reviews on its 7BE profile: opt-in, apart from the verified ones. $import = sevenbe_import_on(); echo '

' . esc_html__( 'Your product reviews on 7BE', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'Show the product reviews your shop already has on your 7BE profile, so it is not empty while verified reviews come in. They are marked "Imported from WooCommerce" and never count in your verified rating.', '7be-ai-shelf' ) . '

'; echo '

' . esc_html__( 'Sent: approved reviews with their rating, words, product and the reviewer\'s name as your shop shows it (7BE shortens it to "Anna B."). Never the reviewer\'s email or IP.', '7be-ai-shelf' ) . '

'; echo '
'; echo ''; wp_nonce_field( 'sevenbe_import' ); echo '

'; submit_button( __( 'Save', '7be-ai-shelf' ), 'secondary', 'sevenbe-import-save', false ); echo '
'; }