From 86f6eb94f7ae115794862e66131cfd0acd336d1f Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 19 Sep 2025 13:23:49 -0600 Subject: [PATCH 1/4] Forms: Add Hostinger Reach feature flag --- .../packages/forms/src/class-jetpack-forms.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/projects/packages/forms/src/class-jetpack-forms.php b/projects/packages/forms/src/class-jetpack-forms.php index 52cf66054903b..703ed4846c5cf 100644 --- a/projects/packages/forms/src/class-jetpack-forms.php +++ b/projects/packages/forms/src/class-jetpack-forms.php @@ -94,6 +94,20 @@ public static function is_mailpoet_enabled() { return apply_filters( 'jetpack_forms_mailpoet_enable', true ); } + /** + * Returns true if Hostinger Reach integration is enabled. + * + * @return boolean + */ + public static function is_hostinger_reach_enabled() { + /** + * Enable Hostinger Reach integration. + * + * @param bool false Whether Hostinger Reach integration be enabled. Default is false. + */ + return apply_filters( 'jetpack_forms_hostinger_reach_enable', false ); + } + /** * Returns true if the Integrations UI should be enabled. * From 579f88b4e32947e8a9e3ecec3f0b448fbe9799fc Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 19 Sep 2025 13:25:06 -0600 Subject: [PATCH 2/4] changelog --- .../packages/forms/changelog/add-forms-hostinger-integration | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/forms/changelog/add-forms-hostinger-integration diff --git a/projects/packages/forms/changelog/add-forms-hostinger-integration b/projects/packages/forms/changelog/add-forms-hostinger-integration new file mode 100644 index 0000000000000..e38ca37c91e0c --- /dev/null +++ b/projects/packages/forms/changelog/add-forms-hostinger-integration @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Forms: add Hostinger Reach integration. From d8499f5c0079a88d3684bcc937f1d1ff85d0d581 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 19 Sep 2025 13:25:54 -0600 Subject: [PATCH 3/4] Add Hostinger Reach to forms endpoint --- .../class-contact-form-endpoint.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index 7a613264da59e..cae8f39265637 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -102,6 +102,20 @@ private function get_supported_integrations() { ), ); + // Conditionally add Hostinger Reach integration behind feature flag. + if ( Jetpack_Forms::is_hostinger_reach_enabled() ) { + $supported_integrations['hostinger-reach'] = array( + 'type' => 'plugin', + 'file' => 'hostinger-reach/hostinger-reach.php', + 'settings_url' => 'admin.php?page=hostinger-reach#/home', + 'marketing_redirect_slug' => null, + 'title' => __( 'Hostinger Reach', 'jetpack-forms' ), + 'subtitle' => __( 'Send newsletters and marketing emails via Hostinger Reach.', 'jetpack-forms' ), + // Overriding this may automatically enable/disable the integration when editing a form. + 'enabled_by_default' => false, + ); + } + /** * Filters the list of supported integrations available in Jetpack Forms. * @@ -1008,6 +1022,10 @@ private function get_plugin_status( $plugin_slug, array $status ) { // Add MailPoet lists to details $status['details']['lists'] = MailPoet_Integration::get_all_lists(); break; + case 'hostinger-reach': + // Hostinger Reach is a plugin that requires additional setup/connection. + $status['needsConnection'] = true; + break; } return $status; @@ -1051,6 +1069,7 @@ public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore V 'formsResponsesUrl' => $switch->get_forms_admin_url(), 'preferredView' => $switch->get_preferred_view(), 'isMailPoetEnabled' => Jetpack_Forms::is_mailpoet_enabled(), + 'isHostingerReachEnabled' => Jetpack_Forms::is_hostinger_reach_enabled(), // From config in class-dashboard.php. 'blogId' => get_current_blog_id(), 'gdriveConnectSupportURL' => esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ), From 62c4f03f2f11f893ccd71c6cb60639acaabc3df1 Mon Sep 17 00:00:00 2001 From: Erick Danzer Date: Fri, 19 Sep 2025 14:11:47 -0600 Subject: [PATCH 4/4] Add draft frontend Hostinger cards --- .../src/blocks/contact-form/attributes.js | 7 + .../hostinger-reach-card.tsx | 214 ++++++++++++++++++ .../jetpack-integrations-modal/index.tsx | 13 ++ .../integrations/hostinger-reach-card.tsx | 89 ++++++++ .../src/dashboard/integrations/index.tsx | 15 ++ .../forms/src/icons/hostinger-reach.tsx | 24 ++ projects/packages/forms/src/types/index.ts | 2 + 7 files changed, 364 insertions(+) create mode 100644 projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hostinger-reach-card.tsx create mode 100644 projects/packages/forms/src/dashboard/integrations/hostinger-reach-card.tsx create mode 100644 projects/packages/forms/src/icons/hostinger-reach.tsx diff --git a/projects/packages/forms/src/blocks/contact-form/attributes.js b/projects/packages/forms/src/blocks/contact-form/attributes.js index c4579b164015d..6b8d4d3d52d7b 100644 --- a/projects/packages/forms/src/blocks/contact-form/attributes.js +++ b/projects/packages/forms/src/blocks/contact-form/attributes.js @@ -49,6 +49,13 @@ export default { listName: null, }, }, + hostingerReach: { + type: 'object', + default: { + listId: null, + listName: null, + }, + }, saveResponses: { type: 'boolean', default: true, diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hostinger-reach-card.tsx b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hostinger-reach-card.tsx new file mode 100644 index 0000000000000..7306599c624c9 --- /dev/null +++ b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/hostinger-reach-card.tsx @@ -0,0 +1,214 @@ +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; +import { + Button, + ExternalLink, + __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis + SelectControl, + ToggleControl, +} from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { createInterpolateElement, useEffect, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import HostingerReachIcon from '../../../../icons/hostinger-reach'; +import IntegrationCard from './integration-card'; +import type { SingleIntegrationCardProps, IntegrationCardData } from '../../../../types'; + +interface HostingerReachCardProps extends SingleIntegrationCardProps { + hostingerReach: { + enabledForForm?: boolean; + listId?: string | null; + listName?: string | null; + }; + setAttributes: ( attrs: { + hostingerReach: { enabledForForm?: boolean; listId?: string | null; listName?: string | null }; + } ) => void; +} + +type HostingerList = { id: string; name: string }; + +const HostingerReachCard = ( { + isExpanded, + onToggle, + hostingerReach, + setAttributes, + data, + refreshStatus, +}: HostingerReachCardProps ) => { + const { isConnected = false, settingsUrl = '' } = data || {}; + + const lists: HostingerList[] = useMemo( + () => + Array.isArray( data?.details?.lists ) ? ( data.details.lists as HostingerList[] ) : [], + [ data?.details?.lists ] + ); + + const selectedBlock = useSelect( select => select( blockEditorStore ).getSelectedBlock(), [] ); + const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); + const hasEmailBlock = selectedBlock?.innerBlocks?.some( + ( { name }: { name: string } ) => name === 'jetpack/field-email' + ); + const consentBlock = selectedBlock?.innerBlocks?.find( + ( { name }: { name: string } ) => name === 'jetpack/field-consent' + ); + + const toggleConsent = async () => { + if ( consentBlock ) { + await removeBlock( consentBlock.clientId, false ); + } else { + const buttonBlockIndex = selectedBlock.innerBlocks.findIndex( + ( { name }: { name: string } ) => name === 'jetpack/button' + ); + const newConsentBlock = await createBlock( 'jetpack/field-consent' ); + await insertBlock( newConsentBlock, buttonBlockIndex, selectedBlock.clientId, false ); + } + }; + + useEffect( () => { + if ( ! hostingerReach.enabledForForm ) { + return; + } + + // If there are no lists, clear the selection + if ( lists.length === 0 ) { + if ( hostingerReach.listId || hostingerReach.listName ) { + setAttributes( { + hostingerReach: { + ...hostingerReach, + listId: null, + listName: null, + }, + } ); + } + return; + } + + const listIsValid = + hostingerReach.listId && lists.some( list => list.id === hostingerReach.listId ); + if ( ! listIsValid ) { + setAttributes( { + hostingerReach: { + ...hostingerReach, + listId: lists[ 0 ].id, + listName: lists[ 0 ].name, + }, + } ); + } + }, [ hostingerReach, lists, setAttributes ] ); + + const cardData: IntegrationCardData = { + ...data, + showHeaderToggle: true, + headerToggleValue: !! hostingerReach?.enabledForForm, + isHeaderToggleEnabled: isConnected, + onHeaderToggleChange: ( value: boolean ) => + setAttributes( { hostingerReach: { ...hostingerReach, enabledForForm: value } } ), + isLoading: ! data || typeof data.isInstalled === 'undefined', + refreshStatus, + trackEventName: 'jetpack_forms_upsell_hostinger_reach_click', + notInstalledMessage: createInterpolateElement( + __( + 'Add powerful email marketing to your forms with Hostinger Reach. Simply install the plugin to start sending emails.', + 'jetpack-forms' + ), + { + a: , // placeholder if we add marketingUrl later + } + ), + notActivatedMessage: __( + 'Hostinger Reach is installed. Just activate the plugin to start sending emails.', + 'jetpack-forms' + ), + }; + + return ( + } + isExpanded={ isExpanded } + onToggle={ onToggle } + cardData={ cardData } + toggleTooltip={ __( 'Grow your audience with Hostinger Reach', 'jetpack-forms' ) } + > + { ! isConnected ? ( +
+

+ { createInterpolateElement( + __( + 'Hostinger Reach is active. There is one step left. Please complete Hostinger Reach setup.', + 'jetpack-forms' + ), + { + a: , + } + ) } +

+ + + + +
+ ) : ( +
+ { lists?.length ? ( + ( { label: list.name, value: list.id } ) ) } + onChange={ value => { + const selected = lists.find( l => l.id === value ); + setAttributes( { + hostingerReach: { + ...hostingerReach, + listId: selected?.id ?? null, + listName: selected?.name ?? null, + }, + } ); + } } + __next40pxDefaultSize={ true } + __nextHasNoMarginBottom={ true } + /> + ) : ( +

+ { __( + 'You do not have any Hostinger Reach lists yet. Click the dashboard button below to create one.', + 'jetpack-forms' + ) } +

+ ) } + { hasEmailBlock && ( +
+ +
+ ) } +

+ + { __( 'View Hostinger Reach dashboard', 'jetpack-forms' ) } + +

+
+ ) } +
+ ); +}; + +export default HostingerReachCard; diff --git a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/index.tsx b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/index.tsx index 6547a26240d32..76f0ce43c3c6e 100644 --- a/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/index.tsx +++ b/projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/index.tsx @@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n'; import AkismetCard from './akismet-card'; import CreativeMailCard from './creative-mail-card'; import GoogleSheetsCard from './google-sheets-card'; +import HostingerReachCard from './hostinger-reach-card'; import JetpackCRMCard from './jetpack-crm-card'; import MailPoetCard from './mailpoet-card'; import SalesforceCard from './salesforce-card'; @@ -35,6 +36,7 @@ const IntegrationsModal = ( { creativemail: false, salesforce: false, mailpoet: false, + hostingerReach: false, } ); if ( ! isOpen ) { @@ -51,6 +53,7 @@ const IntegrationsModal = ( { const mailpoetData = findIntegrationById( 'mailpoet' ); const salesforceData = findIntegrationById( 'salesforce' ); const creativeMailData = findIntegrationById( 'creative-mail-by-constant-contact' ); + const hostingerReachData = findIntegrationById( 'hostinger-reach' ); const toggleCard = ( cardId: string ) => { setExpandedCards( prev => { @@ -124,6 +127,16 @@ const IntegrationsModal = ( { setAttributes={ setAttributes } /> ) } + { hostingerReachData && ( + toggleCard( 'hostingerReach' ) } + data={ hostingerReachData } + refreshStatus={ refreshIntegrations } + hostingerReach={ attributes.hostingerReach } + setAttributes={ setAttributes } + /> + ) } { creativeMailData && ( { + const { isConnected = false, settingsUrl = '', marketingUrl = '' } = data || {}; + + const cardData: IntegrationCardData = { + ...data, + showHeaderToggle: false, + isLoading: ! data || typeof data.isInstalled === 'undefined', + refreshStatus, + trackEventName: 'jetpack_forms_upsell_hostinger_reach_click', + notInstalledMessage: createInterpolateElement( + __( + 'Add powerful email marketing to your forms with Hostinger Reach. Simply install the plugin to start sending emails.', + 'jetpack-forms' + ), + { + a: , + } + ), + notActivatedMessage: __( + 'Hostinger Reach is installed. Just activate the plugin to start sending emails.', + 'jetpack-forms' + ), + }; + + return ( + } + isExpanded={ isExpanded } + onToggle={ onToggle } + cardData={ cardData } + toggleTooltip={ __( 'Grow your audience with Hostinger Reach', 'jetpack-forms' ) } + > + { ! isConnected ? ( +
+

+ { createInterpolateElement( + __( + 'Hostinger Reach is active. There is one step left. Please complete Hostinger Reach setup.', + 'jetpack-forms' + ), + { + a: , + } + ) } +

+ + + + +
+ ) : ( +
+

+ { __( 'You can now send marketing emails with Hostinger Reach.', 'jetpack-forms' ) } +

+ +
+ ) } +
+ ); +}; + +export default HostingerReachDashboardCard; diff --git a/projects/packages/forms/src/dashboard/integrations/index.tsx b/projects/packages/forms/src/dashboard/integrations/index.tsx index efe02db7d2692..4aafee2929702 100644 --- a/projects/packages/forms/src/dashboard/integrations/index.tsx +++ b/projects/packages/forms/src/dashboard/integrations/index.tsx @@ -11,6 +11,7 @@ import { useIntegrationsStatus } from '../../blocks/contact-form/components/jetp import AkismetDashboardCard from './akismet-card'; import CreativeMailDashboardCard from './creative-mail-card'; import GoogleSheetsDashboardCard from './google-sheets-card'; +import HostingerReachDashboardCard from './hostinger-reach-card'; import JetpackCRMDashboardCard from './jetpack-crm-card'; import MailPoetDashboardCard from './mailpoet-card'; import SalesforceDashboardCard from './salesforce-card'; @@ -29,6 +30,7 @@ const Integrations = () => { creativemail: false, salesforce: false, mailpoet: false, + hostingerReach: false, } ); const toggleCard = useCallback( ( cardId: keyof typeof expandedCards ) => { @@ -61,6 +63,10 @@ const Integrations = () => { [ toggleCard ] ); const handleToggleMailPoet = useCallback( () => toggleCard( 'mailpoet' ), [ toggleCard ] ); + const handleToggleHostingerReach = useCallback( + () => toggleCard( 'hostingerReach' ), + [ toggleCard ] + ); const findIntegrationById = ( id: string ) => integrations?.find( ( integration: Integration ) => integration.id === id ); @@ -72,6 +78,7 @@ const Integrations = () => { const mailpoetData = findIntegrationById( 'mailpoet' ); const salesforceData = findIntegrationById( 'salesforce' ); const creativeMailData = findIntegrationById( 'creative-mail-by-constant-contact' ); + const hostingerReachData = findIntegrationById( 'hostinger-reach' ); return (
@@ -137,6 +144,14 @@ const Integrations = () => { borderBottom={ false } /> ) } + { hostingerReachData && ( + + ) }
diff --git a/projects/packages/forms/src/icons/hostinger-reach.tsx b/projects/packages/forms/src/icons/hostinger-reach.tsx new file mode 100644 index 0000000000000..df1285f831abd --- /dev/null +++ b/projects/packages/forms/src/icons/hostinger-reach.tsx @@ -0,0 +1,24 @@ +import { __ } from '@wordpress/i18n'; +import { SVG, Path, SVGProps } from '@wordpress/primitives'; + +const HostingerReachIcon = ( props: SVGProps & { width?: number; height?: number } ) => ( + + + +); + +export default HostingerReachIcon; diff --git a/projects/packages/forms/src/types/index.ts b/projects/packages/forms/src/types/index.ts index 373d26d5d0010..e1446d6a38f2f 100644 --- a/projects/packages/forms/src/types/index.ts +++ b/projects/packages/forms/src/types/index.ts @@ -208,6 +208,8 @@ export type BlockEditorStoreSelect = { export interface FormsConfigData { /** Whether MailPoet integration is enabled across contexts. */ isMailPoetEnabled?: boolean; + /** Whether Hostinger Reach integration is enabled across contexts. */ + isHostingerReachEnabled?: boolean; /** Whether integrations UI is enabled (feature-flagged). */ isIntegrationsEnabled?: boolean; /** Whether the current user can install plugins (install_plugins). */