Skip to content

Commit 88739d9

Browse files
authored
Forms: Add Hostinger Reach integration (#45271)
1 parent ee25f9b commit 88739d9

File tree

16 files changed

+646
-1
lines changed

16 files changed

+646
-1
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Forms: add Hostinger Reach integration.

projects/packages/forms/src/blocks/contact-form/attributes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export default {
5555
listName: null,
5656
},
5757
},
58+
hostingerReach: {
59+
type: 'object',
60+
default: {
61+
groupName: '',
62+
},
63+
},
5864
saveResponses: {
5965
type: 'boolean',
6066
default: true,

projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/active-integrations/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { JetpackIcon } from '@automattic/jetpack-components';
33
import { Spinner, Tooltip } from '@wordpress/components';
44
import { __ } from '@wordpress/i18n';
55
import AkismetIcon from '../../../../../icons/akismet';
6+
import HostingerReachIcon from '../../../../../icons/hostinger-reach';
67
import MailPoetOrangeIcon from '../../../../../icons/mailpoet-orange';
78
import SalesforceCircleIcon from '../../../../../icons/salesforce-circle';
89
import { isValidSalesforceOrgId } from '../salesforce-card';
@@ -44,6 +45,19 @@ export default function ActiveIntegrations( { integrations, attributes, isLoadin
4445
} );
4546
}
4647
break;
48+
case 'hostinger-reach':
49+
if (
50+
integration.isActive &&
51+
integration.isConnected &&
52+
attributes.hostingerReach?.enabledForForm
53+
) {
54+
acc.push( {
55+
...integration,
56+
icon: <HostingerReachIcon width={ 30 } height={ 30 } />,
57+
tooltip: __( 'Hostinger Reach is connected for this form', 'jetpack-forms' ),
58+
} );
59+
}
60+
break;
4761
case 'salesforce':
4862
if (
4963
attributes.salesforceData?.sendToSalesforce &&
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { store as blockEditorStore } from '@wordpress/block-editor';
2+
import { createBlock } from '@wordpress/blocks';
3+
import {
4+
Button,
5+
ExternalLink,
6+
__experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
7+
ToggleControl,
8+
TextControl,
9+
} from '@wordpress/components';
10+
import { useDispatch, useSelect } from '@wordpress/data';
11+
import { createInterpolateElement } from '@wordpress/element';
12+
import { __ } from '@wordpress/i18n';
13+
import HostingerReachIcon from '../../../../icons/hostinger-reach';
14+
import IntegrationCard from './integration-card';
15+
import type { SingleIntegrationCardProps, IntegrationCardData } from '../../../../types';
16+
17+
interface HostingerReachCardProps extends SingleIntegrationCardProps {
18+
hostingerReach: { enabledForForm?: boolean; groupName?: string };
19+
setAttributes: ( attrs: {
20+
hostingerReach: { enabledForForm?: boolean; groupName?: string };
21+
} ) => void;
22+
}
23+
24+
const HostingerReachCard = ( {
25+
isExpanded,
26+
onToggle,
27+
hostingerReach,
28+
setAttributes,
29+
data,
30+
refreshStatus,
31+
}: HostingerReachCardProps ) => {
32+
const { isConnected = false, settingsUrl = '' } = data || {};
33+
34+
const selectedBlock = useSelect( select => select( blockEditorStore ).getSelectedBlock(), [] );
35+
const { insertBlock, removeBlock } = useDispatch( blockEditorStore );
36+
const hasEmailBlock = selectedBlock?.innerBlocks?.some(
37+
( { name }: { name: string } ) => name === 'jetpack/field-email'
38+
);
39+
const consentBlock = selectedBlock?.innerBlocks?.find(
40+
( { name }: { name: string } ) => name === 'jetpack/field-consent'
41+
);
42+
43+
const toggleConsent = async () => {
44+
if ( consentBlock ) {
45+
await removeBlock( consentBlock.clientId, false );
46+
} else {
47+
const buttonBlockIndex = selectedBlock.innerBlocks.findIndex(
48+
( { name }: { name: string } ) => name === 'jetpack/button'
49+
);
50+
const newConsentBlock = await createBlock( 'jetpack/field-consent' );
51+
await insertBlock( newConsentBlock, buttonBlockIndex, selectedBlock.clientId, false );
52+
}
53+
};
54+
55+
const cardData: IntegrationCardData = {
56+
...data,
57+
showHeaderToggle: true,
58+
headerToggleValue: !! hostingerReach?.enabledForForm,
59+
isHeaderToggleEnabled: isConnected,
60+
onHeaderToggleChange: ( value: boolean ) =>
61+
setAttributes( { hostingerReach: { ...hostingerReach, enabledForForm: value } } ),
62+
isLoading: ! data || typeof data.isInstalled === 'undefined',
63+
refreshStatus,
64+
trackEventName: 'jetpack_forms_upsell_hostinger_reach_click',
65+
notInstalledMessage: createInterpolateElement(
66+
__(
67+
'Add powerful email marketing to your forms with Hostinger Reach. Simply install the plugin to start sending emails.',
68+
'jetpack-forms'
69+
),
70+
{
71+
a: <ExternalLink href={ data?.marketingUrl } />,
72+
}
73+
),
74+
notActivatedMessage: __(
75+
'Hostinger Reach is installed. Just activate the plugin to start sending emails.',
76+
'jetpack-forms'
77+
),
78+
};
79+
80+
return (
81+
<IntegrationCard
82+
title={ data?.title }
83+
description={ data?.subtitle }
84+
icon={ <HostingerReachIcon width={ 28 } height={ 28 } /> }
85+
isExpanded={ isExpanded }
86+
onToggle={ onToggle }
87+
cardData={ cardData }
88+
toggleTooltip={ __( 'Grow your audience with Hostinger Reach', 'jetpack-forms' ) }
89+
>
90+
{ ! isConnected ? (
91+
<div>
92+
<p className="integration-card__description">
93+
{ createInterpolateElement(
94+
__(
95+
'Hostinger Reach is active. There is one step left. Please complete <a>Hostinger Reach setup</a>.',
96+
'jetpack-forms'
97+
),
98+
{
99+
a: <ExternalLink href={ settingsUrl } />,
100+
}
101+
) }
102+
</p>
103+
<HStack spacing="3" justify="start">
104+
<Button
105+
variant="secondary"
106+
href={ settingsUrl }
107+
target="_blank"
108+
rel="noopener noreferrer"
109+
__next40pxDefaultSize={ true }
110+
>
111+
{ __( 'Complete Hostinger Reach setup', 'jetpack-forms' ) }
112+
</Button>
113+
<Button variant="tertiary" onClick={ refreshStatus } __next40pxDefaultSize={ true }>
114+
{ __( 'Refresh status', 'jetpack-forms' ) }
115+
</Button>
116+
</HStack>
117+
</div>
118+
) : (
119+
<div>
120+
<div className="integration-card__section">
121+
<TextControl
122+
label={ __( 'Group name (optional)', 'jetpack-forms' ) }
123+
help={ __(
124+
"If empty, contacts will be added under 'Jetpack Forms'.",
125+
'jetpack-forms'
126+
) }
127+
value={ hostingerReach.groupName ?? '' }
128+
onChange={ value =>
129+
setAttributes( {
130+
hostingerReach: { ...hostingerReach, groupName: value },
131+
} )
132+
}
133+
__nextHasNoMarginBottom
134+
/>
135+
</div>
136+
{ hasEmailBlock && (
137+
<div className="integration-card__section">
138+
<ToggleControl
139+
label={ __( 'Add email permission request before submit button', 'jetpack-forms' ) }
140+
checked={ !! consentBlock }
141+
onChange={ toggleConsent }
142+
__nextHasNoMarginBottom
143+
/>
144+
</div>
145+
) }
146+
<p className="integration-card__description">
147+
<ExternalLink href={ settingsUrl }>
148+
{ __( 'View Hostinger Reach dashboard', 'jetpack-forms' ) }
149+
</ExternalLink>
150+
</p>
151+
</div>
152+
) }
153+
</IntegrationCard>
154+
);
155+
};
156+
157+
export default HostingerReachCard;

projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { __ } from '@wordpress/i18n';
1111
import AkismetCard from './akismet-card';
1212
import CreativeMailCard from './creative-mail-card';
1313
import GoogleSheetsCard from './google-sheets-card';
14+
import HostingerReachCard from './hostinger-reach-card';
1415
import JetpackCRMCard from './jetpack-crm-card';
1516
import MailPoetCard from './mailpoet-card';
1617
import SalesforceCard from './salesforce-card';
@@ -35,6 +36,7 @@ const IntegrationsModal = ( {
3536
creativemail: false,
3637
salesforce: false,
3738
mailpoet: false,
39+
hostingerReach: false,
3840
} );
3941

4042
if ( ! isOpen ) {
@@ -51,6 +53,7 @@ const IntegrationsModal = ( {
5153
const mailpoetData = findIntegrationById( 'mailpoet' );
5254
const salesforceData = findIntegrationById( 'salesforce' );
5355
const creativeMailData = findIntegrationById( 'creative-mail-by-constant-contact' );
56+
const hostingerReachData = findIntegrationById( 'hostinger-reach' );
5457

5558
const toggleCard = ( cardId: string ) => {
5659
setExpandedCards( prev => {
@@ -124,6 +127,16 @@ const IntegrationsModal = ( {
124127
setAttributes={ setAttributes }
125128
/>
126129
) }
130+
{ hostingerReachData && (
131+
<HostingerReachCard
132+
isExpanded={ expandedCards.hostingerReach }
133+
onToggle={ () => toggleCard( 'hostingerReach' ) }
134+
data={ hostingerReachData }
135+
refreshStatus={ refreshIntegrations }
136+
hostingerReach={ attributes.hostingerReach }
137+
setAttributes={ setAttributes }
138+
/>
139+
) }
127140
{ creativeMailData && (
128141
<CreativeMailCard
129142
isExpanded={ expandedCards.creativemail }

projects/packages/forms/src/blocks/contact-form/components/jetpack-integrations-modal/integration-card/style.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@
153153
}
154154

155155
&__service-icon--google-sheets,
156-
&__service-icon--mailpoet {
156+
&__service-icon--mailpoet,
157+
&__service-icon--hostinger-reach {
157158
border-radius: 0 !important;
158159
}
159160

projects/packages/forms/src/class-jetpack-forms.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ public static function is_mailpoet_enabled() {
9494
return apply_filters( 'jetpack_forms_mailpoet_enable', true );
9595
}
9696

97+
/**
98+
* Returns true if Hostinger Reach integration is enabled.
99+
*
100+
* @return boolean
101+
*/
102+
public static function is_hostinger_reach_enabled() {
103+
/**
104+
* Enable Hostinger Reach integration.
105+
*
106+
* @param bool false Whether Hostinger Reach integration be enabled. Default is false.
107+
*/
108+
return apply_filters( 'jetpack_forms_hostinger_reach_enable', false );
109+
}
110+
97111
/**
98112
* Returns true if the Integrations UI should be enabled.
99113
*

projects/packages/forms/src/contact-form/class-contact-form-endpoint.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ private function get_supported_integrations() {
101101
),
102102
);
103103

104+
// Conditionally add Hostinger Reach integration behind feature flag.
105+
if ( Jetpack_Forms::is_hostinger_reach_enabled() ) {
106+
$supported_integrations['hostinger-reach'] = array(
107+
'type' => 'plugin',
108+
'file' => 'hostinger-reach/hostinger-reach.php',
109+
'settings_url' => 'admin.php?page=hostinger-reach#/home',
110+
'marketing_redirect_slug' => 'hostinger-reach',
111+
'title' => __( 'Hostinger Reach', 'jetpack-forms' ),
112+
'subtitle' => __( 'Send newsletters and marketing emails via Hostinger Reach.', 'jetpack-forms' ),
113+
// Overriding this may automatically enable/disable the integration when editing a form.
114+
'enabled_by_default' => false,
115+
);
116+
}
117+
104118
/**
105119
* Filters the list of supported integrations available in Jetpack Forms.
106120
*
@@ -1162,6 +1176,29 @@ private function get_plugin_status( $plugin_slug, array $status ) {
11621176
// Add MailPoet lists to details
11631177
$status['details']['lists'] = MailPoet_Integration::get_all_lists();
11641178
break;
1179+
case 'hostinger-reach':
1180+
// Hostinger Reach is a plugin that requires additional setup/connection.
1181+
$status['needsConnection'] = true;
1182+
$status['isConnected'] = false;
1183+
// Determine if Hostinger Reach is connected using its public handler.
1184+
if ( $is_active
1185+
// @phan-suppress-next-line PhanUndeclaredClassReference
1186+
&& class_exists( \Hostinger\Reach\Api\Handlers\ReachApiHandler::class )
1187+
// @phan-suppress-next-line PhanUndeclaredClassReference
1188+
&& class_exists( \Hostinger\Reach\Functions::class )
1189+
// @phan-suppress-next-line PhanUndeclaredClassReference
1190+
&& class_exists( \Hostinger\Reach\Api\ApiKeyManager::class )
1191+
) {
1192+
$reach_handler = new \Hostinger\Reach\Api\Handlers\ReachApiHandler( // @phan-suppress-current-line PhanUndeclaredClassMethod
1193+
new \Hostinger\Reach\Functions(), // @phan-suppress-current-line PhanUndeclaredClassMethod
1194+
new \Hostinger\Reach\Api\ApiKeyManager() // @phan-suppress-current-line PhanUndeclaredClassMethod
1195+
);
1196+
if ( method_exists( $reach_handler, 'is_connected' ) ) {
1197+
// @phan-suppress-next-line PhanUndeclaredClassMethod
1198+
$status['isConnected'] = (bool) $reach_handler->is_connected();
1199+
}
1200+
}
1201+
break;
11651202
}
11661203

11671204
return $status;
@@ -1242,6 +1279,7 @@ public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore V
12421279
// From jpFormsBlocks in class-contact-form-block.php.
12431280
'formsResponsesUrl' => Forms_Dashboard::get_forms_admin_url(),
12441281
'isMailPoetEnabled' => Jetpack_Forms::is_mailpoet_enabled(),
1282+
'isHostingerReachEnabled' => Jetpack_Forms::is_hostinger_reach_enabled(),
12451283
// From config in class-dashboard.php.
12461284
'blogId' => get_current_blog_id(),
12471285
'gdriveConnectSupportURL' => esc_url( Redirect::get_url( 'jetpack-support-contact-form-export' ) ),

projects/packages/forms/src/contact-form/class-contact-form-plugin.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Automattic\Jetpack\Constants;
1212
use Automattic\Jetpack\Extensions\Contact_Form\Contact_Form_Block;
1313
use Automattic\Jetpack\Forms\Jetpack_Forms;
14+
use Automattic\Jetpack\Forms\Service\Hostinger_Reach_Integration;
1415
use Automattic\Jetpack\Forms\Service\MailPoet_Integration;
1516
use Automattic\Jetpack\Forms\Service\Post_To_Url;
1617
use Automattic\Jetpack\Status;
@@ -335,6 +336,16 @@ protected function __construct() {
335336
4
336337
);
337338
}
339+
340+
// Register Hostinger Reach integration hook after the class is loaded.
341+
if ( Jetpack_Forms::is_hostinger_reach_enabled() ) {
342+
add_action(
343+
'grunion_after_feedback_post_inserted',
344+
array( Hostinger_Reach_Integration::class, 'handle_hostinger_reach_integration' ),
345+
16,
346+
4
347+
);
348+
}
338349
}
339350

340351
/**

projects/packages/forms/src/contact-form/class-contact-form.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ public function __construct( $attributes, $content = null, $set_id = true ) {
189189
'confirmationType' => null, // The type of confirmation to show after submitting a form. 'text' for a text message, 'redirect' for a redirect link.
190190
'jetpackCRM' => true, // Whether Jetpack CRM should store the form submission.
191191
'mailpoet' => null,
192+
'hostingerReach' => null,
192193
'className' => null,
193194
'postToUrl' => null,
194195
'salesforceData' => null,

0 commit comments

Comments
 (0)