From 21f69b0c1644e05592b1e5f5cfb9b94d5127bdce Mon Sep 17 00:00:00 2001
From: Dylan Munson <65001528+CodeyGuyDylan@users.noreply.github.com>
Date: Mon, 16 Sep 2024 10:23:13 -0600
Subject: [PATCH] Fix/interstitial button loading states (#39356)
* Fix interstitial button loading states
* changelog
---
.../components/product-detail-card/index.jsx | 82 +++++++++++++++----
.../components/product-detail-table/index.jsx | 32 ++++++--
.../components/product-interstitial/index.jsx | 5 +-
.../_inc/data/products/use-activate.ts | 7 +-
.../fix-interstitial-button-loading-states | 4 +
5 files changed, 107 insertions(+), 23 deletions(-)
create mode 100644 projects/packages/my-jetpack/changelog/fix-interstitial-button-loading-states
diff --git a/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx b/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx
index 97156dabd0fb4..81d8be3b6be23 100644
--- a/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx
+++ b/projects/packages/my-jetpack/_inc/components/product-detail-card/index.jsx
@@ -13,7 +13,7 @@ import { ExternalLink } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, check, plus } from '@wordpress/icons';
import clsx from 'clsx';
-import React, { useCallback } from 'react';
+import React, { useCallback, useState, useEffect } from 'react';
import useProduct from '../../data/products/use-product';
import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state';
import useAnalytics from '../../hooks/use-analytics';
@@ -68,7 +68,8 @@ function Price( { value, currency, isOld } ) {
* @param {boolean} [props.hideTOS] - Whether to hide the Terms of Service text
* @param {number} [props.quantity] - The quantity of the product to purchase
* @param {boolean} [props.highlightLastFeature] - Whether to highlight the last feature of the list of features
- * @param {boolean} [props.isFetching] - Whether the product is being fetched
+ * @param {boolean} [props.isFetching] - Whether the product is being activated
+ * @param {boolean} [props.isFetchingSuccess] - Whether the product was activated successfully
* @return {object} ProductDetailCard react component.
*/
const ProductDetailCard = ( {
@@ -83,6 +84,7 @@ const ProductDetailCard = ( {
quantity = null,
highlightLastFeature = false,
isFetching = false,
+ isFetchingSuccess = false,
} ) => {
const {
fileSystemWriteAccess = 'no',
@@ -364,31 +366,31 @@ const ProductDetailCard = ( {
) }
{ ( ! isBundle || ( isBundle && ! hasPaidPlanForProduct ) ) && (
-
- { ctaLabel }
-
+ label={ ctaLabel }
+ />
) }
{ ! isBundle && trialAvailable && ! hasPaidPlanForProduct && (
-
- { __( 'Start for free', 'jetpack-my-jetpack' ) }
-
+ label={ __( 'Start for free', 'jetpack-my-jetpack' ) }
+ />
) }
{ disclaimers.length > 0 && (
@@ -434,4 +436,52 @@ const ProductDetailCard = ( {
);
};
+const ProductDetailCardButton = ( {
+ component,
+ onClick,
+ hasMainCheckoutStarted,
+ isFetching,
+ isFetchingSuccess,
+ cantInstallPlugin,
+ isPrimary,
+ className,
+ label,
+} ) => {
+ const [ isButtonLoading, setIsButtonLoading ] = useState( false );
+
+ useEffect( () => {
+ // If activation was successful, we will be redirecting the user
+ // so we don't want them to be able to click the button again.
+ if ( ! isFetching && ! isFetchingSuccess ) {
+ setIsButtonLoading( false );
+ }
+ }, [ isFetching, isFetchingSuccess ] );
+
+ // If a button was clicked, we should only show the loading state for that button.
+ const shouldShowLoadingState = hasMainCheckoutStarted || isButtonLoading;
+ // If the any buttons are loading, or we are in the process
+ // of rediredcting the user, we should disable all buttons.
+ const shouldDisableButton =
+ hasMainCheckoutStarted || cantInstallPlugin || isFetching || isFetchingSuccess;
+
+ const handleClick = () => {
+ setIsButtonLoading( true );
+ onClick();
+ };
+
+ return (
+
+ { label }
+
+ );
+};
+
export default ProductDetailCard;
diff --git a/projects/packages/my-jetpack/_inc/components/product-detail-table/index.jsx b/projects/packages/my-jetpack/_inc/components/product-detail-table/index.jsx
index 18a70e2e7ac2f..96882dd816d17 100644
--- a/projects/packages/my-jetpack/_inc/components/product-detail-table/index.jsx
+++ b/projects/packages/my-jetpack/_inc/components/product-detail-table/index.jsx
@@ -11,7 +11,7 @@ import {
import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection';
import { sprintf, __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
-import { useCallback, useMemo } from 'react';
+import { useCallback, useMemo, useState, useEffect } from 'react';
import useProduct from '../../data/products/use-product';
import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state';
import { useRedirectToReferrer } from '../../hooks/use-redirect-to-referrer';
@@ -25,7 +25,8 @@ import { useRedirectToReferrer } from '../../hooks/use-redirect-to-referrer';
* @param {boolean} props.cantInstallPlugin - True when the plugin cannot be automatically installed.
* @param {Function} props.onProductButtonClick - Click handler for the product button.
* @param {object} props.detail - Product detail object.
- * @param {boolean} props.isFetching - True if there is a pending request to load the product.
+ * @param {boolean} props.isFetching - True if there is a pending request to activate the product.
+ * @param {boolean} props.isFetchingSuccess - True if the product activation has been successful.
* @param {string} props.tier - Product tier slug, i.e. 'free' or 'upgraded'.
* @param {Function} props.trackProductButtonClick - Tracks click event for the product button.
* @param {boolean} props.preferProductName - Whether to show the product name instead of the title.
@@ -37,11 +38,13 @@ const ProductDetailTableColumn = ( {
onProductButtonClick,
detail,
isFetching,
+ isFetchingSuccess,
tier,
trackProductButtonClick,
preferProductName,
feature,
} ) => {
+ const [ isButtonLoading, setIsButtonLoading ] = useState( false );
const { siteSuffix = '', myJetpackCheckoutUri = '' } = getMyJetpackWindowInitialState();
// Extract the product details.
@@ -67,6 +70,14 @@ const ProductDetailTableColumn = ( {
quantity = null,
} = tiersPricingForUi[ tier ];
+ useEffect( () => {
+ // If activation was successful, we will be redirecting the user
+ // so we don't want them to be able to click the button again.
+ if ( ! isFetching && ! isFetchingSuccess ) {
+ setIsButtonLoading( false );
+ }
+ }, [ isFetching, isFetchingSuccess ] );
+
// Redirect to the referrer URL when the `redirect_to_referrer` query param is present.
const referrerURL = useRedirectToReferrer();
@@ -145,6 +156,7 @@ const ProductDetailTableColumn = ( {
// Register the click handler for the product button.
const onClick = useCallback( () => {
+ setIsButtonLoading( true );
trackProductButtonClick( { is_free_plan: isFree, cta_text: callToAction } );
onProductButtonClick?.( runCheckout, detail, tier );
}, [
@@ -157,6 +169,13 @@ const ProductDetailTableColumn = ( {
callToAction,
] );
+ // If a button was clicked, we should only show the loading state for that button.
+ const shouldShowLoadingState = hasCheckoutStarted || isButtonLoading;
+ // If the any buttons are loading, or we are in the process
+ // of rediredcting the user, we should disable all buttons.
+ const shouldDisableButton =
+ hasCheckoutStarted || cantInstallPlugin || isFetching || isFetchingSuccess;
+
return (
@@ -178,8 +197,8 @@ const ProductDetailTableColumn = ( {
fullWidth
variant={ isFree ? 'secondary' : 'primary' }
onClick={ onClick }
- isLoading={ hasCheckoutStarted || isFetching }
- disabled={ hasCheckoutStarted || cantInstallPlugin || isFetching }
+ isLoading={ shouldShowLoadingState }
+ disabled={ shouldDisableButton }
>
{ callToAction }
@@ -240,7 +259,8 @@ ProductDetailTableColumn.propTypes = {
* @param {string} props.slug - Product slug.
* @param {Function} props.onProductButtonClick - Click handler for the product button.
* @param {Function} props.trackProductButtonClick - Tracks click event for the product button.
- * @param {boolean} props.isFetching - True if there is a pending request to load the product.
+ * @param {boolean} props.isFetching - True if there is a pending request to activate the product.
+ * @param {boolean} props.isFetchingSuccess - True if the product activation has been successful.
* @param {boolean} props.preferProductName - Whether to show the product name instead of the title.
* @param {string} props.feature - The slug of a specific product feature to highlight.
* @return {object} - ProductDetailTable react component.
@@ -250,6 +270,7 @@ const ProductDetailTable = ( {
onProductButtonClick,
trackProductButtonClick,
isFetching,
+ isFetchingSuccess,
preferProductName,
feature,
} ) => {
@@ -334,6 +355,7 @@ const ProductDetailTable = ( {
feature={ feature }
detail={ detail }
isFetching={ isFetching }
+ isFetchingSuccess={ isFetchingSuccess }
onProductButtonClick={ onProductButtonClick }
trackProductButtonClick={ trackProductButtonClick }
primary={ index === 0 }
diff --git a/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx b/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx
index 6dac803921a02..52177cf8c2e05 100644
--- a/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx
+++ b/projects/packages/my-jetpack/_inc/components/product-interstitial/index.jsx
@@ -77,7 +77,7 @@ export default function ProductInterstitial( {
} ) {
const { detail } = useProduct( slug );
const { detail: bundleDetail } = useProduct( bundle );
- const { activate, isPending: isActivating } = useActivate( slug );
+ const { activate, isPending: isActivating, isSuccess } = useActivate( slug );
// Get the post activation URL for the product.
let redirectUri = detail?.postActivationUrl || null;
@@ -242,6 +242,7 @@ export default function ProductInterstitial( {
trackProductButtonClick={ trackProductOrBundleClick }
preferProductName={ preferProductName }
isFetching={ isActivating || siteIsRegistering }
+ isFetchingSuccess={ isSuccess }
feature={ feature }
/>
) : (
@@ -264,6 +265,7 @@ export default function ProductInterstitial( {
quantity={ quantity }
highlightLastFeature={ highlightLastFeature }
isFetching={ isActivating || siteIsRegistering }
+ isFetchingSuccess={ isSuccess }
/>
) : (
children
diff --git a/projects/packages/my-jetpack/_inc/data/products/use-activate.ts b/projects/packages/my-jetpack/_inc/data/products/use-activate.ts
index caa548cb6eb3d..7c6c463644a39 100644
--- a/projects/packages/my-jetpack/_inc/data/products/use-activate.ts
+++ b/projects/packages/my-jetpack/_inc/data/products/use-activate.ts
@@ -33,7 +33,11 @@ const useActivate = ( productId: string ) => {
const { detail, refetch } = useProduct( productId );
const { recordEvent } = useAnalytics();
- const { mutate: activate, isPending } = useSimpleMutation( {
+ const {
+ mutate: activate,
+ isPending,
+ isSuccess,
+ } = useSimpleMutation( {
name: QUERY_ACTIVATE_PRODUCT_KEY,
query: {
path: `${ REST_API_SITE_PRODUCTS_ENDPOINT }/${ productId }`,
@@ -64,6 +68,7 @@ const useActivate = ( productId: string ) => {
return {
activate,
isPending,
+ isSuccess,
};
};
diff --git a/projects/packages/my-jetpack/changelog/fix-interstitial-button-loading-states b/projects/packages/my-jetpack/changelog/fix-interstitial-button-loading-states
new file mode 100644
index 0000000000000..e0e8442e9ed8c
--- /dev/null
+++ b/projects/packages/my-jetpack/changelog/fix-interstitial-button-loading-states
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+Fix issue on interstitials show both buttons loading when only one is pressed