Skip to content

Commit 1b8d91f

Browse files
authored
Forms: Add CTA to install/activate Akismet on empty spam dashboard (#46212)
* add cta to install and/or activate Akismet * changelog * fix conditional translation * update use-plugin-installation and reuse it * add event * fix flickering message * move use-plugin-installation hook * change copy
1 parent 511000e commit 1b8d91f

File tree

6 files changed

+357
-85
lines changed

6 files changed

+357
-85
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: added
3+
4+
Forms: Add CTA to install/activate Akismet on empty spam dashboard

projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hooks/use-plugin-installation.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/plugin-action-button.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { __ } from '@wordpress/i18n';
77
/**
88
* Internal dependencies
99
*/
10-
import useConfigValue from '../../../../../hooks/use-config-value.ts';
11-
import { usePluginInstallation } from '../hooks/use-plugin-installation.ts';
10+
import { usePluginInstallation } from '../../../../../hooks/use-plugin-installation.ts';
1211

1312
type PluginActionButtonProps = {
1413
slug: string;
@@ -27,18 +26,20 @@ const PluginActionButton = ( {
2726
refreshStatus,
2827
trackEventName,
2928
}: PluginActionButtonProps ) => {
30-
const { isInstalling, installPlugin } = usePluginInstallation(
31-
slug,
32-
pluginFile,
33-
isInstalled,
34-
trackEventName
35-
);
29+
const trackEventProps = {
30+
screen: 'block-editor',
31+
};
3632

37-
// Permissions from consolidated Forms config (shared across editor and dashboard)
38-
const canUserInstallPlugins = useConfigValue( 'canInstallPlugins' );
39-
const canUserActivatePlugins = useConfigValue( 'canActivatePlugins' );
33+
const { isInstalling, installPlugin, canInstallPlugins, canActivatePlugins } =
34+
usePluginInstallation( {
35+
slug,
36+
pluginPath: pluginFile,
37+
isInstalled,
38+
trackEventName,
39+
trackEventProps,
40+
} );
4041

41-
const canPerformAction = isInstalled ? canUserActivatePlugins : canUserInstallPlugins;
42+
const canPerformAction = isInstalled ? canActivatePlugins : canInstallPlugins;
4243
const [ isReconcilingStatus, setIsReconcilingStatus ] = useState( false );
4344
const isDisabled = isInstalling || isReconcilingStatus || ! canPerformAction;
4445

@@ -84,10 +85,10 @@ const PluginActionButton = ( {
8485
);
8586

8687
const getTooltipText = (): string => {
87-
if ( isInstalled && ! canUserActivatePlugins ) {
88+
if ( isInstalled && ! canActivatePlugins ) {
8889
return tooltipTextNoActivatePerms;
8990
}
90-
if ( ! isInstalled && ! canUserInstallPlugins ) {
91+
if ( ! isInstalled && ! canInstallPlugins ) {
9192
return tooltipTextNoInstallPerms;
9293
}
9394
return String( isInstalled ? tooltipTextActivate : tooltipTextInstall );

projects/packages/forms/src/dashboard/components/empty-responses/index.tsx

Lines changed: 175 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,157 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { isSimpleSite } from '@automattic/jetpack-script-data';
15
import {
6+
Button,
7+
ExternalLink,
28
__experimentalText as Text, // eslint-disable-line @wordpress/no-unsafe-wp-apis
39
__experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
410
} from '@wordpress/components';
11+
import { useSelect, useDispatch } from '@wordpress/data';
12+
import { createInterpolateElement, useCallback, useMemo } from '@wordpress/element';
513
import { __, _n, sprintf } from '@wordpress/i18n';
14+
/**
15+
* Internal dependencies
16+
*/
617
import useConfigValue from '../../../hooks/use-config-value.ts';
18+
import { usePluginInstallation } from '../../../hooks/use-plugin-installation.ts';
19+
import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts';
720
import CreateFormButton from '../create-form-button/index.tsx';
21+
/**
22+
* Types
23+
*/
24+
import type {
25+
IntegrationsDispatch,
26+
SelectIntegrations,
27+
} from '../../../store/integrations/index.ts';
28+
import type { Integration } from '../../../types/index.ts';
29+
import type { ReactNode } from 'react';
830

9-
const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => (
31+
type UseInstallAkismetReturn = {
32+
shouldShowAkismetCta: boolean;
33+
wrapperBody: ReactNode;
34+
isInstallingAkismet: boolean;
35+
canPerformAkismetAction: boolean;
36+
wrapperButtonText: string;
37+
handleAkismetSetup: () => Promise< void >;
38+
};
39+
40+
type EmptyResponsesProps = {
41+
status: string;
42+
isSearch: boolean;
43+
readStatusFilter?: 'unread' | 'read';
44+
};
45+
46+
type EmptyWrapperProps = {
47+
heading?: string;
48+
body?: string | ReactNode;
49+
actions?: ReactNode;
50+
};
51+
52+
/**
53+
* Hook to handle Akismet installation and activation.
54+
*
55+
* @return {UseInstallAkismetReturn} An object containing the necessary data and functions to handle Akismet installation and activation.
56+
*/
57+
const useInstallAkismet = (): UseInstallAkismetReturn => {
58+
const { akismetIntegration } = useSelect( ( select: SelectIntegrations ) => {
59+
const store = select( INTEGRATIONS_STORE );
60+
const integrations = store.getIntegrations() || [];
61+
62+
return {
63+
akismetIntegration: integrations.find(
64+
( integration: Integration ) => integration.id === 'akismet'
65+
),
66+
};
67+
}, [] ) as { akismetIntegration?: Integration };
68+
69+
const { refreshIntegrations } = useDispatch( INTEGRATIONS_STORE ) as IntegrationsDispatch;
70+
71+
const akismetIntegrationReady = useMemo(
72+
() => !! akismetIntegration && ! akismetIntegration.__isPartial,
73+
[ akismetIntegration ]
74+
);
75+
76+
const isInstalled = !! akismetIntegration?.isInstalled;
77+
78+
const isAkismetActive = akismetIntegrationReady && isInstalled && !! akismetIntegration?.isActive;
79+
80+
const shouldShowAkismetCta = akismetIntegrationReady && ! isAkismetActive && ! isSimpleSite();
81+
82+
const akismetPluginFile = useMemo(
83+
() => akismetIntegration?.pluginFile ?? 'akismet/akismet',
84+
[ akismetIntegration?.pluginFile ]
85+
);
86+
87+
const wrapperBody: ReactNode = createInterpolateElement(
88+
__(
89+
'Want automatic spam filtering? Akismet Anti-spam protects millions of sites. <moreInfoLink>Learn more.</moreInfoLink>',
90+
'jetpack-forms'
91+
),
92+
{
93+
moreInfoLink: <ExternalLink href="https://akismet.com/" />,
94+
}
95+
);
96+
97+
const activateButtonText = __( 'Activate Akismet Anti-spam', 'jetpack-forms' );
98+
const installAndActivateButtonText = __( 'Install Akismet Anti-spam', 'jetpack-forms' );
99+
const wrapperButtonText = isInstalled ? activateButtonText : installAndActivateButtonText;
100+
101+
const {
102+
isInstalling: isInstallingAkismet,
103+
installPlugin,
104+
canInstallPlugins,
105+
canActivatePlugins,
106+
} = usePluginInstallation( {
107+
slug: 'akismet',
108+
pluginPath: akismetPluginFile,
109+
isInstalled,
110+
onSuccess: refreshIntegrations,
111+
trackEventName: 'jetpack_forms_upsell_akismet_click',
112+
trackEventProps: {
113+
screen: 'dashboard',
114+
},
115+
successNotices: {
116+
install: {
117+
message: __( 'Akismet installed and activated.', 'jetpack-forms' ),
118+
options: { type: 'snackbar', id: 'akismet-install-success' },
119+
},
120+
activate: {
121+
message: __( 'Akismet activated.', 'jetpack-forms' ),
122+
options: { type: 'snackbar', id: 'akismet-install-success' },
123+
},
124+
},
125+
errorNotice: {
126+
message: __( 'Could not set up Akismet. Please try again.', 'jetpack-forms' ),
127+
options: { type: 'snackbar', id: 'akismet-install-error' },
128+
},
129+
} );
130+
131+
const canPerformAkismetAction =
132+
isInstalled && akismetIntegrationReady
133+
? canActivatePlugins !== false
134+
: canInstallPlugins !== false;
135+
136+
const handleAkismetSetup = useCallback( async () => {
137+
if ( isInstallingAkismet || ! akismetIntegrationReady || ! canPerformAkismetAction ) {
138+
return;
139+
}
140+
141+
await installPlugin();
142+
}, [ isInstallingAkismet, akismetIntegrationReady, canPerformAkismetAction, installPlugin ] );
143+
144+
return {
145+
shouldShowAkismetCta,
146+
wrapperBody,
147+
isInstallingAkismet,
148+
canPerformAkismetAction,
149+
wrapperButtonText,
150+
handleAkismetSetup,
151+
};
152+
};
153+
154+
const EmptyWrapper = ( { heading = '', body = '', actions = null }: EmptyWrapperProps ) => (
10155
<VStack alignment="center" spacing="2">
11156
{ heading && (
12157
<Text as="h3" weight="500" size="15">
@@ -18,14 +163,16 @@ const EmptyWrapper = ( { heading = '', body = '', actions = null } ) => (
18163
</VStack>
19164
);
20165

21-
type EmptyResponsesProps = {
22-
status: string;
23-
isSearch: boolean;
24-
readStatusFilter?: 'unread' | 'read';
25-
};
26-
27166
const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesProps ) => {
28167
const emptyTrashDays = useConfigValue( 'emptyTrashDays' ) ?? 0;
168+
const {
169+
shouldShowAkismetCta,
170+
wrapperBody,
171+
isInstallingAkismet,
172+
canPerformAkismetAction,
173+
wrapperButtonText,
174+
handleAkismetSetup,
175+
} = useInstallAkismet();
29176

30177
// Handle search and filter states first
31178
const hasReadStatusFilter = !! readStatusFilter;
@@ -60,7 +207,28 @@ const EmptyResponses = ( { status, isSearch, readStatusFilter }: EmptyResponsesP
60207
'Spam responses are permanently deleted after 15 days.',
61208
'jetpack-forms'
62209
);
210+
63211
if ( status === 'spam' ) {
212+
if ( shouldShowAkismetCta ) {
213+
return (
214+
<EmptyWrapper
215+
heading={ noSpamHeading }
216+
body={ wrapperBody }
217+
actions={
218+
<Button
219+
variant="primary"
220+
isBusy={ isInstallingAkismet }
221+
disabled={ isInstallingAkismet || ! canPerformAkismetAction }
222+
onClick={ handleAkismetSetup }
223+
__next40pxDefaultSize
224+
>
225+
{ wrapperButtonText }
226+
</Button>
227+
}
228+
/>
229+
);
230+
}
231+
64232
return <EmptyWrapper heading={ noSpamHeading } body={ noSpamMessage } />;
65233
}
66234

projects/packages/forms/src/dashboard/inbox/stage/index.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
*/
44
import jetpackAnalytics from '@automattic/jetpack-analytics';
55
import { JetpackLogo } from '@automattic/jetpack-components';
6+
import { isSimpleSite } from '@automattic/jetpack-script-data';
67
import { Badge } from '@automattic/ui';
78
import { ExternalLink, Modal } from '@wordpress/components';
89
import { useResizeObserver, useViewportMatch } from '@wordpress/compose';
10+
import { useSelect } from '@wordpress/data';
911
import { DataViews } from '@wordpress/dataviews/wp';
1012
import { dateI18n, getSettings as getDateSettings } from '@wordpress/date';
1113
import { useCallback, useMemo, useState } from '@wordpress/element';
@@ -19,6 +21,7 @@ import { useSearchParams } from 'react-router';
1921
* Internal dependencies
2022
*/
2123
import useConfigValue from '../../../hooks/use-config-value.ts';
24+
import { INTEGRATIONS_STORE } from '../../../store/integrations/index.ts';
2225
import CreateFormButton from '../../components/create-form-button/index.tsx';
2326
import EmptyResponses from '../../components/empty-responses/index.tsx';
2427
import EmptySpamButton from '../../components/empty-spam-button/index.tsx';
@@ -137,6 +140,23 @@ export default function InboxView() {
137140
totalItems,
138141
totalPages,
139142
} = useInboxData();
143+
const isAkismetStatusPending = useSelect(
144+
select => {
145+
const store = select( INTEGRATIONS_STORE );
146+
const integrations = store.getIntegrations() || [];
147+
const isIntegrationsLoading = store.isIntegrationsLoading();
148+
const akismetIntegration = integrations.find( integration => integration.id === 'akismet' );
149+
150+
return (
151+
statusFilter === 'spam' &&
152+
! isSimpleSite() &&
153+
( isIntegrationsLoading || ! akismetIntegration || akismetIntegration.__isPartial )
154+
);
155+
},
156+
[ statusFilter ]
157+
);
158+
159+
const isInboxLoading = isLoadingData || isAkismetStatusPending;
140160

141161
useEffect( () => {
142162
const _filters = view.filters?.reduce( ( accumulator, { field, value } ) => {
@@ -497,7 +517,7 @@ export default function InboxView() {
497517
fields={ fields }
498518
actions={ actions }
499519
data={ records || EMPTY_ARRAY }
500-
isLoading={ isLoadingData }
520+
isLoading={ isInboxLoading }
501521
view={ view }
502522
onChangeView={ setView }
503523
selection={ selection }

0 commit comments

Comments
 (0)