From 60c07013d40f1fad70e6a8a46c2b89f24e9d7839 Mon Sep 17 00:00:00 2001 From: raicem Date: Thu, 17 Aug 2023 11:54:51 +0300 Subject: [PATCH] Marketplace: Cache Discover page content We were hitting the WCCOM API directly from the front end. However, that limits of in terms of caching and reducing the load on WCCOM. To prevent that, we added a REST API endpoint. This endpoint fetches discover page content from WCCOM and puts in a transient. This is actually how the page works in the previous version. So we were able to reuse a lot of the code. --- .../components/discover/discover.tsx | 18 +++++--- .../product-list-content/no-results.tsx | 4 +- .../contexts/product-list-context.tsx | 18 +++----- .../client/marketplace/utils/functions.tsx | 42 +++++++++++-------- .../includes/admin/class-wc-admin-addons.php | 29 +++++++++---- .../admin/helper/class-wc-helper-admin.php | 41 ++++++++++++++++++ 6 files changed, 108 insertions(+), 44 deletions(-) diff --git a/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx b/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx index b16ecb68098..52842d3625f 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/discover/discover.tsx @@ -8,21 +8,29 @@ import { useEffect, useState } from '@wordpress/element'; */ import ProductList from '../product-list/product-list'; import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions'; +import ProductLoader from '../product-loader/product-loader'; import './discover.scss'; export default function Discover(): JSX.Element | null { const [ productGroups, setProductGroups ] = useState< Array< ProductGroup > >( [] ); + const [ isLoading, setIsLoading ] = useState( false ); useEffect( () => { - fetchDiscoverPageData().then( ( products: Array< ProductGroup > ) => { - setProductGroups( products ); - } ); + setIsLoading( true ); + + fetchDiscoverPageData() + .then( ( products: Array< ProductGroup > ) => { + setProductGroups( products ); + } ) + .finally( () => { + setIsLoading( false ); + } ); }, [] ); - if ( ! productGroups.length ) { - return null; + if ( isLoading ) { + return ; } const groupsList = productGroups.flatMap( ( group ) => group ); diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx index 10285383534..63a69d81407 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/no-results.tsx @@ -25,6 +25,8 @@ export default function NoResults(): JSX.Element { useEffect( () => { if ( query.term ) { setNoResultsTerm( query.term ); + + return; } if ( query.category ) { @@ -42,7 +44,7 @@ export default function NoResults(): JSX.Element { setisLoadingProductGroup( true ); fetchDiscoverPageData() - .then( ( products: Array< ProductGroup > ) => { + .then( ( products: ProductGroup[] ) => { const mostPopularGroup = products.find( ( group ) => group.id === 'most-popular' ); diff --git a/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx b/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx index bdbdb14da56..9104602391d 100644 --- a/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx +++ b/plugins/woocommerce-admin/client/marketplace/contexts/product-list-context.tsx @@ -23,15 +23,9 @@ export const ProductListContext = createContext< ProductListContextType >( { isLoading: false, } ); -type ProductListContextProviderProps = { +export function ProductListContextProvider( props: { children: JSX.Element; - country?: string; - locale?: string; -}; - -export function ProductListContextProvider( - props: ProductListContextProviderProps -): JSX.Element { +} ): JSX.Element { const [ isLoading, setIsLoading ] = useState( false ); const [ productList, setProductList ] = useState< Product[] >( [] ); @@ -47,9 +41,9 @@ export function ProductListContextProvider( const params = new URLSearchParams(); - params.append( 'term', query.term ?? '' ); - params.append( 'country', props.country ?? '' ); - params.append( 'locale', props.locale ?? '' ); + if ( query.term ) { + params.append( 'term', query.term ); + } if ( query.category ) { params.append( 'category', query.category ); @@ -94,7 +88,7 @@ export function ProductListContextProvider( .finally( () => { setIsLoading( false ); } ); - }, [ query, props.country, props.locale ] ); + }, [ query ] ); return ( diff --git a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx index 8695d3e9403..39b9d39d828 100644 --- a/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/utils/functions.tsx @@ -1,9 +1,15 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + /** * Internal dependencies */ import { Product } from '../components/product-list/types'; import { MARKETPLACE_URL } from '../components/constants'; import { CategoryAPIItem } from '../components/category-selector/types'; +import { LOCALE } from '../../utils/admin-settings'; interface ProductGroup { id: string; @@ -13,26 +19,28 @@ interface ProductGroup { } // Fetch data for the discover page from the WooCommerce.com API -const fetchDiscoverPageData = async (): Promise< Array< ProductGroup > > => { - const fetchUrl = MARKETPLACE_URL + '/wp-json/wccom-extensions/2.0/featured'; +async function fetchDiscoverPageData(): Promise< ProductGroup[] > { + let url = '/wc/v3/marketplace/featured'; - return fetch( fetchUrl ) - .then( ( response ) => { - if ( ! response.ok ) { - throw new Error( response.statusText ); - } - return response.json(); - } ) - .then( ( json ) => { - return json; - } ) - .catch( () => { - return []; - } ); -}; + if ( LOCALE.userLocale ) { + url = `${ url }?locale=${ LOCALE.userLocale }`; + } + + try { + return await apiFetch( { path: url.toString() } ); + } catch ( error ) { + return []; + } +} function fetchCategories(): Promise< CategoryAPIItem[] > { - return fetch( MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories' ) + let url = MARKETPLACE_URL + '/wp-json/wccom-extensions/1.0/categories'; + + if ( LOCALE.userLocale ) { + url = `${ url }?locale=${ LOCALE.userLocale }`; + } + + return fetch( url.toString() ) .then( ( response ) => { if ( ! response.ok ) { throw new Error( response.statusText ); diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php index 41b0accf525..6f00b9dbfa0 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php @@ -64,8 +64,24 @@ class WC_Admin_Addons { * @return void */ public static function render_featured() { + $featured = self::fetch_featured(); + + if ( is_wp_error( $featured ) ) { + self::output_empty( $featured->get_error_message() ); + } + + self::output_featured( $featured ); + } + + /** + * Fetch featured products from WCCOM's the Featured 2.0 Endpoint and cache the data for a day. + * + * @return array|WP_Error + */ + public static function fetch_featured() { $locale = get_user_locale(); $featured = self::get_locale_data_from_transient( 'wc_addons_featured', $locale ); + if ( false === $featured ) { $headers = array(); $auth = WC_Helper_Options::get( 'auth' ); @@ -96,9 +112,7 @@ class WC_Admin_Addons { ? __( 'We encountered an SSL error. Please ensure your site supports TLS version 1.2 or above.', 'woocommerce' ) : $raw_featured->get_error_message(); - self::output_empty( $message ); - - return; + return new WP_Error( 'wc-addons-connection-error', $message ); } $response_code = (int) wp_remote_retrieve_response_code( $raw_featured ); @@ -117,18 +131,15 @@ class WC_Admin_Addons { $response_code ); - self::output_empty( $message ); - - return; + return new WP_Error( 'wc-addons-connection-error', $message ); } $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); if ( empty( $featured ) || ! is_array( $featured ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); $message = __( 'Our request to the featured API got a malformed response.', 'woocommerce' ); - self::output_empty( $message ); - return; + return new WP_Error( 'wc-addons-connection-error', $message ); } if ( $featured ) { @@ -136,7 +147,7 @@ class WC_Admin_Addons { } } - self::output_featured( $featured ); + return $featured; } /** diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php index f45fa4c370c..0d1484be925 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php @@ -25,6 +25,7 @@ class WC_Helper_Admin { */ public static function load() { add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_marketplace_settings' ) ); + add_filter( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) ); } /** @@ -82,6 +83,46 @@ class WC_Helper_Admin { return $connect_url; } + + /** + * Registers the REST routes for the featured products endpoint. + * This endpoint is used by the WooCommerce > Extensions > Discover + * page. + */ + public static function register_rest_routes() { + register_rest_route( + 'wc/v3', + '/marketplace/featured', + array( + 'methods' => 'GET', + 'callback' => array( __CLASS__, 'get_featured' ), + 'permission_callback' => array( __CLASS__, 'get_permission' ), + ) + ); + } + + /** + * The Extensions page can only be accessed by users with the manage_woocommerce + * capability. So the API mimics that behavior. + */ + public static function get_permission() { + return current_user_can( 'manage_woocommerce' ); + } + + /** + * Fetch featured procucts from WooCommerce.com and serve them + * as JSON. + */ + public static function get_featured() { + $featured = WC_Admin_Addons::fetch_featured(); + + if ( is_wp_error( $featured ) ) { + wp_send_json_error( array( 'message' => $featured->get_error_message() ) ); + } + + wp_send_json( $featured ); + } + } WC_Helper_Admin::load();