From f7a11be2fc8b528f2236834acb4b8a7ab0416cf9 Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 5 Dec 2025 21:23:46 -0300 Subject: [PATCH 1/8] add cta to install and/or activate Akismet --- .../components/empty-responses/index.tsx | 50 +++++++ .../dashboard/hooks/use-install-akismet.ts | 125 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 7ca6578a8d50e..de9df2d4ec567 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -1,9 +1,17 @@ +/** + * External dependencies + */ import { + Button, __experimentalText as Text, // eslint-disable-line @wordpress/no-unsafe-wp-apis __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis } from '@wordpress/components'; import { __, _n, sprintf } from '@wordpress/i18n'; +/** + * Internal dependencies + */ import useConfigValue from '../../../hooks/use-config-value.ts'; +import useInstallAkismet from '../../hooks/use-install-akismet.ts'; import CreateFormButton from '../create-form-button/index.tsx'; const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => ( @@ -27,6 +35,15 @@ type EmptyResponsesProps = { const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesProps ) => { const emptyTrashDays = useConfigValue( 'emptyTrashDays' ) ?? 0; + const { + shouldShowAkismetCta, + isInstallingAkismet, + isIntegrationsLoading, + canPerformAkismetAction, + isInstalled, + handleAkismetSetup, + } = useInstallAkismet(); + // Handle search and filter states first const hasReadStatusFilter = !! readStatusFilter; const searchHeading = __( 'No results found', 'jetpack-forms' ); @@ -60,7 +77,40 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP 'Spam responses are permanently deleted after 15 days.', 'jetpack-forms' ); + if ( status === 'spam' ) { + const installAndActivateMessage = __( + 'Install and activate Jetpack Akismet Anti-spam to automatically filter form spam.', + 'jetpack-forms' + ); + + const activateMessage = __( + 'Activate Jetpack Akismet Anti-spam to automatically filter form spam.', + 'jetpack-forms' + ); + + if ( shouldShowAkismetCta && ! isIntegrationsLoading ) { + return ( + + { isInstalled + ? __( 'Activate Akismet Anti-spam', 'jetpack-forms' ) + : __( 'Install Akismet Anti-spam', 'jetpack-forms' ) } + + } + /> + ); + } + return ; } diff --git a/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts b/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts new file mode 100644 index 0000000000000..f7eadbf7586d4 --- /dev/null +++ b/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { isSimpleSite } from '@automattic/jetpack-script-data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useCallback, useMemo, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +/** + * Internal dependencies + */ +import { + installAndActivatePlugin, + activatePlugin, +} from '../../blocks/contact-form/util/plugin-management.js'; +import useConfigValue from '../../hooks/use-config-value.ts'; +import { INTEGRATIONS_STORE } from '../../store/integrations/index.ts'; +/** + * Types + */ +import type { SelectIntegrations, IntegrationsDispatch } from '../../store/integrations/index.ts'; +import type { Integration } from '../../types/index.ts'; + +export const useInstallAkismet = () => { + const canInstallPlugins = useConfigValue( 'canInstallPlugins' ); + const canActivatePlugins = useConfigValue( 'canActivatePlugins' ); + + const { akismetIntegration, isIntegrationsLoading } = useSelect( + ( select: SelectIntegrations ) => { + const store = select( INTEGRATIONS_STORE ); + const integrations = store.getIntegrations() || []; + + return { + akismetIntegration: integrations.find( + ( integration: Integration ) => integration.id === 'akismet' + ), + isIntegrationsLoading: store.isIntegrationsLoading(), + }; + }, + [] + ) as { akismetIntegration?: Integration; isIntegrationsLoading: boolean }; + + const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch; + const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); + const [ isInstallingAkismet, setIsInstallingAkismet ] = useState( false ); + + const akismetIntegrationReady = useMemo( + () => !! akismetIntegration && ! akismetIntegration.__isPartial, + [ akismetIntegration ] + ); + + const isAkismetActive = + akismetIntegrationReady && + !! akismetIntegration?.isInstalled && + !! akismetIntegration?.isActive; + + const shouldShowAkismetCta = akismetIntegrationReady && ! isAkismetActive && ! isSimpleSite(); + + const akismetPluginFile = useMemo( + () => akismetIntegration?.pluginFile ?? 'akismet/akismet', + [ akismetIntegration?.pluginFile ] + ); + + const canPerformAkismetAction = + akismetIntegration?.isInstalled && akismetIntegrationReady + ? canActivatePlugins !== false + : canInstallPlugins !== false; + + const handleAkismetSetup = useCallback( async () => { + if ( isInstallingAkismet || ! akismetIntegrationReady || ! canPerformAkismetAction ) { + return; + } + + setIsInstallingAkismet( true ); + + try { + if ( akismetIntegration?.isInstalled ) { + await activatePlugin( akismetPluginFile ); + } else { + await installAndActivatePlugin( 'akismet' ); + } + + createSuccessNotice( + akismetIntegration?.isInstalled + ? __( 'Akismet activated.', 'jetpack-forms' ) + : __( 'Akismet installed and activated.', 'jetpack-forms' ), + { type: 'snackbar', id: 'akismet-install-success' } + ); + + await refreshIntegrations(); + } catch ( error ) { + const message = + error instanceof Error + ? error.message + : __( 'Could not set up Akismet. Please try again.', 'jetpack-forms' ); + + createErrorNotice( message, { + type: 'snackbar', + id: 'akismet-install-error', + } ); + } finally { + setIsInstallingAkismet( false ); + } + }, [ + akismetIntegration?.isInstalled, + akismetIntegrationReady, + akismetPluginFile, + canPerformAkismetAction, + createErrorNotice, + createSuccessNotice, + isInstallingAkismet, + refreshIntegrations, + ] ); + + return { + shouldShowAkismetCta, + handleAkismetSetup, + isInstallingAkismet, + isIntegrationsLoading, + canPerformAkismetAction, + ...akismetIntegration, + }; +}; + +export default useInstallAkismet; From cfe9c925fb8f1e5e80d7adaae3ae3fa2249c626a Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 5 Dec 2025 21:32:44 -0300 Subject: [PATCH 2/8] changelog --- .../packages/forms/changelog/update-forms-spam-akismet-cta | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/forms/changelog/update-forms-spam-akismet-cta diff --git a/projects/packages/forms/changelog/update-forms-spam-akismet-cta b/projects/packages/forms/changelog/update-forms-spam-akismet-cta new file mode 100644 index 0000000000000..ac0f365a0bca2 --- /dev/null +++ b/projects/packages/forms/changelog/update-forms-spam-akismet-cta @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Forms: Add CTA to install/activate Akismet on empty spam dashboard From 8ed7d7f96befaa5bcfc533d738f8a5816664a723 Mon Sep 17 00:00:00 2001 From: Douglas Date: Fri, 5 Dec 2025 21:47:38 -0300 Subject: [PATCH 3/8] fix conditional translation --- .../src/dashboard/components/empty-responses/index.tsx | 7 ++++--- .../forms/src/dashboard/hooks/use-install-akismet.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index de9df2d4ec567..1fce3d1930862 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -90,6 +90,9 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP ); if ( shouldShowAkismetCta && ! isIntegrationsLoading ) { + const activateText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' ); + const installAndActivateText = __( 'Install Akismet Anti-spam', 'jetpack-forms' ); + return ( - { isInstalled - ? __( 'Activate Akismet Anti-spam', 'jetpack-forms' ) - : __( 'Install Akismet Anti-spam', 'jetpack-forms' ) } + { isInstalled ? activateText : installAndActivateText } } /> diff --git a/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts b/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts index f7eadbf7586d4..521df57e135cb 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts @@ -80,10 +80,14 @@ export const useInstallAkismet = () => { await installAndActivatePlugin( 'akismet' ); } + const activatedMessage = __( 'Akismet activated.', 'jetpack-forms' ); + const installedAndActivatedMessage = __( + 'Akismet installed and activated.', + 'jetpack-forms' + ); + createSuccessNotice( - akismetIntegration?.isInstalled - ? __( 'Akismet activated.', 'jetpack-forms' ) - : __( 'Akismet installed and activated.', 'jetpack-forms' ), + akismetIntegration?.isInstalled ? activatedMessage : installedAndActivatedMessage, { type: 'snackbar', id: 'akismet-install-success' } ); From 43f2ce593628a3975b0de1c6faa8d27e2b64c463 Mon Sep 17 00:00:00 2001 From: Douglas Date: Mon, 8 Dec 2025 18:55:04 -0300 Subject: [PATCH 4/8] update use-plugin-installation and reuse it --- .../hooks/use-plugin-installation.ts | 110 ++++++++++-- .../integration-card/plugin-action-button.tsx | 27 +-- .../components/empty-responses/index.tsx | 164 +++++++++++++++--- .../dashboard/hooks/use-install-akismet.ts | 129 -------------- 4 files changed, 246 insertions(+), 184 deletions(-) delete mode 100644 projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts index 2c3d1b6124e2e..598f44bc71077 100644 --- a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts +++ b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts @@ -2,62 +2,138 @@ * External dependencies */ import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; +import { useDispatch } from '@wordpress/data'; import { useState, useCallback } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ +import useConfigValue from '../../../../../hooks/use-config-value.ts'; import { installAndActivatePlugin, activatePlugin } from '../../../util/plugin-management.js'; +type NoticeOptions = Record< string, unknown >; + +type NoticeConfig = { + message?: string; + options?: NoticeOptions; +}; + +type SuccessNotices = { + install?: NoticeConfig; + activate?: NoticeConfig; +}; + +type UsePluginInstallationArgs = { + slug: string; + pluginPath: string; + isInstalled: boolean; + trackEventName?: string; + trackEventProps?: Record< string, unknown >; + onSuccess?: () => void | Promise< void >; + successNotices?: SuccessNotices; + errorNotice?: NoticeConfig; +}; + type PluginInstallation = { isInstalling: boolean; installPlugin: () => Promise< boolean >; + canInstallPlugins: boolean; + canActivatePlugins: boolean; }; /** * Custom hook to handle plugin installation and activation flows. * - * @param {string} slug - The plugin slug (e.g., 'akismet') - * @param {string} pluginPath - The plugin path (e.g., 'akismet/akismet') - * @param {boolean} isInstalled - Whether the plugin is installed - * @param {string} tracksEventName - The name of the tracks event to record - * @return {object} Plugin installation states and handlers + * @param {UsePluginInstallationArgs} args - Hook arguments. + * @return {PluginInstallation} Plugin installation states and handlers. */ -export const usePluginInstallation = ( - slug: string, - pluginPath: string, - isInstalled: boolean, - tracksEventName: string -): PluginInstallation => { +export const usePluginInstallation = ( { + slug, + pluginPath, + isInstalled, + trackEventName, + trackEventProps = {}, + onSuccess, + successNotices, + errorNotice, +}: UsePluginInstallationArgs ): PluginInstallation => { const [ isInstalling, setIsInstalling ] = useState( false ); const { tracks } = useAnalytics(); + const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); + const canInstallPlugins = useConfigValue( 'canInstallPlugins' ); + const canActivatePlugins = useConfigValue( 'canActivatePlugins' ); const installPlugin = useCallback( async () => { setIsInstalling( true ); - if ( tracksEventName ) { - tracks.recordEvent( tracksEventName, { - screen: 'block-editor', + if ( trackEventName ) { + tracks.recordEvent( trackEventName, { intent: isInstalled ? 'activate-plugin' : 'install-plugin', + ...( trackEventProps ?? {} ), } ); } try { if ( isInstalled ) { + if ( ! canActivatePlugins ) { + return false; + } + await activatePlugin( pluginPath ); } else { + if ( ! canInstallPlugins ) { + return false; + } + await installAndActivatePlugin( slug ); } + + const successNoticeConfig = isInstalled ? successNotices?.activate : successNotices?.install; + + if ( successNoticeConfig?.message ) { + createSuccessNotice( successNoticeConfig.message, successNoticeConfig.options ); + } + + if ( onSuccess ) { + await onSuccess(); + } + return true; - } catch { - // Let the component handle the error state + } catch ( error ) { + if ( errorNotice ) { + const noticeMessage = + errorNotice.message || ( error instanceof Error ? error.message : undefined ); + + if ( noticeMessage ) { + createErrorNotice( noticeMessage, errorNotice.options ); + } + } + return false; } finally { setIsInstalling( false ); } - }, [ slug, pluginPath, isInstalled, tracks, tracksEventName ] ); + }, [ + trackEventName, + tracks, + isInstalled, + trackEventProps, + successNotices?.activate, + successNotices?.install, + onSuccess, + canActivatePlugins, + pluginPath, + canInstallPlugins, + slug, + createSuccessNotice, + errorNotice, + createErrorNotice, + ] ); return { isInstalling, installPlugin, + canInstallPlugins, + canActivatePlugins, }; }; diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx index 362ee8bd317c7..b67e0c224576b 100644 --- a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx +++ b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx @@ -7,7 +7,6 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import useConfigValue from '../../../../../hooks/use-config-value.ts'; import { usePluginInstallation } from '../hooks/use-plugin-installation.ts'; type PluginActionButtonProps = { @@ -27,18 +26,20 @@ const PluginActionButton = ( { refreshStatus, trackEventName, }: PluginActionButtonProps ) => { - const { isInstalling, installPlugin } = usePluginInstallation( - slug, - pluginFile, - isInstalled, - trackEventName - ); + const trackEventProps = { + screen: 'block-editor', + }; - // Permissions from consolidated Forms config (shared across editor and dashboard) - const canUserInstallPlugins = useConfigValue( 'canInstallPlugins' ); - const canUserActivatePlugins = useConfigValue( 'canActivatePlugins' ); + const { isInstalling, installPlugin, canInstallPlugins, canActivatePlugins } = + usePluginInstallation( { + slug, + pluginPath: pluginFile, + isInstalled, + trackEventName, + trackEventProps, + } ); - const canPerformAction = isInstalled ? canUserActivatePlugins : canUserInstallPlugins; + const canPerformAction = isInstalled ? canActivatePlugins : canInstallPlugins; const [ isReconcilingStatus, setIsReconcilingStatus ] = useState( false ); const isDisabled = isInstalling || isReconcilingStatus || ! canPerformAction; @@ -84,10 +85,10 @@ const PluginActionButton = ( { ); const getTooltipText = (): string => { - if ( isInstalled && ! canUserActivatePlugins ) { + if ( isInstalled && ! canActivatePlugins ) { return tooltipTextNoActivatePerms; } - if ( ! isInstalled && ! canUserInstallPlugins ) { + if ( ! isInstalled && ! canInstallPlugins ) { return tooltipTextNoInstallPerms; } return String( isInstalled ? tooltipTextActivate : tooltipTextInstall ); diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 1fce3d1930862..795332659a5da 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -1,18 +1,151 @@ /** * External dependencies */ +import { isSimpleSite } from '@automattic/jetpack-script-data'; import { Button, __experimentalText as Text, // eslint-disable-line @wordpress/no-unsafe-wp-apis __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useCallback, useMemo } from '@wordpress/element'; import { __, _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ +import { usePluginInstallation } from '../../../blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts'; import useConfigValue from '../../../hooks/use-config-value.ts'; -import useInstallAkismet from '../../hooks/use-install-akismet.ts'; +import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts'; import CreateFormButton from '../create-form-button/index.tsx'; +/** + * Types + */ +import type { + IntegrationsDispatch, + SelectIntegrations, +} from '../../../store/integrations/index.ts'; +import type { Integration } from '../../../types/index.ts'; + +type UseInstallAkismetReturn = { + shouldShowAkismetCta: boolean; + isIntegrationsLoading: boolean; + wrapperBody: string; + isInstallingAkismet: boolean; + canPerformAkismetAction: boolean; + wrapperButtonText: string; + handleAkismetSetup: () => Promise< void >; +}; + +type EmptyResponsesProps = { + status: string; + isSearch: boolean; + readStatusFilter?: 'unread' | 'read'; +}; + +/** + * Hook to handle Akismet installation and activation. + * + * @return {UseInstallAkismetReturn} An object containing the necessary data and functions to handle Akismet installation and activation. + */ +const useInstallAkismet = (): UseInstallAkismetReturn => { + const { akismetIntegration, isIntegrationsLoading } = useSelect( + ( select: SelectIntegrations ) => { + const store = select( INTEGRATIONS_STORE ); + const integrations = store.getIntegrations() || []; + + return { + akismetIntegration: integrations.find( + ( integration: Integration ) => integration.id === 'akismet' + ), + isIntegrationsLoading: store.isIntegrationsLoading(), + }; + }, + [] + ) as { akismetIntegration?: Integration; isIntegrationsLoading: boolean }; + + const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch; + + const akismetIntegrationReady = useMemo( + () => !! akismetIntegration && ! akismetIntegration.__isPartial, + [ akismetIntegration ] + ); + + const isInstalled = !! akismetIntegration?.isInstalled; + + const isAkismetActive = akismetIntegrationReady && isInstalled && !! akismetIntegration?.isActive; + + const shouldShowAkismetCta = akismetIntegrationReady && ! isAkismetActive && ! isSimpleSite(); + + const akismetPluginFile = useMemo( + () => akismetIntegration?.pluginFile ?? 'akismet/akismet', + [ akismetIntegration?.pluginFile ] + ); + + const installAndActivateBody = __( + 'Install and activate Jetpack Akismet Anti-spam to automatically filter form spam.', + 'jetpack-forms' + ); + + const activateBody = __( + 'Activate Jetpack Akismet Anti-spam to automatically filter form spam.', + 'jetpack-forms' + ); + + const wrapperBody = isInstalled ? activateBody : installAndActivateBody; + + const activateButtonText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' ); + const installAndActivateButtonText = __( 'Install Akismet Anti-spam', 'jetpack-forms' ); + const wrapperButtonText = isInstalled ? activateButtonText : installAndActivateButtonText; + + const { + isInstalling: isInstallingAkismet, + installPlugin, + canInstallPlugins, + canActivatePlugins, + } = usePluginInstallation( { + slug: 'akismet', + pluginPath: akismetPluginFile, + isInstalled, + onSuccess: refreshIntegrations, + successNotices: { + install: { + message: __( 'Akismet installed and activated.', 'jetpack-forms' ), + options: { type: 'snackbar', id: 'akismet-install-success' }, + }, + activate: { + message: __( 'Akismet activated.', 'jetpack-forms' ), + options: { type: 'snackbar', id: 'akismet-install-success' }, + }, + }, + errorNotice: { + message: __( 'Could not set up Akismet. Please try again.', 'jetpack-forms' ), + options: { type: 'snackbar', id: 'akismet-install-error' }, + }, + } ); + + const canPerformAkismetAction = + isInstalled && akismetIntegrationReady + ? canActivatePlugins !== false + : canInstallPlugins !== false; + + const handleAkismetSetup = useCallback( async () => { + if ( isInstallingAkismet || ! akismetIntegrationReady || ! canPerformAkismetAction ) { + return; + } + + await installPlugin(); + }, [ isInstallingAkismet, akismetIntegrationReady, canPerformAkismetAction, installPlugin ] ); + + return { + shouldShowAkismetCta, + isIntegrationsLoading, + wrapperBody, + isInstallingAkismet, + canPerformAkismetAction, + wrapperButtonText, + handleAkismetSetup, + }; +}; const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => ( @@ -26,21 +159,15 @@ const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => ( ); -type EmptyResponsesProps = { - status: string; - isSearch: boolean; - readStatusFilter?: 'unread' | 'read'; -}; - const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesProps ) => { const emptyTrashDays = useConfigValue( 'emptyTrashDays' ) ?? 0; - const { shouldShowAkismetCta, - isInstallingAkismet, isIntegrationsLoading, + wrapperBody, + isInstallingAkismet, canPerformAkismetAction, - isInstalled, + wrapperButtonText, handleAkismetSetup, } = useInstallAkismet(); @@ -79,24 +206,11 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP ); if ( status === 'spam' ) { - const installAndActivateMessage = __( - 'Install and activate Jetpack Akismet Anti-spam to automatically filter form spam.', - 'jetpack-forms' - ); - - const activateMessage = __( - 'Activate Jetpack Akismet Anti-spam to automatically filter form spam.', - 'jetpack-forms' - ); - if ( shouldShowAkismetCta && ! isIntegrationsLoading ) { - const activateText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' ); - const installAndActivateText = __( 'Install Akismet Anti-spam', 'jetpack-forms' ); - return ( - { isInstalled ? activateText : installAndActivateText } + { wrapperButtonText } } /> diff --git a/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts b/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts deleted file mode 100644 index 521df57e135cb..0000000000000 --- a/projects/packages/forms/src/dashboard/hooks/use-install-akismet.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * External dependencies - */ -import { isSimpleSite } from '@automattic/jetpack-script-data'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { useCallback, useMemo, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -/** - * Internal dependencies - */ -import { - installAndActivatePlugin, - activatePlugin, -} from '../../blocks/contact-form/util/plugin-management.js'; -import useConfigValue from '../../hooks/use-config-value.ts'; -import { INTEGRATIONS_STORE } from '../../store/integrations/index.ts'; -/** - * Types - */ -import type { SelectIntegrations, IntegrationsDispatch } from '../../store/integrations/index.ts'; -import type { Integration } from '../../types/index.ts'; - -export const useInstallAkismet = () => { - const canInstallPlugins = useConfigValue( 'canInstallPlugins' ); - const canActivatePlugins = useConfigValue( 'canActivatePlugins' ); - - const { akismetIntegration, isIntegrationsLoading } = useSelect( - ( select: SelectIntegrations ) => { - const store = select( INTEGRATIONS_STORE ); - const integrations = store.getIntegrations() || []; - - return { - akismetIntegration: integrations.find( - ( integration: Integration ) => integration.id === 'akismet' - ), - isIntegrationsLoading: store.isIntegrationsLoading(), - }; - }, - [] - ) as { akismetIntegration?: Integration; isIntegrationsLoading: boolean }; - - const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch; - const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); - const [ isInstallingAkismet, setIsInstallingAkismet ] = useState( false ); - - const akismetIntegrationReady = useMemo( - () => !! akismetIntegration && ! akismetIntegration.__isPartial, - [ akismetIntegration ] - ); - - const isAkismetActive = - akismetIntegrationReady && - !! akismetIntegration?.isInstalled && - !! akismetIntegration?.isActive; - - const shouldShowAkismetCta = akismetIntegrationReady && ! isAkismetActive && ! isSimpleSite(); - - const akismetPluginFile = useMemo( - () => akismetIntegration?.pluginFile ?? 'akismet/akismet', - [ akismetIntegration?.pluginFile ] - ); - - const canPerformAkismetAction = - akismetIntegration?.isInstalled && akismetIntegrationReady - ? canActivatePlugins !== false - : canInstallPlugins !== false; - - const handleAkismetSetup = useCallback( async () => { - if ( isInstallingAkismet || ! akismetIntegrationReady || ! canPerformAkismetAction ) { - return; - } - - setIsInstallingAkismet( true ); - - try { - if ( akismetIntegration?.isInstalled ) { - await activatePlugin( akismetPluginFile ); - } else { - await installAndActivatePlugin( 'akismet' ); - } - - const activatedMessage = __( 'Akismet activated.', 'jetpack-forms' ); - const installedAndActivatedMessage = __( - 'Akismet installed and activated.', - 'jetpack-forms' - ); - - createSuccessNotice( - akismetIntegration?.isInstalled ? activatedMessage : installedAndActivatedMessage, - { type: 'snackbar', id: 'akismet-install-success' } - ); - - await refreshIntegrations(); - } catch ( error ) { - const message = - error instanceof Error - ? error.message - : __( 'Could not set up Akismet. Please try again.', 'jetpack-forms' ); - - createErrorNotice( message, { - type: 'snackbar', - id: 'akismet-install-error', - } ); - } finally { - setIsInstallingAkismet( false ); - } - }, [ - akismetIntegration?.isInstalled, - akismetIntegrationReady, - akismetPluginFile, - canPerformAkismetAction, - createErrorNotice, - createSuccessNotice, - isInstallingAkismet, - refreshIntegrations, - ] ); - - return { - shouldShowAkismetCta, - handleAkismetSetup, - isInstallingAkismet, - isIntegrationsLoading, - canPerformAkismetAction, - ...akismetIntegration, - }; -}; - -export default useInstallAkismet; From c55f6a0df81e74b2f0d6d7475fe8fc5574947df0 Mon Sep 17 00:00:00 2001 From: Douglas Date: Mon, 8 Dec 2025 18:57:40 -0300 Subject: [PATCH 5/8] add event --- .../forms/src/dashboard/components/empty-responses/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 795332659a5da..2b0b7715dec52 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -107,6 +107,10 @@ const useInstallAkismet = (): UseInstallAkismetReturn => { pluginPath: akismetPluginFile, isInstalled, onSuccess: refreshIntegrations, + trackEventName: 'jetpack_forms_upsell_akismet_click', + trackEventProps: { + screen: 'dashboard', + }, successNotices: { install: { message: __( 'Akismet installed and activated.', 'jetpack-forms' ), From a3dad840f33f341b2cd2bbe308da5dcd2b04f852 Mon Sep 17 00:00:00 2001 From: Douglas Date: Mon, 8 Dec 2025 20:02:49 -0300 Subject: [PATCH 6/8] fix flickering message --- .../components/empty-responses/index.tsx | 29 +++++++------------ .../forms/src/dashboard/inbox/stage/index.js | 22 +++++++++++++- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 2b0b7715dec52..5c2193390e5d2 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -28,7 +28,6 @@ import type { Integration } from '../../../types/index.ts'; type UseInstallAkismetReturn = { shouldShowAkismetCta: boolean; - isIntegrationsLoading: boolean; wrapperBody: string; isInstallingAkismet: boolean; canPerformAkismetAction: boolean; @@ -48,20 +47,16 @@ type EmptyResponsesProps = { * @return {UseInstallAkismetReturn} An object containing the necessary data and functions to handle Akismet installation and activation. */ const useInstallAkismet = (): UseInstallAkismetReturn => { - const { akismetIntegration, isIntegrationsLoading } = useSelect( - ( select: SelectIntegrations ) => { - const store = select( INTEGRATIONS_STORE ); - const integrations = store.getIntegrations() || []; - - return { - akismetIntegration: integrations.find( - ( integration: Integration ) => integration.id === 'akismet' - ), - isIntegrationsLoading: store.isIntegrationsLoading(), - }; - }, - [] - ) as { akismetIntegration?: Integration; isIntegrationsLoading: boolean }; + const { akismetIntegration } = useSelect( ( select: SelectIntegrations ) => { + const store = select( INTEGRATIONS_STORE ); + const integrations = store.getIntegrations() || []; + + return { + akismetIntegration: integrations.find( + ( integration: Integration ) => integration.id === 'akismet' + ), + }; + }, [] ) as { akismetIntegration?: Integration }; const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch; @@ -142,7 +137,6 @@ const useInstallAkismet = (): UseInstallAkismetReturn => { return { shouldShowAkismetCta, - isIntegrationsLoading, wrapperBody, isInstallingAkismet, canPerformAkismetAction, @@ -167,7 +161,6 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP const emptyTrashDays = useConfigValue( 'emptyTrashDays' ) ?? 0; const { shouldShowAkismetCta, - isIntegrationsLoading, wrapperBody, isInstallingAkismet, canPerformAkismetAction, @@ -210,7 +203,7 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP ); if ( status === 'spam' ) { - if ( shouldShowAkismetCta && ! isIntegrationsLoading ) { + if ( shouldShowAkismetCta ) { return ( { + const store = select( INTEGRATIONS_STORE ); + const integrations = store.getIntegrations() || []; + const isIntegrationsLoading = store.isIntegrationsLoading(); + const akismetIntegration = integrations.find( integration => integration.id === 'akismet' ); + + return ( + statusFilter === 'spam' && + ! isSimpleSite() && + ( isIntegrationsLoading || ! akismetIntegration || akismetIntegration.__isPartial ) + ); + }, + [ statusFilter ] + ); + + const isInboxLoading = isLoadingData || isAkismetStatusPending; useEffect( () => { const _filters = view.filters?.reduce( ( accumulator, { field, value } ) => { @@ -497,7 +517,7 @@ export default function InboxView() { fields={ fields } actions={ actions } data={ records || EMPTY_ARRAY } - isLoading={ isLoadingData } + isLoading={ isInboxLoading } view={ view } onChangeView={ setView } selection={ selection } From 6af216aebd2f9b9f048b1241d38745e0d180de15 Mon Sep 17 00:00:00 2001 From: Douglas Date: Mon, 8 Dec 2025 20:08:33 -0300 Subject: [PATCH 7/8] move use-plugin-installation hook --- .../integration-card/plugin-action-button.tsx | 2 +- .../src/dashboard/components/empty-responses/index.tsx | 2 +- .../hooks/use-plugin-installation.ts | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) rename projects/packages/forms/src/{blocks/contact-form/components/jetpack-integrations-modal => }/hooks/use-plugin-installation.ts (95%) diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx index b67e0c224576b..bee3a55202200 100644 --- a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx +++ b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx @@ -7,7 +7,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { usePluginInstallation } from '../hooks/use-plugin-installation.ts'; +import { usePluginInstallation } from '../../../../../hooks/use-plugin-installation.ts'; type PluginActionButtonProps = { slug: string; diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 5c2193390e5d2..52293f59a84ca 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -13,8 +13,8 @@ import { __, _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import { usePluginInstallation } from '../../../blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts'; import useConfigValue from '../../../hooks/use-config-value.ts'; +import { usePluginInstallation } from '../../../hooks/use-plugin-installation.ts'; import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts'; import CreateFormButton from '../create-form-button/index.tsx'; /** diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts b/projects/packages/forms/src/hooks/use-plugin-installation.ts similarity index 95% rename from projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts rename to projects/packages/forms/src/hooks/use-plugin-installation.ts index 598f44bc71077..97dfaade677b5 100644 --- a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts +++ b/projects/packages/forms/src/hooks/use-plugin-installation.ts @@ -8,8 +8,11 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ -import useConfigValue from '../../../../../hooks/use-config-value.ts'; -import { installAndActivatePlugin, activatePlugin } from '../../../util/plugin-management.js'; +import { + installAndActivatePlugin, + activatePlugin, +} from '../blocks/contact-form/util/plugin-management.js'; +import useConfigValue from './use-config-value.ts'; type NoticeOptions = Record< string, unknown >; From cca631f98b08d70c47cae099239f5ae1ee34aabe Mon Sep 17 00:00:00 2001 From: Douglas Date: Tue, 9 Dec 2025 18:48:05 -0300 Subject: [PATCH 8/8] change copy --- .../components/empty-responses/index.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx index 52293f59a84ca..dafa76f4f67bd 100644 --- a/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/components/empty-responses/index.tsx @@ -4,11 +4,12 @@ import { isSimpleSite } from '@automattic/jetpack-script-data'; import { Button, + ExternalLink, __experimentalText as Text, // eslint-disable-line @wordpress/no-unsafe-wp-apis __experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useCallback, useMemo } from '@wordpress/element'; +import { createInterpolateElement, useCallback, useMemo } from '@wordpress/element'; import { __, _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -25,10 +26,11 @@ import type { SelectIntegrations, } from '../../../store/integrations/index.ts'; import type { Integration } from '../../../types/index.ts'; +import type { ReactNode } from 'react'; type UseInstallAkismetReturn = { shouldShowAkismetCta: boolean; - wrapperBody: string; + wrapperBody: ReactNode; isInstallingAkismet: boolean; canPerformAkismetAction: boolean; wrapperButtonText: string; @@ -41,6 +43,12 @@ type EmptyResponsesProps = { readStatusFilter?: 'unread' | 'read'; }; +type EmptyWrapperProps = { + heading?: string; + body?: string | ReactNode; + actions?: ReactNode; +}; + /** * Hook to handle Akismet installation and activation. * @@ -76,18 +84,16 @@ const useInstallAkismet = (): UseInstallAkismetReturn => { [ akismetIntegration?.pluginFile ] ); - const installAndActivateBody = __( - 'Install and activate Jetpack Akismet Anti-spam to automatically filter form spam.', - 'jetpack-forms' - ); - - const activateBody = __( - 'Activate Jetpack Akismet Anti-spam to automatically filter form spam.', - 'jetpack-forms' + const wrapperBody: ReactNode = createInterpolateElement( + __( + 'Want automatic spam filtering? Akismet Anti-spam protects millions of sites. Learn more.', + 'jetpack-forms' + ), + { + moreInfoLink: , + } ); - const wrapperBody = isInstalled ? activateBody : installAndActivateBody; - const activateButtonText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' ); const installAndActivateButtonText = __( 'Install Akismet Anti-spam', 'jetpack-forms' ); const wrapperButtonText = isInstalled ? activateButtonText : installAndActivateButtonText; @@ -145,7 +151,7 @@ const useInstallAkismet = (): UseInstallAkismetReturn => { }; }; -const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => ( +const EmptyWrapper = ( { heading = '', body = '', actions = null }: EmptyWrapperProps ) => ( { heading && (