From f5bff59809aefdfbaf92be6296c1f12c4969c965 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 14:40:43 -0300 Subject: [PATCH 01/13] Cross sell --- classes/controllers/FrmHooksController.php | 3 + classes/models/FrmSalesApi.php | 67 +++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/classes/controllers/FrmHooksController.php b/classes/controllers/FrmHooksController.php index 3dd23afb22..b8c647f844 100644 --- a/classes/controllers/FrmHooksController.php +++ b/classes/controllers/FrmHooksController.php @@ -199,6 +199,9 @@ public static function load_admin_hooks() { // Cronjob. add_action( 'admin_init', 'FrmCronController::schedule_events' ); + // Cross sell. + add_action( 'admin_menu', 'FrmSalesApi::menu', 1000 ); + // Deactivation feedback. add_action( 'admin_enqueue_scripts', 'FrmDeactivationFeedbackController::enqueue_assets' ); add_action( 'admin_footer', 'FrmDeactivationFeedbackController::footer_html' ); diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 686d94924e..7f4c710044 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -29,6 +29,20 @@ class FrmSalesApi extends FrmFormApi { */ private static $best_sale; + /** + * @since x.x + * + * @var string|null + */ + private static $cross_sell_text; + + /** + * @since x.x + * + * @var string|null + */ + private static $cross_sell_link; + public function __construct() { $this->set_cache_key(); @@ -52,7 +66,8 @@ protected function set_cache_key() { * @return string */ protected function api_url() { - return 'https://plapi.formidableforms.com/sales/'; + return 'https://dev-site.local/wp-json/s11-sales/v1/list/'; + // return 'https://plapi.formidableforms.com/sales/'; } /** @@ -70,6 +85,26 @@ private function set_sales() { foreach ( $api as $sale ) { $this->add_sale( $sale ); + + if ( is_array( $sale ) && isset( $sale['cross_sell_text'] ) ) { + self::set_cross_sale( $sale ); + } + } + } + + /** + * @since x.x + * + * @param array $data + * @return void + */ + private function set_cross_sale( $data ) { + if ( ! empty( $data['cross_sell_text'] ) ) { + self::$cross_sell_text = $data['cross_sell_text']; + } + + if ( ! empty( $data['cross_sell_link'] ) ) { + self::$cross_sell_link = $data['cross_sell_link']; } } @@ -383,4 +418,34 @@ private static function is_banner_dismissed( $key ) { $dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() ); return is_array( $dismissed_sales ) && in_array( $key, $dismissed_sales, true ); } + + public static function menu() { + if ( false === self::$sales ) { + new self(); + } + + if ( ! self::$cross_sell_text || ! self::$cross_sell_link ) { + return; + } + + add_submenu_page( + 'formidable', + self::$cross_sell_text . ' | Formidable', + self::$cross_sell_text, + 'activate_plugins', + 'frm-sales-api-cross-sell', + function() { + // There is no page. The redirect logic is handled below, before this callback is triggered. + } + ); + + add_action( + 'admin_init', + function() { + if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { + wp_redirect( self::$cross_sell_link ); + } + } + ); + } } From acde4a773cbfe6cb658e52fa207ea8e326b04595 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 14:41:59 -0300 Subject: [PATCH 02/13] Go back to the live url --- classes/models/FrmSalesApi.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 7f4c710044..378e0c8242 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -66,8 +66,7 @@ protected function set_cache_key() { * @return string */ protected function api_url() { - return 'https://dev-site.local/wp-json/s11-sales/v1/list/'; - // return 'https://plapi.formidableforms.com/sales/'; + return 'https://plapi.formidableforms.com/sales/'; } /** From 8d170f80d57b41e94fcf9936e1cf567775bac280 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 14:44:53 -0300 Subject: [PATCH 03/13] phpcs fixes --- classes/models/FrmSalesApi.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 378e0c8242..415460d0f4 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -433,16 +433,17 @@ public static function menu() { self::$cross_sell_text, 'activate_plugins', 'frm-sales-api-cross-sell', - function() { + function () { // There is no page. The redirect logic is handled below, before this callback is triggered. } ); add_action( 'admin_init', - function() { + function () { if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { wp_redirect( self::$cross_sell_link ); + exit; } } ); From 152d8c39403d93505edaf075bbe4aaac2051789d Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:01:19 -0300 Subject: [PATCH 04/13] Add another cypress string --- tests/cypress/e2e/Forms/formPageDataValidation.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cypress/e2e/Forms/formPageDataValidation.cy.js b/tests/cypress/e2e/Forms/formPageDataValidation.cy.js index 5d70a26f85..ae5de81df0 100644 --- a/tests/cypress/e2e/Forms/formPageDataValidation.cy.js +++ b/tests/cypress/e2e/Forms/formPageDataValidation.cy.js @@ -30,6 +30,7 @@ describe( 'Forms page', () => { 'more than just a wordpress form builder', 'get more done in less time with better wordpress forms', 'power your wordpress site like never before', + 'build powerful forms, web apps, dashboards, and more', ] ).to.include( headingText ); } ); } ); From a779d4dbd6ee482d094c52359d4fdcf7072288bb Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:07:41 -0300 Subject: [PATCH 05/13] Add permission check to redirect --- classes/models/FrmSalesApi.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 415460d0f4..14345845f6 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -441,6 +441,10 @@ function () { add_action( 'admin_init', function () { + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { wp_redirect( self::$cross_sell_link ); exit; From af78f0d5731c2818ea132b2609c3805d24a56ca4 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:08:45 -0300 Subject: [PATCH 06/13] Use esc_html --- classes/models/FrmSalesApi.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 14345845f6..d3f50d1ef4 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -429,8 +429,8 @@ public static function menu() { add_submenu_page( 'formidable', - self::$cross_sell_text . ' | Formidable', - self::$cross_sell_text, + esc_html( self::$cross_sell_text ) . ' | Formidable', + esc_html( self::$cross_sell_text ), 'activate_plugins', 'frm-sales-api-cross-sell', function () { From 3912d8271a348a8197298bf07d9e83fc8f190b90 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:09:15 -0300 Subject: [PATCH 07/13] Esc url --- classes/models/FrmSalesApi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index d3f50d1ef4..318e8a6e69 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -446,7 +446,7 @@ function () { } if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { - wp_redirect( self::$cross_sell_link ); + wp_redirect( esc_url( self::$cross_sell_link ) ); exit; } } From 927a8781928f937b676763a0a6f8b973998e7f2a Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:09:43 -0300 Subject: [PATCH 08/13] Use esc_url_raw --- classes/models/FrmSalesApi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 318e8a6e69..420c8852fc 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -446,7 +446,7 @@ function () { } if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { - wp_redirect( esc_url( self::$cross_sell_link ) ); + wp_redirect( esc_url_raw( self::$cross_sell_link ) ); exit; } } From 2040b5eae28055ac52947c672c83073e945643f5 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:11:04 -0300 Subject: [PATCH 09/13] Use sanitize --- classes/models/FrmSalesApi.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 420c8852fc..aed4cdd80a 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -99,11 +99,11 @@ private function set_sales() { */ private function set_cross_sale( $data ) { if ( ! empty( $data['cross_sell_text'] ) ) { - self::$cross_sell_text = $data['cross_sell_text']; + self::$cross_sell_text = sanitize_text_field( $data['cross_sell_text'] ); } if ( ! empty( $data['cross_sell_link'] ) ) { - self::$cross_sell_link = $data['cross_sell_link']; + self::$cross_sell_link = esc_url_raw( $data['cross_sell_link'] ); } } @@ -446,7 +446,7 @@ function () { } if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) { - wp_redirect( esc_url_raw( self::$cross_sell_link ) ); + wp_redirect( self::$cross_sell_link ); exit; } } From 5a1701d6613ccd9089242f5e465fefbf65631cce Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Fri, 10 Oct 2025 15:11:35 -0300 Subject: [PATCH 10/13] Use this --- classes/models/FrmSalesApi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index aed4cdd80a..561867a99d 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -86,7 +86,7 @@ private function set_sales() { $this->add_sale( $sale ); if ( is_array( $sale ) && isset( $sale['cross_sell_text'] ) ) { - self::set_cross_sale( $sale ); + $this->set_cross_sale( $sale ); } } } From e940ec5a6f41d90294fab739b6f4f44e63965ebc Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 15 Oct 2025 13:36:59 -0300 Subject: [PATCH 11/13] Update cross sell to check for an array --- classes/models/FrmSalesApi.php | 53 +++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 561867a99d..85a4faebdc 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -66,7 +66,8 @@ protected function set_cache_key() { * @return string */ protected function api_url() { - return 'https://plapi.formidableforms.com/sales/'; + return 'https://dev-site.local/wp-json/s11-sales/v1/list/'; + // return 'https://plapi.formidableforms.com/sales/'; } /** @@ -86,25 +87,63 @@ private function set_sales() { $this->add_sale( $sale ); if ( is_array( $sale ) && isset( $sale['cross_sell_text'] ) ) { - $this->set_cross_sale( $sale ); + $this->set_cross_sell( $sale ); } } } /** + * Check for a special array in the sales API array. + * Normally cross_sell_text and cross_sell_link are not set. + * But one array, that isn't actually a sale, contains cross sell data. + * This should be near the end of the array. + * * @since x.x * * @param array $data * @return void */ - private function set_cross_sale( $data ) { - if ( ! empty( $data['cross_sell_text'] ) ) { - self::$cross_sell_text = sanitize_text_field( $data['cross_sell_text'] ); + private function set_cross_sell( $data ) { + if ( ! self::cross_sell_is_valid( $data ) ) { + return; + } + + $cross_sell_text = $data['cross_sell_text']; + $cross_sell_links = $data['cross_sell_link']; + $index = self::determine_cross_sell_index( $cross_sell_text ); + + self::$cross_sell_text = sanitize_text_field( $cross_sell_text[ $index ] ); + self::$cross_sell_link = esc_url_raw( $cross_sell_links[ $index ] ); + } + + /** + * Check that both cross_sell_text and cross_sell_link are set and are arrays of the same size. + * + * @since x.x + * + * @param array $data + * @return bool + */ + private function cross_sell_is_valid( $data ) { + if ( empty( $data['cross_sell_text'] ) || empty( $data['cross_sell_link'] ) ) { + return false; } - if ( ! empty( $data['cross_sell_link'] ) ) { - self::$cross_sell_link = esc_url_raw( $data['cross_sell_link'] ); + if ( ! is_array( $data['cross_sell_text'] ) || ! is_array( $data['cross_sell_link'] ) ) { + return false; } + + return count( $data['cross_sell_link'] ) === count( $data['cross_sell_text'] ); + } + + /** + * @param array $cross_sell_text + * + * @return int + */ + private static function determine_cross_sell_index( $cross_sell_text ) { + // TODO + return 0; } /** From 58d5c6c29ec645dc626ba257f9894f0f689e89b4 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 15 Oct 2025 13:57:46 -0300 Subject: [PATCH 12/13] Determine cross sell index --- classes/models/FrmSalesApi.php | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 85a4faebdc..52d8502167 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -137,12 +137,41 @@ private function cross_sell_is_valid( $data ) { } /** - * @param array $cross_sell_text + * Determine which cross sell text to use. + * These are shown in order for 30 days before moving on to the next one. + * + * @since x.x * + * @param array $cross_sell_text * @return int */ private static function determine_cross_sell_index( $cross_sell_text ) { - // TODO + $option_name = 'frm_cross_sell_settings'; + $cross_sell_settings = get_option( $option_name ); + + if ( ! is_array( $cross_sell_settings ) ) { + $cross_sell_settings = array( + reset( $cross_sell_text ) => time(), + ); + update_option( $option_name, $cross_sell_settings ); + return 0; + } + + foreach ( $cross_sell_text as $index => $current_text ) { + if ( ! isset( $cross_sell_settings[ $current_text ] ) ) { + $cross_sell_settings[ $current_text ] = time(); + update_option( $option_name, $cross_sell_settings ); + return $index; + } + + $time_elapsed = time() - $cross_sell_settings[ $current_text ]; + if ( $time_elapsed < DAY_IN_SECONDS * 30 ) { + return $index; + } + } + + // If all options are expired, reset the option. + delete_option( $option_name ); return 0; } From ade78d92839010209d681e849939df717400957e Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Wed, 15 Oct 2025 14:01:12 -0300 Subject: [PATCH 13/13] Put back the API url --- classes/models/FrmSalesApi.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/classes/models/FrmSalesApi.php b/classes/models/FrmSalesApi.php index 52d8502167..2b5d539c5d 100644 --- a/classes/models/FrmSalesApi.php +++ b/classes/models/FrmSalesApi.php @@ -66,8 +66,7 @@ protected function set_cache_key() { * @return string */ protected function api_url() { - return 'https://dev-site.local/wp-json/s11-sales/v1/list/'; - // return 'https://plapi.formidableforms.com/sales/'; + return 'https://plapi.formidableforms.com/sales/'; } /**