From 4eff746c44e52691a673afa991a735aad07a0cd1 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:01:54 +0300 Subject: [PATCH 01/13] Add plugin feedback hooks to FrmHooksController for NPS survey functionality --- classes/controllers/FrmHooksController.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/classes/controllers/FrmHooksController.php b/classes/controllers/FrmHooksController.php index d5383d9eb9..088d85c6ff 100644 --- a/classes/controllers/FrmHooksController.php +++ b/classes/controllers/FrmHooksController.php @@ -222,6 +222,10 @@ public static function load_admin_hooks() { FrmSMTPController::load_hooks(); FrmOnboardingWizardController::load_admin_hooks(); FrmAddonsController::load_admin_hooks(); + + // Plugin feedback (NPS survey). + FrmPluginFeedbackController::load_admin_hooks(); + new FrmPluginSearch(); } @@ -325,6 +329,10 @@ public static function load_ajax_hooks() { // Welcome Tour. add_action( 'wp_ajax_frm_mark_checklist_step_as_completed', 'FrmWelcomeTourController::ajax_mark_checklist_step_as_completed' ); add_action( 'wp_ajax_frm_dismiss_welcome_tour', 'FrmWelcomeTourController::ajax_dismiss_welcome_tour' ); + + // Plugin feedback. + add_action( 'wp_ajax_frm_submit_lite_plugin_feedback', 'FrmPluginFeedbackController::ajax_submit_plugin_feedback' ); + add_action( 'wp_ajax_frm_dismiss_lite_plugin_feedback', 'FrmPluginFeedbackController::ajax_dismiss_plugin_feedback' ); } /** From 94c999c3106f5a6b2d5ebd654aa562b4797a9208 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:02:01 +0300 Subject: [PATCH 02/13] Add FrmPluginFeedbackController to handle NPS feedback collection for Lite users --- .../FrmPluginFeedbackController.php | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 classes/controllers/FrmPluginFeedbackController.php diff --git a/classes/controllers/FrmPluginFeedbackController.php b/classes/controllers/FrmPluginFeedbackController.php new file mode 100644 index 0000000000..83c6470b5e --- /dev/null +++ b/classes/controllers/FrmPluginFeedbackController.php @@ -0,0 +1,387 @@ += ( $threshold_days * DAY_IN_SECONDS ); + } + + /** + * @return void + */ + public static function enqueue_assets() { + $version = FrmAppHelper::plugin_version(); + + wp_enqueue_script( 'formidable-lite-plugin-feedback', FrmAppHelper::plugin_url() . '/js/plugin-feedback.js', array( 'formidable_dom' ), $version, true ); + wp_enqueue_style( 'formidable-lite-plugin-feedback', FrmAppHelper::plugin_url() . '/css/components/plugin-feedback.css', array(), $version ); + } + + /** + * @return void + */ + public static function show_plugin_feedback() { + $current = self::get_current_year_feedback(); + $step = isset( $current['nps-score'] ) ? 'reasons' : 'nps'; + $reasons = self::get_reasons(); + + include FrmAppHelper::plugin_path() . '/classes/views/shared/plugin-feedback.php'; + } + + /** + * @return void + */ + public static function ajax_submit_plugin_feedback() { + check_ajax_referer( 'frm_ajax', 'nonce' ); + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( FrmAppHelper::pro_is_included() ) { + wp_send_json_error( array( 'type' => 'pro-active' ) ); + } + + self::$user_id = get_current_user_id(); + + self::maybe_save_nps_and_send_response(); + self::submit_feedback_to_remote(); + } + + /** + * @return void + */ + public static function ajax_dismiss_plugin_feedback() { + check_ajax_referer( 'frm_ajax', 'nonce' ); + FrmAppHelper::permission_check( 'frm_change_settings' ); + + if ( FrmAppHelper::pro_is_included() ) { + wp_send_json_error( array( 'type' => 'pro-active' ) ); + } + + self::$user_id = get_current_user_id(); + + self::submit_feedback_to_remote(); + } + + /** + * @return void + */ + private static function maybe_save_nps_and_send_response() { + if ( ! isset( $_POST['nps-score'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + return; + } + + $nps_score = (int) FrmAppHelper::get_post_param( 'nps-score', 10, 'absint' ); + + if ( $nps_score < 0 || $nps_score > 10 ) { + wp_send_json_error( array( 'type' => 'invalid-nps' ) ); + } + + self::set_current_year_feedback( 'nps-score', $nps_score ); + wp_send_json_success( array( 'message' => __( 'Feedback score saved successfully.', 'formidable' ) ) ); + } + + /** + * @return void + */ + private static function submit_feedback_to_remote() { + $current = self::get_current_year_feedback(); + + if ( ! isset( $current['nps-score'] ) ) { + self::set_current_year_feedback( 'submitted', true ); + wp_send_json_success( array( 'message' => __( 'Feedback dismissed successfully.', 'formidable' ) ) ); + } + + $remote_response = wp_remote_post( + 'https://formidableforms.com/wp-admin/admin-ajax.php?action=frm_forms_preview&form=plugin-feedback-lite', + array( + 'timeout' => 30, + 'body' => http_build_query( self::build_remote_body() ), + ) + ); + + if ( is_wp_error( $remote_response ) ) { + wp_send_json_error( + array( + 'type' => 'server-error', + 'message' => __( 'Failed to submit feedback to remote service.', 'formidable' ), + ) + ); + } + + $response_code = wp_remote_retrieve_response_code( $remote_response ); + + if ( WP_Http::OK !== $response_code ) { + wp_send_json_error( + array( + 'type' => 'server-error', + 'message' => __( 'Remote service returned an error.', 'formidable' ), + ) + ); + } + + self::set_current_year_feedback( 'submitted', true ); + wp_send_json_success( array( 'message' => __( 'Feedback submitted successfully.', 'formidable' ) ) ); + } + + /** + * Builds the payload sent to formidableforms.com. form_id and item_meta keys + * are placeholders until Marketing finalizes the destination form; swapping them + * should be a small edit here. + * + * @return array + */ + private static function build_remote_body() { + $feedback = self::get_current_year_feedback(); + $nps = isset( $feedback['nps-score'] ) ? $feedback['nps-score'] : ''; + + return array( + 'l' => base64_encode( (string) get_option( 'frm-usage-uuid' ) ), + 'form_key' => 'plugin-feedback-lite', + 'frm_action' => 'create', + 'form_id' => 0, + 'item_key' => '', + 'item_meta[0]' => '', + 'item_meta[NPS]' => $nps, + 'item_meta[RSN]' => self::format_reasons_list( self::get_posted_reasons() ), + 'item_meta[DTL]' => FrmAppHelper::get_post_param( 'details', '' ), + 'item_meta[URL]' => site_url(), + 'item_meta[SRC]' => self::SOURCE, + 'item_meta[VER]' => FrmAppHelper::plugin_version(), + ); + } + + /** + * @return array + */ + private static function get_posted_reasons() { + $reasons = json_decode( FrmAppHelper::get_post_param( 'reasons', '[]' ), true ); + $reasons = rest_sanitize_value_from_schema( + $reasons, + array( + 'type' => 'array', + 'items' => array( + 'enum' => array_keys( self::get_reasons() ), + 'type' => 'string', + ), + ) + ); + + if ( ! $reasons && ! FrmAppHelper::get_post_param( 'dismissed', false, 'rest_sanitize_boolean' ) ) { + wp_send_json_error( array( 'type' => 'invalid-reasons' ) ); + } + + return $reasons; + } + + /** + * @param array $reason_keys + * @return string + */ + private static function format_reasons_list( $reason_keys ) { + if ( ! $reason_keys ) { + return ''; + } + + $reasons = self::get_reasons(); + $formatted_reasons = array_map( + static function ( $key ) use ( $reasons ) { + return '- ' . $reasons[ $key ]; + }, + $reason_keys + ); + + return implode( "\n", $formatted_reasons ); + } + + /** + * @return array + */ + private static function get_plugin_feedback() { + if ( self::$plugin_feedback ) { + return self::$plugin_feedback; + } + + self::$plugin_feedback = get_user_meta( self::$user_id, self::PLUGIN_FEEDBACK_META_KEY, true ); + + if ( ! is_array( self::$plugin_feedback ) ) { + self::$plugin_feedback = array( + self::get_current_year() => array( + 'submitted' => false, + 'source' => self::SOURCE, + ), + ); + } elseif ( ! isset( self::$plugin_feedback[ self::get_current_year() ] ) ) { + self::$plugin_feedback[ self::get_current_year() ] = array( + 'submitted' => false, + 'source' => self::SOURCE, + ); + } + + return self::$plugin_feedback; + } + + /** + * @return array + */ + private static function get_current_year_feedback() { + return self::get_plugin_feedback()[ self::get_current_year() ]; + } + + /** + * @param string $key + * @param mixed $value + * + * @return void + */ + private static function set_current_year_feedback( $key, $value ) { + self::get_plugin_feedback(); + self::$plugin_feedback[ self::get_current_year() ][ $key ] = $value; + self::$plugin_feedback[ self::get_current_year() ]['source'] = self::SOURCE; + update_user_meta( self::$user_id, self::PLUGIN_FEEDBACK_META_KEY, self::$plugin_feedback ); + } + + /** + * @return int + */ + private static function get_current_year() { + if ( self::$current_year ) { + return self::$current_year; + } + + self::$current_year = (int) wp_date( 'Y' ); + return self::$current_year; + } + + /** + * English-only — sent to a remote service, so intentionally not translatable. + * + * @return array + */ + private static function get_reasons() { + return array( + 'pricing' => 'Pricing and plans', + 'form-builder' => 'Form builder flexibility', + 'customization' => 'Customization options', + 'integrations' => 'Integrations', + 'advanced-fields' => 'Advanced fields', + 'customer-support' => 'Customer support', + 'templates' => 'Template selection', + 'performance' => 'Performance/Speed', + 'calculations' => 'Calculations & formulas', + 'documentation' => 'Documentation / tutorials', + ); + } +} From a338f7fcd36db62ffb9435620c46a05dfb143fe6 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:02:10 +0300 Subject: [PATCH 03/13] Add echo_nps method to FrmHtmlHelper for rendering NPS radio scale with stylesheet enqueueing --- classes/helpers/FrmHtmlHelper.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/classes/helpers/FrmHtmlHelper.php b/classes/helpers/FrmHtmlHelper.php index f47d42fcff..f7b2cde684 100644 --- a/classes/helpers/FrmHtmlHelper.php +++ b/classes/helpers/FrmHtmlHelper.php @@ -131,4 +131,30 @@ public static function echo_unit_input( $args = array() ) { '', + 'class' => '', + 'name' => 'nps_score', + 'value' => '0', + 'negative_statement' => __( 'Not satisfied', 'formidable' ), + 'positive_statement' => __( 'Very satisfied', 'formidable' ), + ); + $args = wp_parse_args( $args, $defaults ); + + include FrmAppHelper::plugin_path() . '/classes/views/shared/nps.php'; + } } From bfb4d652d827ad1451cfa107288a983997f1a6d6 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:02:18 +0300 Subject: [PATCH 04/13] Add NPS Score template for rendering Net Promoter Score radio buttons --- classes/views/shared/nps.php | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 classes/views/shared/nps.php diff --git a/classes/views/shared/nps.php b/classes/views/shared/nps.php new file mode 100644 index 0000000000..f495ea35d5 --- /dev/null +++ b/classes/views/shared/nps.php @@ -0,0 +1,65 @@ + 'frm-nps frm-flex-col frm-gap-xs', +); + +if ( ! empty( $args['id'] ) ) { + $nps_attrs['id'] = $args['id']; +} + +if ( ! empty( $args['class'] ) ) { + $nps_attrs['class'] .= ' ' . $args['class']; +} + +$input_attrs = array( + 'type' => 'radio', + 'name' => $args['name'], + 'class' => 'frm_hidden', +); +?> +
> +
+ + /> + + +
+ +
+
+ ' . esc_html( $args['negative_statement'] ) . '' + ); + ?> +
+ +
+ ' . esc_html( $args['positive_statement'] ) . '' + ); + ?> +
+
+
From 245450951db6eea29b1eed9bf872223ed977bcae Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:02:26 +0300 Subject: [PATCH 05/13] Add plugin feedback template for user experience feedback collection --- classes/views/shared/plugin-feedback.php | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 classes/views/shared/plugin-feedback.php diff --git a/classes/views/shared/plugin-feedback.php b/classes/views/shared/plugin-feedback.php new file mode 100644 index 0000000000..c59a871268 --- /dev/null +++ b/classes/views/shared/plugin-feedback.php @@ -0,0 +1,74 @@ + +
+ + + + +
+
+

+

+ 'frm-plugin-feedback-nps', + 'class' => 'frm-mt-xs', + 'name' => 'plugin-feedback-nps-score', + 'value' => '10', + ) + ); + ?> +
+ +
+

+
+ $label ) { ?> + + +
+
+ + +
+
+ + 'frm-plugin-feedback-error', + 'errors' => array( + 'invalid-nps' => __( 'NPS score is invalid.', 'formidable' ), + 'invalid-reasons' => __( 'Please select at least one reason.', 'formidable' ), + 'server-error' => __( 'Failed to submit feedback, try again later.', 'formidable' ), + ), + 'class' => 'frm-items-center', + ) + ); + ?> + + +
+ +
+

+

+
+
From 4eb31b35089fb43bee6dc6da697494b561d8c1ed Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:02:33 +0300 Subject: [PATCH 06/13] Add CSS styles for NPS buttons and plugin feedback component --- css/components/nps.css | 33 ++++++++++++++ css/components/plugin-feedback.css | 71 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 css/components/nps.css create mode 100644 css/components/plugin-feedback.css diff --git a/css/components/nps.css b/css/components/nps.css new file mode 100644 index 0000000000..32cb4a3d6a --- /dev/null +++ b/css/components/nps.css @@ -0,0 +1,33 @@ +.frm-nps__buttons { + flex-wrap: wrap; + gap: 6px; +} + +.frm-nps__button { + flex: 0 0 auto; + width: 44px; + height: 44px; + font-size: var(--text-sm); + color: var(--grey-900); + border: 1px solid var(--grey-300); + border-radius: var(--small-radius); + cursor: pointer; + transition: background-color 150ms ease-out, + border-color 150ms ease-out, + color 150ms ease-out; +} + +.frm-nps__button:hover, +.frm-nps__button:focus, +.frm-nps__buttons input[type="radio"]:focus + .frm-nps__button, +.frm-nps__buttons input[type="radio"]:checked + .frm-nps__button, +.frm-nps__button.frm-nps__button--active { + background-color: var(--primary-500); + border-color: var(--primary-500); + color: #fff; +} + +.frm-nps__statements { + font-size: var(--text-xs); + color: var(--grey-500); +} diff --git a/css/components/plugin-feedback.css b/css/components/plugin-feedback.css new file mode 100644 index 0000000000..00e9879550 --- /dev/null +++ b/css/components/plugin-feedback.css @@ -0,0 +1,71 @@ +#frm-plugin-feedback, +#frm-plugin-feedback * { + box-sizing: border-box; +} + +#frm-plugin-feedback { + position: fixed; + bottom: var(--gap-md); + right: var(--gap-md); + width: 100%; + max-width: 548px; + background-color: #fff; + border: 1px solid var(--grey-100); + border-radius: var(--medium-radius); + padding: var(--gap-xl) var(--gap-md) var(--gap-md); + box-shadow: var(--box-shadow-xl); + z-index: 9999; +} + +#frm-plugin-feedback[data-step="nps"] { + max-width: 594px; +} + +#frm-plugin-feedback:not([data-step="thank-you"]) p { + color: var(--grey-900) !important; + line-height: 1 !important; +} + +#frm-plugin-feedback .dismiss { + top: var(--gap-sm); + right: var(--gap-sm); +} + +#frm-plugin-feedback .frm-validation-error { + margin: 0; +} + +#frm-plugin-feedback .frm-validation-error[frm-error="invalid-nps"] span[frm-error="invalid-nps"], +#frm-plugin-feedback .frm-validation-error[frm-error="invalid-reasons"] span[frm-error="invalid-reasons"], +#frm-plugin-feedback .frm-validation-error[frm-error="server-error"] span[frm-error="server-error"] { + display: inline-block; +} + +/* Steps */ +#frm-plugin-feedback-reasons-step .frm_grid_container { + gap: var(--gap-xs); +} + +#frm-plugin-feedback-reasons-step .frm-option-box { + justify-content: flex-start; + gap: var(--gap-xs); + color: var(--grey-900); + padding: var(--gap-sm) 12px; + background-color: var(--grey-100); + border-color: var(--grey-100); +} + +#frm-plugin-feedback-reasons-step .frm-option-box:hover { + background-color: var(--grey-200); + border-color: var(--grey-200); +} + +#frm-plugin-feedback-reasons-step .frm-option-box.frm-checked { + background-color: var(--primary-50); + border-color: var(--primary-500); +} + +#frm-plugin-feedback-details { + resize: none; + height: 90px; +} From dbcd4c4ec754eb604c828438f503f83582825145 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:03:01 +0300 Subject: [PATCH 07/13] Add JavaScript functionality for submitting and managing plugin feedback --- .../plugin-feedback/submitFeedbackEvents.js | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 js/src/plugin-feedback/submitFeedbackEvents.js diff --git a/js/src/plugin-feedback/submitFeedbackEvents.js b/js/src/plugin-feedback/submitFeedbackEvents.js new file mode 100644 index 0000000000..c4096de2b8 --- /dev/null +++ b/js/src/plugin-feedback/submitFeedbackEvents.js @@ -0,0 +1,122 @@ +/** + * Internal dependencies + */ +import { hideError, showError } from './utils'; + +const { doJsonPost } = frmDom.ajax; + +const HIDDEN_CLASS = 'frm_hidden'; +const CLASS_PREFIX = 'frm-plugin-feedback'; +const LOADING_CLASS = 'frm_loading_button'; + +const pluginFeedback = document.getElementById( CLASS_PREFIX ); +const form = document.getElementById( `${ CLASS_PREFIX }-form` ); +const submitButton = form?.querySelector( 'button[type="submit"]' ); +const npsStep = document.getElementById( `${ CLASS_PREFIX }-nps-step` ); +const reasonsStep = document.getElementById( `${ CLASS_PREFIX }-reasons-step` ); +const thankYouStep = document.getElementById( `${ CLASS_PREFIX }-thank-you-step` ); + +/** + * Adds event listeners for submitting plugin feedback. + * + * @private + * @return {void} + */ +function addSubmitFeedbackEventListeners() { + if ( ! pluginFeedback || ! form ) { + return; + } + + form.addEventListener( 'submit', onSubmitFeedback ); + pluginFeedback.querySelector( '.dismiss' )?.addEventListener( 'click', onDismissFeedback ); +} + +/** + * Handles form submission for plugin feedback. + * + * @private + * @param {Event} event The form submit event. + * @return {void} + */ +async function onSubmitFeedback( event ) { + event.preventDefault(); + + submitButton.classList.add( LOADING_CLASS ); + + const step = pluginFeedback.dataset.step; + const formData = new FormData(); + + if ( 'nps' === step ) { + const npsScore = form.querySelector( 'input[name="plugin-feedback-nps-score"]:checked' ); + formData.append( 'nps-score', npsScore?.value ); + } else { + const reasons = form.querySelectorAll( 'input[name="plugin-feedback-reasons"]:checked' ); + formData.append( 'reasons', JSON.stringify( Array.from( reasons ).map( ( reason ) => reason.value ) ) ); + formData.append( 'details', form.querySelector( 'textarea[name="plugin-feedback-details"]' )?.value ); + } + + try { + await doJsonPost( 'submit_lite_plugin_feedback', formData ); + } catch ( error ) { + showError( error.type ); + if ( error.message ) { + console.error( 'Feedback submission error:', error.message ); + } + return; + } finally { + submitButton.classList.remove( LOADING_CLASS ); + } + + hideError(); + updateFeedbackStep( step ); +} + +/** + * Updates the feedback step and shows/hides appropriate step elements. + * + * @private + * @param {string} step The current step ('nps' or 'reasons'). + * @return {void} + */ +function updateFeedbackStep( step ) { + if ( 'nps' === step ) { + pluginFeedback.dataset.step = 'reasons'; + npsStep.classList.add( HIDDEN_CLASS ); + reasonsStep.classList.remove( HIDDEN_CLASS ); + } else { + pluginFeedback.dataset.step = 'thank-you'; + reasonsStep.classList.add( HIDDEN_CLASS ); + form.classList.add( HIDDEN_CLASS ); + thankYouStep.classList.remove( HIDDEN_CLASS ); + } +} + +/** + * Handles dismiss button click. + * + * @private + * @param {Event} event The click event. + * @return {void} + */ +async function onDismissFeedback( event ) { + event.preventDefault(); + + pluginFeedback.remove(); + + if ( 'thank-you' === pluginFeedback.dataset.step ) { + return; + } + + const formData = new FormData(); + formData.append( 'dismissed', true ); + + try { + await doJsonPost( 'dismiss_lite_plugin_feedback', formData ); + } catch ( error ) { + if ( error.message ) { + console.error( 'Feedback submission error:', error.message ); + } + } +} + +export default addSubmitFeedbackEventListeners; From f607182b78a806b9700926c563060dbaf0a447af Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:03:07 +0300 Subject: [PATCH 08/13] Add utility functions for showing and hiding error messages in plugin feedback form --- js/src/plugin-feedback/utils.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 js/src/plugin-feedback/utils.js diff --git a/js/src/plugin-feedback/utils.js b/js/src/plugin-feedback/utils.js new file mode 100644 index 0000000000..bee35d70ef --- /dev/null +++ b/js/src/plugin-feedback/utils.js @@ -0,0 +1,21 @@ +const error = document.getElementById( 'frm-plugin-feedback-error' ); + +/** + * Shows an error message for a form field. + * + * @param {string} type The categorization of the error (e.g., "invalid", "empty"). + * @return {void} + */ +export function showError( type ) { + error.setAttribute( 'frm-error', type ); + error.classList.remove( 'frm_hidden' ); +} + +/** + * Hides the error message. + * + * @return {void} + */ +export function hideError() { + error.classList.add( 'frm_hidden' ); +} From 7e5de4208b469acbe43d9f936ae3d4688f5436e4 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:03:14 +0300 Subject: [PATCH 09/13] Add initial JavaScript setup for plugin feedback functionality --- js/src/plugin-feedback/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 js/src/plugin-feedback/index.js diff --git a/js/src/plugin-feedback/index.js b/js/src/plugin-feedback/index.js new file mode 100644 index 0000000000..bf8809cf3e --- /dev/null +++ b/js/src/plugin-feedback/index.js @@ -0,0 +1,15 @@ +/** + * WordPress dependencies + */ +import domReady from '@wordpress/dom-ready'; + +/** + * Internal dependencies + */ +import { addOptionBoxEvents } from 'core/events'; +import addSubmitFeedbackEventListeners from './submitFeedbackEvents'; + +domReady( () => { + addSubmitFeedbackEventListeners(); + addOptionBoxEvents(); +} ); From 3d7f266d97a8564a6bd511fe66636f6927db4b46 Mon Sep 17 00:00:00 2001 From: Sherv Date: Wed, 22 Apr 2026 19:03:22 +0300 Subject: [PATCH 10/13] Add entry for plugin feedback in webpack configuration --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 0ada33e408..29beda4a5e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,6 +49,7 @@ const entries = { 'formidable-settings-components': './js/src/settings-components/index.js', 'formidable-web-components': './js/src/web-components/index.js', 'welcome-tour': './js/src/welcome-tour', + 'plugin-feedback': './js/src/plugin-feedback/index.js', }, // SCSS entries scss: { From 03fd22f9c08992701ae35ce7acc4e015259c30c9 Mon Sep 17 00:00:00 2001 From: Sherv Date: Tue, 28 Apr 2026 18:51:54 +0300 Subject: [PATCH 11/13] Refactor FrmPluginFeedbackController to improve code structure and accessibility. Changed private properties to protected, updated method visibility, and streamlined feedback display logic. --- .../FrmPluginFeedbackController.php | 272 ++++++++++++------ 1 file changed, 177 insertions(+), 95 deletions(-) diff --git a/classes/controllers/FrmPluginFeedbackController.php b/classes/controllers/FrmPluginFeedbackController.php index 83c6470b5e..7fd7db30e3 100644 --- a/classes/controllers/FrmPluginFeedbackController.php +++ b/classes/controllers/FrmPluginFeedbackController.php @@ -10,8 +10,7 @@ } /** - * Collects an NPS score and qualitative feedback from Lite users ~20 days after install, - * unless Formidable Pro is loaded (in which case Pro's own survey takes over). + * Collects an NPS score and feedback from Lite users after install. * * @since 6.26.1 */ @@ -34,38 +33,46 @@ class FrmPluginFeedbackController { /** * @var int */ - private static $user_id; + protected static $user_id; /** * @var array */ - private static $plugin_feedback; + protected static $plugin_feedback; /** * @var int */ - private static $current_year; + protected static $current_year; /** * @return void */ public static function load_admin_hooks() { - self::$user_id = get_current_user_id(); - - if ( ! self::should_show_plugin_feedback() ) { + if ( ! static::should_show_plugin_feedback() ) { return; } + $user_id = get_current_user_id(); + $class = get_called_class(); + add_filter( 'frm_should_show_floating_links', '__return_false' ); - add_action( 'admin_enqueue_scripts', self::class . '::enqueue_assets' ); - add_action( 'admin_footer', self::class . '::show_plugin_feedback', 1 ); + add_action( 'admin_enqueue_scripts', array( $class, 'enqueue_assets' ) ); + add_action( 'admin_footer', array( $class, 'show_plugin_feedback' ), 1 ); + } + + /** + * @return bool + */ + protected static function should_show_plugin_feedback() { + return static::passes_common_gates() && static::passes_product_specific_gates(); } /** * @return bool */ - private static function should_show_plugin_feedback() { - if ( ! self::$user_id ) { + protected static function passes_common_gates() { + if ( ! static::$user_id ) { return false; } @@ -73,11 +80,11 @@ private static function should_show_plugin_feedback() { return false; } - if ( self::is_local_environment() ) { + if ( static::is_local_environment() ) { return false; } - if ( FrmAppHelper::pro_is_included() ) { + if ( static::pro_is_blocking() ) { return false; } @@ -89,55 +96,85 @@ private static function should_show_plugin_feedback() { return false; } - if ( ! self::has_reached_install_age_threshold() ) { - return false; - } + $current = static::get_current_year_feedback(); + return ! empty( $current['submitted'] ) ? false : true; + } - $current = self::get_current_year_feedback(); - if ( ! empty( $current['submitted'] ) ) { - return false; - } + /** + * @return bool + */ + protected static function passes_product_specific_gates() { + return static::has_reached_install_age_threshold(); + } - return true; + /** + * @return bool + */ + protected static function pro_is_blocking() { + return FrmAppHelper::pro_is_included(); } /** * @return bool */ - private static function is_local_environment() { + protected static function is_local_environment() { return in_array( wp_get_environment_type(), array( 'local', 'development' ), true ); } /** * @return bool */ - private static function has_reached_install_age_threshold() { + protected static function has_reached_install_age_threshold() { $install_time = (int) get_option( 'frm_first_activation' ); if ( ! $install_time ) { return false; } $threshold_days = (int) apply_filters( 'frm_lite_plugin_feedback_threshold_days', 20 ); - return ( time() - $install_time ) >= ( $threshold_days * DAY_IN_SECONDS ); + return time() - $install_time >= $threshold_days * DAY_IN_SECONDS; + } + + /** + * @return array + */ + protected static function get_config() { + return array( + 'script' => array( + 'handle' => 'formidable-lite-plugin-feedback', + 'url' => FrmAppHelper::plugin_url() . '/js/plugin-feedback.js', + ), + 'style' => array( + 'handle' => 'formidable-lite-plugin-feedback', + 'url' => FrmAppHelper::plugin_url() . '/css/components/plugin-feedback.css', + ), + 'ajax' => array( + 'submit' => 'submit_lite_plugin_feedback', + 'dismiss' => 'dismiss_lite_plugin_feedback', + ), + 'remote' => 'https://formidableforms.com/wp-admin/admin-ajax.php?action=frm_forms_preview&form=plugin-feedback-lite', + 'remote_form_key' => 'plugin-feedback-lite', + ); } /** * @return void */ public static function enqueue_assets() { + $config = static::get_config(); $version = FrmAppHelper::plugin_version(); - wp_enqueue_script( 'formidable-lite-plugin-feedback', FrmAppHelper::plugin_url() . '/js/plugin-feedback.js', array( 'formidable_dom' ), $version, true ); - wp_enqueue_style( 'formidable-lite-plugin-feedback', FrmAppHelper::plugin_url() . '/css/components/plugin-feedback.css', array(), $version ); + wp_enqueue_script( $config['script']['handle'], $config['script']['url'], array( 'formidable_dom' ), $version, true ); + wp_enqueue_style( $config['style']['handle'], $config['style']['url'], array(), $version ); } /** * @return void */ public static function show_plugin_feedback() { - $current = self::get_current_year_feedback(); + $current = static::get_current_year_feedback(); $step = isset( $current['nps-score'] ) ? 'reasons' : 'nps'; - $reasons = self::get_reasons(); + $reasons = static::get_reasons(); + $config = static::get_config(); include FrmAppHelper::plugin_path() . '/classes/views/shared/plugin-feedback.php'; } @@ -149,14 +186,14 @@ public static function ajax_submit_plugin_feedback() { check_ajax_referer( 'frm_ajax', 'nonce' ); FrmAppHelper::permission_check( 'frm_change_settings' ); - if ( FrmAppHelper::pro_is_included() ) { + if ( static::pro_is_blocking() ) { wp_send_json_error( array( 'type' => 'pro-active' ) ); } - self::$user_id = get_current_user_id(); + static::$user_id = get_current_user_id(); - self::maybe_save_nps_and_send_response(); - self::submit_feedback_to_remote(); + static::maybe_save_nps_and_send_response(); + static::submit_feedback_to_remote(); } /** @@ -166,19 +203,19 @@ public static function ajax_dismiss_plugin_feedback() { check_ajax_referer( 'frm_ajax', 'nonce' ); FrmAppHelper::permission_check( 'frm_change_settings' ); - if ( FrmAppHelper::pro_is_included() ) { + if ( static::pro_is_blocking() ) { wp_send_json_error( array( 'type' => 'pro-active' ) ); } - self::$user_id = get_current_user_id(); + static::$user_id = get_current_user_id(); - self::submit_feedback_to_remote(); + static::submit_feedback_to_remote(); } /** * @return void */ - private static function maybe_save_nps_and_send_response() { + protected static function maybe_save_nps_and_send_response() { if ( ! isset( $_POST['nps-score'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing return; } @@ -189,26 +226,27 @@ private static function maybe_save_nps_and_send_response() { wp_send_json_error( array( 'type' => 'invalid-nps' ) ); } - self::set_current_year_feedback( 'nps-score', $nps_score ); + static::set_current_year_feedback( 'nps-score', $nps_score ); wp_send_json_success( array( 'message' => __( 'Feedback score saved successfully.', 'formidable' ) ) ); } /** * @return void */ - private static function submit_feedback_to_remote() { - $current = self::get_current_year_feedback(); + protected static function submit_feedback_to_remote() { + $current = static::get_current_year_feedback(); if ( ! isset( $current['nps-score'] ) ) { - self::set_current_year_feedback( 'submitted', true ); + static::set_current_year_feedback( 'submitted', true ); + static::after_submission_commit(); wp_send_json_success( array( 'message' => __( 'Feedback dismissed successfully.', 'formidable' ) ) ); } $remote_response = wp_remote_post( - 'https://formidableforms.com/wp-admin/admin-ajax.php?action=frm_forms_preview&form=plugin-feedback-lite', + static::get_config()['remote'], array( 'timeout' => 30, - 'body' => http_build_query( self::build_remote_body() ), + 'body' => http_build_query( static::build_remote_body() ), ) ); @@ -232,48 +270,93 @@ private static function submit_feedback_to_remote() { ); } - self::set_current_year_feedback( 'submitted', true ); + static::set_current_year_feedback( 'submitted', true ); + static::after_submission_commit(); wp_send_json_success( array( 'message' => __( 'Feedback submitted successfully.', 'formidable' ) ) ); } /** - * Builds the payload sent to formidableforms.com. form_id and item_meta keys - * are placeholders until Marketing finalizes the destination form; swapping them - * should be a small edit here. - * * @return array */ - private static function build_remote_body() { - $feedback = self::get_current_year_feedback(); - $nps = isset( $feedback['nps-score'] ) ? $feedback['nps-score'] : ''; - + protected static function field_map() { return array( - 'l' => base64_encode( (string) get_option( 'frm-usage-uuid' ) ), - 'form_key' => 'plugin-feedback-lite', - 'frm_action' => 'create', - 'form_id' => 0, - 'item_key' => '', - 'item_meta[0]' => '', - 'item_meta[NPS]' => $nps, - 'item_meta[RSN]' => self::format_reasons_list( self::get_posted_reasons() ), - 'item_meta[DTL]' => FrmAppHelper::get_post_param( 'details', '' ), - 'item_meta[URL]' => site_url(), - 'item_meta[SRC]' => self::SOURCE, - 'item_meta[VER]' => FrmAppHelper::plugin_version(), + 'nps' => 'NPS', + 'reasons' => 'RSN', + 'details' => 'DTL', + 'url' => 'URL', + 'source' => 'SRC', + 'version' => 'VER', ); } /** * @return array */ - private static function get_posted_reasons() { + protected static function build_remote_body() { + $map = static::field_map(); + $config = static::get_config(); + $feedback = static::get_current_year_feedback(); + + $body = array( + 'l' => base64_encode( (string) static::get_remote_identifier() ), + 'form_key' => isset( $config['remote_form_key'] ) ? $config['remote_form_key'] : '', + 'frm_action' => 'create', + 'form_id' => static::get_remote_form_id(), + 'item_key' => '', + 'item_meta[0]' => '', + ); + + $values = array( + 'nps' => isset( $feedback['nps-score'] ) ? $feedback['nps-score'] : '', + 'reasons' => static::format_reasons_list( static::get_posted_reasons() ), + 'details' => FrmAppHelper::get_post_param( 'details', '' ), + 'url' => site_url(), + 'source' => static::SOURCE, + 'version' => FrmAppHelper::plugin_version(), + ); + + foreach ( $values as $key => $value ) { + if ( ! isset( $map[ $key ] ) ) { + continue; + } + + $body[ 'item_meta[' . $map[ $key ] . ']' ] = $value; + } + + return $body; + } + + /** + * @return string + */ + protected static function get_remote_identifier() { + return (string) get_option( 'frm-usage-uuid' ); + } + + /** + * @return int + */ + protected static function get_remote_form_id() { + return 0; + } + + /** + * @return void + */ + protected static function after_submission_commit() { + } + + /** + * @return array + */ + protected static function get_posted_reasons() { $reasons = json_decode( FrmAppHelper::get_post_param( 'reasons', '[]' ), true ); $reasons = rest_sanitize_value_from_schema( $reasons, array( 'type' => 'array', 'items' => array( - 'enum' => array_keys( self::get_reasons() ), + 'enum' => array_keys( static::get_reasons() ), 'type' => 'string', ), ) @@ -290,12 +373,12 @@ private static function get_posted_reasons() { * @param array $reason_keys * @return string */ - private static function format_reasons_list( $reason_keys ) { + protected static function format_reasons_list( $reason_keys ) { if ( ! $reason_keys ) { return ''; } - $reasons = self::get_reasons(); + $reasons = static::get_reasons(); $formatted_reasons = array_map( static function ( $key ) use ( $reasons ) { return '- ' . $reasons[ $key ]; @@ -309,68 +392,67 @@ static function ( $key ) use ( $reasons ) { /** * @return array */ - private static function get_plugin_feedback() { - if ( self::$plugin_feedback ) { - return self::$plugin_feedback; + protected static function get_plugin_feedback() { + if ( static::$plugin_feedback ) { + return static::$plugin_feedback; } - self::$plugin_feedback = get_user_meta( self::$user_id, self::PLUGIN_FEEDBACK_META_KEY, true ); + static::$plugin_feedback = get_user_meta( static::$user_id, static::PLUGIN_FEEDBACK_META_KEY, true ); - if ( ! is_array( self::$plugin_feedback ) ) { - self::$plugin_feedback = array( - self::get_current_year() => array( + if ( ! is_array( static::$plugin_feedback ) ) { + static::$plugin_feedback = array( + static::get_current_year() => array( 'submitted' => false, - 'source' => self::SOURCE, + 'source' => static::SOURCE, ), ); - } elseif ( ! isset( self::$plugin_feedback[ self::get_current_year() ] ) ) { - self::$plugin_feedback[ self::get_current_year() ] = array( + } elseif ( ! isset( static::$plugin_feedback[ static::get_current_year() ] ) ) { + static::$plugin_feedback[ static::get_current_year() ] = array( 'submitted' => false, - 'source' => self::SOURCE, + 'source' => static::SOURCE, ); } - return self::$plugin_feedback; + return static::$plugin_feedback; } /** * @return array */ - private static function get_current_year_feedback() { - return self::get_plugin_feedback()[ self::get_current_year() ]; + protected static function get_current_year_feedback() { + return static::get_plugin_feedback()[ static::get_current_year() ]; } /** * @param string $key * @param mixed $value - * * @return void */ - private static function set_current_year_feedback( $key, $value ) { - self::get_plugin_feedback(); - self::$plugin_feedback[ self::get_current_year() ][ $key ] = $value; - self::$plugin_feedback[ self::get_current_year() ]['source'] = self::SOURCE; - update_user_meta( self::$user_id, self::PLUGIN_FEEDBACK_META_KEY, self::$plugin_feedback ); + protected static function set_current_year_feedback( $key, $value ) { + static::get_plugin_feedback(); + static::$plugin_feedback[ static::get_current_year() ][ $key ] = $value; + static::$plugin_feedback[ static::get_current_year() ]['source'] = static::SOURCE; + update_user_meta( static::$user_id, static::PLUGIN_FEEDBACK_META_KEY, static::$plugin_feedback ); } /** * @return int */ - private static function get_current_year() { - if ( self::$current_year ) { - return self::$current_year; + protected static function get_current_year() { + if ( static::$current_year ) { + return static::$current_year; } - self::$current_year = (int) wp_date( 'Y' ); - return self::$current_year; + static::$current_year = (int) wp_date( 'Y' ); + return static::$current_year; } /** - * English-only — sent to a remote service, so intentionally not translatable. + * Not translatable: sent to a remote service. * * @return array */ - private static function get_reasons() { + protected static function get_reasons() { return array( 'pricing' => 'Pricing and plans', 'form-builder' => 'Form builder flexibility', From bfeb1effc328551e9ea46f6123a49a3db99ea1d6 Mon Sep 17 00:00:00 2001 From: Sherv Date: Tue, 28 Apr 2026 18:52:14 +0300 Subject: [PATCH 12/13] Enhance plugin feedback component by adding data attributes for submit and dismiss actions to improve AJAX functionality. --- classes/views/shared/plugin-feedback.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/views/shared/plugin-feedback.php b/classes/views/shared/plugin-feedback.php index c59a871268..11a681272b 100644 --- a/classes/views/shared/plugin-feedback.php +++ b/classes/views/shared/plugin-feedback.php @@ -9,7 +9,7 @@ die( 'You are not allowed to call this page directly.' ); } ?> -
+
From 47372a0dbc9dae558fc2f9c9cca733818fe002dc Mon Sep 17 00:00:00 2001 From: Sherv Date: Tue, 28 Apr 2026 18:52:54 +0300 Subject: [PATCH 13/13] Update submitFeedbackEvents.js to utilize dynamic data attributes for submit and dismiss actions, enhancing AJAX request handling. --- js/src/plugin-feedback/submitFeedbackEvents.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/src/plugin-feedback/submitFeedbackEvents.js b/js/src/plugin-feedback/submitFeedbackEvents.js index c4096de2b8..15c5c68e84 100644 --- a/js/src/plugin-feedback/submitFeedbackEvents.js +++ b/js/src/plugin-feedback/submitFeedbackEvents.js @@ -16,6 +16,9 @@ const npsStep = document.getElementById( `${ CLASS_PREFIX }-nps-step` ); const reasonsStep = document.getElementById( `${ CLASS_PREFIX }-reasons-step` ); const thankYouStep = document.getElementById( `${ CLASS_PREFIX }-thank-you-step` ); +const submitAction = pluginFeedback?.dataset.submitAction; +const dismissAction = pluginFeedback?.dataset.dismissAction; + /** * Adds event listeners for submitting plugin feedback. * @@ -56,7 +59,7 @@ async function onSubmitFeedback( event ) { } try { - await doJsonPost( 'submit_lite_plugin_feedback', formData ); + await doJsonPost( submitAction, formData ); } catch ( error ) { showError( error.type ); if ( error.message ) { @@ -111,7 +114,7 @@ async function onDismissFeedback( event ) { formData.append( 'dismissed', true ); try { - await doJsonPost( 'dismiss_lite_plugin_feedback', formData ); + await doJsonPost( dismissAction, formData ); } catch ( error ) { if ( error.message ) { console.error( 'Feedback submission error:', error.message );