From bfbeaacccae0cc79ff0350f8440e0421f396cce7 Mon Sep 17 00:00:00 2001 From: Dylan Munson <65001528+CodeyGuyDylan@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:33:48 -0700 Subject: [PATCH] Fix/connection status inconsistencies (#40632) * Update connection status card to align with connection banner * unify the way we connect sites on My Jetpack * Unify user connection flows in My Jetpack * changelog * Fix tests * Add comment * Scroll to top all the time when connecting site * Add slight delay to scroll to top --- projects/packages/my-jetpack/_inc/admin.jsx | 88 ++++++++----------- .../connected-product-card/index.tsx | 2 +- .../components/connection-screen/index.tsx | 4 + .../connection-status-card/index.tsx | 41 +++++++-- .../connection-status-card/test/component.tsx | 41 +++++++-- .../connection-status-card/types.ts | 5 +- .../components/connections-section/index.jsx | 2 +- .../_inc/components/plans-section/index.tsx | 2 +- .../components/product-card/action-button.tsx | 25 ++++-- .../_inc/components/product-card/index.tsx | 30 ++++++- .../product-card/secondary-button.tsx | 4 +- .../welcome-flow/ConnectionStep.tsx | 63 ++----------- .../_inc/components/welcome-flow/index.tsx | 14 +-- .../packages/my-jetpack/_inc/constants.ts | 1 + .../_inc/context/notices/noticeTemplates.ts | 11 --- .../_inc/hooks/use-analytics/index.ts | 8 +- .../_inc/hooks/use-connect-site/index.ts | 81 +++++++++++++++++ .../hooks/use-my-jetpack-navigate/index.ts | 14 ++- .../use-site-connection-notice.tsx | 29 +++--- .../packages/my-jetpack/_inc/providers.tsx | 25 ++++++ .../fix-connection-status-inconsistencies | 4 + 21 files changed, 305 insertions(+), 189 deletions(-) create mode 100644 projects/packages/my-jetpack/_inc/hooks/use-connect-site/index.ts create mode 100644 projects/packages/my-jetpack/_inc/providers.tsx create mode 100644 projects/packages/my-jetpack/changelog/fix-connection-status-inconsistencies diff --git a/projects/packages/my-jetpack/_inc/admin.jsx b/projects/packages/my-jetpack/_inc/admin.jsx index 5bcaddd417d6d..4d2830eeefb76 100644 --- a/projects/packages/my-jetpack/_inc/admin.jsx +++ b/projects/packages/my-jetpack/_inc/admin.jsx @@ -1,8 +1,6 @@ /** * External dependencies */ -import { ThemeProvider } from '@automattic/jetpack-components'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRoot } from '@wordpress/element'; import { useEffect } from 'react'; import { HashRouter, Navigate, Routes, Route, useLocation } from 'react-router-dom'; @@ -32,10 +30,9 @@ import { import JetpackAiProductPage from './components/product-interstitial/jetpack-ai/product-page'; import RedeemTokenScreen from './components/redeem-token-screen'; import { MyJetpackRoutes } from './constants'; -import NoticeContextProvider from './context/notices/noticeContext'; -import ValueStoreContextProvider from './context/value-store/valueStoreContext'; import { getMyJetpackWindowInitialState } from './data/utils/get-my-jetpack-window-state'; import './style.module.scss'; +import Providers from './providers'; /** * Component to scroll window to top on route change. @@ -50,57 +47,44 @@ function ScrollToTop() { } const MyJetpack = () => { - const queryClient = new QueryClient(); const { loadAddLicenseScreen } = getMyJetpackWindowInitialState(); return ( - - - - - - - - } /> - } /> - } /> - { /* Redirect the old route for Anti Spam */ } - } - /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } - /> - } /> - { loadAddLicenseScreen && ( - } /> - ) } - } /> - } /> - } /> - } /> - } /> - } /> - - - - - - + + + + + } /> + } /> + } /> + { /* Redirect the old route for Anti Spam */ } + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + { loadAddLicenseScreen && ( + } /> + ) } + } /> + } /> + } /> + } /> + } /> + } /> + + + ); }; diff --git a/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx b/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx index 0f2218763f4a8..5953fda2cb767 100644 --- a/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/connected-product-card/index.tsx @@ -66,7 +66,7 @@ const ConnectedProductCard: FC< ConnectedProductCardProps > = ( { manageUrl, } = detail; - const navigateToConnectionPage = useMyJetpackNavigate( MyJetpackRoutes.Connection ); + const navigateToConnectionPage = useMyJetpackNavigate( MyJetpackRoutes.ConnectionSkipPricing ); /* * Redirect only if connected diff --git a/projects/packages/my-jetpack/_inc/components/connection-screen/index.tsx b/projects/packages/my-jetpack/_inc/components/connection-screen/index.tsx index 52954e68d9a06..f8da3fddc6c45 100644 --- a/projects/packages/my-jetpack/_inc/components/connection-screen/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/connection-screen/index.tsx @@ -1,5 +1,6 @@ import { Container, Col, AdminPage } from '@automattic/jetpack-components'; import { __ } from '@wordpress/i18n'; +import { useSearchParams } from 'react-router-dom'; import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; import useMyJetpackReturnToPage from '../../hooks/use-my-jetpack-return-to-page'; import CloseLink from '../close-link'; @@ -9,6 +10,8 @@ import styles from './styles.module.scss'; import type { FC } from 'react'; const ConnectionScreen: FC = () => { + const [ searchParams ] = useSearchParams(); + const shouldSkipPricing = searchParams.get( 'skip_pricing' ) === 'true'; const returnToPage = useMyJetpackReturnToPage(); const { apiRoot, apiNonce, registrationNonce } = useMyJetpackConnection(); @@ -28,6 +31,7 @@ const ConnectionScreen: FC = () => { apiRoot={ apiRoot } apiNonce={ apiNonce } registrationNonce={ registrationNonce } + skipPricingPage={ shouldSkipPricing } footer={ } /> diff --git a/projects/packages/my-jetpack/_inc/components/connection-status-card/index.tsx b/projects/packages/my-jetpack/_inc/components/connection-status-card/index.tsx index 404d0fb5bbc95..f2e8827c01da6 100644 --- a/projects/packages/my-jetpack/_inc/components/connection-status-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/connection-status-card/index.tsx @@ -9,6 +9,7 @@ import { useAllProducts } from '../../data/products/use-product'; import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; import getProductSlugsThatRequireUserConnection from '../../data/utils/get-product-slugs-that-require-user-connection'; import useAnalytics from '../../hooks/use-analytics'; +import useConnectSite from '../../hooks/use-connect-site'; import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; import cloud from './cloud.svg'; import emptyAvatar from './empty-avatar.svg'; @@ -33,6 +34,11 @@ const ConnectionListItem: ConnectionListItemType = ( { let icon = check; let statusStyles = ''; + if ( status === 'info' ) { + icon = null; + statusStyles = ''; + } + if ( status === 'success' ) { icon = check; statusStyles = styles.success; @@ -50,13 +56,13 @@ const ConnectionListItem: ConnectionListItemType = ( { if ( status === 'unlock' ) { icon = lockOutline; - statusStyles = styles.unlock; + statusStyles = ''; } return (
- + { icon && } { text } { actionText && status !== 'success' && ( @@ -77,9 +83,17 @@ const ConnectionItemButton: ConnectionItemButtonType = ( { actionText, onClick } const getSiteConnectionLineData: getSiteConnectionLineDataType = ( { isRegistered, hasSiteConnectionBrokenModules, - handleConnectUser, + handleConnectSite, + siteIsRegistering, openManageSiteConnectionDialog, } ) => { + if ( siteIsRegistering ) { + return { + text: __( 'Connecting your siteā€¦', 'jetpack-my-jetpack' ), + status: 'info', + }; + } + if ( isRegistered ) { return { onClick: openManageSiteConnectionDialog, @@ -91,7 +105,7 @@ const getSiteConnectionLineData: getSiteConnectionLineDataType = ( { if ( hasSiteConnectionBrokenModules ) { return { - onClick: handleConnectUser, + onClick: handleConnectSite, text: __( 'Missing site connection to enable some features.', 'jetpack-my-jetpack' ), actionText: __( 'Connect', 'jetpack-my-jetpack' ), status: 'error', @@ -99,7 +113,7 @@ const getSiteConnectionLineData: getSiteConnectionLineDataType = ( { } return { - onClick: handleConnectUser, + onClick: handleConnectSite, text: __( 'Start with Jetpack.', 'jetpack-my-jetpack' ), actionText: __( 'Connect your site with one click', 'jetpack-my-jetpack' ), status: 'warning', @@ -194,13 +208,16 @@ const ConnectionStatusCard: ConnectionStatusCardType = ( { const { isRegistered, isUserConnected, userConnectionData } = useMyJetpackConnection( { redirectUri, } ); - + const { siteIsRegistering } = useMyJetpackConnection( { + skipUserConnection: true, + redirectUri, + } ); + const { lifecycleStats } = getMyJetpackWindowInitialState(); const { recordEvent } = useAnalytics(); const [ isManageConnectionDialogOpen, setIsManageConnectionDialogOpen ] = useState( false ); const { setConnectionStatus, setUserIsConnecting } = useDispatch( CONNECTION_STORE_ID ); const connectUserFn = onConnectUser || setUserIsConnecting; const avatar = userConnectionData.currentUser?.wpcomUser?.avatar; - const { lifecycleStats } = getMyJetpackWindowInitialState(); const { brokenModules } = lifecycleStats || {}; const products = useAllProducts(); const hasProductsThatRequireUserConnection = @@ -272,6 +289,13 @@ const ConnectionStatusCard: ConnectionStatusCardType = ( { [ connectUserFn, recordEvent, tracksEventData ] ); + const { connectSite: handleConnectSite } = useConnectSite( { + tracksInfo: { + event: 'jetpack_myjetpack_connection_connect_site', + properties: tracksEventData, + }, + } ); + const getConnectionLineStyles = () => { if ( isRegistered ) { return ''; @@ -283,7 +307,8 @@ const ConnectionStatusCard: ConnectionStatusCardType = ( { const siteConnectionLineData = getSiteConnectionLineData( { isRegistered, hasSiteConnectionBrokenModules, - handleConnectUser, + handleConnectSite, + siteIsRegistering, openManageSiteConnectionDialog, } ); diff --git a/projects/packages/my-jetpack/_inc/components/connection-status-card/test/component.tsx b/projects/packages/my-jetpack/_inc/components/connection-status-card/test/component.tsx index d5e88d4dd6ce2..6f3a89ad32ebc 100644 --- a/projects/packages/my-jetpack/_inc/components/connection-status-card/test/component.tsx +++ b/projects/packages/my-jetpack/_inc/components/connection-status-card/test/component.tsx @@ -2,6 +2,7 @@ import '@testing-library/jest-dom'; import { CONNECTION_STORE_ID } from '@automattic/jetpack-connection'; import { render, renderHook, screen } from '@testing-library/react'; import { useSelect } from '@wordpress/data'; +import Providers from '../../../providers'; import ConnectionStatusCard from '../index'; import type { StateProducts, MyJetpackInitialState } from '../../../data/types'; @@ -58,7 +59,9 @@ const setConnectionStore = ( { hasConnectedOwner = false, } = {} ) => { let storeSelect; - renderHook( () => useSelect( select => ( storeSelect = select( CONNECTION_STORE_ID ) ), [] ) ); + renderHook( () => useSelect( select => ( storeSelect = select( CONNECTION_STORE_ID ) ), [] ), { + wrapper: Providers, + } ); jest .spyOn( storeSelect, 'getConnectionStatus' ) .mockReset() @@ -80,7 +83,11 @@ describe( 'ConnectionStatusCard', () => { describe( 'When the site is not registered and has no broken modules', () => { const setup = () => { - return render( ); + return render( + + + + ); }; it( 'renders the correct copy for the site connection line item', () => { @@ -103,7 +110,11 @@ describe( 'ConnectionStatusCard', () => { window.myJetpackInitialState.lifecycleStats.brokenModules.needs_site_connection = [ 'anti-spam', ]; - return render( ); + return render( + + + + ); }; it( 'renders the correct copy for the site connection line item', () => { @@ -125,7 +136,11 @@ describe( 'ConnectionStatusCard', () => { describe( 'There are no products that require user connection', () => { const setup = () => { setConnectionStore( { isRegistered: true } ); - return render( ); + return render( + + + + ); }; it( 'renders the correct site connection line item', () => { @@ -145,7 +160,11 @@ describe( 'ConnectionStatusCard', () => { const setup = () => { setConnectionStore( { isRegistered: true } ); window.myJetpackInitialState.products.items[ 'anti-spam' ].requires_user_connection = true; - return render( ); + return render( + + + + ); }; it( 'renders the correct site connection line item', () => { @@ -168,7 +187,11 @@ describe( 'ConnectionStatusCard', () => { window.myJetpackInitialState.lifecycleStats.brokenModules.needs_user_connection = [ 'anti-spam', ]; - return render( ); + return render( + + + + ); }; it( 'renders the correct site connection line item', () => { @@ -189,7 +212,11 @@ describe( 'ConnectionStatusCard', () => { describe( 'When the user has connected their WordPress.com account', () => { const setup = () => { setConnectionStore( { isRegistered: true, isUserConnected: true, hasConnectedOwner: true } ); - return render( ); + return render( + + + + ); }; it( 'renders the correct site connection line item', () => { diff --git a/projects/packages/my-jetpack/_inc/components/connection-status-card/types.ts b/projects/packages/my-jetpack/_inc/components/connection-status-card/types.ts index d236b530e9cfa..3691852f4cc5e 100644 --- a/projects/packages/my-jetpack/_inc/components/connection-status-card/types.ts +++ b/projects/packages/my-jetpack/_inc/components/connection-status-card/types.ts @@ -1,6 +1,6 @@ import type { FC, MouseEvent } from 'react'; -type StatusType = 'warning' | 'error' | 'unlock' | 'success'; +type StatusType = 'warning' | 'error' | 'unlock' | 'success' | 'info'; interface ConnectionListItemProps { text: string; @@ -19,7 +19,8 @@ export type ConnectionItemButtonType = FC< { interface getSiteConnectionLineDataProps { isRegistered: boolean; hasSiteConnectionBrokenModules: boolean; - handleConnectUser: ( e: MouseEvent< HTMLButtonElement > ) => void; + siteIsRegistering: boolean; + handleConnectSite: ( e: MouseEvent< HTMLButtonElement > ) => void; openManageSiteConnectionDialog: ( e: MouseEvent ) => void; } diff --git a/projects/packages/my-jetpack/_inc/components/connections-section/index.jsx b/projects/packages/my-jetpack/_inc/components/connections-section/index.jsx index ffb1da1e64cd9..a1ba26eff727f 100644 --- a/projects/packages/my-jetpack/_inc/components/connections-section/index.jsx +++ b/projects/packages/my-jetpack/_inc/components/connections-section/index.jsx @@ -12,7 +12,7 @@ import ConnectionStatusCard from '../connection-status-card'; */ export default function ConnectionsSection() { const { apiRoot, apiNonce, topJetpackMenuItemUrl, connectedPlugins } = useMyJetpackConnection(); - const navigate = useMyJetpackNavigate( MyJetpackRoutes.Connection ); + const navigate = useMyJetpackNavigate( MyJetpackRoutes.ConnectionSkipPricing ); const products = useAllProducts(); const onDisconnected = () => document?.location?.reload( true ); // TODO: replace with a better experience. const productsThatRequireUserConnection = getProductSlugsThatRequireUserConnection( products ); diff --git a/projects/packages/my-jetpack/_inc/components/plans-section/index.tsx b/projects/packages/my-jetpack/_inc/components/plans-section/index.tsx index b4b3aef6dba03..59922bcd6ef4f 100644 --- a/projects/packages/my-jetpack/_inc/components/plans-section/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/plans-section/index.tsx @@ -178,7 +178,7 @@ const PlanSectionFooter: FC< PlanSectionHeaderAndFooterProps > = ( { numberOfPur recordEvent( 'jetpack_myjetpack_plans_purchase_click' ); }, [ recordEvent ] ); - const navigateToConnectionPage = useMyJetpackNavigate( MyJetpackRoutes.Connection ); + const navigateToConnectionPage = useMyJetpackNavigate( MyJetpackRoutes.ConnectionSkipPricing ); const activateLicenseClickHandler = useCallback( () => { recordEvent( 'jetpack_myjetpack_activate_license_click' ); if ( ! isUserConnected ) { diff --git a/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx b/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx index 82878556ba7c6..eb77e4e25c744 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/action-button.tsx @@ -7,14 +7,16 @@ import { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { PRODUCT_STATUSES } from '../../constants'; import useProduct from '../../data/products/use-product'; import useAnalytics from '../../hooks/use-analytics'; +import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; import useOutsideAlerter from '../../hooks/use-outside-alerter'; import styles from './style.module.scss'; import { ProductCardProps } from '.'; import type { SecondaryButtonProps } from './secondary-button'; -import type { FC, ComponentProps } from 'react'; +import type { FC, ComponentProps, MouseEvent } from 'react'; type ActionButtonProps< A = () => void > = ProductCardProps & { - onFixConnection?: A; + onFixUserConnection?: A; + onFixSiteConnection?: ( { e }: { e: MouseEvent< HTMLButtonElement > } ) => void; onManage?: A; onAdd?: A; onInstall?: A; @@ -34,7 +36,8 @@ const ActionButton: FC< ActionButtonProps > = ( { additionalActions, primaryActionOverride, onManage, - onFixConnection, + onFixUserConnection, + onFixSiteConnection, isFetching, isInstallingStandalone, className, @@ -50,6 +53,7 @@ const ActionButton: FC< ActionButtonProps > = ( { const [ currentAction, setCurrentAction ] = useState< ComponentProps< typeof Button > >( {} ); const { detail } = useProduct( slug ); const { manageUrl, purchaseUrl, managePaidPlanPurchaseUrl, renewPaidPlanPurchaseUrl } = detail; + const { siteIsRegistering } = useMyJetpackConnection(); const isManageDisabled = ! manageUrl; const dropdownRef = useRef( null ); const chevronRef = useRef( null ); @@ -57,7 +61,10 @@ const ActionButton: FC< ActionButtonProps > = ( { slug === 'jetpack-ai' && debug( slug, detail ); - const isBusy = isFetching || isInstallingStandalone; + const isBusy = + isFetching || + isInstallingStandalone || + ( siteIsRegistering && status === PRODUCT_STATUSES.SITE_CONNECTION_ERROR ); const hasAdditionalActions = additionalActions?.length > 0; const buttonState = useMemo< Partial< SecondaryButtonProps > >( () => { @@ -160,20 +167,19 @@ const ActionButton: FC< ActionButtonProps > = ( { case PRODUCT_STATUSES.SITE_CONNECTION_ERROR: return { ...buttonState, - href: '#/connection', variant: 'primary', label: __( 'Connect', 'jetpack-my-jetpack' ), - onClick: onFixConnection, + onClick: onFixSiteConnection, ...( primaryActionOverride && PRODUCT_STATUSES.SITE_CONNECTION_ERROR in primaryActionOverride && primaryActionOverride[ PRODUCT_STATUSES.SITE_CONNECTION_ERROR ] ), }; case PRODUCT_STATUSES.USER_CONNECTION_ERROR: return { - href: '#/connection', + href: '#/connection?skip_pricing=true', variant: 'primary', label: __( 'Connect', 'jetpack-my-jetpack' ), - onClick: onFixConnection, + onClick: onFixUserConnection, ...( primaryActionOverride && PRODUCT_STATUSES.USER_CONNECTION_ERROR in primaryActionOverride && primaryActionOverride[ PRODUCT_STATUSES.USER_CONNECTION_ERROR ] ), @@ -228,7 +234,8 @@ const ActionButton: FC< ActionButtonProps > = ( { buttonState, slug, onAdd, - onFixConnection, + onFixUserConnection, + onFixSiteConnection, onActivate, onInstall, onLearnMore, diff --git a/projects/packages/my-jetpack/_inc/components/product-card/index.tsx b/projects/packages/my-jetpack/_inc/components/product-card/index.tsx index 5fbe96fd6f7e5..ba235dfd9fa8b 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/index.tsx @@ -4,6 +4,8 @@ import { useCallback, useEffect } from 'react'; import { PRODUCT_STATUSES } from '../../constants'; import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; import useAnalytics from '../../hooks/use-analytics'; +import useConnectSite from '../../hooks/use-connect-site'; +import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; import Card from '../card'; import ActionButton from './action-button'; import PriceComponent from './pricing-component'; @@ -13,7 +15,7 @@ import Status from './status'; import styles from './style.module.scss'; import type { AdditionalAction, SecondaryAction } from './types'; import type { MutateCallback } from '../../data/use-simple-mutation'; -import type { FC, MouseEventHandler, ReactNode } from 'react'; +import type { FC, MouseEvent, MouseEventHandler, ReactNode } from 'react'; export type ProductCardProps = { children?: ReactNode; @@ -86,6 +88,15 @@ const ProductCard: FC< ProductCardProps > = props => { } ); const { recordEvent } = useAnalytics(); + const { siteIsRegistering } = useMyJetpackConnection(); + const isLoading = + isFetching || ( siteIsRegistering && status === PRODUCT_STATUSES.SITE_CONNECTION_ERROR ); + const { connectSite } = useConnectSite( { + tracksInfo: { + event: 'jetpack_myjetpack_product_card_fix_site_connection', + properties: {}, + }, + } ); /** * Calls the passed function onActivate after firing Tracks event @@ -118,12 +129,22 @@ const ProductCard: FC< ProductCardProps > = props => { /** * Calls the passed function onFixConnection after firing Tracks event */ - const fixConnectionHandler = useCallback( () => { + const fixUserConnectionHandler = useCallback( () => { recordEvent( 'jetpack_myjetpack_product_card_fixconnection_click', { product: slug, } ); }, [ slug, recordEvent ] ); + /** + * Calls the passed function onFixSiteConnection after firing Tracks event + */ + const fixSiteConnectionHandler = useCallback( + ( { e }: { e: MouseEvent< HTMLButtonElement > } ) => { + connectSite( e ); + }, + [ connectSite ] + ); + /** * Calls when the "Learn more" button is clicked */ @@ -182,7 +203,8 @@ const ProductCard: FC< ProductCardProps > = props => { = props => {
diff --git a/projects/packages/my-jetpack/_inc/components/product-card/secondary-button.tsx b/projects/packages/my-jetpack/_inc/components/product-card/secondary-button.tsx index 66f123af9e6b0..6e00803829ef8 100644 --- a/projects/packages/my-jetpack/_inc/components/product-card/secondary-button.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-card/secondary-button.tsx @@ -1,6 +1,6 @@ import { Button } from '@automattic/jetpack-components'; import { __ } from '@wordpress/i18n'; -import type { FC, ReactNode } from 'react'; +import type { FC, ReactNode, MouseEvent } from 'react'; export type SecondaryButtonProps = { href?: string; @@ -9,7 +9,7 @@ export type SecondaryButtonProps = { weight?: 'bold' | 'regular'; label?: string; shouldShowButton?: () => boolean; - onClick?: () => void; + onClick?: ( () => void ) | ( ( { e }: { e: MouseEvent< HTMLButtonElement > } ) => void ); isExternalLink?: boolean; icon?: ReactNode; iconSize?: number; diff --git a/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx b/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx index c7ec82668dd02..e4478788930b0 100644 --- a/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx +++ b/projects/packages/my-jetpack/_inc/components/welcome-flow/ConnectionStep.tsx @@ -1,69 +1,24 @@ -import { Col, Button, Text, TermsOfService, getRedirectUrl } from '@automattic/jetpack-components'; +import { Col, Button, Text, TermsOfService } from '@automattic/jetpack-components'; import { __ } from '@wordpress/i18n'; -import { useCallback, useContext } from 'react'; -import { NoticeContext } from '../../context/notices/noticeContext'; -import { NOTICE_SITE_CONNECTION_ERROR } from '../../context/notices/noticeTemplates'; -import useProductsByOwnership from '../../data/products/use-products-by-ownership'; -import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; -import useAnalytics from '../../hooks/use-analytics'; +import useConnectSite from '../../hooks/use-connect-site'; import styles from './style.module.scss'; import { WelcomeFlowExperiment } from '.'; import type { Dispatch, SetStateAction } from 'react'; type ConnectionStepProps = { - onActivateSite: ( e?: Event ) => Promise< void >; onUpdateWelcomeFlowExperiment?: Dispatch< SetStateAction< WelcomeFlowExperiment > >; isActivating: boolean; }; -/** - * Component that renders the Welcome banner on My Jetpack. - * - * @param {object} props - ConnectioStepProps - * @param {Function} props.onActivateSite - Alias for handleRegisterSite - * @param {boolean} props.isActivating - Alias for siteIsRegistering - * @return {object} The ConnectionStep component. - */ -const ConnectionStep = ( { onActivateSite, isActivating }: ConnectionStepProps ) => { - const { recordEvent } = useAnalytics(); - const { setNotice, resetNotice } = useContext( NoticeContext ); - - const { siteSuffix, adminUrl } = getMyJetpackWindowInitialState(); - const connectAfterCheckoutUrl = `?connect_after_checkout=true&admin_url=${ encodeURIComponent( - adminUrl - ) }&from_site_slug=${ siteSuffix }&source=my-jetpack`; - const redirectUri = `&redirect_to=${ encodeURIComponent( window.location.href ) }`; - const query = `${ connectAfterCheckoutUrl }${ redirectUri }&unlinked=1`; - const jetpackPlansPath = getRedirectUrl( 'jetpack-my-jetpack-site-only-plans', { query } ); - +const ConnectionStep = ( { isActivating }: ConnectionStepProps ) => { const activationButtonLabel = __( 'Activate Jetpack in one click', 'jetpack-my-jetpack' ); - const { refetch: refetchOwnershipData } = useProductsByOwnership(); - - const onConnectSiteClick = useCallback( async () => { - resetNotice(); - - recordEvent( 'jetpack_myjetpack_welcome_banner_connect_site_click' ); - - try { - await onActivateSite(); - - recordEvent( 'jetpack_myjetpack_welcome_banner_connect_site_success' ); - // Redirect user to the plans page after connection - window.location.href = jetpackPlansPath; - } catch { - setNotice( NOTICE_SITE_CONNECTION_ERROR, resetNotice ); - } finally { - refetchOwnershipData(); - } - }, [ - jetpackPlansPath, - onActivateSite, - recordEvent, - refetchOwnershipData, - resetNotice, - setNotice, - ] ); + const { connectSite: onConnectSiteClick } = useConnectSite( { + tracksInfo: { + event: 'jetpack_myjetpack_welcome_banner_connect_site', + properties: {}, + }, + } ); return ( <> diff --git a/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx b/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx index 1489aad58cbfc..c281570efbdc2 100644 --- a/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx +++ b/projects/packages/my-jetpack/_inc/components/welcome-flow/index.tsx @@ -34,15 +34,10 @@ const WelcomeFlow: FC< Props > = ( { const { dismissWelcomeBanner } = useWelcomeBanner(); const { recommendedModules, submitEvaluation, saveEvaluationResult } = useEvaluationRecommendations(); - const { - siteIsRegistered, - siteIsRegistering, - isUserConnected, - isSiteConnected, - handleRegisterSite, - } = useMyJetpackConnection( { - skipUserConnection: true, - } ); + const { siteIsRegistered, siteIsRegistering, isUserConnected, isSiteConnected } = + useMyJetpackConnection( { + skipUserConnection: true, + } ); const [ isProcessingEvaluation, setIsProcessingEvaluation ] = useState( false ); const [ prevStep, setPrevStep ] = useState( '' ); @@ -162,7 +157,6 @@ const WelcomeFlow: FC< Props > = ( { > { 'connection' === currentStep && ( diff --git a/projects/packages/my-jetpack/_inc/constants.ts b/projects/packages/my-jetpack/_inc/constants.ts index 1503114ca4de0..c85bd53950bd8 100644 --- a/projects/packages/my-jetpack/_inc/constants.ts +++ b/projects/packages/my-jetpack/_inc/constants.ts @@ -6,6 +6,7 @@ export const MY_JETPACK_PRODUCT_CHECKOUT = 'my-jetpack-product-checkout'; export const MyJetpackRoutes = { Home: '/', Connection: '/connection', + ConnectionSkipPricing: '/connection?skip_pricing=true', AddAkismet: '/add-akismet', AddAntiSpam: '/add-anti-spam', // Old route for Anti Spam AddBackup: '/add-backup', diff --git a/projects/packages/my-jetpack/_inc/context/notices/noticeTemplates.ts b/projects/packages/my-jetpack/_inc/context/notices/noticeTemplates.ts index f224cc00db52e..fe178f8272c6a 100644 --- a/projects/packages/my-jetpack/_inc/context/notices/noticeTemplates.ts +++ b/projects/packages/my-jetpack/_inc/context/notices/noticeTemplates.ts @@ -4,17 +4,6 @@ import { Notice } from './types'; export const WELCOME_BANNER_NOTICE_IDS: string[] = [ 'site-connection-success-notice' ]; -export const NOTICE_SITE_CONNECTED: Notice = { - message: __( 'Your site has been successfully connected.', 'jetpack-my-jetpack' ), - options: { - id: 'site-connection-success-notice', - level: 'success', - actions: [], - priority: NOTICE_PRIORITY_HIGH, - hideCloseButton: false, - }, -}; - export const NOTICE_SITE_CONNECTION_ERROR: Notice = { message: __( 'Site connection failed. Please try again.', 'jetpack-my-jetpack' ), options: { diff --git a/projects/packages/my-jetpack/_inc/hooks/use-analytics/index.ts b/projects/packages/my-jetpack/_inc/hooks/use-analytics/index.ts index 75d0f62e04787..48e6e26d88232 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-analytics/index.ts +++ b/projects/packages/my-jetpack/_inc/hooks/use-analytics/index.ts @@ -3,10 +3,10 @@ import { useCallback, useEffect } from 'react'; import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; import useMyJetpackConnection from '../use-my-jetpack-connection'; -type TracksRecordEvent = ( - event: `jetpack_${ string }`, // Enforces the event name to start with "jetpack_" - properties?: Record< Lowercase< string >, unknown > -) => void; +export type TracksEvent = `jetpack_${ string }`; // Enforces the event name to start with "jetpack_" +export type TracksProperties = Record< Lowercase< string >, unknown >; // Defines the shape of the properties object + +type TracksRecordEvent = ( event: TracksEvent, properties?: TracksProperties ) => void; const useAnalytics = () => { const { diff --git a/projects/packages/my-jetpack/_inc/hooks/use-connect-site/index.ts b/projects/packages/my-jetpack/_inc/hooks/use-connect-site/index.ts new file mode 100644 index 0000000000000..7f6eabdbcb3c5 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/hooks/use-connect-site/index.ts @@ -0,0 +1,81 @@ +import { getRedirectUrl } from '@automattic/jetpack-components'; +import { useCallback, useContext } from 'react'; +import { NoticeContext } from '../../context/notices/noticeContext'; +import { NOTICE_SITE_CONNECTION_ERROR } from '../../context/notices/noticeTemplates'; +import useProductsByOwnership from '../../data/products/use-products-by-ownership'; +import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state'; +import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection'; +import useAnalytics from '../use-analytics'; +import type { TracksEvent, TracksProperties } from '../use-analytics'; +import type { MouseEvent } from 'react'; + +interface useConnectSiteProps { + tracksInfo: { + event: TracksEvent; + properties: TracksProperties; + }; +} + +const useConnectSite = ( { tracksInfo }: useConnectSiteProps ) => { + const { event, properties: tracksEventData } = tracksInfo; + + const { setNotice, resetNotice } = useContext( NoticeContext ); + + const { recordEvent } = useAnalytics(); + const { refetch: refetchOwnershipData } = useProductsByOwnership(); + + const { siteSuffix, adminUrl, myJetpackCheckoutUri } = getMyJetpackWindowInitialState(); + const redirectUri = `&redirect_to=${ myJetpackCheckoutUri }`; + + const connectAfterCheckoutUrl = `?connect_after_checkout=true&admin_url=${ encodeURIComponent( + adminUrl + ) }&from_site_slug=${ siteSuffix }&source=my-jetpack`; + const query = `${ connectAfterCheckoutUrl }${ redirectUri }&unlinked=1`; + const jetpackPlansPath = getRedirectUrl( 'jetpack-my-jetpack-site-only-plans', { query } ); + const { handleRegisterSite } = useMyJetpackConnection( { + skipUserConnection: true, + redirectUri, + } ); + + const connectSite = useCallback( + async ( e: MouseEvent< HTMLButtonElement > ) => { + e && e.preventDefault(); + + setTimeout( () => { + window.scrollTo( { + top: 0, + left: 0, + behavior: 'smooth', + } ); + }, 100 ); + + recordEvent( `${ event }_click`, tracksEventData ); + + try { + await handleRegisterSite(); + + recordEvent( `${ event }_success`, tracksEventData ); + + window.location.href = jetpackPlansPath; + } catch { + setNotice( NOTICE_SITE_CONNECTION_ERROR, resetNotice ); + } finally { + refetchOwnershipData(); + } + }, + [ + handleRegisterSite, + jetpackPlansPath, + recordEvent, + refetchOwnershipData, + resetNotice, + setNotice, + tracksEventData, + event, + ] + ); + + return { connectSite }; +}; + +export default useConnectSite; diff --git a/projects/packages/my-jetpack/_inc/hooks/use-my-jetpack-navigate/index.ts b/projects/packages/my-jetpack/_inc/hooks/use-my-jetpack-navigate/index.ts index b35c1d9a2bbfd..1a4a0936d3576 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-my-jetpack-navigate/index.ts +++ b/projects/packages/my-jetpack/_inc/hooks/use-my-jetpack-navigate/index.ts @@ -3,18 +3,14 @@ import { useNavigate } from 'react-router-dom'; import { MyJetpackRoutes } from '../../constants'; import type { NavigateOptions } from 'react-router-dom'; -/** - * Custom My Jetpack navigator hook - * - * @param {string} route - route to navigate to - * @return {Function} - navigate function - */ -export default function useMyJetpackNavigate( +const useMyJetpackNavigate = ( route: ( typeof MyJetpackRoutes )[ keyof typeof MyJetpackRoutes ] -) { +) => { const navigate = useNavigate(); return useCallback( ( options?: NavigateOptions ) => navigate( route, options ), [ navigate, route ] ); -} +}; + +export default useMyJetpackNavigate; diff --git a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-site-connection-notice.tsx b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-site-connection-notice.tsx index 7234d51aa341b..aee80801246ee 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-site-connection-notice.tsx +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-site-connection-notice.tsx @@ -4,27 +4,34 @@ import { useContext, useEffect } from 'react'; import { MyJetpackRoutes } from '../../constants'; import { NOTICE_PRIORITY_HIGH } from '../../context/constants'; import { NoticeContext } from '../../context/notices/noticeContext'; -import { NOTICE_SITE_CONNECTED } from '../../context/notices/noticeTemplates'; import { useAllProducts } from '../../data/products/use-product'; import useProductsByOwnership from '../../data/products/use-products-by-ownership'; import getProductSlugsThatRequireUserConnection from '../../data/utils/get-product-slugs-that-require-user-connection'; import useAnalytics from '../use-analytics'; +import useConnectSite from '../use-connect-site'; import useMyJetpackConnection from '../use-my-jetpack-connection'; import useMyJetpackNavigate from '../use-my-jetpack-navigate'; import type { NoticeOptions } from '../../context/notices/types'; +import type { MouseEvent } from 'react'; type RedBubbleAlerts = Window[ 'myJetpackInitialState' ][ 'redBubbleAlerts' ]; const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => { const { recordEvent } = useAnalytics(); const { setNotice, resetNotice } = useContext( NoticeContext ); - const { handleRegisterSite, siteIsRegistering } = useMyJetpackConnection( { + const { siteIsRegistering, isSiteConnected } = useMyJetpackConnection( { skipUserConnection: true, } ); const products = useAllProducts(); - const navToConnection = useMyJetpackNavigate( MyJetpackRoutes.Connection ); + const navToConnection = useMyJetpackNavigate( MyJetpackRoutes.ConnectionSkipPricing ); const redBubbleSlug = 'missing-connection'; const connectionError = redBubbleAlerts[ redBubbleSlug ]; + const { connectSite } = useConnectSite( { + tracksInfo: { + event: 'jetpack_my_jetpack_site_connection_notice_cta', + properties: {}, + }, + } ); const { refetch: refetchOwnershipData } = useProductsByOwnership(); @@ -37,20 +44,13 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => { getProductSlugsThatRequireUserConnection( products ); const requiresUserConnection = connectionError.type === 'user'; - const onActionButtonClick = () => { + const onActionButtonClick = ( { e }: { e: MouseEvent< HTMLButtonElement > } ) => { if ( requiresUserConnection ) { recordEvent( 'jetpack_my_jetpack_user_connection_notice_cta_click' ); navToConnection(); + } else { + connectSite( e ); } - - recordEvent( 'jetpack_my_jetpack_site_connection_notice_cta_click' ); - handleRegisterSite().then( () => { - setNotice( NOTICE_SITE_CONNECTED, resetNotice ); - delete redBubbleAlerts[ redBubbleSlug ]; - window.myJetpackInitialState.redBubbleAlerts = redBubbleAlerts; - - refetchOwnershipData(); - } ); }; const oneProductMessage = sprintf( @@ -121,7 +121,8 @@ const useSiteConnectionNotice = ( redBubbleAlerts: RedBubbleAlerts ) => { options: noticeOptions, } ); }, [ - handleRegisterSite, + isSiteConnected, + connectSite, navToConnection, products, recordEvent, diff --git a/projects/packages/my-jetpack/_inc/providers.tsx b/projects/packages/my-jetpack/_inc/providers.tsx new file mode 100644 index 0000000000000..d9d0569c4b59e --- /dev/null +++ b/projects/packages/my-jetpack/_inc/providers.tsx @@ -0,0 +1,25 @@ +import { ThemeProvider } from '@automattic/jetpack-components'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import NoticeContextProvider from './context/notices/noticeContext'; +import ValueStoreContextProvider from './context/value-store/valueStoreContext'; +import type { ReactNode, FC } from 'react'; + +interface ProvidersProps { + children: ReactNode; +} + +const Providers: FC< ProvidersProps > = ( { children } ) => { + const queryClient = new QueryClient(); + + return ( + + + + { children } + + + + ); +}; + +export default Providers; diff --git a/projects/packages/my-jetpack/changelog/fix-connection-status-inconsistencies b/projects/packages/my-jetpack/changelog/fix-connection-status-inconsistencies new file mode 100644 index 0000000000000..90071d8c3e8c7 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/fix-connection-status-inconsistencies @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Unify connection flows in My Jetpack