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.
This commit is contained in:
raicem 2023-08-17 11:54:51 +03:00
parent c42e6bcc5b
commit 60c07013d4
6 changed files with 108 additions and 44 deletions

View File

@ -8,21 +8,29 @@ import { useEffect, useState } from '@wordpress/element';
*/ */
import ProductList from '../product-list/product-list'; import ProductList from '../product-list/product-list';
import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions'; import { fetchDiscoverPageData, ProductGroup } from '../../utils/functions';
import ProductLoader from '../product-loader/product-loader';
import './discover.scss'; import './discover.scss';
export default function Discover(): JSX.Element | null { export default function Discover(): JSX.Element | null {
const [ productGroups, setProductGroups ] = useState< const [ productGroups, setProductGroups ] = useState<
Array< ProductGroup > Array< ProductGroup >
>( [] ); >( [] );
const [ isLoading, setIsLoading ] = useState( false );
useEffect( () => { useEffect( () => {
fetchDiscoverPageData().then( ( products: Array< ProductGroup > ) => { setIsLoading( true );
setProductGroups( products );
} ); fetchDiscoverPageData()
.then( ( products: Array< ProductGroup > ) => {
setProductGroups( products );
} )
.finally( () => {
setIsLoading( false );
} );
}, [] ); }, [] );
if ( ! productGroups.length ) { if ( isLoading ) {
return null; return <ProductLoader />;
} }
const groupsList = productGroups.flatMap( ( group ) => group ); const groupsList = productGroups.flatMap( ( group ) => group );

View File

@ -25,6 +25,8 @@ export default function NoResults(): JSX.Element {
useEffect( () => { useEffect( () => {
if ( query.term ) { if ( query.term ) {
setNoResultsTerm( query.term ); setNoResultsTerm( query.term );
return;
} }
if ( query.category ) { if ( query.category ) {
@ -42,7 +44,7 @@ export default function NoResults(): JSX.Element {
setisLoadingProductGroup( true ); setisLoadingProductGroup( true );
fetchDiscoverPageData() fetchDiscoverPageData()
.then( ( products: Array< ProductGroup > ) => { .then( ( products: ProductGroup[] ) => {
const mostPopularGroup = products.find( const mostPopularGroup = products.find(
( group ) => group.id === 'most-popular' ( group ) => group.id === 'most-popular'
); );

View File

@ -23,15 +23,9 @@ export const ProductListContext = createContext< ProductListContextType >( {
isLoading: false, isLoading: false,
} ); } );
type ProductListContextProviderProps = { export function ProductListContextProvider( props: {
children: JSX.Element; children: JSX.Element;
country?: string; } ): JSX.Element {
locale?: string;
};
export function ProductListContextProvider(
props: ProductListContextProviderProps
): JSX.Element {
const [ isLoading, setIsLoading ] = useState( false ); const [ isLoading, setIsLoading ] = useState( false );
const [ productList, setProductList ] = useState< Product[] >( [] ); const [ productList, setProductList ] = useState< Product[] >( [] );
@ -47,9 +41,9 @@ export function ProductListContextProvider(
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append( 'term', query.term ?? '' ); if ( query.term ) {
params.append( 'country', props.country ?? '' ); params.append( 'term', query.term );
params.append( 'locale', props.locale ?? '' ); }
if ( query.category ) { if ( query.category ) {
params.append( 'category', query.category ); params.append( 'category', query.category );
@ -94,7 +88,7 @@ export function ProductListContextProvider(
.finally( () => { .finally( () => {
setIsLoading( false ); setIsLoading( false );
} ); } );
}, [ query, props.country, props.locale ] ); }, [ query ] );
return ( return (
<ProductListContext.Provider value={ contextValue }> <ProductListContext.Provider value={ contextValue }>

View File

@ -1,9 +1,15 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { Product } from '../components/product-list/types'; import { Product } from '../components/product-list/types';
import { MARKETPLACE_URL } from '../components/constants'; import { MARKETPLACE_URL } from '../components/constants';
import { CategoryAPIItem } from '../components/category-selector/types'; import { CategoryAPIItem } from '../components/category-selector/types';
import { LOCALE } from '../../utils/admin-settings';
interface ProductGroup { interface ProductGroup {
id: string; id: string;
@ -13,26 +19,28 @@ interface ProductGroup {
} }
// Fetch data for the discover page from the WooCommerce.com API // Fetch data for the discover page from the WooCommerce.com API
const fetchDiscoverPageData = async (): Promise< Array< ProductGroup > > => { async function fetchDiscoverPageData(): Promise< ProductGroup[] > {
const fetchUrl = MARKETPLACE_URL + '/wp-json/wccom-extensions/2.0/featured'; let url = '/wc/v3/marketplace/featured';
return fetch( fetchUrl ) if ( LOCALE.userLocale ) {
.then( ( response ) => { url = `${ url }?locale=${ LOCALE.userLocale }`;
if ( ! response.ok ) { }
throw new Error( response.statusText );
} try {
return response.json(); return await apiFetch( { path: url.toString() } );
} ) } catch ( error ) {
.then( ( json ) => { return [];
return json; }
} ) }
.catch( () => {
return [];
} );
};
function fetchCategories(): Promise< CategoryAPIItem[] > { 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 ) => { .then( ( response ) => {
if ( ! response.ok ) { if ( ! response.ok ) {
throw new Error( response.statusText ); throw new Error( response.statusText );

View File

@ -64,8 +64,24 @@ class WC_Admin_Addons {
* @return void * @return void
*/ */
public static function render_featured() { 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(); $locale = get_user_locale();
$featured = self::get_locale_data_from_transient( 'wc_addons_featured', $locale ); $featured = self::get_locale_data_from_transient( 'wc_addons_featured', $locale );
if ( false === $featured ) { if ( false === $featured ) {
$headers = array(); $headers = array();
$auth = WC_Helper_Options::get( 'auth' ); $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' ) ? __( 'We encountered an SSL error. Please ensure your site supports TLS version 1.2 or above.', 'woocommerce' )
: $raw_featured->get_error_message(); : $raw_featured->get_error_message();
self::output_empty( $message ); return new WP_Error( 'wc-addons-connection-error', $message );
return;
} }
$response_code = (int) wp_remote_retrieve_response_code( $raw_featured ); $response_code = (int) wp_remote_retrieve_response_code( $raw_featured );
@ -117,18 +131,15 @@ class WC_Admin_Addons {
$response_code $response_code
); );
self::output_empty( $message ); return new WP_Error( 'wc-addons-connection-error', $message );
return;
} }
$featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) );
if ( empty( $featured ) || ! is_array( $featured ) ) { if ( empty( $featured ) || ! is_array( $featured ) ) {
do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' );
$message = __( 'Our request to the featured API got a malformed response.', 'woocommerce' ); $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 ) { if ( $featured ) {
@ -136,7 +147,7 @@ class WC_Admin_Addons {
} }
} }
self::output_featured( $featured ); return $featured;
} }
/** /**

View File

@ -25,6 +25,7 @@ class WC_Helper_Admin {
*/ */
public static function load() { public static function load() {
add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'add_marketplace_settings' ) ); 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; 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(); WC_Helper_Admin::load();