Skip to content

Free Listings + Paid Ads: Fetch the number of syncable products for the product feed status section #1706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions js/src/get-started-page/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import './index.scss';
import useSyncableProductsCalculation from '.~/hooks/useSyncableProductsCalculation';
import BenefitsCard from './benefits-card';
import CustomerQuotesCard from './customer-quotes-card';
import Faqs from './faqs';
Expand All @@ -11,6 +17,13 @@ import GetStartedWithVideoCard from './get-started-with-video-card';
import UnsupportedNotices from './unsupported-notices';

const GetStartedPage = () => {
const { request } = useSyncableProductsCalculation();

// Trigger the calculation here for later use during the onboarding flow.
useEffect( () => {
request();
}, [ request ] );

return (
<div className="woocommerce-marketing-google-get-started-page">
<UnsupportedNotices />
Expand Down
62 changes: 62 additions & 0 deletions js/src/hooks/useSyncableProductsCalculation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { useEffect, useCallback } from '@wordpress/element';

/**
* Internal dependencies
*/
import { API_NAMESPACE } from '.~/data/constants';
import useApiFetchCallback from './useApiFetchCallback';
import useCountdown from './useCountdown';

/**
* @typedef {Object} SyncableProductsCalculation
* @property {number|null} count The number of syncable products. `null` if it's still in the calculation.
* @property {()=>Promise} request The function requesting the start of a calculation.
* @property {()=>Promise} retrieve The function retrieving the result of a requested calculation by polling with 5-second timer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure but I guess instead of by polling you mean by pulling?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just my 2 cents, to me polling is usually used when a request is periodically repeated (which seems to be the case here). Either one could be correct though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the client side has no way of knowing when the needed data will be ready, it queries the data by asking periodically. The implementation here matches the scenario, so the term polling is used.

*/

const getOptions = {
path: `${ API_NAMESPACE }/mc/syncable-products-count`,
};
const postOptions = {
...getOptions,
method: 'POST',
};

/**
* A hook for communicating with the calculation of the syncable products and for returning the result.
*
* If a shop has a large number of products, requesting the result with a single API may encounter
* a timeout or out-of-memory problem. Therefore, we use an API to schedule a batch for calculating,
* and another one to poll the result.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*
* @return {SyncableProductsCalculation} Payload containing the result of calculation and the functions for requesting and retrieving a calculation.
*/
export default function useSyncableProductsCalculation() {
const { second, callCount, startCountdown } = useCountdown();
const [ fetch, { data } ] = useApiFetchCallback( getOptions );
const [ request ] = useApiFetchCallback( postOptions );

// The number 0 is a valid value.
const count = data?.count ?? null;

const retrieve = useCallback( () => {
const promise = fetch();
promise.finally( () => startCountdown( 5 ) );
return promise;
}, [ fetch, startCountdown ] );

useEffect( () => {
if ( second === 0 && callCount > 0 && count === null ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why we need second === 0 && callCount > 0 and the dependency of second?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if callCount is each time we call startCountdown or it is each time the counter reset (after reaching 5)

Copy link
Member Author

@eason9487 eason9487 Oct 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need second === 0 && callCount > 0 and the dependency of second?

It's up to the hook's consumer to decide when to call retrieve, and then retrieve will request data and schedule a timer for the next call of retrieve triggered by this useEffect. But useEffect will always be called a time once the associated component is mounted, therefore, this condition is to skip the call when mounted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if callCount is each time we call startCountdown or it is each time the counter reset (after reaching 5)

* @property {number} callCount The calling count of `startCountdown` function.

retrieve();
}
}, [ second, callCount, count, retrieve ] );

return {
request,
retrieve,
count,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@
* External dependencies
*/
import { sprintf, __, _n } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';
import { Flex, FlexItem, FlexBlock } from '@wordpress/components';
import { Spinner } from '@woocommerce/components';

/**
* Internal dependencies
*/
import Section from '.~/wcdl/section';
import AppDocumentationLink from '.~/components/app-documentation-link';
import SyncIcon from '.~/components/sync-icon';
import SuccessIcon from '.~/components/success-icon';
import AppTooltip from '.~/components/app-tooltip';
import getNumberOfSyncProducts from '.~/utils/getNumberOfSyncProducts';
import useSyncableProductsCalculation from '.~/hooks/useSyncableProductsCalculation';
import './product-feed-status-section.scss';

function ProductQuantity( { quantity } ) {
if ( ! Number.isInteger( quantity ) ) {
return null;
}

const text = sprintf(
// translators: %d: number of products will be synced to Google Merchant Center.
_n( '%d product', '%d products', quantity, 'google-listings-and-ads' ),
Expand Down Expand Up @@ -51,25 +49,14 @@ function ProductQuantity( { quantity } ) {
* and show the number of products will be synced to Google Merchant Center.
*/
export default function ProductFeedStatusSection() {
/*
const { data, hasFinishedResolution } = useAppSelectDispatch(
'getMCProductStatistics'
);
*/
// TODO: Replace the dummy data with the above code later to use the adjusted API.
const data = {
statistics: {
active: 1,
expiring: 2,
pending: 3,
disapproved: 4,
not_synced: 5,
},
};
const hasFinishedResolution = true;
const productQuantity = hasFinishedResolution
? getNumberOfSyncProducts( data.statistics )
: null;
const { retrieve, count } = useSyncableProductsCalculation();

// Retrieve the result of calculation that was requested when entering the Get Started page.
useEffect( () => {
retrieve();
}, [ retrieve ] );

const isReady = Number.isInteger( count );

return (
<Section
Expand All @@ -89,15 +76,28 @@ export default function ProductFeedStatusSection() {
<Section.Card.Body>
<Flex align="flex-start" gap={ 3 }>
<FlexItem>
<SyncIcon />
{ isReady ? (
<SuccessIcon size={ 20 } />
) : (
<Spinner />
) }
</FlexItem>
<FlexBlock>
<Section.Card.Title>
{ __(
'Your product listings are being uploaded',
'google-listings-and-ads'
{ isReady ? (
<>
{ __(
'Your product listings are ready to be uploaded',
'google-listings-and-ads'
) }
<ProductQuantity quantity={ count } />
</>
) : (
__(
'Preparing your product listings',
'google-listings-and-ads'
)
) }
<ProductQuantity quantity={ productQuantity } />
</Section.Card.Title>
{ __(
'Google will review your product listings within 3-5 days. Once approved, your products will automatically be live and searchable on Google. You’ll be notified if there are any product feed issues.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
.gla-product-feed-status-section {
.gla-sync-icon {
fill: $gla-color-green;
transform: rotateZ(90deg);
.woocommerce-spinner {
width: 20px;
height: 20px;
min-width: 20px;

&__circle {
stroke: $gray-700;
stroke-width: 8px;
}
}

.wcdl-subsection-title {
Expand Down