From 394604e00708552d18d969886348966e8815cadf Mon Sep 17 00:00:00 2001 From: Emanuele Buccelli Date: Tue, 23 Jul 2024 12:08:47 +0200 Subject: [PATCH 01/25] Add Vertex as Help Center CODEOWNERS (#92892) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 133428fad92c46..56823d9a775b11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,3 +150,6 @@ # Renovate config /renovate.json5 @noahtallen /.github/workflows/renovate.yml @noahtallen + +# Support +/packages/help-center @Automattic/vertex \ No newline at end of file From 9b687d4f9a64c6ef630c2ac2748997b128a09edf Mon Sep 17 00:00:00 2001 From: Miguel Torres <1233880+mmtr@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:51:12 +0200 Subject: [PATCH 02/25] Footer credit: Remove setting/upsell for block themes (#92872) --- client/my-sites/site-settings/form-general.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/my-sites/site-settings/form-general.jsx b/client/my-sites/site-settings/form-general.jsx index 38af208055e6d6..ef5e3d2a77bcce 100644 --- a/client/my-sites/site-settings/form-general.jsx +++ b/client/my-sites/site-settings/form-general.jsx @@ -29,6 +29,7 @@ import SiteLanguagePicker from 'calypso/components/language-picker/site-language import Notice from 'calypso/components/notice'; import NoticeAction from 'calypso/components/notice/notice-action'; import Timezone from 'calypso/components/timezone'; +import { useActiveThemeQuery } from 'calypso/data/themes/use-active-theme-query'; import { preventWidows } from 'calypso/lib/formatting'; import scrollToAnchor from 'calypso/lib/scroll-to-anchor'; import { domainManagementEdit } from 'calypso/my-sites/domains/paths'; @@ -577,6 +578,7 @@ export class SiteSettingsFormGeneral extends Component { isWpcomStagingSite, isUnlaunchedSite: propsisUnlaunchedSite, adminInterfaceIsWPAdmin, + hasBlockTheme, } = this.props; const classes = clsx( 'site-settings__general-settings', { 'is-loading': isRequestingSettings, @@ -625,7 +627,7 @@ export class SiteSettingsFormGeneral extends Component { /> { this.renderAdminInterface() } { ! isWpcomStagingSite && this.giftOptions() } - { ! isWPForTeamsSite && ! ( siteIsJetpack && ! siteIsAtomic ) && ( + { ! hasBlockTheme && ! isWPForTeamsSite && ! ( siteIsJetpack && ! siteIsAtomic ) && (
{ const { globalStylesInUse, shouldLimitGlobalStyles } = useSiteGlobalStylesStatus( props.site?.ID ); + const { data: activeThemeData } = useActiveThemeQuery( props.site?.ID ?? -1, !! props.site ); + const hasBlockTheme = activeThemeData?.[ 0 ]?.is_block_theme ?? false; return ( ); }; From d5cffb75e7328c5af2e45f39cea2ee71e88a33e2 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Tue, 23 Jul 2024 13:42:09 +0200 Subject: [PATCH 03/25] Help Center: Shorten support URL cache (#92897) --- packages/help-center/src/components/help-center.tsx | 3 +-- packages/help-center/src/data/use-support-status.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/help-center/src/components/help-center.tsx b/packages/help-center/src/components/help-center.tsx index 006e4e8230cb11..484bbcf4d46fb8 100644 --- a/packages/help-center/src/components/help-center.tsx +++ b/packages/help-center/src/components/help-center.tsx @@ -14,7 +14,7 @@ import { useHelpCenterContext, type HelpCenterRequiredInformation, } from '../contexts/HelpCenterContext'; -import { useChatStatus, useStillNeedHelpURL, useActionHooks } from '../hooks'; +import { useChatStatus, useActionHooks } from '../hooks'; import { useOpeningCoordinates } from '../hooks/use-opening-coordinates'; import { HELP_CENTER_STORE } from '../stores'; import { Container } from '../types'; @@ -44,7 +44,6 @@ const HelpCenter: React.FC< Container > = ( { } }, [ currentUser ] ); - useStillNeedHelpURL(); useActionHooks(); const { hasActiveChats, isEligibleForChat } = useChatStatus(); diff --git a/packages/help-center/src/data/use-support-status.ts b/packages/help-center/src/data/use-support-status.ts index 5632602fd0bf29..417cb3a54ad6f1 100644 --- a/packages/help-center/src/data/use-support-status.ts +++ b/packages/help-center/src/data/use-support-status.ts @@ -28,6 +28,6 @@ export function useSupportStatus( enabled = true ) { enabled, refetchOnWindowFocus: false, placeholderData: keepPreviousData, - staleTime: 120 * 1000, // 2 mins. It has to be short, because the user can change the plan to access support. + staleTime: 100, // 100 ms, just to prevent refreshing the data on every render. } ); } From f6c6aa19b5c1889c09682e218596e17a14e75591 Mon Sep 17 00:00:00 2001 From: Clemen Date: Tue, 23 Jul 2024 12:54:35 +0100 Subject: [PATCH 04/25] Activate the Partner Directory section in production. (#92818) --- config/a8c-for-agencies-production.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/a8c-for-agencies-production.json b/config/a8c-for-agencies-production.json index 84ad08a2caa760..fa06fc39de5f11 100644 --- a/config/a8c-for-agencies-production.json +++ b/config/a8c-for-agencies-production.json @@ -48,7 +48,7 @@ "a8c-for-agencies-signup": true, "a8c-for-agencies-referrals": true, "a8c-for-agencies-settings": false, - "a8c-for-agencies-partner-directory": false, + "a8c-for-agencies-partner-directory": true, "a8c-for-agencies-migrations": true, "a8c-for-agencies-client": true }, From de846ae9a5bb6d1fc34970a67da2e88e42e3ff00 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 23 Jul 2024 08:17:43 -0400 Subject: [PATCH 05/25] Hosting Overview: Update Support Card (#92616) * update copy * remove HE thumbnails * open wapuu instead of /help * update tracking --- client/my-sites/hosting/support-card/index.js | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/client/my-sites/hosting/support-card/index.js b/client/my-sites/hosting/support-card/index.js index ac65ceed38ac0f..d21d63e16bcb9c 100644 --- a/client/my-sites/hosting/support-card/index.js +++ b/client/my-sites/hosting/support-card/index.js @@ -1,7 +1,9 @@ import { Button } from '@automattic/components'; +import { HelpCenter } from '@automattic/data-stores'; +import { useStillNeedHelpURL } from '@automattic/help-center/src/hooks'; +import { useDispatch as useDataStoreDispatch } from '@wordpress/data'; import { useTranslate } from 'i18n-calypso'; import { useDispatch } from 'react-redux'; -import { HappinessEngineersTray } from 'calypso/components/happiness-engineers-tray'; import { HostingCard } from 'calypso/components/hosting-card'; import { composeAnalytics, @@ -12,7 +14,7 @@ import { import './style.scss'; -function trackNavigateToContactSupport() { +function trackNavigateGetHelpClick() { return composeAnalytics( recordGoogleEvent( 'Hosting Configuration', 'Clicked "Contact us" Button in Support card' ), recordTracksEvent( 'calypso_hosting_configuration_contact_support' ), @@ -20,21 +22,24 @@ function trackNavigateToContactSupport() { ); } +const HELP_CENTER_STORE = HelpCenter.register(); + export default function SupportCard() { const translate = useTranslate(); const dispatch = useDispatch(); + const { setShowHelpCenter, setInitialRoute } = useDataStoreDispatch( HELP_CENTER_STORE ); + const { url } = useStillNeedHelpURL(); + + const onClick = () => { + setInitialRoute( url ); + setShowHelpCenter( true ); + dispatch( trackNavigateGetHelpClick() ); + }; return ( - - -

- { translate( - 'If you need help or have any questions, our Happiness Engineers are here when you need them.' - ) } -

- + +

{ translate( 'Our AI assistant can help, or connect you to our support team.' ) }

+
); } From 0d9ca0455d9ffdcc3957ad5b5f24a433c74b4139 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 23 Jul 2024 08:19:27 -0400 Subject: [PATCH 06/25] Update `/help` header and footer copy (#92846) * update header * update header --- client/me/help/help-contact-us-footer.tsx | 6 +++--- client/me/help/help-contact-us-header.tsx | 4 +++- client/me/help/main.jsx | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/me/help/help-contact-us-footer.tsx b/client/me/help/help-contact-us-footer.tsx index 7d0dbd4d445ac0..ebd719ce0ef433 100644 --- a/client/me/help/help-contact-us-footer.tsx +++ b/client/me/help/help-contact-us-footer.tsx @@ -27,13 +27,13 @@ const HelpContactUsFooter: FC = () => {
-

{ __( 'Contact support' ) }

+

{ __( "Haven't found your answer?" ) }

- { __( "Can't find the answer? Drop us a line and we'll lend a hand." ) } + { __( 'Our AI assistant can help, or connect you to our support team.' ) }

diff --git a/client/me/help/help-contact-us-header.tsx b/client/me/help/help-contact-us-header.tsx index 4d76b9f31fbf14..038610f000fafe 100644 --- a/client/me/help/help-contact-us-header.tsx +++ b/client/me/help/help-contact-us-header.tsx @@ -23,7 +23,9 @@ const HelpContactUsHeader: FC = () => { return (
- +
); }; diff --git a/client/me/help/main.jsx b/client/me/help/main.jsx index a5f9c0004c4edd..d79c72a11e4ba8 100644 --- a/client/me/help/main.jsx +++ b/client/me/help/main.jsx @@ -241,7 +241,7 @@ class Help extends PureComponent { From 722bf627a965190f8a89600b6721e3a95d784f0d Mon Sep 17 00:00:00 2001 From: Clemen Date: Tue, 23 Jul 2024 13:36:27 +0100 Subject: [PATCH 07/25] A4A > Partner Directory: Update the application wizard copies and the coming soon messages (#92893) * Update getBrandMeta * Update copies and show comming soon messages. * Update the notice copy to "Your profile has been saved!!" * Remove an extra ! in the notice message. --- .../partner-directory/dashboard/index.tsx | 126 +++++++++++------- .../partner-directory/lib/get-brand-meta.tsx | 26 +++- 2 files changed, 96 insertions(+), 56 deletions(-) diff --git a/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx b/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx index f54fde7159d231..7108d6c57607a8 100644 --- a/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/dashboard/index.tsx @@ -20,7 +20,7 @@ import { PARTNER_DIRECTORY_AGENCY_DETAILS_SLUG, PARTNER_DIRECTORY_AGENCY_EXPERTISE_SLUG, } from '../constants'; -import { getBrandMeta } from '../lib/get-brand-meta'; +import { getBrandMeta, BrandMeta } from '../lib/get-brand-meta'; import { AgencyDirectoryApplication } from '../types'; import { mapAgencyDetailsFormData, @@ -101,7 +101,7 @@ const PartnerDirectoryDashboard = () => { response && reduxDispatch( setActiveAgency( response ) ); reduxDispatch( - successNotice( translate( 'Your profile has been published!' ), { + successNotice( translate( 'Your profile has been saved!' ), { duration: 6000, } ) ); @@ -238,12 +238,67 @@ const PartnerDirectoryDashboard = () => { // The Agency application is completed: At least a directory was approved and published if ( isCompleted ) { + const getDirectoryDescription = ( + brandMeta: BrandMeta, + application: DirectoryApplicationStatus + ) => { + const showPopoverOnLoad = + directoryApplicationStatuses.filter( ( { key } ) => key === 'rejected' ).length === 1; + + if ( application.key === 'approved' && brandMeta.isAvailable ) { + // Application approved and visible in the directory + return ( + <> + +
+ + + ); + } + // Application pending or rejected + return ( + <> + +
+ { + // Application approved, but the Directory page is not available yet + application.key === 'approved' && ! brandMeta.isAvailable + ? translate( 'This partner directory is launching soon.' ) + : '' + } +
+ + ); + }; + return (
{ translate( - 'Congratulations! Your agency is now listed in our partner directory.', - 'Congratulations! Your agency is now listed in our partner directories.', + 'Thank you! You’ll be notified when the partner directory is live.', + 'Thank you! You’ll be notified when the partner directories are live.', + // todo: Once the partner directory are live use the copy below: + //'Congratulations! Your agency is now listed in our partner directory.', + //'Congratulations! Your agency is now listed in our partner directories.', { count: directoryApplicationStatuses.filter( ( { key } ) => key === 'approved' ) .length, @@ -251,52 +306,17 @@ const PartnerDirectoryDashboard = () => { ) }
{ directoryApplicationStatuses.length > 0 && - directoryApplicationStatuses.map( ( { brand, status, type, key } ) => { - const brandMeta = getBrandMeta( brand, agency ); - const showPopoverOnLoad = - directoryApplicationStatuses.filter( ( { key } ) => key === 'rejected' ).length === 1; + directoryApplicationStatuses.map( ( application: DirectoryApplicationStatus ) => { + const brandMeta = getBrandMeta( application.brand, agency ); + return ( - -
- - - ) : ( - - ) - } + heading={ application.brand } + description={ getDirectoryDescription( brandMeta, application ) } /> ); } ) } @@ -334,7 +354,7 @@ const PartnerDirectoryDashboard = () => { return ( <>
- { translate( `Boost your agency's visibility across Automattic platforms.` ) } + { translate( `Boost your agency’s visibility across Automattic listings.` ) }
@@ -413,11 +433,17 @@ const PartnerDirectoryDashboard = () => { stepNumber={ currentApplicationStep > 2 ? undefined : 3 } icon={ currentApplicationStep > 2 ? check : undefined } heading={ translate( 'New clients will find you' ) } - description={ translate( - 'Your agency will appear in the partner directories you select and get approved for, including WordPress.com, Woo.com, Pressable.com, and Jetpack.com.' - ) } + description={ + <> + { translate( + 'Your agency will appear in the partner directories you select and get approved for, including WordPress.com, Woo.com, Pressable.com, and Jetpack.com.' + ) } +
+ { translate( 'These partner directories are launching soon.' ) } + + } buttonProps={ { - children: translate( 'Publish' ), + children: translate( 'Done' ), onClick: onPublishProfileClick, primary: applicationWasSubmitted, disabled: diff --git a/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx b/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx index 07b6ff510d10c3..7ca4821ca7a8ed 100644 --- a/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx +++ b/client/a8c-for-agencies/sections/partner-directory/lib/get-brand-meta.tsx @@ -2,7 +2,16 @@ import { WooLogo, WordPressLogo, JetpackLogo } from '@automattic/components'; import pressableIcon from 'calypso/assets/images/pressable/pressable-icon.svg'; import { Agency } from 'calypso/state/a8c-for-agencies/types'; -export const getBrandMeta = ( brand: string, agency?: Agency | null ) => { +export type BrandMeta = { + brand: string; + icon: JSX.Element | undefined; + url: string; + urlProfile: string; + isAvailable: boolean; + className?: string; +}; + +export const getBrandMeta = ( brand: string, agency?: Agency | null ): BrandMeta => { const agencySlug = agency?.name .toLowerCase() @@ -14,40 +23,45 @@ export const getBrandMeta = ( brand: string, agency?: Agency | null ) => { switch ( brand ) { case 'WordPress.com': return { + brand: brand, icon: , url: 'https://wordpress.com/development-services/', urlProfile: `https://wordpress.com/development-services/${ agencySlug }/${ agencyId }`, - isPublic: true, + isAvailable: false, }; case 'WooCommerce.com': return { + brand: brand, icon: , className: 'partner-directory-dashboard__woo-icon', url: 'https://woocommerce.com/development-services/', urlProfile: `https://woocommerce.com/development-services/${ agencySlug }/${ agencyId }`, - isPublic: false, + isAvailable: false, }; case 'Pressable.com': return { + brand: brand, icon: , url: 'https://pressable.com/development-services/', urlProfile: `https://pressable.com/development-services/${ agencySlug }/${ agencyId }`, - isPublic: false, + isAvailable: false, }; case 'Jetpack.com': return { + brand: brand, icon: , url: 'https://jetpack.com/development-services/', urlProfile: `https://jetpack.com/development-services/${ agencySlug }/${ agencyId }`, - isPublic: true, + isAvailable: false, }; default: return { + brand: 'Unknown', icon: undefined, url: '', urlProfile: '', - isPublic: false, + isAvailable: false, }; } }; From 554c57540d84c7c0b02b1e4231c1afdba8f78e34 Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Tue, 23 Jul 2024 15:53:45 +0200 Subject: [PATCH 08/25] Zendesk Widget: Pass site ID to when submitting user fields (#92810) --- client/blocks/importer/wordpress/import-everything/index.tsx | 1 + .../wordpress/import-everything/migration-error/index.tsx | 4 ++++ client/components/chat-button/index.tsx | 3 +++ .../precancellation-chat-button/index.tsx | 1 + .../step-components/educational-content-step.tsx | 2 ++ .../cancel-purchase-form/step-components/upsell-step.tsx | 1 + .../help-center/src/components/help-center-contact-form.tsx | 1 + .../src/components/help-center-contact-support-option.tsx | 1 + packages/help-center/src/components/help-center.tsx | 2 +- packages/zendesk-client/src/types.ts | 5 +++++ packages/zendesk-client/src/use-open-zendesk-messaging.ts | 2 ++ 11 files changed, 22 insertions(+), 1 deletion(-) diff --git a/client/blocks/importer/wordpress/import-everything/index.tsx b/client/blocks/importer/wordpress/import-everything/index.tsx index fafb2d652cc657..bf96d9de0378df 100644 --- a/client/blocks/importer/wordpress/import-everything/index.tsx +++ b/client/blocks/importer/wordpress/import-everything/index.tsx @@ -243,6 +243,7 @@ export class ImportEverything extends SectionMigrate { stepNavigator?.goToImportCapturePage?.() } diff --git a/client/blocks/importer/wordpress/import-everything/migration-error/index.tsx b/client/blocks/importer/wordpress/import-everything/migration-error/index.tsx index 021e15e81e97d7..1e982b5c778abd 100644 --- a/client/blocks/importer/wordpress/import-everything/migration-error/index.tsx +++ b/client/blocks/importer/wordpress/import-everything/migration-error/index.tsx @@ -23,6 +23,7 @@ const HELP_CENTER_STORE = HelpCenter.register(); interface Props { sourceSiteUrl: string; targetSiteUrl: string; + targetSiteID: string | number; status: MigrationStatusError | null; resetMigration: () => void; goToImportCapturePage: () => void; @@ -39,6 +40,7 @@ export const MigrationError = ( props: Props ) => { resetMigration, goToImportCapturePage, goToImportContentOnlyPage, + targetSiteID, } = props; const translate = useTranslate(); const { isEligibleForChat } = useChatStatus(); @@ -59,6 +61,7 @@ export const MigrationError = ( props: Props ) => { if ( isMessagingAvailable && canConnectToZendeskMessaging ) { openZendeskWidget( { siteUrl: targetSiteUrl, + siteId: targetSiteID, message: `${ status }: Import onboarding flow; migration failed`, onSuccess: () => { resetStore(); @@ -75,6 +78,7 @@ export const MigrationError = ( props: Props ) => { targetSiteUrl, status, isMessagingAvailable, + targetSiteID, canConnectToZendeskMessaging, setInitialRoute, setShowHelpCenter, diff --git a/client/components/chat-button/index.tsx b/client/components/chat-button/index.tsx index db1aa15e629244..c4023a18bd9e94 100644 --- a/client/components/chat-button/index.tsx +++ b/client/components/chat-button/index.tsx @@ -23,6 +23,7 @@ type Props = { onError?: () => void; primary?: boolean; siteUrl?: string; + siteId?: string | number; children?: React.ReactNode; withHelpCenter?: boolean; section?: string; @@ -62,6 +63,7 @@ const ChatButton: FC< Props > = ( { initialMessage, onClick, onError, + siteId = null, primary = false, siteUrl, withHelpCenter = true, @@ -119,6 +121,7 @@ const ChatButton: FC< Props > = ( { openZendeskWidget( { message: initialMessage, siteUrl, + siteId, onError, onSuccess: () => { onClick?.(); diff --git a/client/components/marketing-survey/cancel-purchase-form/precancellation-chat-button/index.tsx b/client/components/marketing-survey/cancel-purchase-form/precancellation-chat-button/index.tsx index 8d7bdf32f1a0ac..833943654149c6 100644 --- a/client/components/marketing-survey/cancel-purchase-form/precancellation-chat-button/index.tsx +++ b/client/components/marketing-survey/cancel-purchase-form/precancellation-chat-button/index.tsx @@ -57,6 +57,7 @@ const PrecancellationChatButton: FC< Props > = ( { chatIntent="PRECANCELLATION" initialMessage={ initialMessage } siteUrl={ siteUrl } + siteId={ purchase?.siteId } className={ clsx( 'precancellation-chat-button__main-button', className ) } onClick={ handleClick } section="pre-cancellation" diff --git a/client/components/marketing-survey/cancel-purchase-form/step-components/educational-content-step.tsx b/client/components/marketing-survey/cancel-purchase-form/step-components/educational-content-step.tsx index bcefc80e77c1d2..8c297eb5063d07 100644 --- a/client/components/marketing-survey/cancel-purchase-form/step-components/educational-content-step.tsx +++ b/client/components/marketing-survey/cancel-purchase-form/step-components/educational-content-step.tsx @@ -264,6 +264,7 @@ export default function EducationalCotnentStep( { type, site, ...props }: StepPr "User is contacting us from pre-cancellation form. Cancellation reason they've given: " + props.cancellationReason, siteUrl: site.URL, + siteId: site.ID, } ); } } variant="link" @@ -295,6 +296,7 @@ export default function EducationalCotnentStep( { type, site, ...props }: StepPr "User is contacting us from pre-cancellation form. Cancellation reason they've given: " + props.cancellationReason, siteUrl: site.URL, + siteId: site.ID, } ); } } variant="link" diff --git a/client/components/marketing-survey/cancel-purchase-form/step-components/upsell-step.tsx b/client/components/marketing-survey/cancel-purchase-form/step-components/upsell-step.tsx index bab34f697b9f6e..a85d18329dcb1e 100644 --- a/client/components/marketing-survey/cancel-purchase-form/step-components/upsell-step.tsx +++ b/client/components/marketing-survey/cancel-purchase-form/step-components/upsell-step.tsx @@ -147,6 +147,7 @@ export default function UpsellStep( { upsell, site, purchase, ...props }: StepPr "User is contacting us from pre-cancellation form. Cancellation reason they've given: " + props.cancellationReason, siteUrl: site.URL, + siteId: site.ID, } ); props.closeDialog(); } } diff --git a/packages/help-center/src/components/help-center-contact-form.tsx b/packages/help-center/src/components/help-center-contact-form.tsx index 9423faffa50108..2ea96caf539e9f 100644 --- a/packages/help-center/src/components/help-center-contact-form.tsx +++ b/packages/help-center/src/components/help-center-contact-form.tsx @@ -326,6 +326,7 @@ export const HelpCenterContactForm = ( props: HelpCenterContactFormProps ) => { aiChatId: aiChatId, message: initialChatMessage, siteUrl: supportSite.URL, + siteId: supportSite.ID, onError: () => setHasSubmittingError( true ), onSuccess: () => { resetStore(); diff --git a/packages/help-center/src/components/help-center-contact-support-option.tsx b/packages/help-center/src/components/help-center-contact-support-option.tsx index 3ad8997a55d2f4..b67ab450648dec 100644 --- a/packages/help-center/src/components/help-center-contact-support-option.tsx +++ b/packages/help-center/src/components/help-center-contact-support-option.tsx @@ -77,6 +77,7 @@ const HelpCenterContactSupportOption = ( { const zendeskWidgetProps = { aiChatId: escapedWapuuChatId, siteUrl: site?.URL, + siteId: site?.ID, onError: () => setHasSubmittingError( true ), onSuccess: () => { resetStore(); diff --git a/packages/help-center/src/components/help-center.tsx b/packages/help-center/src/components/help-center.tsx index 484bbcf4d46fb8..9649de3872f7ff 100644 --- a/packages/help-center/src/components/help-center.tsx +++ b/packages/help-center/src/components/help-center.tsx @@ -50,7 +50,7 @@ const HelpCenter: React.FC< Container > = ( { const { isMessagingScriptLoaded } = useLoadZendeskMessaging( 'zendesk_support_chat_key', ( isHelpCenterShown && isEligibleForChat ) || hasActiveChats, - isEligibleForChat && hasActiveChats + isEligibleForChat || hasActiveChats ); useZendeskMessagingBindings( HELP_CENTER_STORE, hasActiveChats, isMessagingScriptLoaded ); diff --git a/packages/zendesk-client/src/types.ts b/packages/zendesk-client/src/types.ts index 784de603a2b8c2..0182b61aefb057 100644 --- a/packages/zendesk-client/src/types.ts +++ b/packages/zendesk-client/src/types.ts @@ -16,6 +16,10 @@ export type UserFields = { messaging_plan?: string; messaging_source?: string; messaging_url?: string; + /** + * Site ID of the site the user is currently on. + */ + messaging_site_id: string | number | null; }; export type MessagingAuth = { @@ -34,6 +38,7 @@ export type MessagingMetadata = { aiChatId?: string; message?: string; siteUrl?: string; + siteId?: string | number | null; onError?: () => void; onSuccess?: () => void; }; diff --git a/packages/zendesk-client/src/use-open-zendesk-messaging.ts b/packages/zendesk-client/src/use-open-zendesk-messaging.ts index 17090483c995a4..324166864ce74e 100644 --- a/packages/zendesk-client/src/use-open-zendesk-messaging.ts +++ b/packages/zendesk-client/src/use-open-zendesk-messaging.ts @@ -17,6 +17,7 @@ export function useOpenZendeskMessaging( aiChatId, message = '', siteUrl = 'No site selected', + siteId = null, onError, onSuccess, }: MessagingMetadata ) => { @@ -26,6 +27,7 @@ export function useOpenZendeskMessaging( messaging_initial_message: message, messaging_plan: '', // Will be filled out by backend messaging_url: siteUrl, + messaging_site_id: siteId, } ) .then( () => { onSuccess?.(); From 4ba19518df89b94918fef1a0d2d15e8d5ad5f3a6 Mon Sep 17 00:00:00 2001 From: Gabriel Caires Date: Tue, 23 Jul 2024 15:30:12 +0100 Subject: [PATCH 09/25] Use the stepper login feature (#92339) --- .../stepper/declarative-flow/ai-assembler.ts | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/client/landing/stepper/declarative-flow/ai-assembler.ts b/client/landing/stepper/declarative-flow/ai-assembler.ts index 6bfda81ec19081..32dacf1a3e3de8 100644 --- a/client/landing/stepper/declarative-flow/ai-assembler.ts +++ b/client/landing/stepper/declarative-flow/ai-assembler.ts @@ -7,12 +7,11 @@ import { useEffect } from 'react'; import { useSelector } from 'react-redux'; import wpcomRequest from 'wpcom-proxy-request'; import { useQueryTheme } from 'calypso/components/data/query-theme'; -import { useFlowLocale } from 'calypso/landing/stepper/hooks/use-flow-locale'; import { skipLaunchpad } from 'calypso/landing/stepper/utils/skip-launchpad'; import { getCurrentUserSiteCount, isUserLoggedIn } from 'calypso/state/current-user/selectors'; import { getTheme } from 'calypso/state/themes/selectors'; import { ONBOARD_STORE, SITE_STORE } from '../stores'; -import { useLoginUrl } from '../utils/path'; +import { stepsWithRequiredLogin } from '../utils/steps-with-required-login'; import { recordSubmitStep } from './internals/analytics/record-submit-step'; import { STEPS } from './internals/steps'; import { ProcessingResult } from './internals/steps-repository/processing-step/constants'; @@ -64,7 +63,7 @@ const withAIAssemblerFlow: Flow = { }, useSteps() { - return [ + return stepsWithRequiredLogin( [ STEPS.CHECK_SITES, STEPS.NEW_OR_EXISTING_SITE, STEPS.SITE_PICKER, @@ -77,7 +76,7 @@ const withAIAssemblerFlow: Flow = { STEPS.DOMAINS, STEPS.SITE_LAUNCH, STEPS.CELEBRATION, - ]; + ] ); }, useStepNavigation( _currentStep, navigate ) { @@ -322,29 +321,15 @@ const withAIAssemblerFlow: Flow = { currentPath.includes( `setup/${ flowName }/check-sites` ); const userAlreadyHasSites = currentUserSiteCount && currentUserSiteCount > 0; - const locale = useFlowLocale(); - const logInUrl = useLoginUrl( { - variationName: flowName, - redirectTo: window.location.href.replace( window.location.origin, '' ), - locale, - } ); - useEffect( () => { - if ( ! isLoggedIn ) { - window.location.assign( logInUrl ); - } else if ( isCreateSite && ! userAlreadyHasSites ) { + if ( isLoggedIn && isCreateSite && ! userAlreadyHasSites ) { window.location.assign( `/setup/${ flowName }/create-site` ); } }, [] ); let result: AssertConditionResult = { state: AssertConditionState.SUCCESS }; - if ( ! isLoggedIn ) { - result = { - state: AssertConditionState.CHECKING, - message: `${ flowName } requires a logged in user`, - }; - } else if ( isCreateSite && ! userAlreadyHasSites ) { + if ( isLoggedIn && isCreateSite && ! userAlreadyHasSites ) { result = { state: AssertConditionState.CHECKING, message: `${ flowName } with no preexisting sites`, From d1885b8c04d72918e027028de930ce7f7172d266 Mon Sep 17 00:00:00 2001 From: Gabriel Caires Date: Tue, 23 Jul 2024 15:30:45 +0100 Subject: [PATCH 10/25] Stepper: Update the `videopress` flow to use the new login strategy (#92393) * Move it to optional * Use custom login path --- .../declarative-flow/internals/types.ts | 2 +- .../stepper/declarative-flow/videopress.ts | 47 +++++++------------ 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/client/landing/stepper/declarative-flow/internals/types.ts b/client/landing/stepper/declarative-flow/internals/types.ts index 476c6c32760119..53b441bf48cf95 100644 --- a/client/landing/stepper/declarative-flow/internals/types.ts +++ b/client/landing/stepper/declarative-flow/internals/types.ts @@ -126,7 +126,7 @@ export type Flow = { * A custom login path to use instead of the default login path. */ customLoginPath?: string; - extraQueryParams: Record< string, string | number >; + extraQueryParams?: Record< string, string | number >; }; useSteps: UseStepsHook; useStepNavigation: UseStepNavigationHook< ReturnType< Flow[ 'useSteps' ] > >; diff --git a/client/landing/stepper/declarative-flow/videopress.ts b/client/landing/stepper/declarative-flow/videopress.ts index 8349c808fc4561..df0527369856ed 100644 --- a/client/landing/stepper/declarative-flow/videopress.ts +++ b/client/landing/stepper/declarative-flow/videopress.ts @@ -17,6 +17,7 @@ import { useSiteIdParam } from '../hooks/use-site-id-param'; import { useSiteSlug } from '../hooks/use-site-slug'; import { PLANS_STORE, SITE_STORE, USER_STORE, ONBOARD_STORE } from '../stores'; import './internals/videopress.scss'; +import { stepsWithRequiredLogin } from '../utils/steps-with-required-login'; import ChooseADomain from './internals/steps-repository/choose-a-domain'; import Launchpad from './internals/steps-repository/launchpad'; import ProcessingStep from './internals/steps-repository/processing-step'; @@ -32,18 +33,32 @@ const videopress: Flow = { return translate( 'Video' ); }, isSignupFlow: true, + useLoginParams() { + return { + customLoginPath: '/start/videopress-account/user', + extraQueryParams: { + pageTitle: translate( 'Video Portfolio' ), + flow: VIDEOPRESS_FLOW, + }, + }; + }, useSteps() { - return [ + const publicSteps = [ { slug: 'intro', asyncComponent: () => import( './internals/steps-repository/intro' ), }, { slug: 'videomakerSetup', component: VideomakerSetup }, + ]; + + const privateSteps = stepsWithRequiredLogin( [ { slug: 'options', component: SiteOptions }, { slug: 'chooseADomain', component: ChooseADomain }, { slug: 'processing', component: ProcessingStep }, { slug: 'launchpad', component: Launchpad }, - ]; + ] ); + + return [ ...publicSteps, ...privateSteps ]; }, useStepNavigation( _currentStep, navigate ) { @@ -65,7 +80,6 @@ const videopress: Flow = { } } - const name = this.name; const { getSelectedStyleVariation } = useSelect( ( select ) => select( ONBOARD_STORE ) as OnboardSelect, [] @@ -74,10 +88,6 @@ const videopress: Flow = { useDispatch( ONBOARD_STORE ); const siteId = useSiteIdParam(); const _siteSlug = useSiteSlug(); - const userIsLoggedIn = useSelect( - ( select ) => ( select( USER_STORE ) as UserSelect ).isCurrentUserLoggedIn(), - [] - ); const _siteTitle = useSelect( ( select ) => ( select( ONBOARD_STORE ) as OnboardSelect ).getSelectedSiteTitle(), [] @@ -118,19 +128,7 @@ const videopress: Flow = { const siteSlug = useSiteSlug(); - const stepValidateUserIsLoggedIn = () => { - if ( ! userIsLoggedIn ) { - navigate( 'intro' ); - return false; - } - return true; - }; - const stepValidateSiteTitle = () => { - if ( ! stepValidateUserIsLoggedIn() ) { - return false; - } - if ( ! _siteTitle.length ) { navigate( 'options' ); return false; @@ -304,9 +302,6 @@ const videopress: Flow = { case 'intro': clearOnboardingSiteOptions(); break; - case 'options': - stepValidateUserIsLoggedIn(); - break; case 'chooseADomain': stepValidateSiteTitle(); break; @@ -324,13 +319,7 @@ const videopress: Flow = { return navigate( 'videomakerSetup' ); case 'videomakerSetup': - if ( userIsLoggedIn ) { - return navigate( 'options' ); - } - - return window.location.replace( - `/start/videopress-account/user/${ locale }?variationName=${ name }&flow=${ name }&pageTitle=Video%20Portfolio&redirect_to=/setup/videopress/options` - ); + return navigate( 'options' ); case 'options': { const { siteTitle, tagline } = providedDependencies; From 519b629bd43cb57361e15aa08d85125a1d0b9ab7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 23 Jul 2024 18:10:06 +0200 Subject: [PATCH 11/25] New message for rate limiting users requests (#92891) --- packages/odie-client/src/query/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/odie-client/src/query/index.ts b/packages/odie-client/src/query/index.ts index a29c33b3ff96c2..c246f4b62ac299 100644 --- a/packages/odie-client/src/query/index.ts +++ b/packages/odie-client/src/query/index.ts @@ -97,6 +97,12 @@ export const useOdieSendMessage = (): UseMutationResult< __i18n_text_domain__ ); + /* translators: Error message when Wapuu user's exceed free messages limit */ + const wapuuRateLimitMessage = __( + "Hi there! You've hit your AI usage limit. Upgrade your plan for unlimited Wapuu support! You can still get user support using the buttons below.", + __i18n_text_domain__ + ); + return useMutation< { chat_id: string; messages: Message[] }, unknown, @@ -200,13 +206,17 @@ export const useOdieSendMessage = (): UseMutationResult< onSettled: () => { setIsLoading( false ); }, - onError: ( _, __, context ) => { + onError: ( response, __, context ) => { if ( ! context ) { throw new Error( 'Context is undefined' ); } + + const { data } = response as { data: { status: number } }; + const isRateLimitError = data.status === 429; + const { internal_message_id } = context; const message = { - content: wapuuErrorMessage, + content: isRateLimitError ? wapuuRateLimitMessage : wapuuErrorMessage, internal_message_id, role: 'bot', type: 'error', From 0ad3276556013c36327a75898a6b10b3dd761f66 Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Tue, 23 Jul 2024 14:32:39 -0400 Subject: [PATCH 12/25] fix sidebar animation and style bleed at sites (#92911) --- client/hosting/sites/components/dotcom-style.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/hosting/sites/components/dotcom-style.scss b/client/hosting/sites/components/dotcom-style.scss index 216691d81920d0..302490a46e7cb4 100644 --- a/client/hosting/sites/components/dotcom-style.scss +++ b/client/hosting/sites/components/dotcom-style.scss @@ -286,13 +286,18 @@ } // Styles collapsed site list. -.wpcom-site .is-global-sidebar-visible { +.wpcom-site .is-global-sidebar-visible.is-group-sites-dashboard, +.wpcom-site .is-global-sidebar-visible.is-group-sites { .layout__content { transition: padding-left 220ms ease-out; min-height: 100vh; } .layout__secondary { - transition: width 220ms ease-out; + transition: transform 0.15s ease-in-out, width 220ms ease-out; + + @media (max-width: $break-mobile) { + transition: transform 0.15s ease-in-out, opacity 0.15s ease-out; + } .sidebar__header, .sidebar__body, From 97f063597fa85e32b38bf0b890f4a2117d1ac3f7 Mon Sep 17 00:00:00 2001 From: Paulo Marcos Trentin Date: Tue, 23 Jul 2024 18:42:22 -0300 Subject: [PATCH 13/25] Updated tests to use the new selectors after Gutenberg's 18.8.0 release (#92913) --- .../calypso-e2e/src/lib/blocks/block-flows/instagram.ts | 6 +----- packages/calypso-e2e/src/lib/blocks/block-flows/twitter.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/calypso-e2e/src/lib/blocks/block-flows/instagram.ts b/packages/calypso-e2e/src/lib/blocks/block-flows/instagram.ts index 660a50622c15e0..e2695f85bd9061 100644 --- a/packages/calypso-e2e/src/lib/blocks/block-flows/instagram.ts +++ b/packages/calypso-e2e/src/lib/blocks/block-flows/instagram.ts @@ -42,11 +42,7 @@ export class InstagramBlockFlow implements BlockFlow { } ) .click(); - // @todo Remove the first option once Gutenberg v18.8.0 is deployed everywhere. - await Promise.race( [ - editorCanvas.getByTitle( 'Embedded content from instagram.com' ).waitFor(), - editorCanvas.getByTitle( 'Embedded content from www.instagram.com' ).waitFor(), - ] ); + await editorCanvas.getByTitle( 'Embedded content from www.instagram.com' ).waitFor(); } /** diff --git a/packages/calypso-e2e/src/lib/blocks/block-flows/twitter.ts b/packages/calypso-e2e/src/lib/blocks/block-flows/twitter.ts index b7a1dda94719c8..cfce25a7080d89 100644 --- a/packages/calypso-e2e/src/lib/blocks/block-flows/twitter.ts +++ b/packages/calypso-e2e/src/lib/blocks/block-flows/twitter.ts @@ -10,7 +10,7 @@ const selectors = { embedUrlInput: `${ blockParentSelector } input`, embedButton: `${ blockParentSelector } button:has-text("Embed")`, // @todo Remove first option once Gutenberg v18.8.0 is deployed everywhere. - editorTwitterIframe: `iframe[title="Embedded content from twitter"],iframe[title="Embedded content from twitter.com"]`, + editorTwitterIframe: `iframe[title="Embedded content from twitter.com"]`, publishedTwitterIframe: `iframe[title="X Post"]`, }; From e71e432f604a4335ea8842ce7f6cbbe96449e4aa Mon Sep 17 00:00:00 2001 From: Brian Willis Date: Tue, 23 Jul 2024 17:27:18 -0500 Subject: [PATCH 14/25] Update domain label for multiyear purchase (#92776) * Update domain label for multiyear purchase --- .../wpcom-checkout/src/checkout-line-items.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/wpcom-checkout/src/checkout-line-items.tsx b/packages/wpcom-checkout/src/checkout-line-items.tsx index bc8dfc7fe7560d..039b099c44a7bf 100644 --- a/packages/wpcom-checkout/src/checkout-line-items.tsx +++ b/packages/wpcom-checkout/src/checkout-line-items.tsx @@ -687,7 +687,7 @@ export function LineItemSublabelAndPrice( { product }: { product: ResponseCartPr isTitanMail( product ) ) { if ( product.months_per_bill_period === 12 || product.months_per_bill_period === null ) { - const billingInterval = translate( 'billed annually' ); + const billingInterval = GetBillingIntervalLabel( { product } ); return ( <> : { billingInterval } @@ -757,11 +757,12 @@ export function LineItemSublabelAndPrice( { product }: { product: ResponseCartPr if ( ( isDomainRegistration || isDomainMapping ) && product.months_per_bill_period === 12 ) { const premiumLabel = product.extra?.premium ? translate( 'Premium' ) : ''; - return ( <> { premiumLabel } - { ! product.is_included_for_100yearplan && <>: { translate( 'billed annually' ) } } + { ! product.is_included_for_100yearplan && ( + <>: { GetBillingIntervalLabel( { product } ) } + ) } ); } @@ -818,6 +819,16 @@ export function LineItemSublabelAndPrice( { product }: { product: ResponseCartPr return ; } +function GetBillingIntervalLabel( { product }: { product: ResponseCartProduct } ) { + const translate = useTranslate(); + if ( product.volume > 1 ) { + return translate( 'billed %(total_years)s years, then annually', { + args: { total_years: product.volume }, + } ); + } + return translate( 'billed annually' ); +} + export function LineItemBillingInterval( { product }: { product: ResponseCartProduct } ) { const translate = useTranslate(); From 9e2dad38c14fc2448bfc473fbc88148a5f9ba73c Mon Sep 17 00:00:00 2001 From: Brian Willis Date: Tue, 23 Jul 2024 17:55:50 -0500 Subject: [PATCH 15/25] Hide coupon link and Update Coupon Label in Checkout for Affiliate Onboarding Flows (#92740) * Hide coupon box --- .../checkout/src/components/cost-overrides-list.tsx | 13 ++++++++++--- .../src/components/wp-checkout-order-review.tsx | 4 +++- .../src/components/wp-order-review-line-items.tsx | 8 +++++++- client/my-sites/checkout/utils.ts | 6 ++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/client/my-sites/checkout/src/components/cost-overrides-list.tsx b/client/my-sites/checkout/src/components/cost-overrides-list.tsx index 9ef0ba658c1824..8cedce2a71105d 100644 --- a/client/my-sites/checkout/src/components/cost-overrides-list.tsx +++ b/client/my-sites/checkout/src/components/cost-overrides-list.tsx @@ -23,7 +23,10 @@ import { } from '@automattic/wpcom-checkout'; import styled from '@emotion/styled'; import { useTranslate } from 'i18n-calypso'; +import { useSelector } from 'calypso/state'; +import { getIsOnboardingAffiliateFlow } from 'calypso/state/signup/flow/selectors'; import useCartKey from '../../use-cart-key'; +import { getAffiliateCouponLabel } from '../../utils'; import type { Theme } from '@automattic/composite-checkout'; import type { LineItemCostOverrideForDisplay } from '@automattic/wpcom-checkout'; @@ -319,14 +322,18 @@ export function CouponCostOverride( { const translate = useTranslate(); const { formStatus } = useFormStatus(); const isDisabled = formStatus !== FormStatus.READY; + const isOnboardingAffiliateFlow = useSelector( getIsOnboardingAffiliateFlow ); if ( ! responseCart.coupon || ! responseCart.coupon_savings_total_integer ) { return null; } + // translators: The label of the coupon line item in checkout, including the coupon code - const label = translate( 'Coupon: %(couponCode)s', { - args: { couponCode: responseCart.coupon }, - } ); + const label = isOnboardingAffiliateFlow + ? getAffiliateCouponLabel() + : translate( 'Coupon: %(couponCode)s', { + args: { couponCode: responseCart.coupon }, + } ); return (
diff --git a/client/my-sites/checkout/src/components/wp-checkout-order-review.tsx b/client/my-sites/checkout/src/components/wp-checkout-order-review.tsx index 85f266131968e9..f6de57d615e59d 100644 --- a/client/my-sites/checkout/src/components/wp-checkout-order-review.tsx +++ b/client/my-sites/checkout/src/components/wp-checkout-order-review.tsx @@ -14,6 +14,7 @@ import { useSelector, useDispatch } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { NON_PRIMARY_DOMAINS_TO_FREE_USERS } from 'calypso/state/current-user/constants'; import { currentUserHasFlag, getCurrentUser } from 'calypso/state/current-user/selectors'; +import { getIsOnboardingAffiliateFlow } from 'calypso/state/signup/flow/selectors'; import getSelectedSite from 'calypso/state/ui/selectors/get-selected-site'; import Coupon from './coupon'; import { WPOrderReviewLineItems, WPOrderReviewSection } from './wp-order-review-line-items'; @@ -194,6 +195,7 @@ export function CouponFieldArea( { const { formStatus } = useFormStatus(); const translate = useTranslate(); const { setCouponFieldValue } = couponFieldStateProps; + const isOnboardingAffiliateFlow = useSelector( getIsOnboardingAffiliateFlow ); useEffect( () => { if ( couponStatus === 'applied' ) { @@ -202,7 +204,7 @@ export function CouponFieldArea( { } }, [ couponStatus, setCouponFieldValue ] ); - if ( isPurchaseFree || couponStatus === 'applied' ) { + if ( isPurchaseFree || couponStatus === 'applied' || isOnboardingAffiliateFlow ) { return null; } diff --git a/client/my-sites/checkout/src/components/wp-order-review-line-items.tsx b/client/my-sites/checkout/src/components/wp-order-review-line-items.tsx index b311779934aeb4..e01e05427cad45 100644 --- a/client/my-sites/checkout/src/components/wp-order-review-line-items.tsx +++ b/client/my-sites/checkout/src/components/wp-order-review-line-items.tsx @@ -23,8 +23,10 @@ import { has100YearPlan } from 'calypso/lib/cart-values/cart-items'; import { isWcMobileApp } from 'calypso/lib/mobile-app'; import { useGetProductVariants } from 'calypso/my-sites/checkout/src/hooks/product-variants'; import { getSignupCompleteFlowName } from 'calypso/signup/storageUtils'; -import { useDispatch } from 'calypso/state'; +import { useDispatch, useSelector } from 'calypso/state'; import { recordTracksEvent } from 'calypso/state/analytics/actions'; +import { getIsOnboardingAffiliateFlow } from 'calypso/state/signup/flow/selectors'; +import { getAffiliateCouponLabel } from '../../utils'; import { AkismetProQuantityDropDown } from './akismet-pro-quantity-dropdown'; import { ItemVariationPicker } from './item-variation-picker'; import type { OnChangeAkProQuantity } from './akismet-pro-quantity-dropdown'; @@ -90,6 +92,10 @@ export function WPOrderReviewLineItems( { const reduxDispatch = useDispatch(); const creditsLineItem = getCreditsLineItemFromCart( responseCart ); const couponLineItem = getCouponLineItemFromCart( responseCart ); + const isOnboardingAffiliateFlow = useSelector( getIsOnboardingAffiliateFlow ); + if ( isOnboardingAffiliateFlow && couponLineItem ) { + couponLineItem.label = getAffiliateCouponLabel(); + } const { formStatus } = useFormStatus(); const isDisabled = formStatus !== FormStatus.READY; const hasPartnerCoupon = getPartnerCoupon( { diff --git a/client/my-sites/checkout/utils.ts b/client/my-sites/checkout/utils.ts index 4b2ed8b174c5dc..76a39e05f1498b 100644 --- a/client/my-sites/checkout/utils.ts +++ b/client/my-sites/checkout/utils.ts @@ -1,4 +1,5 @@ import { doesStringResembleDomain } from '@automattic/onboarding'; +import { translate } from 'i18n-calypso'; import { untrailingslashit } from 'calypso/lib/route'; import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; import { getSelectedSite } from 'calypso/state/ui/selectors'; @@ -129,3 +130,8 @@ export function isContextJetpackSitelessCheckout( context: Context ): boolean { export function isContextSourceMyJetpack( context: Context ): boolean { return context.query?.source === 'my-jetpack'; } + +export function getAffiliateCouponLabel(): string { + // translators: The label of the coupon line item in checkout + return translate( 'Exclusive Offer Applied' ); +} From b497fc132bb45fce2f71e37033738337a3a4f36f Mon Sep 17 00:00:00 2001 From: Grzegorz Chudzinski-Pawlowski <112354940+grzegorz-cp@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:24:05 +1200 Subject: [PATCH 16/25] Stats: Don't restrict dashboard access for VIP (#92917) * Stats: Don't restrict dashboard access for VIP * Move VIP check to shouldGateStats --------- Co-authored-by: Dognose --- client/my-sites/stats/hooks/use-should-gate-stats.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/my-sites/stats/hooks/use-should-gate-stats.ts b/client/my-sites/stats/hooks/use-should-gate-stats.ts index 38c27787e41792..fce64ce6265be2 100644 --- a/client/my-sites/stats/hooks/use-should-gate-stats.ts +++ b/client/my-sites/stats/hooks/use-should-gate-stats.ts @@ -3,6 +3,7 @@ import { FEATURE_STATS_PAID } from '@automattic/calypso-products'; import { useSelector } from 'calypso/state'; import getSiteFeatures from 'calypso/state/selectors/get-site-features'; import isAtomicSite from 'calypso/state/selectors/is-site-wpcom-atomic'; +import isVipSite from 'calypso/state/selectors/is-vip-site'; import siteHasFeature from 'calypso/state/selectors/site-has-feature'; import { isJetpackSite, getSiteOption } from 'calypso/state/sites/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; @@ -134,6 +135,14 @@ export const shouldGateStats = ( state: object, siteId: number | null, statType: return false; } + // Do not paywall VIP sites. + // `is_vip` is not correctly placed in Odyssey, so we need to check `options.is_vip` as well. + const isVip = + isVipSite( state as object, siteId as number ) || getSiteOption( state, siteId, 'is_vip' ); + if ( isVip ) { + return false; + } + const isSiteCommercial = getSiteOption( state, siteId, 'is_commercial' ) || false; if ( isSiteCommercial ) { // Paywall basic stats for commercial sites with monthly views reaching the paywall threshold. From d5696454162cc10d2b99bba6591fcfbd7ca81e0f Mon Sep 17 00:00:00 2001 From: Grzegorz Chudzinski-Pawlowski <112354940+grzegorz-cp@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:26:20 +1200 Subject: [PATCH 17/25] Stats: Restoring a link to a summary page for empty cards (#92885) --- .../modules/stats-authors/stats-authors.tsx | 13 +++++++-- .../modules/stats-clicks/stats-clicks.tsx | 9 +++++++ .../stats-countries/stats-countries.tsx | 9 +++++++ .../stats-downloads/stats-downloads.tsx | 9 +++++++ .../modules/stats-emails/stats-emails.tsx | 9 +++++++ .../stats-referrers/stats-referrers.tsx | 9 +++++++ .../modules/stats-search/stats-search.tsx | 9 +++++++ .../stats-top-posts/stats-top-posts.tsx | 9 +++++++ .../stats-utm/stats-module-utm-wrapper.tsx | 2 ++ .../modules/stats-utm/stats-module-utm.jsx | 11 +++++++- .../modules/stats-videos/stats-videos.tsx | 9 +++++++ .../stats/features/modules/types.d.ts | 2 ++ client/my-sites/stats/site.jsx | 27 ++++++++++++++++--- .../src/horizontal-bar-list/stats-card.scss | 7 ++++- 14 files changed, 126 insertions(+), 8 deletions(-) diff --git a/client/my-sites/stats/features/modules/stats-authors/stats-authors.tsx b/client/my-sites/stats/features/modules/stats-authors/stats-authors.tsx index a464bbc9a236ea..d355125ef612b0 100644 --- a/client/my-sites/stats/features/modules/stats-authors/stats-authors.tsx +++ b/client/my-sites/stats/features/modules/stats-authors/stats-authors.tsx @@ -19,11 +19,12 @@ import StatsModule from '../../../stats-module'; import StatsCardSkeleton from '../shared/stats-card-skeleton'; import type { StatsDefaultModuleProps, StatsStateProps } from '../types'; -const StatClicks: React.FC< StatsDefaultModuleProps > = ( { +const StatAuthors: React.FC< StatsDefaultModuleProps > = ( { period, query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -101,10 +102,18 @@ const StatClicks: React.FC< StatsDefaultModuleProps > = ( { ) } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } ); }; -export default StatClicks; +export default StatAuthors; diff --git a/client/my-sites/stats/features/modules/stats-clicks/stats-clicks.tsx b/client/my-sites/stats/features/modules/stats-clicks/stats-clicks.tsx index 52e8c9636ec4b3..f276c27a1ac2b2 100644 --- a/client/my-sites/stats/features/modules/stats-clicks/stats-clicks.tsx +++ b/client/my-sites/stats/features/modules/stats-clicks/stats-clicks.tsx @@ -24,6 +24,7 @@ const StatsClicks: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -100,6 +101,14 @@ const StatsClicks: React.FC< StatsDefaultModuleProps > = ( { ) } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-countries/stats-countries.tsx b/client/my-sites/stats/features/modules/stats-countries/stats-countries.tsx index 89861a51b3bdf8..4c8c01f7ef286b 100644 --- a/client/my-sites/stats/features/modules/stats-countries/stats-countries.tsx +++ b/client/my-sites/stats/features/modules/stats-countries/stats-countries.tsx @@ -24,6 +24,7 @@ const StatsCountries: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -100,6 +101,14 @@ const StatsCountries: React.FC< StatsDefaultModuleProps > = ( { ) } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-downloads/stats-downloads.tsx b/client/my-sites/stats/features/modules/stats-downloads/stats-downloads.tsx index dd8417c5ab357e..3cf5996262b396 100644 --- a/client/my-sites/stats/features/modules/stats-downloads/stats-downloads.tsx +++ b/client/my-sites/stats/features/modules/stats-downloads/stats-downloads.tsx @@ -27,6 +27,7 @@ const StatsDownloads: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -102,6 +103,14 @@ const StatsDownloads: React.FC< StatsDefaultModuleProps > = ( { ) } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-emails/stats-emails.tsx b/client/my-sites/stats/features/modules/stats-emails/stats-emails.tsx index 0a1c7995e33039..fc33095095d6e2 100644 --- a/client/my-sites/stats/features/modules/stats-emails/stats-emails.tsx +++ b/client/my-sites/stats/features/modules/stats-emails/stats-emails.tsx @@ -25,6 +25,7 @@ const StatsEmails: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, }: StatsDefaultModuleProps ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -103,6 +104,14 @@ const StatsEmails: React.FC< StatsDefaultModuleProps > = ( { cards={ } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-referrers/stats-referrers.tsx b/client/my-sites/stats/features/modules/stats-referrers/stats-referrers.tsx index 117f22dc162897..c6d25dbffd91e5 100644 --- a/client/my-sites/stats/features/modules/stats-referrers/stats-referrers.tsx +++ b/client/my-sites/stats/features/modules/stats-referrers/stats-referrers.tsx @@ -24,6 +24,7 @@ const StatsReferrers: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -101,6 +102,14 @@ const StatsReferrers: React.FC< StatsDefaultModuleProps > = ( { cards={ } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-search/stats-search.tsx b/client/my-sites/stats/features/modules/stats-search/stats-search.tsx index d0d3b923f10f08..f0dc7de6a4f8f3 100644 --- a/client/my-sites/stats/features/modules/stats-search/stats-search.tsx +++ b/client/my-sites/stats/features/modules/stats-search/stats-search.tsx @@ -24,6 +24,7 @@ const StatSearch: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, }: StatsDefaultModuleProps ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -97,6 +98,14 @@ const StatSearch: React.FC< StatsDefaultModuleProps > = ( { ) } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-top-posts/stats-top-posts.tsx b/client/my-sites/stats/features/modules/stats-top-posts/stats-top-posts.tsx index 7d0afc4ebfc313..98edc8802222b2 100644 --- a/client/my-sites/stats/features/modules/stats-top-posts/stats-top-posts.tsx +++ b/client/my-sites/stats/features/modules/stats-top-posts/stats-top-posts.tsx @@ -24,6 +24,7 @@ const StatsTopPosts: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, } ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -107,6 +108,14 @@ const StatsTopPosts: React.FC< StatsDefaultModuleProps > = ( { } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-utm/stats-module-utm-wrapper.tsx b/client/my-sites/stats/features/modules/stats-utm/stats-module-utm-wrapper.tsx index 2877ca0df2f036..d1b0301b1c5236 100644 --- a/client/my-sites/stats/features/modules/stats-utm/stats-module-utm-wrapper.tsx +++ b/client/my-sites/stats/features/modules/stats-utm/stats-module-utm-wrapper.tsx @@ -20,6 +20,7 @@ const StatsModuleUTMWrapper: React.FC< StatsAdvancedModuleWrapperProps > = ( { query, summary, className, + summaryUrl, } ) => { const isNewEmptyStateEnabled = config.isEnabled( 'stats/empty-module-traffic' ); const isGatedByShouldGateStats = config.isEnabled( 'stats/restricted-dashboard' ); @@ -77,6 +78,7 @@ const StatsModuleUTMWrapper: React.FC< StatsAdvancedModuleWrapperProps > = ( { hideSummaryLink={ hideSummaryLink } postId={ postId } summary={ summary } + summaryUrl={ summaryUrl } /> ) } diff --git a/client/my-sites/stats/features/modules/stats-utm/stats-module-utm.jsx b/client/my-sites/stats/features/modules/stats-utm/stats-module-utm.jsx index 7da47e88747c0a..94ac613ea8fdc5 100644 --- a/client/my-sites/stats/features/modules/stats-utm/stats-module-utm.jsx +++ b/client/my-sites/stats/features/modules/stats-utm/stats-module-utm.jsx @@ -65,6 +65,7 @@ const StatsModuleUTM = ( { isLoading, query, postId, + summaryUrl, } ) => { const isNewEmptyStateEnabled = config.isEnabled( 'stats/empty-module-traffic' ); const siteId = useSelector( getSelectedSiteId ); @@ -161,7 +162,7 @@ const StatsModuleUTM = ( { { ! showLoader && ! data?.length && ( // no data and new empty state enabled } isEmpty @@ -187,6 +188,14 @@ const StatsModuleUTM = ( { cards={ } /> } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } { ! showLoader && diff --git a/client/my-sites/stats/features/modules/stats-videos/stats-videos.tsx b/client/my-sites/stats/features/modules/stats-videos/stats-videos.tsx index aa94c684b8d21c..ff71f733b94c68 100644 --- a/client/my-sites/stats/features/modules/stats-videos/stats-videos.tsx +++ b/client/my-sites/stats/features/modules/stats-videos/stats-videos.tsx @@ -25,6 +25,7 @@ const StatsVideos: React.FC< StatsDefaultModuleProps > = ( { query, moduleStrings, className, + summaryUrl, }: StatsDefaultModuleProps ) => { const translate = useTranslate(); const siteId = useSelector( getSelectedSiteId ) as number; @@ -100,6 +101,14 @@ const StatsVideos: React.FC< StatsDefaultModuleProps > = ( { cards={ } /> } + footerAction={ + summaryUrl + ? { + url: summaryUrl, + label: translate( 'View more' ), + } + : undefined + } /> ) } diff --git a/client/my-sites/stats/features/modules/types.d.ts b/client/my-sites/stats/features/modules/types.d.ts index 4d43168888c6c6..92b746b5922f72 100644 --- a/client/my-sites/stats/features/modules/types.d.ts +++ b/client/my-sites/stats/features/modules/types.d.ts @@ -10,6 +10,7 @@ type StatsDefaultModuleProps = { value: string; empty: string; }; + summaryUrl?: string; }; type StatsAdvancedModuleWrapperProps = { @@ -19,6 +20,7 @@ type StatsAdvancedModuleWrapperProps = { query: StatsQueryType; summary?: boolean; className?: string; + summaryUrl?: string; }; type StatsPeriodType = { diff --git a/client/my-sites/stats/site.jsx b/client/my-sites/stats/site.jsx index 8e930b73c8ebc5..e84fedddc2da61 100644 --- a/client/my-sites/stats/site.jsx +++ b/client/my-sites/stats/site.jsx @@ -211,6 +211,19 @@ class StatsSite extends Component { } } + getStatHref( period, path, siteSlug ) { + return period && path && siteSlug + ? '/stats/' + + period?.period + + '/' + + path + + '/' + + siteSlug + + '?startDate=' + + period?.startOf?.format( 'YYYY-MM-DD' ) + : undefined; + } + renderStats( isInternal ) { const { date, @@ -431,6 +444,7 @@ class StatsSite extends Component { moduleStrings={ moduleStrings.posts } period={ this.props.period } query={ query } + summaryUrl={ this.getStatHref( this.props.period, 'posts', slug ) } className={ clsx( 'stats__flexible-grid-item--60', 'stats__flexible-grid-item--full--large', @@ -458,6 +472,7 @@ class StatsSite extends Component { moduleStrings={ moduleStrings.referrers } period={ this.props.period } query={ query } + summaryUrl={ this.getStatHref( this.props.period, 'referrers', slug ) } className={ clsx( 'stats__flexible-grid-item--40--once-space', 'stats__flexible-grid-item--full--large', @@ -480,7 +495,7 @@ class StatsSite extends Component { moduleStrings={ moduleStrings.countries } period={ this.props.period } query={ query } - summary={ false } + summaryUrl={ this.getStatHref( this.props.period, 'countryviews', slug ) } className={ clsx( 'stats__flexible-grid-item--full' ) } /> ) } @@ -491,6 +506,7 @@ class StatsSite extends Component { siteId={ siteId } period={ this.props.period } query={ query } + summaryUrl={ this.getStatHref( this.props.period, 'utm', slug ) } summary={ false } className={ clsx( 'stats__flexible-grid-item--60', @@ -521,12 +537,10 @@ class StatsSite extends Component { { /* If UTM card or update card is not visible, shift "Clicks" and reduct to 1/2 for easier stacking */ } { isNewStateEnabled && ( Date: Wed, 24 Jul 2024 03:44:24 -0400 Subject: [PATCH 18/25] Masterbar cleanup - consolidate siteId values used. (#92906) --- client/layout/masterbar/logged-in.jsx | 31 ++++++++++----------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/client/layout/masterbar/logged-in.jsx b/client/layout/masterbar/logged-in.jsx index 8c3548b598973a..e7caa123980324 100644 --- a/client/layout/masterbar/logged-in.jsx +++ b/client/layout/masterbar/logged-in.jsx @@ -58,7 +58,6 @@ import { getMostRecentlySelectedSiteId, getSectionGroup, getSectionName, - getSelectedSiteId, } from 'calypso/state/ui/selectors'; import Item from './item'; import Masterbar from './masterbar'; @@ -167,20 +166,20 @@ class MasterbarLoggedIn extends Component { * * This code makes it possible to reset the failed migration state when clicking My Sites too. */ - const { migrationStatus, currentSelectedSiteId } = this.props; + const { migrationStatus, siteId } = this.props; - if ( currentSelectedSiteId && migrationStatus === 'error' ) { + if ( siteId && migrationStatus === 'error' ) { /** * Reset the in-memory site lock for the currently selected site */ - this.props.updateSiteMigrationMeta( currentSelectedSiteId, 'inactive', null, null ); + this.props.updateSiteMigrationMeta( siteId, 'inactive', null, null ); /** * Reset the migration on the backend */ wpcom.req .post( { - path: `/sites/${ currentSelectedSiteId }/reset-migration`, + path: `/sites/${ siteId }/reset-migration`, apiNamespace: 'wpcom/v2', } ) .catch( () => {} ); @@ -727,12 +726,12 @@ class MasterbarLoggedIn extends Component { } renderHelpCenter() { - const { currentSelectedSiteId, translate } = this.props; + const { siteId, translate } = this.props; return ( @@ -846,13 +845,10 @@ export default connect( // Falls back to using the user's primary site if no site has been selected // by the user yet - const currentSelectedSiteId = getSelectedSiteId( state ); - const siteId = - currentSelectedSiteId || getMostRecentlySelectedSiteId( state ) || getPrimarySiteId( state ); + const siteId = getMostRecentlySelectedSiteId( state ) || getPrimarySiteId( state ); const sitePlanSlug = getSitePlanSlug( state, siteId ); const isMigrationInProgress = - isSiteMigrationInProgress( state, currentSelectedSiteId ) || - isSiteMigrationActiveRoute( state ); + isSiteMigrationInProgress( state, siteId ) || isSiteMigrationActiveRoute( state ); const siteCount = getCurrentUserSiteCount( state ) ?? 0; const site = getSite( state, siteId ); @@ -875,16 +871,11 @@ export default connect( isSupportSession: isSupportSession( state ), isInEditor: getSectionName( state ) === 'gutenberg-editor', isMigrationInProgress, - migrationStatus: getSiteMigrationStatus( state, currentSelectedSiteId ), - currentSelectedSiteId, + migrationStatus: getSiteMigrationStatus( state, siteId ), isClassicView, - currentSelectedSiteSlug: currentSelectedSiteId - ? getSiteSlug( state, currentSelectedSiteId ) - : undefined, + currentSelectedSiteSlug: siteId ? getSiteSlug( state, siteId ) : undefined, previousPath: getPreviousRoute( state ), - isJetpackNotAtomic: - isJetpackSite( state, currentSelectedSiteId ) && - ! isAtomicSite( state, currentSelectedSiteId ), + isJetpackNotAtomic: isJetpackSite( state, siteId ) && ! isAtomicSite( state, siteId ), currentLayoutFocus: getCurrentLayoutFocus( state ), hasDismissedThePopover: getPreference( state, MENU_POPOVER_PREFERENCE_KEY ), isFetchingPrefs: isFetchingPreferences( state ), From ead328d5a897fea39e5d11f575f232ebeb6e70cb Mon Sep 17 00:00:00 2001 From: Addison Stavlo Date: Wed, 24 Jul 2024 03:56:48 -0400 Subject: [PATCH 19/25] Sites dashboard - revert recent changes to selectedSiteId handling. (#92904) --- client/hosting/sites/controller.tsx | 4 ++ .../sites/hooks/use-sync-selected-site.ts | 9 +--- client/state/global-sidebar/selectors.ts | 42 ++++--------------- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/client/hosting/sites/controller.tsx b/client/hosting/sites/controller.tsx index d528a4900726c2..901a2631d8f5c7 100644 --- a/client/hosting/sites/controller.tsx +++ b/client/hosting/sites/controller.tsx @@ -7,6 +7,7 @@ import { removeQueryArgs } from '@wordpress/url'; import AsyncLoad from 'calypso/components/async-load'; import PageViewTracker from 'calypso/lib/analytics/page-view-tracker'; import { removeNotice } from 'calypso/state/notices/actions'; +import { setAllSitesSelected } from 'calypso/state/ui/actions'; import { getSelectedSite } from 'calypso/state/ui/selectors'; import SitesDashboard from './components/sites-dashboard'; import type { Context, Context as PageJSContext } from '@automattic/calypso-router'; @@ -119,6 +120,9 @@ export function sitesDashboard( context: Context, next: () => void ) { ); + // By definition, Sites Dashboard does not select any one specific site + context.store.dispatch( setAllSitesSelected() ); + next(); } diff --git a/client/hosting/sites/hooks/use-sync-selected-site.ts b/client/hosting/sites/hooks/use-sync-selected-site.ts index 55aab3ada6221b..bd9771bf209c69 100644 --- a/client/hosting/sites/hooks/use-sync-selected-site.ts +++ b/client/hosting/sites/hooks/use-sync-selected-site.ts @@ -1,5 +1,5 @@ import { SiteDetails } from '@automattic/data-stores'; -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useDispatch } from 'calypso/state'; import { setSelectedSiteId } from 'calypso/state/ui/actions'; import type { DataViewsState } from 'calypso/a8c-for-agencies/components/items-dashboard/items-dataviews/interfaces'; @@ -10,16 +10,9 @@ export function useSyncSelectedSite( selectedSite: SiteDetails | null | undefined ) { const dispatch = useDispatch(); - const isInitialLoad = useRef( true ); // Update selected site globally as soon as it is clicked from the table. useEffect( () => { - // Prevent clearing the selected site on initial load. - if ( isInitialLoad.current && ! dataViewsState.selectedItem ) { - isInitialLoad.current = false; - return; - } - isInitialLoad.current = false; dispatch( setSelectedSiteId( dataViewsState.selectedItem?.ID ) ); }, [ dispatch, dataViewsState.selectedItem ] ); diff --git a/client/state/global-sidebar/selectors.ts b/client/state/global-sidebar/selectors.ts index a4a9b3b4bfb44e..cf496989eb0006 100644 --- a/client/state/global-sidebar/selectors.ts +++ b/client/state/global-sidebar/selectors.ts @@ -61,18 +61,6 @@ export const getShouldShowGlobalSidebar = ( ); }; -interface CollapsedDataHelper { - shouldShowForAnimation: boolean; - selectedSiteId: number | null | undefined; - sectionGroup: string; -} - -const collapsedDataHelper: CollapsedDataHelper = { - shouldShowForAnimation: false, - selectedSiteId: null, - sectionGroup: '', -}; - export const getShouldShowCollapsedGlobalSidebar = ( state: AppState, siteId: number | null, @@ -82,37 +70,21 @@ export const getShouldShowCollapsedGlobalSidebar = ( const isSitesDashboard = sectionGroup === 'sites-dashboard'; const isSiteDashboard = getShouldShowSiteDashboard( state, siteId, sectionGroup, sectionName ); - if ( collapsedDataHelper.sectionGroup !== sectionGroup ) { - if ( isSitesDashboard ) { - // Set or refresh the initial value when loading into the dashboard. - collapsedDataHelper.selectedSiteId = siteId; - } else { - // Clear this once we are off the sites dashboard. - collapsedDataHelper.shouldShowForAnimation = false; - } - // Keep track of section group to evaluate things when this changes. - collapsedDataHelper.sectionGroup = sectionGroup; - } - - // When selected site changes on the dashboard, show for animation. - if ( + // A site is just clicked and the global sidebar is in collapsing animation. + const isSiteJustSelectedFromSitesDashboard = isSitesDashboard && !! siteId && - collapsedDataHelper.selectedSiteId !== siteId && - ! collapsedDataHelper.shouldShowForAnimation - ) { - collapsedDataHelper.shouldShowForAnimation = true; - collapsedDataHelper.selectedSiteId = siteId; - } + isInRoute( state, [ + '/sites', // started collapsing when still in sites dashboard + ...Object.values( SITE_DASHBOARD_ROUTES ), // has just stopped collapsing when in one of the paths in site dashboard + ] ); const isPluginsScheduledUpdatesEditMode = isScheduledUpdatesMultisiteCreateRoute( state ) || isScheduledUpdatesMultisiteEditRoute( state ); return ( - collapsedDataHelper.shouldShowForAnimation || - isSiteDashboard || - isPluginsScheduledUpdatesEditMode + isSiteJustSelectedFromSitesDashboard || isSiteDashboard || isPluginsScheduledUpdatesEditMode ); }; From 11647c79152554f5b9e2b40ef3304c9fc62a5517 Mon Sep 17 00:00:00 2001 From: Miguel San Segundo <1881481+miksansegundo@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:20:21 +0700 Subject: [PATCH 20/25] Package "page-pattern-modal": publishing to npm (#92871) * Fix for error TS7016: Could not find a declaration file for module '@automattic/calypso-config' * Update version and fix command not found: copy-assets * Run copy-assets command globally * Fix for could not find a declaration file for module '@automattic/calypso-build' * Bump version * Fix npm publish errors for the dependency @automattic/onboarding * Remove unused dependency * Bump version * Update yarn lock file * Fix styles when package is imported * Bump version * Revert styles fix * Bump version --- packages/i18n-utils/tsconfig.json | 6 +++++- packages/onboarding/package.json | 6 +++--- packages/onboarding/tsconfig.json | 6 +++++- packages/page-pattern-modal/package.json | 5 ++--- packages/page-pattern-modal/tsconfig.json | 2 +- yarn.lock | 2 +- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/i18n-utils/tsconfig.json b/packages/i18n-utils/tsconfig.json index 0d3cc51ad8bf73..0848e916de4078 100644 --- a/packages/i18n-utils/tsconfig.json +++ b/packages/i18n-utils/tsconfig.json @@ -7,5 +7,9 @@ }, "include": [ "src" ], "exclude": [ "**/test/*" ], - "references": [ { "path": "../languages" }, { "path": "../calypso-url" } ] + "references": [ + { "path": "../languages" }, + { "path": "../calypso-url" }, + { "path": "../calypso-config" } + ] } diff --git a/packages/onboarding/package.json b/packages/onboarding/package.json index cb3ed25bdd9779..539c88f7f46f27 100644 --- a/packages/onboarding/package.json +++ b/packages/onboarding/package.json @@ -24,11 +24,12 @@ "types": "dist/types", "scripts": { "clean": "tsc --build ./tsconfig.json ./tsconfig-cjs.json --clean && rm -rf dist", - "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json && copy-assets && npx copyfiles ./styles/** dist", + "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json && yarn run -T copy-assets && npx copyfiles ./styles/** dist", "prepack": "yarn run clean && yarn run build", "watch": "tsc --build ./tsconfig.json --watch" }, "dependencies": { + "@automattic/calypso-config": "workspace:^", "@automattic/components": "workspace:^", "@automattic/data-stores": "workspace:^", "@wordpress/components": "^28.2.0", @@ -58,6 +59,5 @@ "peerDependencies": { "@wordpress/i18n": "^5.2.0", "react": "^18.2.0" - }, - "private": true + } } diff --git a/packages/onboarding/tsconfig.json b/packages/onboarding/tsconfig.json index dd762b0f4895a9..d9d2d2a51a3611 100644 --- a/packages/onboarding/tsconfig.json +++ b/packages/onboarding/tsconfig.json @@ -7,5 +7,9 @@ }, "include": [ "src" ], "exclude": [ "**/test/*" ], - "references": [ { "path": "../components" }, { "path": "../data-stores" } ] + "references": [ + { "path": "../components" }, + { "path": "../data-stores" }, + { "path": "../calypso-config" } + ] } diff --git a/packages/page-pattern-modal/package.json b/packages/page-pattern-modal/package.json index c07f132ce3af48..b8a786d72e44ac 100644 --- a/packages/page-pattern-modal/package.json +++ b/packages/page-pattern-modal/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/page-pattern-modal", - "version": "1.0.0-alpha.0", + "version": "1.1.4", "description": "Automattic Page Pattern Modal.", "homepage": "https://github.com/Automattic/wp-calypso", "license": "GPL-2.0-or-later", @@ -27,7 +27,6 @@ ], "types": "dist/types", "dependencies": { - "@automattic/onboarding": "workspace:^", "@automattic/typography": "workspace:^", "@wordpress/block-editor": "^13.2.0", "@wordpress/blocks": "^13.2.0", @@ -55,7 +54,7 @@ }, "scripts": { "clean": "tsc --build ./tsconfig.json ./tsconfig-cjs.json --clean && rm -rf dist", - "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json && copy-assets", + "build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json && yarn run -T copy-assets", "prepack": "yarn run clean && yarn run build" } } diff --git a/packages/page-pattern-modal/tsconfig.json b/packages/page-pattern-modal/tsconfig.json index f7212f67c2ea19..96c32d21af2c59 100644 --- a/packages/page-pattern-modal/tsconfig.json +++ b/packages/page-pattern-modal/tsconfig.json @@ -8,5 +8,5 @@ }, "include": [ "src" ], "exclude": [ "**/test/*" ], - "references": [ { "path": "../onboarding" } ] + "references": [] } diff --git a/yarn.lock b/yarn.lock index 230edff518e27f..17fba54e12fd9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1523,6 +1523,7 @@ __metadata: version: 0.0.0-use.local resolution: "@automattic/onboarding@workspace:packages/onboarding" dependencies: + "@automattic/calypso-config": "workspace:^" "@automattic/calypso-typescript-config": "workspace:^" "@automattic/components": "workspace:^" "@automattic/data-stores": "workspace:^" @@ -1557,7 +1558,6 @@ __metadata: resolution: "@automattic/page-pattern-modal@workspace:packages/page-pattern-modal" dependencies: "@automattic/calypso-typescript-config": "workspace:^" - "@automattic/onboarding": "workspace:^" "@automattic/typography": "workspace:^" "@testing-library/react": "npm:^15.0.7" "@wordpress/block-editor": "npm:^13.2.0" From 28535f4bcf05921fea8e9cae0edbb0ea24a1b9e2 Mon Sep 17 00:00:00 2001 From: Yashwin Poojary Date: Wed, 24 Jul 2024 13:53:03 +0530 Subject: [PATCH 21/25] A4A > Marketplace: Add Hosting page redesign feature flag (#92895) * Add Hosting page redesign feature flag * address PR comments --- .../hosting-overview/hosting-list/index.tsx | 1 + .../hosting-overview/hosting-v2/index.tsx | 12 ++++++++++++ .../sections/marketplace/hosting-overview/index.tsx | 7 ++++--- config/a8c-for-agencies-development.json | 3 ++- config/a8c-for-agencies-horizon.json | 3 ++- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-v2/index.tsx diff --git a/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-list/index.tsx b/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-list/index.tsx index 296d9a98788866..99919992c7d8fb 100644 --- a/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-list/index.tsx +++ b/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-list/index.tsx @@ -27,6 +27,7 @@ export default function HostingList( { selectedSite }: Props ) { const { data } = useProductsQuery( false, true ); const isAutomatedReferrals = isEnabled( 'a4a-automated-referrals' ); + const { marketplaceType } = useContext( MarketplaceTypeContext ); // Hide the section if it's automated referrals marketplace diff --git a/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-v2/index.tsx b/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-v2/index.tsx new file mode 100644 index 00000000000000..47acfba05ed82e --- /dev/null +++ b/client/a8c-for-agencies/sections/marketplace/hosting-overview/hosting-v2/index.tsx @@ -0,0 +1,12 @@ +import { useTranslate } from 'i18n-calypso'; + +export default function HostingV2() { + const translate = useTranslate(); + + return ( + <> + { translate( "Choose the hosting tailored for your client's needs." ) } + Append the URL with `?flags=-a4a-hosting-page-redesign` to see the old design. + + ); +} diff --git a/client/a8c-for-agencies/sections/marketplace/hosting-overview/index.tsx b/client/a8c-for-agencies/sections/marketplace/hosting-overview/index.tsx index 43342294adadc3..816058e6912508 100644 --- a/client/a8c-for-agencies/sections/marketplace/hosting-overview/index.tsx +++ b/client/a8c-for-agencies/sections/marketplace/hosting-overview/index.tsx @@ -1,3 +1,4 @@ +import { isEnabled } from '@automattic/calypso-config'; import page from '@automattic/calypso-router'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; @@ -18,9 +19,11 @@ import withMarketplaceType from '../hoc/with-marketplace-type'; import useShoppingCart from '../hooks/use-shopping-cart'; import ShoppingCart from '../shopping-cart'; import HostingList from './hosting-list'; +import HostingV2 from './hosting-v2'; function Hosting() { const translate = useTranslate(); + const isNewHostingPage = isEnabled( 'a4a-hosting-page-redesign' ); const { selectedCartItems, onRemoveCartItem, showCart, setShowCart, toggleCart } = useShoppingCart(); @@ -64,9 +67,7 @@ function Hosting() { - - - + { isNewHostingPage ? : } ); } diff --git a/config/a8c-for-agencies-development.json b/config/a8c-for-agencies-development.json index 77c4ef2fe42a64..8ec8e9146aab07 100644 --- a/config/a8c-for-agencies-development.json +++ b/config/a8c-for-agencies-development.json @@ -40,7 +40,8 @@ "a4a/site-migration": true, "a4a-logged-out-signup": true, "a4a-automated-referrals": true, - "a4a-partner-directory": true + "a4a-partner-directory": true, + "a4a-hosting-page-redesign": true }, "enable_all_sections": false, "sections": { diff --git a/config/a8c-for-agencies-horizon.json b/config/a8c-for-agencies-horizon.json index a4858fb59ff768..3597155b0b5bb3 100644 --- a/config/a8c-for-agencies-horizon.json +++ b/config/a8c-for-agencies-horizon.json @@ -33,7 +33,8 @@ "a4a/site-migration": true, "a4a-logged-out-signup": true, "a4a-automated-referrals": true, - "a4a-partner-directory": true + "a4a-partner-directory": true, + "a4a-hosting-page-redesign": true }, "enable_all_sections": false, "sections": { From acbdbba2d737290db309a98b5a306da5c771c1a4 Mon Sep 17 00:00:00 2001 From: Christos Date: Wed, 24 Jul 2024 11:41:45 +0300 Subject: [PATCH 22/25] Plans (State): Update checkout upsell-nudge to utilise data-store pricing (#92714) --- .../business-plan-upgrade-upsell/index.jsx | 6 +- .../my-sites/checkout/upsell-nudge/index.tsx | 185 +++++++++--------- .../upsell-nudge/test/upsell-nudge.tsx | 64 ++++++ .../calypso-products/src/plans-utilities.ts | 3 +- .../src/plans/hooks/use-current-plan-term.ts | 18 ++ packages/data-stores/src/plans/index.ts | 1 + 6 files changed, 185 insertions(+), 92 deletions(-) create mode 100644 packages/data-stores/src/plans/hooks/use-current-plan-term.ts diff --git a/client/my-sites/checkout/upsell-nudge/business-plan-upgrade-upsell/index.jsx b/client/my-sites/checkout/upsell-nudge/business-plan-upgrade-upsell/index.jsx index 70b27d66a14334..e96672d9e9bce9 100644 --- a/client/my-sites/checkout/upsell-nudge/business-plan-upgrade-upsell/index.jsx +++ b/client/my-sites/checkout/upsell-nudge/business-plan-upgrade-upsell/index.jsx @@ -158,9 +158,13 @@ export class BusinessPlanUpgradeUpsell extends PureComponent { ), }, args: { - fullPrice: formatCurrency( planRawPrice, currencyCode, { stripZeros: true } ), + fullPrice: formatCurrency( planRawPrice, currencyCode, { + stripZeros: true, + isSmallestUnit: true, + } ), discountPrice: formatCurrency( planDiscountedRawPrice, currencyCode, { stripZeros: true, + isSmallestUnit: true, } ), planName: getPlan( PLAN_PREMIUM )?.getTitle() ?? '', comment: 'A monetary value at the end, e.g. $25', diff --git a/client/my-sites/checkout/upsell-nudge/index.tsx b/client/my-sites/checkout/upsell-nudge/index.tsx index 988004f823a496..7957a84a979ad8 100644 --- a/client/my-sites/checkout/upsell-nudge/index.tsx +++ b/client/my-sites/checkout/upsell-nudge/index.tsx @@ -1,22 +1,22 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; import { isMonthly, getPlanByPathSlug, TERM_MONTHLY, - Product, isPlan, + PlanSlug, } from '@automattic/calypso-products'; import { RazorpayHookProvider } from '@automattic/calypso-razorpay'; import page from '@automattic/calypso-router'; import { StripeHookProvider } from '@automattic/calypso-stripe'; import { CompactCard, Gridicon } from '@automattic/components'; +import { Plans, ProductsList } from '@automattic/data-stores'; import { withShoppingCart, createRequestCartProduct } from '@automattic/shopping-cart'; import { isURL } from '@wordpress/url'; import clsx from 'clsx'; import debugFactory from 'debug'; import { localize, useTranslate } from 'i18n-calypso'; -import { pick } from 'lodash'; import { Component } from 'react'; -import { connect } from 'react-redux'; import QueryProductsList from 'calypso/components/data/query-products-list'; import QuerySitePlans from 'calypso/components/data/query-site-plans'; import QuerySites from 'calypso/components/data/query-sites'; @@ -27,29 +27,18 @@ import getThankYouPageUrl from 'calypso/my-sites/checkout/get-thank-you-page-url import ProfessionalEmailUpsell from 'calypso/my-sites/checkout/upsell-nudge/professional-email-upsell'; import withCartKey from 'calypso/my-sites/checkout/with-cart-key'; import { IntervalLength } from 'calypso/my-sites/email/email-providers-comparison/interval-length'; +import useCheckPlanAvailabilityForPurchase from 'calypso/my-sites/plans-features-main/hooks/use-check-plan-availability-for-purchase'; import { retrieveSignupDestination, clearSignupDestinationCookie, persistSignupDestination, } from 'calypso/signup/storageUtils'; -import { recordTracksEvent } from 'calypso/state/analytics/actions'; -import { getCurrentUserCurrencyCode } from 'calypso/state/currency-code/selectors'; +import { useSelector } from 'calypso/state'; import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; -import { - getProductsList, - getProductDisplayCost, - getProductBySlug, - isProductsListFetching, -} from 'calypso/state/products-list/selectors'; -import getCurrentPlanTerm from 'calypso/state/selectors/get-current-plan-term'; +import { getProductsList, isProductsListFetching } from 'calypso/state/products-list/selectors'; import getUpgradePlanSlugFromPath from 'calypso/state/selectors/get-upgrade-plan-slug-from-path'; -import { - isRequestingSitePlans, - getPlansBySiteId, - getSitePlanRawPrice, - getPlanDiscountedRawPrice, -} from 'calypso/state/sites/plans/selectors'; -import { getSitePlan, getSiteSlug } from 'calypso/state/sites/selectors'; +import { isRequestingSitePlans, getPlansBySiteId } from 'calypso/state/sites/plans/selectors'; +import { getSiteSlug } from 'calypso/state/sites/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; import PurchaseModal from '../purchase-modal'; import { @@ -59,8 +48,6 @@ import { import { BusinessPlanUpgradeUpsell } from './business-plan-upgrade-upsell'; import { QuickstartSessionsRetirement } from './quickstart-sessions-retirement'; import type { WithShoppingCartProps, MinimalRequestCartProduct } from '@automattic/shopping-cart'; -import type { IAppState } from 'calypso/state/types'; - import './style.scss'; const debug = debugFactory( 'calypso:upsell-nudge' ); @@ -84,20 +71,17 @@ export interface UpsellNudgeManualProps { // Below are provided by HOCs export interface UpsellNudgeAutomaticProps extends WithShoppingCartProps { - currencyCode: string | null; + currencyCode: string | undefined; isLoading?: boolean; hasProductsList?: boolean; hasSitePlans?: boolean; product: MinimalRequestCartProduct | undefined; - productDisplayCost?: string | null; planRawPrice?: number | null; planDiscountedRawPrice?: number | null; isLoggedIn?: boolean; siteSlug?: string | null; - currentProduct?: Product | Record< string, never >; selectedSiteId: string | number | undefined | null; hasSevenDayRefundPeriod?: boolean; - trackUpsellButtonClick: ( key: string ) => void; translate: ReturnType< typeof useTranslate >; currentPlanTerm: string; } @@ -111,6 +95,12 @@ interface UpsellNudgeState { showPurchaseModal: boolean; } +const trackUpsellButtonClick = ( eventName: string ) => { + // Track upsell get started / accept / decline events + recordTracksEvent( eventName, { section: 'checkout' } ); + return; +}; + export class UpsellNudge extends Component< UpsellNudgeProps, UpsellNudgeState > { state: UpsellNudgeState = { cartItem: null, @@ -277,7 +267,7 @@ export class UpsellNudge extends Component< UpsellNudgeProps, UpsellNudgeState > }; handleClickDecline = ( shouldHideUpsellNudges = true ) => { - const { trackUpsellButtonClick, upsellType } = this.props; + const { upsellType } = this.props; trackUpsellButtonClick( `calypso_${ upsellType.replace( /-/g, '_' ) }_decline_button_click` ); @@ -308,7 +298,7 @@ export class UpsellNudge extends Component< UpsellNudgeProps, UpsellNudgeState > }; handleClickAccept = async ( buttonAction: string ) => { - const { siteSlug, trackUpsellButtonClick, upgradeItem, upsellType } = this.props; + const { siteSlug, upgradeItem, upsellType } = this.props; debug( 'accept upsell clicked' ); trackUpsellButtonClick( @@ -448,11 +438,6 @@ export class UpsellNudge extends Component< UpsellNudgeProps, UpsellNudgeState > }; } -const trackUpsellButtonClick = ( eventName: string ) => { - // Track upsell get started / accept / decline events - return recordTracksEvent( eventName, { section: 'checkout' } ); -}; - const getProductSlug = ( upsellType: string, productAlias: string, planTerm: string ) => { switch ( upsellType ) { case BUSINESS_PLAN_UPGRADE_UPSELL: @@ -468,62 +453,82 @@ const getProductSlug = ( upsellType: string, productAlias: string, planTerm: str } }; -export default connect( - ( state: IAppState, props: UpsellNudgeManualProps ) => { - const { siteSlugParam, upgradeItem, upsellType } = props; - const selectedSiteId = getSelectedSiteId( state ); - const productsList = getProductsList( state ); - const sitePlans = getPlansBySiteId( state, undefined ).data; - const siteSlug = selectedSiteId ? getSiteSlug( state, selectedSiteId ) : siteSlugParam; - const planSlug = getUpgradePlanSlugFromPath( - state, - selectedSiteId ?? 0, - props.upgradeItem ?? '' - ); - const annualDiscountPrice = getPlanDiscountedRawPrice( state, selectedSiteId ?? 0, planSlug, { - returnMonthly: false, - } ); - const annualPrice = getSitePlanRawPrice( state, selectedSiteId ?? 0, planSlug, { - returnMonthly: false, - } ); - - const currentPlanTerm = getCurrentPlanTerm( state, selectedSiteId ?? 0 ) ?? TERM_MONTHLY; - const currentSitePlan = getSitePlan( state, selectedSiteId ?? 0 ); - const currentProduct = getProductBySlug( state, currentSitePlan?.product_slug ?? '' ) || {}; - const productSlug = getProductSlug( upsellType, upgradeItem ?? '', currentPlanTerm ); - const productProperties = pick( getProductBySlug( state, productSlug ?? '' ), [ - 'product_slug', - 'product_id', - ] ); - const product = - productProperties.product_slug && productProperties.product_id - ? createRequestCartProduct( { - product_slug: productProperties.product_slug, - product_id: productProperties.product_id, - } ) - : undefined; - - return { - currencyCode: getCurrentUserCurrencyCode( state ), - currentPlanTerm, - currentProduct, - isLoading: isProductsListFetching( state ) || isRequestingSitePlans( state, selectedSiteId ), - hasProductsList: Object.keys( productsList ).length > 0, - hasSitePlans: sitePlans ? sitePlans.length > 0 : undefined, - product, - productDisplayCost: getProductDisplayCost( state, productSlug ?? '' ), - planRawPrice: annualPrice, - planDiscountedRawPrice: annualDiscountPrice, - isLoggedIn: isUserLoggedIn( state ), - siteSlug, - selectedSiteId, - hasSevenDayRefundPeriod: isMonthly( planSlug ), - productSlug, - }; - }, - { - trackUpsellButtonClick, - } -)( - withIsEligibleForOneClickCheckout( withCartKey( withShoppingCart( localize( UpsellNudge ) ) ) ) +const WrappedUpsellNudge = ( + props: UpsellNudgeManualProps & WithIsEligibleForOneClickCheckoutProps & WithShoppingCartProps +) => { + const { siteSlugParam, upgradeItem, upsellType } = props; + const translate = useTranslate(); + const isLoggedIn = useSelector( isUserLoggedIn ); + const selectedSiteId = useSelector( getSelectedSiteId ); + const products = ProductsList.useProducts(); + const currentPlanTerm = Plans.useCurrentPlanTerm( { siteId: selectedSiteId } ); + const upsellProductSlug = getProductSlug( + upsellType, + upgradeItem ?? '', + currentPlanTerm ?? TERM_MONTHLY + ); + const upsellProduct = + upsellProductSlug && products.data?.[ upsellProductSlug as ProductsList.StoreProductSlug ]; + const cartProduct = + upsellProduct?.productSlug && upsellProduct?.id + ? createRequestCartProduct( { + product_slug: upsellProduct.productSlug, + product_id: upsellProduct.id, + } ) + : undefined; + const planSlug = useSelector( ( state ) => + getUpgradePlanSlugFromPath( state, selectedSiteId ?? 0, upgradeItem ?? '' ) + ); + const siteSlug = + useSelector( ( state ) => getSiteSlug( state, selectedSiteId ) ) ?? siteSlugParam; + + /** + * Redux site-plans replaceable by data-store `Plans.useSitePlans` + * - Needs confirmation whether consumed later + */ + const sitePlans = useSelector( + ( state ) => getPlansBySiteId( state, selectedSiteId ?? undefined ).data // (the beauty of inconsistency / .data) + ); + const isLoadingSitePlans = useSelector( ( state ) => + isRequestingSitePlans( state, selectedSiteId ) + ); + + /** + * Redux products-list replaceable by data-store `Products.useProducts` + * - Needs confirmation whether consumed later + */ + const productsList = useSelector( getProductsList ); // (the beauty of inconsistency / no .data) + const isLoadingProductsList = useSelector( isProductsListFetching ); + + const pricing = Plans.usePricingMetaForGridPlans( { + planSlugs: [ planSlug as PlanSlug ], + siteId: selectedSiteId, + useCheckPlanAvailabilityForPurchase, + coupon: undefined, + storageAddOns: null, + withProratedDiscounts: true, + } ); + + return ( + 0 : undefined } + hasProductsList={ Object.keys( productsList ).length > 0 } + currentPlanTerm={ currentPlanTerm ?? TERM_MONTHLY } + product={ cartProduct } + isLoggedIn={ isLoggedIn } + siteSlug={ siteSlug } + selectedSiteId={ selectedSiteId } + translate={ translate } + /> + ); +}; + +export default withIsEligibleForOneClickCheckout( + withCartKey( withShoppingCart( localize( WrappedUpsellNudge ) ) ) ); diff --git a/client/my-sites/checkout/upsell-nudge/test/upsell-nudge.tsx b/client/my-sites/checkout/upsell-nudge/test/upsell-nudge.tsx index 034418a4edd8b0..fb2e51c84d4166 100644 --- a/client/my-sites/checkout/upsell-nudge/test/upsell-nudge.tsx +++ b/client/my-sites/checkout/upsell-nudge/test/upsell-nudge.tsx @@ -3,6 +3,7 @@ */ import page from '@automattic/calypso-router'; +import { Plans, ProductsList } from '@automattic/data-stores'; import { createShoppingCartManagerClient, getEmptyResponseCart, @@ -28,11 +29,59 @@ import UpsellNudge, { BUSINESS_PLAN_UPGRADE_UPSELL, PROFESSIONAL_EMAIL_UPSELL } import type { StoredPaymentMethodCard } from '../../../../lib/checkout/payment-methods'; jest.mock( '@automattic/calypso-router', () => jest.fn() ); +jest.mock( '@automattic/data-stores', () => ( { + ...jest.requireActual( '@automattic/data-stores' ), + ProductsList: { + ...jest.requireActual( '@automattic/data-stores' ).ProductsList, + useProducts: jest.fn(), + }, + Plans: { + ...jest.requireActual( '@automattic/data-stores' ).Plans, + usePlans: jest.fn(), + useCurrentPlan: jest.fn(), + usePricingMetaForGridPlans: jest.fn(), + }, +} ) ); const mockCountries: CountryListItem[] = [ { code: 'US', has_postal_codes: true, name: 'United States', vat_supported: false }, ]; +const mockDataStorePlans = { + 'business-bundle': { + planSlug: 'business-bundle', + pricing: { + currencyCode: 'USD', + billPeriod: 365, + originalPrice: { + full: 30000, + monthly: 30000, + }, + discountedPrice: { + full: 30000, + monthly: 30000, + }, + }, + }, +}; + +const mockDataStoreProducts = { + 'business-bundle': { + id: 1008, + productSlug: 'business-bundle', + }, + + wp_titan_mail_yearly: { + id: 401, + productSlug: 'wp_titan_mail_yearly', + }, + + wp_titan_mail_monthly: { + id: 400, + productSlug: 'wp_titan_mail_monthly', + }, +}; + const mockProducts = { 'business-bundle': { product_id: 1008, @@ -199,6 +248,21 @@ describe( 'UpsellNudge', () => { nock( 'https://public-api.wordpress.com' ) .get( '/rest/v1.1/products?type=all' ) .reply( 200, () => mockProducts ); + Plans.useCurrentPlan.mockImplementation( () => ( { + [ 'business-bundle' ]: mockDataStorePlans[ 'business-bundle' ], + } ) ); + Plans.usePlans.mockImplementation( () => ( { + data: { + [ 'business-bundle' ]: mockDataStorePlans, + }, + } ) ); + Plans.usePricingMetaForGridPlans.mockImplementation( () => ( { + [ 'business-bundle' ]: { + ...mockDataStorePlans[ 'business-bundle' ].pricing, + billingPeriod: mockDataStorePlans[ 'business-bundle' ].pricing.billPeriod, + }, + } ) ); + ProductsList.useProducts.mockImplementation( () => ( { data: mockDataStoreProducts } ) ); } ); afterAll( () => { diff --git a/packages/calypso-products/src/plans-utilities.ts b/packages/calypso-products/src/plans-utilities.ts index b2093e41dfb569..7aa549bf66585c 100644 --- a/packages/calypso-products/src/plans-utilities.ts +++ b/packages/calypso-products/src/plans-utilities.ts @@ -26,6 +26,7 @@ import { PLAN_CENTENNIAL_PERIOD, TERM_CENTENNIALLY, } from './constants'; +import { BillingTerm } from './types'; export { getPlanSlugForTermVariant } from './get-plan-term-variant'; export { getPlanMultipleTermsVariantSlugs } from './get-plan-multiple-terms-variant-slugs'; @@ -73,7 +74,7 @@ export function getTermDuration( term: string ): number | undefined { * @param {number} days Term duration in days * @returns {string} TERM_ constant */ -export function getTermFromDuration( days: number ): string | undefined { +export function getTermFromDuration( days: number ): BillingTerm[ 'term' ] | undefined { switch ( days ) { case PLAN_MONTHLY_PERIOD: return TERM_MONTHLY; diff --git a/packages/data-stores/src/plans/hooks/use-current-plan-term.ts b/packages/data-stores/src/plans/hooks/use-current-plan-term.ts new file mode 100644 index 00000000000000..27cb746d0ded5d --- /dev/null +++ b/packages/data-stores/src/plans/hooks/use-current-plan-term.ts @@ -0,0 +1,18 @@ +import { type BillingTerm, getTermFromDuration } from '@automattic/calypso-products'; +import usePlans from '../queries/use-plans'; +import useCurrentPlan from './use-current-plan'; + +interface Props { + siteId?: string | number | null; +} + +const useCurrentPlanTerm = ( { siteId }: Props ): BillingTerm[ 'term' ] | undefined => { + const plans = usePlans( { coupon: undefined } ).data; + const currentPlan = useCurrentPlan( { siteId } ); + + return plans && currentPlan + ? getTermFromDuration( plans[ currentPlan.planSlug ]?.pricing?.billPeriod ) + : undefined; +}; + +export default useCurrentPlanTerm; diff --git a/packages/data-stores/src/plans/index.ts b/packages/data-stores/src/plans/index.ts index 07b29e132413c9..a59649a06ae538 100644 --- a/packages/data-stores/src/plans/index.ts +++ b/packages/data-stores/src/plans/index.ts @@ -28,6 +28,7 @@ export { default as usePlans } from './queries/use-plans'; export { default as useSitePlans } from './queries/use-site-plans'; /** Hooks/Selectors */ export { default as useCurrentPlan } from './hooks/use-current-plan'; +export { default as useCurrentPlanTerm } from './hooks/use-current-plan-term'; export { default as useIntroOffers } from './hooks/use-intro-offers'; export { default as useIntroOffersForWooExpress } from './hooks/use-intro-offers-for-woo-express'; export { default as usePricingMetaForGridPlans } from './hooks/use-pricing-meta-for-grid-plans'; From 0745b555b1dda8daf31b7ed1b4675a8368ac394f Mon Sep 17 00:00:00 2001 From: Yashwin Poojary Date: Wed, 24 Jul 2024 15:05:16 +0530 Subject: [PATCH 23/25] A4A > Sites: Implement site connection error preview (#92544) * Implement site error preview * Add header border * Fix icon alignment * Implement remove site in the error preview * Use instead of gridicons * Update KB article link for A4A --- .../features/a4a/overview-preview-pane.tsx | 46 +++- .../features/a4a/site-error-column/index.tsx | 49 +++++ .../features/a4a/site-error-column/style.scss | 16 ++ .../features/a4a/site-error-preview/index.tsx | 203 ++++++++++++++++++ .../a4a/site-error-preview/style.scss | 80 +++++++ .../jetpack/jetpack-sites-dataviews.tsx | 45 ++-- .../sections/sites/site-preview-pane/types.ts | 1 + .../sections/sites/sites-dashboard/index.tsx | 1 + .../sites-overview/site-actions/index.tsx | 2 +- .../site-remove-confirmation-dialog/index.tsx | 7 +- 10 files changed, 421 insertions(+), 29 deletions(-) create mode 100644 client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/index.tsx create mode 100644 client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/style.scss create mode 100644 client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/index.tsx create mode 100644 client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/style.scss diff --git a/client/a8c-for-agencies/sections/sites/features/a4a/overview-preview-pane.tsx b/client/a8c-for-agencies/sections/sites/features/a4a/overview-preview-pane.tsx index 33541551831867..9b3c6af647b246 100644 --- a/client/a8c-for-agencies/sections/sites/features/a4a/overview-preview-pane.tsx +++ b/client/a8c-for-agencies/sections/sites/features/a4a/overview-preview-pane.tsx @@ -1,6 +1,7 @@ import { isEnabled } from '@automattic/calypso-config'; +import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; -import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import { useCallback, useContext, useEffect, useMemo } from 'react'; import ItemPreviewPane, { createFeaturePreview, } from 'calypso/a8c-for-agencies/components/items-dashboard/item-preview-pane'; @@ -21,6 +22,8 @@ import { PreviewPaneProps } from 'calypso/a8c-for-agencies/sections/sites/site-p import SitesDashboardContext from 'calypso/a8c-for-agencies/sections/sites/sites-dashboard-context'; import { useJetpackAgencyDashboardRecordTrackEvent } from 'calypso/jetpack-cloud/sections/agency-dashboard/hooks'; import { A4A_SITES_DASHBOARD_DEFAULT_FEATURE } from '../../constants'; +import { useFetchTestConnections } from '../../hooks/use-fetch-test-connection'; +import useFormattedSites from '../../hooks/use-formatted-sites'; import HostingOverviewPreview from '../hosting/overview'; import { JetpackActivityPreview } from '../jetpack/activity'; import { JetpackBackupPreview } from '../jetpack/backup'; @@ -29,6 +32,7 @@ import { JetpackMonitorPreview } from '../jetpack/jetpack-monitor'; import { JetpackPluginsPreview } from '../jetpack/jetpack-plugins'; import { JetpackStatsPreview } from '../jetpack/jetpack-stats'; import { JetpackScanPreview } from '../jetpack/scan'; +import SiteErrorPreview from './site-error-preview'; import '../jetpack/style.scss'; import '../../site-preview-pane/a4a-style.scss'; @@ -39,10 +43,14 @@ export function OverviewPreviewPane( { className, isSmallScreen = false, hasError = false, + onRefetchSite, }: PreviewPaneProps ) { const translate = useTranslate(); const recordEvent = useJetpackAgencyDashboardRecordTrackEvent( [ site ], ! isSmallScreen ); + const connectionStatus = useFetchTestConnections( true, [ site ] ); + const formattedSite = useFormattedSites( [ site ], connectionStatus ); + const trackEvent = useCallback( ( eventName: string ) => { recordEvent( eventName ); @@ -50,6 +58,9 @@ export function OverviewPreviewPane( { [ recordEvent ] ); + // Show error pane if there is an error + const showErrorPane = formattedSite?.[ 0 ].site.error ?? false; + const { selectedSiteFeature, setSelectedSiteFeature } = useContext( SitesDashboardContext ); useEffect( () => { @@ -61,6 +72,32 @@ export function OverviewPreviewPane( { }; }, [] ); + const errorFeatures = useMemo( + () => [ + createFeaturePreview( + 'error', + null, + true, + selectedSiteFeature, + setSelectedSiteFeature, + + ), + ], + [ + closeSitePreviewPane, + onRefetchSite, + selectedSiteFeature, + setSelectedSiteFeature, + site, + trackEvent, + ] + ); + // Jetpack features: Boost, Backup, Monitor, Stats const features = useMemo( () => [ @@ -163,10 +200,13 @@ export function OverviewPreviewPane( { return ( ); diff --git a/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/index.tsx b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/index.tsx new file mode 100644 index 00000000000000..89a4ade17d10c5 --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/index.tsx @@ -0,0 +1,49 @@ +import { Button, Gridicon, Tooltip } from '@automattic/components'; +import { useTranslate } from 'i18n-calypso'; +import { useRef, useState } from 'react'; + +import './style.scss'; + +export default function SiteErrorColumn( { + isA4APluginInstalled, + openSitePreviewPane, +}: { + isA4APluginInstalled?: boolean; + openSitePreviewPane: () => void; +} ) { + const translate = useTranslate(); + + const [ showPopover, setShowPopover ] = useState( false ); + + const wrapperRef = useRef< HTMLDivElement | null >( null ); + + const tooltip = isA4APluginInstalled + ? translate( "Automattic for Agencies can't connect to this site." ) + : translate( "Jetpack can't connect to this site." ); + + return ( + +
setShowPopover( true ) } + onMouseLeave={ () => setShowPopover( false ) } + onMouseDown={ () => setShowPopover( false ) } + role="button" + tabIndex={ 0 } + ref={ wrapperRef } + > + + + { translate( 'Site connectivity issue: {{button}}Fix now{{/button}}', { + components: { + button:
+ + { tooltip } + +
+ ); +} diff --git a/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/style.scss b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/style.scss new file mode 100644 index 00000000000000..8a2ae58615820c --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-column/style.scss @@ -0,0 +1,16 @@ + +.site-error-column { + .sites-dataview__site-error { + font-size: 0.75rem; + + .button { + text-decoration: underline; + font-size: 0.75rem; + padding: 0; + width: auto; + position: relative; + inset-block-start: 1px; + } + } +} + diff --git a/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/index.tsx b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/index.tsx new file mode 100644 index 00000000000000..c5d78155bf62fe --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/index.tsx @@ -0,0 +1,203 @@ +import { Button, CompactCard } from '@automattic/components'; +import { localizeUrl } from '@automattic/i18n-utils'; +import { Icon, external, trash } from '@wordpress/icons'; +import { useTranslate } from 'i18n-calypso'; +import { useCallback, useState } from 'react'; +import useRemoveSiteMutation from 'calypso/a8c-for-agencies/data/sites/use-remove-site'; +import FormattedHeader from 'calypso/components/formatted-header'; +import { SiteRemoveConfirmationDialog } from 'calypso/jetpack-cloud/sections/agency-dashboard/sites-overview/site-remove-confirmation-dialog'; +import { useDispatch } from 'calypso/state'; +import { successNotice } from 'calypso/state/notices/actions'; +import type { Site } from '../../../types'; + +import './style.scss'; + +export const A4A_PLUGIN_SLUG = 'automattic-for-agencies-client'; +const JETPACK_PLUGIN_SLUG = 'jetpack'; + +export default function SiteErrorPreview( { + site, + trackEvent, + onRefetchSite, + closeSitePreviewPane, +}: { + site: Site; + trackEvent: ( eventName: string ) => void; + onRefetchSite?: () => Promise< unknown >; + closeSitePreviewPane?: () => void; +} ) { + const translate = useTranslate(); + const dispatch = useDispatch(); + + const [ showRemoveSiteDialog, setShowRemoveSiteDialog ] = useState( false ); + const [ isPendingRefetch, setIsPendingRefetch ] = useState( false ); + + const { mutate: removeSite, isPending } = useRemoveSiteMutation(); + + const onRemoveSite = useCallback( () => { + if ( site.a4a_site_id ) { + removeSite( + { siteId: site.a4a_site_id }, + { + onSuccess: () => { + setIsPendingRefetch( true ); + // Add 1 second delay to refetch sites to give time for site profile to be reindexed properly. + setTimeout( () => { + onRefetchSite?.()?.then( () => { + setIsPendingRefetch( false ); + setShowRemoveSiteDialog( false ); + dispatch( + successNotice( translate( 'The site has been successfully removed.' ), { + displayOnNextPage: true, + } ) + ); + closeSitePreviewPane?.(); + } ); + }, 1000 ); + }, + } + ); + } + }, [ closeSitePreviewPane, dispatch, onRefetchSite, removeSite, site.a4a_site_id, translate ] ); + + const isA4APluginInstalled = site.enabled_plugin_slugs?.includes( A4A_PLUGIN_SLUG ); + + const troubleshootingHref = isA4APluginInstalled + ? localizeUrl( + 'https://agencieshelp.automattic.com/knowledge-base/fix-automattic-for-agencies-plugin-issues/' + ) + : localizeUrl( + 'https://jetpack.com/support/getting-started-with-jetpack/fixing-jetpack-connection-issues/' + ); + + const page = isA4APluginInstalled ? A4A_PLUGIN_SLUG : JETPACK_PLUGIN_SLUG; + + const disconnectHref = `${ site.url_with_scheme }/wp-admin/options-general.php?page=${ page }`; + + const handleRemoveSite = () => { + trackEvent( 'calypso_a4a_site_indicator_disconnect_remove_site' ); + setShowRemoveSiteDialog( true ); + }; + + return ( + <> +
+ { isA4APluginInstalled + ? translate( "Automattic for Agencies can't connect to this site." ) + : translate( "Jetpack can't connect to this site." ) } +
+
+ { translate( "Try the following steps to fix your site's connection:" ) } +
+
+ + + + + + + + + + + + + + + + + + + + +
+ { showRemoveSiteDialog && ( + setShowRemoveSiteDialog( false ) } + onConfirm={ onRemoveSite } + busy={ isPending || isPendingRefetch } + /> + ) } + + ); +} diff --git a/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/style.scss b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/style.scss new file mode 100644 index 00000000000000..df281f0831b909 --- /dev/null +++ b/client/a8c-for-agencies/sections/sites/features/a4a/site-error-preview/style.scss @@ -0,0 +1,80 @@ +@import "@wordpress/base-styles/breakpoints"; +@import "@wordpress/base-styles/mixins"; + +.main.a4a-layout.sites-dashboard .site-error-preview { + .preview-pane__navigation.is-hidden { + display: none; + } + + .item-preview__content { + padding: 24px; + display: block; + + @include break-large { + padding: 48px; + } + } + + .item-preview__header { + border: 1px solid var(--color-accent-5); + } +} + +.site-error-preview__title { + @include a4a-font-heading-lg; + margin-block-end: 16px; +} + +.site-error-preview__description { + @include a4a-font-body-md; + margin-block-end: 16px; +} + +.site-error-preview__actions { + + .card { + border-radius: 8px; /* stylelint-disable-line scales/radii */ + margin-block-end: 16px; + + .formatted-header { + margin: 0; + + .button { + margin-block-start: 16px; + } + + @include break-large { + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; + + > div { + width: 70%; + flex: 1; + } + + .button { + margin-block-start: 0; + } + } + } + + .formatted-header__title { + @include a4a-font-heading-md; + margin-block-end: 4px; + color: var(--color-primary-100); + } + + .formatted-header__subtitle { + @include a4a-font-body-md; + margin-block-end: 0; + } + } + + .button svg { + position: relative; + inset-block-start: 1px; + inset-inline-start: 4px; + } +} diff --git a/client/a8c-for-agencies/sections/sites/features/jetpack/jetpack-sites-dataviews.tsx b/client/a8c-for-agencies/sections/sites/features/jetpack/jetpack-sites-dataviews.tsx index 612391435d4bae..1ad8f752b7bc21 100644 --- a/client/a8c-for-agencies/sections/sites/features/jetpack/jetpack-sites-dataviews.tsx +++ b/client/a8c-for-agencies/sections/sites/features/jetpack/jetpack-sites-dataviews.tsx @@ -26,6 +26,8 @@ import TextPlaceholder from 'calypso/jetpack-cloud/sections/partner-portal/text- import { useFetchTestConnections } from '../../hooks/use-fetch-test-connection'; import useFormattedSites from '../../hooks/use-formatted-sites'; import { AllowedTypes, Site, SiteData } from '../../types'; +import SiteErrorColumn from '../a4a/site-error-column'; +import { A4A_PLUGIN_SLUG } from '../a4a/site-error-preview'; import type { MouseEvent, KeyboardEvent } from 'react'; export const JetpackSitesDataViews = ( { @@ -76,15 +78,13 @@ export const JetpackSitesDataViews = ( { return; } - if ( site.is_connection_healthy ) { - setDataViewsState( ( prevState: DataViewsState ) => ( { - ...prevState, - selectedItem: site, - type: DATAVIEWS_LIST, - } ) ); - } + setDataViewsState( ( prevState: DataViewsState ) => ( { + ...prevState, + selectedItem: site, + type: DATAVIEWS_LIST, + } ) ); }, - [ setDataViewsState ] + [ isNotProduction, setDataViewsState ] ); const renderField = useCallback( @@ -175,12 +175,14 @@ export const JetpackSitesDataViews = ( { } const site = item.site.value; + const isA4APluginInstalled = site.enabled_plugin_slugs?.includes( A4A_PLUGIN_SLUG ); + if ( item.site.type === 'error' ) { return ( -
- - { translate( "Jetpack can't connect to this site." ) } -
+ openSitePreviewPane( item.site.value ) } + /> ); } @@ -382,21 +384,21 @@ export const JetpackSitesDataViews = ( { { item.site.error && } { /* eslint-disable-next-line jsx-a11y/no-static-element-interactions */ }
e.stopPropagation() } onKeyDown={ ( e: KeyboardEvent ) => e.stopPropagation() } > { ( ! item.site.value.sticker?.includes( 'migration-in-progress' ) || isNotProduction ) && ( <> - + { ! item.site.error && ( + + ) }