Skip to content

Commit

Permalink
Fix/interstitial button loading states (#39356)
Browse files Browse the repository at this point in the history
* Fix interstitial button loading states

* changelog
  • Loading branch information
CodeyGuyDylan authored and gogdzl committed Oct 25, 2024
1 parent b2bdf53 commit 4406f9d
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = ( {
Expand All @@ -83,6 +84,7 @@ const ProductDetailCard = ( {
quantity = null,
highlightLastFeature = false,
isFetching = false,
isFetchingSuccess = false,
} ) => {
const {
fileSystemWriteAccess = 'no',
Expand Down Expand Up @@ -364,31 +366,31 @@ const ProductDetailCard = ( {
) }

{ ( ! isBundle || ( isBundle && ! hasPaidPlanForProduct ) ) && (
<Text
<ProductDetailCardButton
component={ ProductDetailButton }
onClick={ clickHandler }
isLoading={ isFetching || hasMainCheckoutStarted }
disabled={ cantInstallPlugin }
hasMainCheckoutStarted={ hasMainCheckoutStarted }
isFetching={ isFetching }
isFetchingSuccess={ isFetchingSuccess }
cantInstallPlugin={ cantInstallPlugin }
isPrimary={ ! isBundle }
className={ styles[ 'checkout-button' ] }
variant="body"
>
{ ctaLabel }
</Text>
label={ ctaLabel }
/>
) }

{ ! isBundle && trialAvailable && ! hasPaidPlanForProduct && (
<Text
<ProductDetailCardButton
component={ ProductDetailButton }
onClick={ trialClickHandler }
isLoading={ isFetching || hasTrialCheckoutStarted }
disabled={ cantInstallPlugin }
hasMainCheckoutStarted={ hasTrialCheckoutStarted }
isFetching={ isFetching }
isFetchingSuccess={ isFetchingSuccess }
cantInstallPlugin={ cantInstallPlugin }
isPrimary={ false }
className={ [ styles[ 'checkout-button' ], styles[ 'free-product-checkout-button' ] ] }
variant="body"
>
{ __( 'Start for free', 'jetpack-my-jetpack' ) }
</Text>
label={ __( 'Start for free', 'jetpack-my-jetpack' ) }
/>
) }

{ disclaimers.length > 0 && (
Expand Down Expand Up @@ -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 (
<Text
component={ component }
onClick={ handleClick }
isLoading={ shouldShowLoadingState }
disabled={ shouldDisableButton }
isPrimary={ isPrimary }
className={ className }
variant="body"
>
{ label }
</Text>
);
};

export default ProductDetailCard;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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();

Expand Down Expand Up @@ -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 );
}, [
Expand All @@ -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 (
<PricingTableColumn primary={ ! isFree }>
<PricingTableHeader>
Expand All @@ -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 }
</Button>
Expand Down Expand Up @@ -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.
Expand All @@ -250,6 +270,7 @@ const ProductDetailTable = ( {
onProductButtonClick,
trackProductButtonClick,
isFetching,
isFetchingSuccess,
preferProductName,
feature,
} ) => {
Expand Down Expand Up @@ -334,6 +355,7 @@ const ProductDetailTable = ( {
feature={ feature }
detail={ detail }
isFetching={ isFetching }
isFetchingSuccess={ isFetchingSuccess }
onProductButtonClick={ onProductButtonClick }
trackProductButtonClick={ trackProductButtonClick }
primary={ index === 0 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -242,6 +242,7 @@ export default function ProductInterstitial( {
trackProductButtonClick={ trackProductOrBundleClick }
preferProductName={ preferProductName }
isFetching={ isActivating || siteIsRegistering }
isFetchingSuccess={ isSuccess }
feature={ feature }
/>
) : (
Expand All @@ -264,6 +265,7 @@ export default function ProductInterstitial( {
quantity={ quantity }
highlightLastFeature={ highlightLastFeature }
isFetching={ isActivating || siteIsRegistering }
isFetchingSuccess={ isSuccess }
/>
</Col>
<Col
Expand All @@ -282,6 +284,7 @@ export default function ProductInterstitial( {
quantity={ quantity }
highlightLastFeature={ highlightLastFeature }
isFetching={ isActivating }
isFetchingSuccess={ isSuccess }
/>
) : (
children
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`,
Expand Down Expand Up @@ -64,6 +68,7 @@ const useActivate = ( productId: string ) => {
return {
activate,
isPending,
isSuccess,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix issue on interstitials show both buttons loading when only one is pressed

0 comments on commit 4406f9d

Please sign in to comment.