Skip to content
4 changes: 4 additions & 0 deletions projects/packages/forms/changelog/add-form-response-webhooks
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Forms: add form response webhook support.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Form Webhooks: add logging and filter flag for initialization
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Form Webhooks: clean up and consolidate webhooks to take over the unused-yet-present postToUrl
4 changes: 4 additions & 0 deletions projects/packages/forms/src/blocks/contact-form/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,8 @@ export default {
type: 'array',
default: [],
},
webhooks: {
type: 'array',
default: [],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { TextControl, ToggleControl, ExternalLink } from '@wordpress/components';
import { useState, useEffect, createInterpolateElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

const WebhooksSettings = ( { setAttributes, webhooks } ) => {
// For now, we only support one webhook, but the data structure supports multiple
const firstWebhook = webhooks?.[ 0 ] || null;

const [ localWebhookId, setLocalWebhookId ] = useState( firstWebhook?.webhook_id || '' );
const [ localWebhookUrl, setLocalWebhookUrl ] = useState( firstWebhook?.url || '' );
const [ localWebhookEnabled, setLocalWebhookEnabled ] = useState(
firstWebhook?.enabled || false
);

// Sync local state with attributes when webhook changes
useEffect( () => {
if ( firstWebhook ) {
setLocalWebhookId( firstWebhook.webhook_id || '' );
setLocalWebhookUrl( firstWebhook.url || '' );
setLocalWebhookEnabled( firstWebhook.enabled || false );
}
}, [ firstWebhook ] );

const updateWebhook = ( id, url, enabled ) => {
if ( ! url && ! enabled ) {
// If URL is empty and webhook is disabled, remove it from the array
setAttributes( { webhooks: [] } );
return;
}

const webhook = {
webhook_id: id || '',
url: url || '',
format: 'json', // Default to json, no UI for changing this yet
method: 'POST', // Default to POST, no UI for changing this yet
enabled: enabled,
};

setAttributes( { webhooks: [ webhook ] } );
};

return (
<>
<ToggleControl
label={ __( 'Enable webhook', 'jetpack-forms' ) }
help={ createInterpolateElement(
__(
'Send form submission data to an external URL. <webhookDocsLink>Learn more about webhooks.</webhookDocsLink>',
'jetpack-forms'
),
{
webhookDocsLink: (
<ExternalLink href="https://jetpack.com/support/jetpack-forms/webhooks/" />
),
}
) }
checked={ localWebhookEnabled }
onChange={ value => {
setLocalWebhookEnabled( value );
updateWebhook( localWebhookId, localWebhookUrl, value );
} }
__nextHasNoMarginBottom={ true }
/>
{ localWebhookEnabled && (
<>
<TextControl
label={ __( 'Webhook ID', 'jetpack-forms' ) }
value={ localWebhookId }
onChange={ value => {
setLocalWebhookId( value );
updateWebhook( value, localWebhookUrl, localWebhookEnabled );
} }
placeholder="webhook-1"
help={ __( 'A unique identifier for this webhook.', 'jetpack-forms' ) }
__nextHasNoMarginBottom={ true }
__next40pxDefaultSize={ true }
/>
<TextControl
label={ __( 'Webhook URL', 'jetpack-forms' ) }
value={ localWebhookUrl }
onChange={ value => {
setLocalWebhookUrl( value );
updateWebhook( localWebhookId, value, localWebhookEnabled );
} }
placeholder="https://example.com/webhook"
help={ __(
'Enter the URL where form submission data should be sent.',
'jetpack-forms'
) }
type="url"
__nextHasNoMarginBottom={ true }
__next40pxDefaultSize={ true }
/>
</>
) }
</>
);
};

export default WebhooksSettings;
22 changes: 22 additions & 0 deletions projects/packages/forms/src/blocks/contact-form/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { childBlocks } from './child-blocks.js';
import { ContactFormPlaceholder } from './components/jetpack-contact-form-placeholder.js';
import ContactFormSkeletonLoader from './components/jetpack-contact-form-skeleton-loader.js';
import NotificationsSettings from './components/notifications-settings.js';
import WebhooksSettings from './components/webhooks-settings.js';
import useFormBlockDefaults from './shared/hooks/use-form-block-defaults.js';
import VariationPicker from './variation-picker.js';
import './util/form-styles.js';
Expand Down Expand Up @@ -118,6 +119,14 @@ type CustomThankyouType =
| 'message' // custom message
| 'redirect'; // redirect to a new URL

type Webhook = {
webhook_id: string;
url: string;
format: 'urlencoded' | 'json';
method: 'POST' | 'GET' | 'PUT';
enabled: boolean;
};

type JetpackContactFormAttributes = {
to: string;
subject: string;
Expand All @@ -133,7 +142,9 @@ type JetpackContactFormAttributes = {
disableGoBack: boolean;
disableSummary: boolean;
notificationRecipients: string[];
webhooks: Webhook[];
};

type JetpackContactFormEditProps = {
name: string;
attributes: JetpackContactFormAttributes;
Expand Down Expand Up @@ -166,8 +177,10 @@ function JetpackContactFormEdit( {
disableGoBack,
disableSummary,
notificationRecipients,
webhooks,
} = attributes;
const showFormIntegrations = useConfigValue( 'isIntegrationsEnabled' );
const showWebhooks = useConfigValue( 'isWebhooksEnabled' );
const instanceId = useInstanceId( JetpackContactFormEdit );

// Backward compatibility for the deprecated customThankyou attribute.
Expand Down Expand Up @@ -913,6 +926,15 @@ function JetpackContactFormEdit( {
<IntegrationControls attributes={ attributes } setAttributes={ setAttributes } />
</Suspense>
) }
{ showWebhooks && (
<PanelBody
title={ __( 'Webhooks', 'jetpack-forms' ) }
className="jetpack-contact-form__panel"
initialOpen={ false }
>
<WebhooksSettings webhooks={ webhooks } setAttributes={ setAttributes } />
</PanelBody>
) }
<PanelBody
title={ __( 'Responses storage', 'jetpack-forms' ) }
className="jetpack-contact-form__panel jetpack-contact-form__responses-storage-panel"
Expand Down
14 changes: 14 additions & 0 deletions projects/packages/forms/src/class-jetpack-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,18 @@ public static function is_integrations_enabled() {
*/
return apply_filters( 'jetpack_forms_is_integrations_enabled', true );
}

/**
* Returns true if webhooks are enabled.
*
* @return boolean
*/
public static function is_webhooks_enabled() {
/**
* Whether to enable webhooks for Jetpack Forms.
*
* @param bool false Whether webhooks should be enabled. Default false.
*/
return apply_filters( 'jetpack_forms_webhooks_enabled', false );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore V
'siteURL' => ( new Status() )->get_site_suffix(),
'hasFeedback' => ( new Forms_Dashboard() )->has_feedback(),
'isIntegrationsEnabled' => Jetpack_Forms::is_integrations_enabled(),
'isWebhooksEnabled' => Jetpack_Forms::is_webhooks_enabled(),
'dashboardURL' => Forms_Dashboard::get_forms_admin_url(),
// New data.
'canInstallPlugins' => current_user_can( 'install_plugins' ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\Extensions\Contact_Form\Contact_Form_Block;
use Automattic\Jetpack\Forms\Jetpack_Forms;
use Automattic\Jetpack\Forms\Service\Form_Webhooks;
use Automattic\Jetpack\Forms\Service\Hostinger_Reach_Integration;
use Automattic\Jetpack\Forms\Service\MailPoet_Integration;
use Automattic\Jetpack\Forms\Service\Post_To_Url;
Expand Down Expand Up @@ -1555,9 +1556,23 @@ public function process_form_submission() {
return $form->errors;
}

if ( ! empty( $form->attributes['salesforceData'] ) || ! empty( $form->attributes['postToUrl'] ) ) {
if ( ! empty( $form->attributes['salesforceData'] ) ) {
Post_To_Url::init();
}

// Deprecate postToUrl, migrate to webhooks in case someone put it to work.
if ( ! empty( $form->attributes['postToUrl'] ) ) {
// webhooks should be a collection.
// Turn postToUrl into a collection and merge with existing webhooks.
$form->attributes['webhooks'] = array_merge(
$form->attributes['webhooks'] ?? array(),
array( $form->attributes['postToUrl'] )
);
}

if ( Jetpack_Forms::is_webhooks_enabled() && ! empty( $form->attributes['webhooks'] ) ) {
Form_Webhooks::init();
}
// Process the form
return $form->process_submission();
}
Expand Down Expand Up @@ -1722,10 +1737,24 @@ public function process_form_submission() {
return $form->errors;
}

if ( ! empty( $form->attributes['salesforceData'] ) || ! empty( $form->attributes['postToUrl'] ) ) {
if ( ! empty( $form->attributes['salesforceData'] ) ) {
Post_To_Url::init();
}

// Deprecate postToUrl, migrate to webhooks in case someone put it to work.
if ( ! empty( $form->attributes['postToUrl'] ) ) {
// webhooks should be a collection.
// Turn postToUrl into a collection and merge with existing webhooks.
$form->attributes['webhooks'] = array_merge(
$form->attributes['webhooks'] ?? array(),
array( $form->attributes['postToUrl'] )
);
}

if ( ! empty( $form->attributes['webhooks'] ) ) {
Form_Webhooks::init();
}

// Process the form
return $form->process_submission();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ public function __construct( $attributes, $content = null, $set_id = true ) {
'saveResponses' => 'yes',
'emailNotifications' => 'yes',
'notificationRecipients' => array(), // Array of user IDs who should receive form response notifications.
'webhooks' => array(), // Array of webhooks to send the form data to.
'disableGoBack' => $attributes['disableGoBack'] ?? false,
'disableSummary' => $attributes['disableSummary'] ?? false,
'formTitle' => $attributes['formTitle'] ?? '',
Expand Down
Loading
Loading