diff --git a/client/dashboard/sites/site-launch-celebration-modal/index.tsx b/client/dashboard/sites/site-launch-celebration-modal/index.tsx index 4fa00c896141..86685de20504 100644 --- a/client/dashboard/sites/site-launch-celebration-modal/index.tsx +++ b/client/dashboard/sites/site-launch-celebration-modal/index.tsx @@ -6,6 +6,7 @@ import { Button, Modal, } from '@wordpress/components'; +import { useEvent } from '@wordpress/compose'; import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { copy, globe } from '@wordpress/icons'; @@ -21,50 +22,47 @@ interface SiteLaunchCelebrationModalProps { site: Pick< Site, 'ID' | 'slug' | 'URL' | 'launch_status' > & { plan?: Pick< Required< Site >[ 'plan' ], 'is_free' | 'product_slug' >; }; + onOpen?(): void; + onClose?(): void; } -export default function SiteLaunchCelebrationModal( { site }: SiteLaunchCelebrationModalProps ) { +export default function SiteLaunchCelebrationModal( { + site, + onOpen: externalOnOpen, + onClose, +}: SiteLaunchCelebrationModalProps ) { const [ isOpen, setIsOpen ] = useState( false ); const [ clipboardCopied, setClipboardCopied ] = useState( false ); const { recordTracksEvent } = useAnalytics(); const { queries } = useAppContext(); - const { data: domains = [], isFetchedAfterMount: isDomainsFetched } = useQuery( { + const { data: domains = [], isFetchedAfterMount: isDomainsDataReady } = useQuery( { ...queries.domainsQuery(), enabled: isOpen, select: ( data ) => data.filter( ( domain ) => domain.blog_id === site.ID ), } ); const copyButtonRef = useRef< HTMLButtonElement >( null ); + const onOpen = useEvent( () => { + externalOnOpen?.(); + setIsOpen( true ); + + // Track the modal view + recordTracksEvent( 'calypso_launchpad_celebration_modal_view', { + product_slug: site?.plan?.product_slug, + } ); + } ); + // Check if celebration modal should be shown based on URL param and site launch status useEffect( () => { const hasCelebrateLaunch = new URLSearchParams( window.location.search ).has( 'celebrateLaunch' ); if ( site.launch_status === 'launched' && hasCelebrateLaunch ) { - setIsOpen( true ); - } - }, [ site.launch_status ] ); - - useEffect( () => { - // Only run cleanup and analytics when modal is open - if ( ! isOpen ) { - return; + onOpen(); } + }, [ site.launch_status, onOpen ] ); - // Remove the celebrateLaunch URL param without reloading the page - window.history.replaceState( - null, - '', - removeQueryArgs( window.location.href, 'celebrateLaunch' ) - ); - - // Track the modal view - recordTracksEvent( 'calypso_launchpad_celebration_modal_view', { - product_slug: site?.plan?.product_slug, - } ); - }, [ isOpen, site?.plan?.product_slug, recordTracksEvent ] ); - - if ( ! isOpen || ! isDomainsFetched ) { + if ( ! isOpen || ! isDomainsDataReady ) { return null; } @@ -148,7 +146,17 @@ export default function SiteLaunchCelebrationModal( { site }: SiteLaunchCelebrat className="celebration-modal" title={ __( 'Congrats, your site is live!' ) } size="medium" - onRequestClose={ () => setIsOpen( false ) } + onRequestClose={ () => { + setIsOpen( false ); + onClose?.(); + + // Remove the celebrateLaunch URL param without reloading the page + window.history.replaceState( + null, + '', + removeQueryArgs( window.location.href, 'celebrateLaunch' ) + ); + } } > diff --git a/client/dashboard/sites/site-launch-celebration-modal/test/index.test.tsx b/client/dashboard/sites/site-launch-celebration-modal/test/index.test.tsx index 427f2c568ad1..c2adf7ecae83 100644 --- a/client/dashboard/sites/site-launch-celebration-modal/test/index.test.tsx +++ b/client/dashboard/sites/site-launch-celebration-modal/test/index.test.tsx @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import nock from 'nock'; import { render } from '../../../test-utils'; @@ -142,17 +142,22 @@ describe( '', () => { } ); describe( 'Query Parameter Removal', () => { - test( 'removes celebrateLaunch query param when modal opens', async () => { + test( 'removes celebrateLaunch query param when modal closes', async () => { setupCelebrateLaunchUrl(); - + const user = userEvent.setup(); const mockSite = createMockSite(); render( ); // Wait for modal to render await screen.findByRole( 'dialog' ); + // Close the modal + await user.click( screen.getByRole( 'button', { name: 'Close' } ) ); + // After component mounts and modal opens, the URL should not contain celebrateLaunch param - expect( window.location.href ).not.toContain( 'celebrateLaunch' ); + await waitFor( () => { + expect( window.location.href ).not.toContain( 'celebrateLaunch' ); + } ); } ); } ); diff --git a/client/layout/index.jsx b/client/layout/index.jsx index b6c0c52e4cb5..e6e88d491ed4 100644 --- a/client/layout/index.jsx +++ b/client/layout/index.jsx @@ -42,15 +42,19 @@ import { getSidebarType, SidebarType } from 'calypso/state/global-sidebar/select import { isUserNewerThan, WEEK_IN_MILLISECONDS } from 'calypso/state/guided-tours/contexts'; import { getCurrentOAuth2Client } from 'calypso/state/oauth2-clients/ui/selectors'; import { isReaderMSDEnabled } from 'calypso/state/reader-ui/selectors'; +import getInitialQueryArguments from 'calypso/state/selectors/get-initial-query-arguments'; import getIsBlazePro from 'calypso/state/selectors/get-is-blaze-pro'; +import getPrimarySiteId from 'calypso/state/selectors/get-primary-site-id'; import hasGravatarDomainQueryParam from 'calypso/state/selectors/has-gravatar-domain-query-param'; import isAtomicSite from 'calypso/state/selectors/is-site-automated-transfer'; import isWooJPCFlow from 'calypso/state/selectors/is-woo-jpc-flow'; import { getIsOnboardingAffiliateFlow } from 'calypso/state/signup/flow/selectors'; import { isJetpackSite } from 'calypso/state/sites/selectors'; +import getSite from 'calypso/state/sites/selectors/get-site'; import { isSupportSession } from 'calypso/state/support/selectors'; import { getCurrentLayoutFocus } from 'calypso/state/ui/layout-focus/selectors'; import { + getMostRecentlySelectedSiteId, getSelectedSiteId, getSidebarIsCollapsed, masterbarIsVisible, @@ -144,6 +148,7 @@ class Layout extends Component { super( props ); this.state = { isDesktop: isWithinBreakpoint( '>=782px' ), + initiallyUnlaunchedSite: false, }; } @@ -164,6 +169,15 @@ class Layout extends Component { refreshColorScheme( prevProps.colorScheme, this.props.colorScheme ); } + static getDerivedStateFromProps( props ) { + if ( props.site?.launch_status === 'unlaunched' ) { + return { + initiallyUnlaunchedSite: true, + }; + } + return null; + } + renderMasterbar( loadHelpCenterIcon ) { if ( this.props.masterbarIsHidden ) { return ; @@ -202,6 +216,7 @@ class Layout extends Component { /> ) } + ); + } + render() { const sectionClass = clsx( 'layout', `focus-${ this.props.currentLayoutFocus }`, { [ 'is-group-' + this.props.sectionGroup ]: this.props.sectionGroup, @@ -360,6 +389,7 @@ class Layout extends Component { { ! this.props.isMSDEnabledForReader && ( ) } + { this.renderCelebrateSiteLaunchModal() } ); } @@ -370,7 +400,13 @@ export default withCurrentRoute( const dashboard = getDashboardFromHostname( window?.location?.hostname ); const sectionGroup = currentSection?.group ?? null; const sectionName = currentSection?.name ?? null; - const siteId = getSelectedSiteId( state ); + + // Falls back to using the user's primary site if no site has been selected + // by the user yet + const siteId = + getSelectedSiteId( state ) || + getMostRecentlySelectedSiteId( state ) || + getPrimarySiteId( state ); const sectionJitmPath = getMessagePathForJITM( currentRoute ); const isJetpackLogin = currentRoute.startsWith( '/log-in/jetpack' ); const isJetpack = @@ -484,11 +520,13 @@ export default withCurrentRoute( sectionGroup, sectionName, sectionJitmPath, + hasCelebrateLaunchQueryParam: getInitialQueryArguments( state )?.celebrateLaunch === 'true', currentLayoutFocus: getCurrentLayoutFocus( state ), colorScheme, needsColorScheme, isFetchingColorScheme: isFetchingAdminColor( state, siteId ), siteId, + site: getSite( state, siteId ), // We avoid requesting sites in the Jetpack Connect authorization step, because this would // request all sites before authorization has finished. That would cause the "all sites" // request to lack the newly authorized site, and when the request finishes after diff --git a/client/layout/masterbar/item.tsx b/client/layout/masterbar/item.tsx index dd74e164dfa6..cb640f68a5f8 100644 --- a/client/layout/masterbar/item.tsx +++ b/client/layout/masterbar/item.tsx @@ -35,6 +35,7 @@ interface MasterbarItemProps { variant?: string; ariaLabel?: string; openSubMenuOnClick?: boolean; + isBusy?: boolean; } class MasterbarItem extends Component< MasterbarItemProps > { @@ -244,6 +245,7 @@ class MasterbarItem extends Component< MasterbarItemProps > { onMouseEnter: this.preload, disabled: this.props.disabled, 'aria-label': this.props.ariaLabel, + isBusy: this.props.isBusy, }; return ( diff --git a/client/layout/masterbar/logged-in.jsx b/client/layout/masterbar/logged-in.jsx index 7e667755b428..67313e20a45b 100644 --- a/client/layout/masterbar/logged-in.jsx +++ b/client/layout/masterbar/logged-in.jsx @@ -27,7 +27,6 @@ import { getPreference } from 'calypso/state/preferences/selectors'; import getCurrentRoute from 'calypso/state/selectors/get-current-route'; import getEditorUrl from 'calypso/state/selectors/get-editor-url'; import getPreviousRoute from 'calypso/state/selectors/get-previous-route'; -import getPrimarySiteId from 'calypso/state/selectors/get-primary-site-id'; import getSiteMigrationStatus from 'calypso/state/selectors/get-site-migration-status'; import hasGravatarDomainQueryParam from 'calypso/state/selectors/has-gravatar-domain-query-param'; import isDomainOnlySite from 'calypso/state/selectors/is-domain-only-site'; @@ -57,7 +56,7 @@ import isSimpleSite from 'calypso/state/sites/selectors/is-simple-site'; import { isSupportSession } from 'calypso/state/support/selectors'; import { activateNextLayoutFocus, setNextLayoutFocus } from 'calypso/state/ui/layout-focus/actions'; import { getCurrentLayoutFocus } from 'calypso/state/ui/layout-focus/selectors'; -import { getMostRecentlySelectedSiteId, getSectionGroup } from 'calypso/state/ui/selectors'; +import { getSectionGroup } from 'calypso/state/ui/selectors'; import Item from './item'; import Masterbar from './masterbar'; import { AgentsManagerIcon } from './masterbar-agents-manager/agents-manager-icon'; @@ -848,12 +847,9 @@ class MasterbarLoggedIn extends Component { export { MasterbarLoggedIn }; export default connect( - ( state ) => { + ( state, { siteId } ) => { const sectionGroup = getSectionGroup( state ); - // Falls back to using the user's primary site if no site has been selected - // by the user yet - const siteId = getMostRecentlySelectedSiteId( state ) || getPrimarySiteId( state ); const sitePlanSlug = getSitePlanSlug( state, siteId ); const isMigrationInProgress = isSiteMigrationInProgress( state, siteId ) || isSiteMigrationActiveRoute( state ); diff --git a/client/layout/masterbar/masterbar-launch-button.tsx b/client/layout/masterbar/masterbar-launch-button.tsx index 5dd5d729784a..97f325e0b2e7 100644 --- a/client/layout/masterbar/masterbar-launch-button.tsx +++ b/client/layout/masterbar/masterbar-launch-button.tsx @@ -1,13 +1,11 @@ import { siteLaunchMutation } from '@automattic/api-queries'; import { useMutation } from '@tanstack/react-query'; import { Button } from '@wordpress/components'; +import { addQueryArgs } from '@wordpress/url'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; -import { useGetDomainsQuery } from 'calypso/data/domains/use-get-domains-query'; -import useHomeLayoutQuery from 'calypso/data/home/use-home-layout-query'; import { useExperiment } from 'calypso/lib/explat'; -import { useCelebrateLaunchModal } from 'calypso/my-sites/customer-home/cards/launchpad/use-celebrate-launch-modal'; -import CelebrateLaunchModal from 'calypso/my-sites/customer-home/components/celebrate-launch-modal'; +import { useCelebrateLaunchModalSideEffects } from 'calypso/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects'; import { useDispatch, useSelector } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { launchSiteOrRedirectToLaunchSignupFlow } from 'calypso/state/sites/launch/actions'; @@ -18,18 +16,10 @@ export const MasterbarLaunchButton = ( { siteId }: { siteId: number } ) => { const translate = useTranslate(); const dispatch = useDispatch(); const site = useSelector( ( state ) => getSite( state, siteId ) ); - const { data: allDomains = [], isFetchedAfterMount } = useGetDomainsQuery( site?.ID ?? null, { - retry: false, - } ); - const layout = useHomeLayoutQuery( siteId ); - const { isOpen, setModalIsOpen, handleSiteLaunched } = useCelebrateLaunchModal( siteId, layout ); - const launchSiteMutation = useMutation( { - ...siteLaunchMutation( siteId ), - onSuccess: () => { - handleSiteLaunched( !! site?.is_wpcom_atomic ); - }, - } ); + const launchSiteMutation = useMutation( siteLaunchMutation( siteId ) ); + + const { onSiteLaunched } = useCelebrateLaunchModalSideEffects( siteId ); const [ isLoading, data ] = useExperiment( 'calypso_standardized_site_launch_gating' ); @@ -37,12 +27,19 @@ export const MasterbarLaunchButton = ( { siteId }: { siteId: number } ) => { dispatch( recordTracksEvent( 'calypso_masterbar_launch_site' ) ); if ( data?.variationName === 'gated_site_launch' ) { - window.location.assign( `/start/launch-site?siteSlug=${ site?.slug }` ); + window.location.assign( + addQueryArgs( '/start/launch-site', { + siteSlug: site?.slug, + back_to: window.location.pathname, + } ) + ); return; } if ( data?.variationName === 'ungated_site_launch' ) { - launchSiteMutation.mutate(); + launchSiteMutation.mutate( undefined, { + onSuccess: () => onSiteLaunched( !! site?.is_wpcom_atomic ), + } ); return; } @@ -50,34 +47,26 @@ export const MasterbarLaunchButton = ( { siteId }: { siteId: number } ) => { }; return ( - <> - - - - - } - onClick={ onLaunchSiteClick } - > - { translate( 'Launch site' ) } - - { isOpen && isFetchedAfterMount && ( - - ) } - + + + + + } + onClick={ onLaunchSiteClick } + > + { translate( 'Launch site' ) } + ); }; diff --git a/client/my-sites/customer-home/cards/launchpad/intent-newsletter.tsx b/client/my-sites/customer-home/cards/launchpad/intent-newsletter.tsx index f363821cf315..a49c6288ae03 100644 --- a/client/my-sites/customer-home/cards/launchpad/intent-newsletter.tsx +++ b/client/my-sites/customer-home/cards/launchpad/intent-newsletter.tsx @@ -1,40 +1,21 @@ -import { useGetDomainsQuery } from 'calypso/data/domains/use-get-domains-query'; -import useHomeLayoutQuery from 'calypso/data/home/use-home-layout-query'; +import { useCelebrateLaunchModalSideEffects } from 'calypso/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects'; import { useSelector } from 'calypso/state'; import { getSite } from 'calypso/state/sites/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; -import CelebrateLaunchModal from '../../components/celebrate-launch-modal'; -import { useCelebrateLaunchModal } from './use-celebrate-launch-modal'; import CustomerHomeLaunchpad from '.'; import type { AppState } from 'calypso/types'; const LaunchpadIntentNewsletter = ( { checklistSlug }: { checklistSlug: string } ): JSX.Element => { const siteId = useSelector( getSelectedSiteId ) || 0; const site = useSelector( ( state: AppState ) => getSite( state, siteId ) ); - const { data: allDomains = [] } = useGetDomainsQuery( site?.ID ?? null, { - retry: false, - } ); - const layoutQuery = useHomeLayoutQuery( siteId || null ); - const { isOpen, setModalIsOpen, handleSiteLaunched } = useCelebrateLaunchModal( - siteId, - layoutQuery - ); + const { onSiteLaunched } = useCelebrateLaunchModalSideEffects( siteId ); return ( - <> - handleSiteLaunched( !! site?.is_wpcom_atomic ) } - > - { isOpen && ( - - ) } - + onSiteLaunched( !! site?.is_wpcom_atomic ) } + /> ); }; diff --git a/client/my-sites/customer-home/cards/launchpad/pre-launch.tsx b/client/my-sites/customer-home/cards/launchpad/pre-launch.tsx index f34be606907b..00fdbddd208f 100644 --- a/client/my-sites/customer-home/cards/launchpad/pre-launch.tsx +++ b/client/my-sites/customer-home/cards/launchpad/pre-launch.tsx @@ -1,11 +1,8 @@ -import { useGetDomainsQuery } from 'calypso/data/domains/use-get-domains-query'; -import useHomeLayoutQuery from 'calypso/data/home/use-home-layout-query'; import { useExperiment } from 'calypso/lib/explat'; +import { useCelebrateLaunchModalSideEffects } from 'calypso/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects'; import { useSelector } from 'calypso/state'; import { getSite } from 'calypso/state/sites/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; -import CelebrateLaunchModal from '../../components/celebrate-launch-modal'; -import { useCelebrateLaunchModal } from './use-celebrate-launch-modal'; import CustomerHomeLaunchpad from '.'; import type { Task } from '@automattic/launchpad'; import type { AppState } from 'calypso/types'; @@ -18,12 +15,8 @@ const LaunchpadPreLaunch = ( props: LaunchpadPreLaunchProps ): JSX.Element => { const siteId = useSelector( getSelectedSiteId ) || 0; const site = useSelector( ( state: AppState ) => getSite( state, siteId ) ); const checklistSlug = site?.options?.site_intent ?? ''; - const { data: allDomains = [], isFetchedAfterMount } = useGetDomainsQuery( site?.ID ?? null, { - retry: false, - } ); - const layout = useHomeLayoutQuery( siteId || null ); - const { isOpen, setModalIsOpen, handleSiteLaunched } = useCelebrateLaunchModal( siteId, layout ); + const { onSiteLaunched } = useCelebrateLaunchModalSideEffects( siteId ); const [ , experimentData ] = useExperiment( 'calypso_standardized_site_launch_gating' ); const experimentAssignment = experimentData?.variationName; @@ -48,20 +41,11 @@ const LaunchpadPreLaunch = ( props: LaunchpadPreLaunchProps ): JSX.Element => { }; return ( - <> - handleSiteLaunched( !! site?.is_wpcom_atomic ) } - /> - { isOpen && isFetchedAfterMount && ( - - ) } - + onSiteLaunched( !! site?.is_wpcom_atomic ) } + /> ); }; diff --git a/client/my-sites/customer-home/cards/launchpad/use-celebrate-launch-modal.ts b/client/my-sites/customer-home/cards/launchpad/use-celebrate-launch-modal.ts deleted file mode 100644 index d06e7efaa6a1..000000000000 --- a/client/my-sites/customer-home/cards/launchpad/use-celebrate-launch-modal.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { updateLaunchpadSettings } from '@automattic/data-stores'; -import { useState } from 'react'; -import { useDispatch } from 'calypso/state'; -import { requestSite } from 'calypso/state/sites/actions'; - -export function useCelebrateLaunchModal( siteId: number, layout: { refetch: () => void } | null ) { - const [ isOpen, setIsOpen ] = useState( false ); - const dispatch = useDispatch(); - - const setModalIsOpen = ( isOpen: boolean ) => { - setIsOpen( isOpen ); - - if ( isOpen ) { - // Site launched, update site data - dispatch( requestSite( siteId ) ); - } else { - // Modal closed, update the launchpad data/checklist - layout?.refetch(); - } - }; - - const handleSiteLaunched = ( isWpcomAtomic: boolean ) => { - setModalIsOpen( true ); - // currently the action to update site_launch status on atomic doesn't fire - // this is a workaround until that is fixed - if ( isWpcomAtomic ) { - updateLaunchpadSettings( siteId, { - checklist_statuses: { site_launched: true }, - } ); - } - }; - - return { - isOpen, - setModalIsOpen, - handleSiteLaunched, - }; -} diff --git a/client/my-sites/customer-home/celebrate-site-launch-modal/index.tsx b/client/my-sites/customer-home/celebrate-site-launch-modal/index.tsx new file mode 100644 index 000000000000..35b1635e5510 --- /dev/null +++ b/client/my-sites/customer-home/celebrate-site-launch-modal/index.tsx @@ -0,0 +1,41 @@ +import { useMemo } from 'react'; +import { AnalyticsProvider } from 'calypso/dashboard/app/analytics'; +import SiteLaunchCelebrationModal from 'calypso/dashboard/sites/site-launch-celebration-modal'; +import useHomeLayoutQuery from 'calypso/data/home/use-home-layout-query'; +import { recordTracksEvent } from 'calypso/lib/analytics/tracks'; +import { useDispatch, useSelector } from 'calypso/state'; +import { setSiteLaunchCelebrationModalOpen } from 'calypso/state/sites/launch/actions'; +import { getSite } from 'calypso/state/sites/selectors'; + +const CelebrateSiteLaunchModal = ( { siteId }: { siteId: number } ) => { + const site = useSelector( ( state ) => getSite( state, siteId ) ); + + const dispatch = useDispatch(); + const analyticsClient = useMemo( () => { + return { + recordTracksEvent, + recordPageView() {}, // Unused by this component + }; + }, [] ); + + const layout = useHomeLayoutQuery( siteId ); + + return ( + + { site && ( + { + dispatch( setSiteLaunchCelebrationModalOpen( true ) ); + } } + onClose={ () => { + dispatch( setSiteLaunchCelebrationModalOpen( false ) ); + layout?.refetch(); + } } + /> + ) } + + ); +}; + +export default CelebrateSiteLaunchModal; diff --git a/client/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects.ts b/client/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects.ts new file mode 100644 index 000000000000..b1ed68ed9878 --- /dev/null +++ b/client/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects.ts @@ -0,0 +1,27 @@ +import { updateLaunchpadSettings } from '@automattic/data-stores'; +import { useDispatch } from 'calypso/state'; +import { requestSite } from 'calypso/state/sites/actions'; + +export function useCelebrateLaunchModalSideEffects( siteId: number ) { + const dispatch = useDispatch(); + + const addCelebrateLaunchQueryParams = () => { + const url = new URL( window.location.href ); + url.searchParams.set( 'celebrateLaunch', 'true' ); + window.history.replaceState( {}, '', url.toString() ); + }; + + return { + addCelebrateLaunchQueryParams, + onSiteLaunched: ( isWpcomAtomic: boolean ) => { + addCelebrateLaunchQueryParams(); + dispatch( requestSite( siteId ) ); + + if ( isWpcomAtomic ) { + updateLaunchpadSettings( siteId, { + checklist_statuses: { site_launched: true }, + } ); + } + }, + }; +} diff --git a/client/my-sites/customer-home/components/celebrate-launch-modal.jsx b/client/my-sites/customer-home/components/celebrate-launch-modal.jsx deleted file mode 100644 index 64007491cd14..000000000000 --- a/client/my-sites/customer-home/components/celebrate-launch-modal.jsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Gridicon, ConfettiAnimation, Tooltip } from '@automattic/components'; -import { useHasEnTranslation } from '@automattic/i18n-utils'; -import { Button, Modal } from '@wordpress/components'; -import { Icon, copy } from '@wordpress/icons'; -import { useTranslate } from 'i18n-calypso'; -import { useEffect, useState, useRef } from 'react'; -import { useDispatch } from 'react-redux'; -import ClipboardButton from 'calypso/components/forms/clipboard-button'; -import { omitUrlParams } from 'calypso/lib/url'; -import { recordTracksEvent } from 'calypso/state/analytics/actions'; -import { createSiteDomainObject } from 'calypso/state/sites/domains/assembler'; - -import './celebrate-launch-modal.scss'; - -function CelebrateLaunchModal( { setModalIsOpen, site, allDomains } ) { - const dispatch = useDispatch(); - const translate = useTranslate(); - const isPaidPlan = ! site?.plan?.is_free; - const isBilledMonthly = site?.plan?.product_slug?.includes( 'monthly' ); - - const transformedDomains = allDomains.map( createSiteDomainObject ); - const [ clipboardCopied, setClipboardCopied ] = useState( false ); - const clipboardButtonEl = useRef( null ); - const customDomains = transformedDomains.filter( ( domain ) => ! domain.isWPCOMDomain ); - const hasCustomDomain = customDomains.length > 0; - const hasEnTranslation = useHasEnTranslation(); - const siteDomain = hasCustomDomain ? customDomains[ 0 ].domain : site.slug; - useEffect( () => { - // remove the celebrateLaunch URL param without reloading the page as soon as the modal loads - // make sure the modal is shown only once - window.history.replaceState( - null, - '', - omitUrlParams( window.location.href, 'celebrateLaunch' ) - ); - - dispatch( - recordTracksEvent( `calypso_launchpad_celebration_modal_view`, { - product_slug: site?.plan?.product_slug, - } ) - ); - }, [] ); - - function renderUpsellContent() { - let contentElement; - let buttonText; - let buttonHref; - - if ( ! isPaidPlan && ! hasCustomDomain ) { - contentElement = ( -

- { translate( - 'Supercharge your website with a {{strong}}custom address{{/strong}} that matches your blog, brand, or business.', - { components: { strong: } } - ) } -

- ); - buttonText = hasEnTranslation( 'Get your domain' ) - ? translate( 'Get your domain' ) - : translate( 'Claim your domain' ); - buttonHref = `/domains/add/${ site.slug }`; - } else if ( isPaidPlan && isBilledMonthly && ! hasCustomDomain ) { - contentElement = ( -

- { translate( - 'Interested in a custom domain? It’s free for the first year when you switch to annual billing.' - ) } -

- ); - buttonText = hasEnTranslation( 'Get your domain' ) - ? translate( 'Get your domain' ) - : translate( 'Claim your domain' ); - buttonHref = `/domains/add/${ site.slug }`; - } else if ( isPaidPlan && ! hasCustomDomain ) { - contentElement = ( -

- { translate( - 'Your paid plan includes a domain name {{strong}}free for one year{{/strong}}. Choose one that’s easy to remember and even easier to share.', - { components: { strong: } } - ) } -

- ); - buttonText = hasEnTranslation( 'Get your free domain' ) - ? translate( 'Get your free domain' ) - : translate( 'Claim your free domain' ); - buttonHref = `/domains/add/${ site.slug }`; - } else if ( hasCustomDomain ) { - return null; - } - - return ( -
-
{ contentElement }
- -
- ); - } - - return ( - setModalIsOpen( false ) } className="launched__modal"> - -
-
-

- { translate( 'Congrats, your site is live!' ) } -

-

- { translate( 'Now you can head over to your site and share it with the world.' ) } -

-
-
-
-
-

{ siteDomain }

- setClipboardCopied( true ) } - onMouseLeave={ () => setClipboardCopied( false ) } - ref={ clipboardButtonEl } - > - - - - { translate( 'Copied to clipboard!' ) } - -
- - -
-
-
- { renderUpsellContent() } -
- ); -} - -export default CelebrateLaunchModal; diff --git a/client/my-sites/customer-home/components/celebrate-launch-modal.scss b/client/my-sites/customer-home/components/celebrate-launch-modal.scss deleted file mode 100644 index f26a4d9b1683..000000000000 --- a/client/my-sites/customer-home/components/celebrate-launch-modal.scss +++ /dev/null @@ -1,156 +0,0 @@ -@import "https://fonts.googleapis.com/css?family=Noto+Serif:400,400i,700,700i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese&display=swap"; - -$breakpoint-mobile: 782px; //Mobile size. - -.launched__modal { - max-width: 640px; - - .components-modal__header { - padding: 48px; - } - - .components-modal__content { - margin: 0; - padding: 0; - } - - &-content { - padding: 48px; - padding-bottom: 40px; - display: flex; - flex-direction: column; - gap: 32px; - - @media (min-width: $breakpoint-mobile) { - min-width: 640px; - } - } - - &-heading { - color: var(--studio-gray-100); - font-family: Recoleta, "Noto Serif", Georgia, "Times New Roman", Times, serif; - font-size: 2rem; - line-height: 40px; - font-weight: 400; - letter-spacing: 0.2px; - margin: 0; - padding-bottom: 8px; - } - - &-domain { - display: flex; - justify-content: center; - align-items: center; - max-width: 100%; - @media (min-width: $breakpoint-mobile) { - max-width: 75%; - } - } - - &-body, - &-domain-text { - margin: 0; - color: var(--studio-gray-80); - font-size: 1rem; - line-height: 24px; - } - - &-domain-text { - padding: 0 8px; - - // prevent text overflow - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - - &-buttons { - display: flex; - flex-direction: row; - justify-content: end; - gap: 16px; - } - - &-site { - padding: 8px; - background: var(--studio-gray-0); - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - @media (min-width: $breakpoint-mobile) { - flex-direction: row; - } - } - - .launchpad__clipboard-button { - min-width: 18px; - opacity: 0; - } - - .launchpad__clipboard-button:focus { - opacity: 1; - } - - &-site:hover { - .launchpad__clipboard-button { - opacity: 1; - } - } - - &-customize { - color: var(--studio-blue-50); - font-size: 0.875rem; - display: inline-flex; - flex-direction: row; - justify-content: start; - gap: 6px; - padding: 0; - margin: 0; - margin-top: 16px; - } - - &-upsell { - border-top: 1px solid var(--studio-gray-5); - background: var(--studio-gray-0); - display: flex; - justify-content: center; - align-items: center; - padding: 32px 48px; - gap: 32px; - flex-direction: column; - @media (min-width: $breakpoint-mobile) { - flex-direction: row; - } - .components-button { - height: 42px; - } - } - - &-upsell-content p { - margin-bottom: 0; - } - - &-upsell-content-highlight { - font-weight: bold; - } - - &-view-site { - display: flex; - gap: 4px; - - font-weight: 500; - font-size: 14px; - line-height: 20px; - letter-spacing: -0.154px; - color: #101517; - } - - &-view-site:visited { - color: #101517; - } - - &-view-site:hover { - text-decoration: underline; - } -} diff --git a/client/my-sites/customer-home/components/full-screen-launchpad.tsx b/client/my-sites/customer-home/components/full-screen-launchpad.tsx index 249b84cf1110..a990d6f02244 100644 --- a/client/my-sites/customer-home/components/full-screen-launchpad.tsx +++ b/client/my-sites/customer-home/components/full-screen-launchpad.tsx @@ -28,9 +28,11 @@ import './full-screen-launchpad.scss'; export const FullScreenLaunchpad = ( { onClose, onSiteLaunch, + beforeSiteLaunchRefetch, }: { onClose: () => void; onSiteLaunch: () => void; + beforeSiteLaunchRefetch: () => void; } ): JSX.Element | null => { const dispatch = useDispatch(); const { __ } = useI18n(); @@ -71,6 +73,7 @@ export const FullScreenLaunchpad = ( { await refetch?.(); await layout?.refetch(); + beforeSiteLaunchRefetch(); await dispatch( requestSite( siteId ) ); recordTracksEvent( 'calypso_full_screen_launchpad_launch_site', { context: launchpadContext, diff --git a/client/my-sites/customer-home/components/home-content/index.jsx b/client/my-sites/customer-home/components/home-content/index.jsx index bb7704985ac6..7321a78bd1cf 100644 --- a/client/my-sites/customer-home/components/home-content/index.jsx +++ b/client/my-sites/customer-home/components/home-content/index.jsx @@ -16,7 +16,6 @@ import NoticeAction from 'calypso/components/notice/notice-action'; import ResurrectedWelcomeModalGate from 'calypso/components/resurrected-welcome-modal'; import { dashboardLink } from 'calypso/dashboard/utils/link'; import useDomainDiagnosticsQuery from 'calypso/data/domains/diagnostics/use-domain-diagnostics-query'; -import { useGetDomainsQuery } from 'calypso/data/domains/use-get-domains-query'; import useHomeLayoutQuery, { getCacheKey } from 'calypso/data/home/use-home-layout-query'; import useSkipCurrentViewMutation from 'calypso/data/home/use-skip-current-view-mutation'; import { usePurchasePlanNotification } from 'calypso/landing/stepper/declarative-flow/internals/hooks/use-purchase-plan-notification'; @@ -24,6 +23,7 @@ import TrackComponentView from 'calypso/lib/analytics/track-component-view'; import { setDomainNotice } from 'calypso/lib/domains/set-domain-notice'; import { preventWidows } from 'calypso/lib/formatting'; import { getQueryArgs } from 'calypso/lib/query-args'; +import { useCelebrateLaunchModalSideEffects } from 'calypso/my-sites/customer-home/celebrate-site-launch-modal/use-side-effects'; import Primary from 'calypso/my-sites/customer-home/locations/primary'; import Secondary from 'calypso/my-sites/customer-home/locations/secondary'; import Tertiary from 'calypso/my-sites/customer-home/locations/tertiary'; @@ -44,6 +44,7 @@ import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-act import isUserRegistrationDaysWithinRange from 'calypso/state/selectors/is-user-registration-days-within-range'; import { getDomainsBySiteId } from 'calypso/state/sites/domains/selectors'; import { launchSite } from 'calypso/state/sites/launch/actions'; +import { getIsSiteLaunchCelebrationModalOpen } from 'calypso/state/sites/launch/selectors'; import { isSiteOnWooExpressEcommerceTrial } from 'calypso/state/sites/plans/selectors'; import { canCurrentUserUseCustomerHome, @@ -52,7 +53,6 @@ import { } from 'calypso/state/sites/selectors'; import isJetpackSite from 'calypso/state/sites/selectors/is-jetpack-site'; import { getSelectedSite, getSelectedSiteId } from 'calypso/state/ui/selectors'; -import CelebrateLaunchModal from '../celebrate-launch-modal'; import { FullScreenLaunchpad } from '../full-screen-launchpad'; import openSyncUrlInStudio from './studio-deeplink'; @@ -76,7 +76,9 @@ const HomeContent = ( { isAdmin, dashboardOptIn, } ) => { - const [ celebrateLaunchModalIsOpen, setCelebrateLaunchModalIsOpen ] = useState( false ); + const celebrateLaunchModalIsOpen = useSelector( ( state ) => + getIsSiteLaunchCelebrationModalOpen( state, siteId ) + ); const [ launchedSiteId, setLaunchedSiteId ] = useState( null ); const queryClient = useQueryClient(); const translate = useTranslate(); @@ -85,14 +87,6 @@ const HomeContent = ( { const { data: layout, isLoading, error: homeLayoutError } = useHomeLayoutQuery( siteId ); const { skipCurrentView } = useSkipCurrentViewMutation( siteId ); - const { - data: allDomains = [], - isSuccess, - isFetchedAfterMount, - } = useGetDomainsQuery( site?.ID ?? null, { - retry: false, - } ); - const [ focusedLaunchpadDismissed, setFocusedLaunchpadDismissed ] = useState( false ); const siteDomains = useSelector( ( state ) => getDomainsBySiteId( state, siteId ) ); @@ -114,12 +108,6 @@ const HomeContent = ( { usePurchasePlanNotification( siteId, site?.plan?.product_slug ); - useEffect( () => { - if ( getQueryArgs().celebrateLaunch === 'true' && isSuccess && isFetchedAfterMount ) { - setCelebrateLaunchModalIsOpen( true ); - } - }, [ isSuccess, isFetchedAfterMount ] ); - useEffect( () => { if ( ! isSiteLaunching && launchedSiteId === siteId ) { queryClient.invalidateQueries( { queryKey: getCacheKey( siteId ) } ); @@ -157,6 +145,8 @@ const HomeContent = ( { Array.isArray( layout?.secondary ) && layout.secondary.length > 0; + const { addCelebrateLaunchQueryParams } = useCelebrateLaunchModalSideEffects( siteId ); + if ( ! canUserUseCustomerHome ) { const title = translate( 'This page is not available on this site.' ); return ; @@ -170,8 +160,8 @@ const HomeContent = ( { await updateLaunchpadSettings( siteId, { launchpad_screen: 'skipped' } ); skipCurrentView( null, true ); } } + beforeSiteLaunchRefetch={ addCelebrateLaunchQueryParams } onSiteLaunch={ () => { - setCelebrateLaunchModalIsOpen( true ); setFocusedLaunchpadDismissed( true ); } } /> @@ -362,13 +352,6 @@ const HomeContent = ( { ) : null } - { celebrateLaunchModalIsOpen && ( - - ) } diff --git a/client/my-sites/customer-home/components/home-content/test/home-content.tsx b/client/my-sites/customer-home/components/home-content/test/home-content.tsx index bc3ec043309a..29afcb2e282c 100644 --- a/client/my-sites/customer-home/components/home-content/test/home-content.tsx +++ b/client/my-sites/customer-home/components/home-content/test/home-content.tsx @@ -1,13 +1,14 @@ /** * @jest-environment jsdom */ -// @ts-nocheck - TODO: Fix TypeScript issues import { updateLaunchpadSettings } from '@automattic/data-stores'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, render, screen, waitFor } from '@testing-library/react'; +import nock from 'nock'; import React from 'react'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; +import CelebrateSiteLaunchModal from 'calypso/my-sites/customer-home/celebrate-site-launch-modal'; import HomeContent from '../index'; jest.mock( '@automattic/data-stores', () => ( { @@ -15,12 +16,19 @@ jest.mock( '@automattic/data-stores', () => ( { updateLaunchpadSettings: jest.fn().mockResolvedValue( {} ), } ) ); -// Add this mock for FullScreenLaunchpad jest.mock( '../../full-screen-launchpad', () => ( { - FullScreenLaunchpad: ( { onClose, onSiteLaunch } ) => ( + // @ts-expect-error - TODO: Fix TypeScript issues + FullScreenLaunchpad: ( { onClose, onSiteLaunch, beforeSiteLaunchRefetch } ) => (
- +
), } ) ); @@ -30,6 +38,8 @@ jest.mock( 'calypso/components/resurrected-welcome-modal', () => () => null ); const testSite = { ID: 1, slug: 'test-site', + launch_status: 'launched', + plan: { is_free: true, product_slug: 'free_plan' }, options: { site_creation_flow: 'onboarding', }, @@ -38,6 +48,13 @@ const testSite = { let mockLayoutViewName = ''; const mockLayout = { view_name: mockLayoutViewName }; +const mockDomainsApi = ( domains: unknown[] = [] ) => { + nock( 'https://public-api.wordpress.com' ) + .get( '/rest/v1.2/all-domains' ) + .query( true ) + .reply( 200, { domains } ); +}; + jest.mock( 'calypso/data/home/use-home-layout-query', () => { const getCacheKey = ( siteId: number ) => [ 'home-layout', siteId ]; return { @@ -107,6 +124,15 @@ describe( 'HomeContent', () => { beforeEach( () => { queryClient.clear(); mockLayout.view_name = mockLayoutViewName; + nock.cleanAll(); + mockDomainsApi( [] ); + const url = new URL( window.location.href ); + url.search = ''; + window.history.replaceState( {}, '', url.pathname + url.search ); + } ); + + afterEach( () => { + nock.cleanAll(); } ); describe( 'Focused Launchpad integration', () => { @@ -161,7 +187,13 @@ describe( 'HomeContent', () => { it( 'should show celebrate launch modal when site is launched', async () => { mockLayoutViewName = 'VIEW_FOCUSED_LAUNCHPAD'; - renderWithProviders( ); + window.history.pushState( {}, '', '/home?celebrateLaunch=true' ); + renderWithProviders( + <> + + + + ); const launchButton = screen.getByText( 'Launch your site' ); await act( async () => { diff --git a/client/my-sites/customer-home/test/celebrate-launch-modal.tsx b/client/my-sites/customer-home/test/celebrate-launch-modal.tsx deleted file mode 100644 index d590364fb45d..000000000000 --- a/client/my-sites/customer-home/test/celebrate-launch-modal.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { render, screen } from '@testing-library/react'; -import React from 'react'; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import CelebrateLaunchModal from '../components/celebrate-launch-modal'; - -const mockStore = configureStore(); - -describe( 'CelebrateLaunchModal', () => { - const mockSetModalIsOpen = jest.fn(); - - const defaultProps = { - setModalIsOpen: mockSetModalIsOpen, - site: { - slug: 'test-site.wordpress.com', - URL: 'https://test-site.wordpress.com', - plan: { - is_free: true, - product_slug: 'free_plan', - }, - }, - allDomains: [], - }; - - test( 'renders site slug when no custom domain is present', () => { - const store = mockStore( {} ); - render( - - - - ); - - expect( screen.getByText( 'test-site.wordpress.com' ) ).toBeInTheDocument(); - } ); - - test( 'renders custom domain when present', () => { - const store = mockStore( {} ); - const customDomain = 'mycustomdomain.com'; - const propsWithCustomDomain = { - ...defaultProps, - allDomains: [ - { - domain: customDomain, - type: 'REGISTERED', - wpcom_domain: false, - }, - ], - }; - - render( - - - - ); - - expect( screen.getByText( customDomain ) ).toBeInTheDocument(); - } ); - - test( 'prefers custom domain over site slug when both are present', () => { - const store = mockStore( {} ); - const customDomain = 'mycustomdomain.com'; - const propsWithBothDomains = { - ...defaultProps, - allDomains: [ - { - domain: customDomain, - type: 'REGISTERED', - wpcom_domain: false, - }, - { - domain: 'test-site.wordpress.com', - type: 'WPCOM', - wpcom_domain: true, - }, - ], - }; - - render( - - - - ); - - expect( screen.getByText( customDomain ) ).toBeInTheDocument(); - expect( screen.queryByText( 'test-site.wordpress.com' ) ).not.toBeInTheDocument(); - } ); -} ); diff --git a/client/state/action-types.ts b/client/state/action-types.ts index df5c661978d3..014762ee9823 100644 --- a/client/state/action-types.ts +++ b/client/state/action-types.ts @@ -871,6 +871,7 @@ export const SITE_KEYRINGS_UPDATE_SUCCESS = 'SITE_KEYRINGS_UPDATE_SUCCESS'; export const SITE_LAUNCH = 'SITE_LAUNCH'; export const SITE_LAUNCH_FAILURE = 'SITE_LAUNCH_FAILURE'; export const SITE_LAUNCH_SUCCESS = 'SITE_LAUNCH_SUCCESS'; +export const SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET = 'SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET'; export const SITE_MIGRATION_STATUS_UPDATE = 'SITE_MIGRATION_STATUS_UPDATE'; export const SITE_PLAN_OWNERSHIP_TRANSFER = 'SITE_PLAN_OWNERSHIP_TRANSFER'; export const SITE_PLANS_FETCH = 'SITE_PLANS_FETCH'; diff --git a/client/state/sites/launch/actions.js b/client/state/sites/launch/actions.js index 069f0d5a77f0..3bea904276fe 100644 --- a/client/state/sites/launch/actions.js +++ b/client/state/sites/launch/actions.js @@ -1,5 +1,10 @@ import { addQueryArgs } from 'calypso/lib/url'; -import { SITE_LAUNCH, SITE_LAUNCH_FAILURE, SITE_LAUNCH_SUCCESS } from 'calypso/state/action-types'; +import { + SITE_LAUNCH, + SITE_LAUNCH_FAILURE, + SITE_LAUNCH_SUCCESS, + SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET, +} from 'calypso/state/action-types'; import 'calypso/state/data-layer/wpcom/sites/launch'; import isUnlaunchedSite from 'calypso/state/selectors/is-unlaunched-site'; import { getDomainsBySiteId } from 'calypso/state/sites/domains/selectors'; @@ -28,6 +33,11 @@ export const launchSiteFailure = ( siteId ) => ( { siteId, } ); +export const setSiteLaunchCelebrationModalOpen = ( isOpen ) => ( { + type: SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET, + isOpen, +} ); + /** * @param {number} siteId * @param {string?} source diff --git a/client/state/sites/launch/reducers.ts b/client/state/sites/launch/reducers.ts index 4e93123dc254..a6291fbc12a3 100644 --- a/client/state/sites/launch/reducers.ts +++ b/client/state/sites/launch/reducers.ts @@ -1,6 +1,11 @@ import { combineReducers } from '@wordpress/data'; import { AnyAction } from 'redux'; -import { SITE_LAUNCH, SITE_LAUNCH_SUCCESS, SITE_LAUNCH_FAILURE } from 'calypso/state/action-types'; +import { + SITE_LAUNCH, + SITE_LAUNCH_SUCCESS, + SITE_LAUNCH_FAILURE, + SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET, +} from 'calypso/state/action-types'; const addInProgressSiteLaunch = ( state: number[], siteId: number ) => { if ( state.includes( siteId ) ) { @@ -25,7 +30,17 @@ export const siteLaunchesInProgress = ( state: number[] = [], action: AnyAction } }; +const celebrationModalOpen = ( state: boolean = false, action: AnyAction ) => { + switch ( action.type ) { + case SITE_LAUNCH_CELEBRATION_MODAL_OPEN_SET: + return action.isOpen; + default: + return state; + } +}; + export default combineReducers( { // Add more site launch related reducers here if needed inProgress: siteLaunchesInProgress, + celebrationModalOpen, } ); diff --git a/client/state/sites/launch/selectors.ts b/client/state/sites/launch/selectors.ts index c1a85479ba43..3af7b35f959c 100644 --- a/client/state/sites/launch/selectors.ts +++ b/client/state/sites/launch/selectors.ts @@ -2,6 +2,7 @@ type State = { sites?: { launch?: { inProgress?: number[]; + celebrationModalOpen?: boolean; }; }; }; @@ -13,3 +14,7 @@ export const getIsSiteLaunchInProgress = ( state: State, siteId: number ) => { } return siteLaunchesInProgress?.includes( siteId ); }; + +export const getIsSiteLaunchCelebrationModalOpen = ( state: State ) => { + return state?.sites?.launch?.celebrationModalOpen; +};