diff --git a/.stylelintrc.json b/.stylelintrc.json index 0a295e6a78..443983e4c1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -6,6 +6,7 @@ "**/web-components/**", "**/frm_admin.css", "**/frm-settings-components.css", + "**/frm-legacy-paypal.css", "**/font_icons.css", "**/frm_testing_mode.css", "**/welcome-tour.css", diff --git a/.windsurf/rules/formidable/frm-css.md b/.windsurf/rules/formidable/frm-css.md index 503c8ef4e9..1872a2cc0d 100644 --- a/.windsurf/rules/formidable/frm-css.md +++ b/.windsurf/rules/formidable/frm-css.md @@ -1,5 +1,5 @@ --- -trigger: "glob" +trigger: glob globs: ["**/*.css", "**/*.scss", "**/*.less"] description: "WordPress CSS coding standards with Formidable Forms patterns. Auto-applies when working with CSS files." --- diff --git a/classes/controllers/FrmAddonsController.php b/classes/controllers/FrmAddonsController.php index e602706ed8..74dde99a27 100644 --- a/classes/controllers/FrmAddonsController.php +++ b/classes/controllers/FrmAddonsController.php @@ -157,7 +157,8 @@ public static function list_addons() { 'title' => 'Formidable Forms Pro', 'slug' => 'formidable-pro', 'released' => '2011-02-05', - 'docs' => 'knowledgebase/', + 'docs' => 'knowledgebase/what-is-the-difference-between-the-lite-free-and-pro-version/', + 'docs_label' => __( 'Why Upgrade', 'formidable' ), 'categories' => array( 'basic', 'plus', 'business', 'elite' ), 'excerpt' => 'Create calculators, surveys, smart forms, and data-driven applications. Build directories, real estate listings, job boards, and much more.', ), diff --git a/classes/controllers/FrmAppController.php b/classes/controllers/FrmAppController.php index 551527a7e8..d2b0d7bced 100644 --- a/classes/controllers/FrmAppController.php +++ b/classes/controllers/FrmAppController.php @@ -777,6 +777,7 @@ public static function admin_js() { } do_action( 'frm_enqueue_builder_scripts' ); + self::maybe_enqueue_legacy_paypal_assets(); self::include_upgrade_overlay(); self::include_info_overlay(); } elseif ( FrmAppHelper::is_view_builder_page() ) { @@ -936,6 +937,27 @@ public static function admin_enqueue_scripts() { FrmUsageController::load_scripts(); } + /** + * Enqueue legacy PayPal action settings styles when the PayPal add-on is active. + * + * @since x.x + * + * @return void + */ + private static function maybe_enqueue_legacy_paypal_assets() { + if ( ! FrmAppHelper::is_form_builder_page() || ! class_exists( 'FrmPaymentsController' ) ) { + return; + } + + wp_register_style( + 'formidable-legacy-paypal', + FrmAppHelper::plugin_url() . '/css/admin/frm-legacy-paypal.css', + array( 'formidable-admin' ), + FrmAppHelper::plugin_version() + ); + wp_enqueue_style( 'formidable-legacy-paypal' ); + } + /** * Enqueue required assets for the Legacy Views editor (Views v4.x). * diff --git a/classes/controllers/FrmFormActionsController.php b/classes/controllers/FrmFormActionsController.php index 05da258b6c..fb4e692ba3 100644 --- a/classes/controllers/FrmFormActionsController.php +++ b/classes/controllers/FrmFormActionsController.php @@ -38,6 +38,7 @@ public static function register_post_types() { ) ); + self::maybe_setup_unlicensed_action_gate(); self::actions_init(); } @@ -56,25 +57,25 @@ public static function register_actions() { 'email' => 'FrmEmailAction', 'wppost' => 'FrmDefPostAction', 'register' => 'FrmDefRegAction', - 'paypal' => 'FrmDefPayPalAction', - 'payment' => 'FrmTransLiteAction', 'quiz' => 'FrmDefQuizAction', 'quiz_outcome' => 'FrmDefQuizOutcomeAction', - 'mailchimp' => 'FrmDefMlcmpAction', + 'paypal' => 'FrmDefPayPalAction', + 'payment' => 'FrmTransLiteAction', 'api' => 'FrmDefApiAction', - 'salesforce' => 'FrmDefSalesforceAction', + 'mailchimp' => 'FrmDefMlcmpAction', 'activecampaign' => 'FrmDefActiveCampaignAction', 'constantcontact' => 'FrmDefConstContactAction', 'getresponse' => 'FrmDefGetResponseAction', - 'hubspot' => 'FrmDefHubspotAction', - 'zapier' => 'FrmDefZapierAction', - 'n8n' => 'FrmDefN8NAction', - 'twilio' => 'FrmDefTwilioAction', - 'highrise' => 'FrmDefHighriseAction', 'mailpoet' => 'FrmDefMailpoetAction', - 'aweber' => 'FrmDefAweberAction', 'convertkit' => 'FrmDefConvertKitAction', + 'aweber' => 'FrmDefAweberAction', + 'twilio' => 'FrmDefTwilioAction', + 'salesforce' => 'FrmDefSalesforceAction', + 'hubspot' => 'FrmDefHubspotAction', + 'highrise' => 'FrmDefHighriseAction', + 'zapier' => 'FrmDefZapierAction', 'googlespreadsheet' => 'FrmDefGoogleSpreadsheetAction', + 'n8n' => 'FrmDefN8NAction', ); $action_classes = apply_filters( 'frm_registered_form_actions', $action_classes ); @@ -86,6 +87,50 @@ public static function register_actions() { foreach ( $action_classes as $action_class ) { self::$registered_actions->register( $action_class ); } + + self::apply_default_action_descriptions(); + } + + /** + * Sets default descriptions on registered actions from a central list. + * + * Keeps the description when an add-on replaces a base action class without its own. + * + * @since x.x + * + * @return void + */ + private static function apply_default_action_descriptions() { + $descriptions = array( + 'on_submit' => __( 'Success messages', 'formidable' ), + 'email' => __( 'Autoresponder alerts', 'formidable' ), + 'wppost' => __( 'Content publishing', 'formidable' ), + 'register' => __( 'Account creation', 'formidable' ), + 'payment' => __( 'Transaction alerts', 'formidable' ), + 'paypal' => __( 'Payment gateway', 'formidable' ), + 'quiz' => __( 'Automated grading', 'formidable' ), + 'quiz_outcome' => __( 'Result logic', 'formidable' ), + 'aweber' => __( 'List triggers', 'formidable' ), + 'mailchimp' => __( 'Subscription confirmation', 'formidable' ), + 'zapier' => __( 'App automation', 'formidable' ), + 'n8n' => __( 'Workflow automation', 'formidable' ), + 'twilio' => __( 'Text notifications', 'formidable' ), + 'activecampaign' => __( 'Contact automation', 'formidable' ), + 'salesforce' => __( 'Lead automation', 'formidable' ), + 'constantcontact' => __( 'Content distribution', 'formidable' ), + 'getresponse' => __( 'Success notifications', 'formidable' ), + 'hubspot' => __( 'CRM alerts', 'formidable' ), + 'mailpoet' => __( 'Plugin automation', 'formidable' ), + 'api' => __( 'System integration', 'formidable' ), + 'googlespreadsheet' => __( 'Spreadsheet sync', 'formidable' ), + 'convertkit' => __( 'Broadcast publishing', 'formidable' ), + ); + + foreach ( self::$registered_actions->actions as $action ) { + if ( $action->action_options['description'] === '' && isset( $descriptions[ $action->id_base ] ) ) { + $action->action_options['description'] = $descriptions[ $action->id_base ]; + } + } } /** @@ -170,16 +215,19 @@ public static function form_action_groups() { 'name' => '', 'icon' => 'frmfont frm_shuffle_icon', 'actions' => array( + 'on_submit', 'email', 'wppost', 'register', 'quiz', 'quiz_outcome', - 'twilio', + 'api', + 'googlespreadsheet', + 'n8n', ), ), 'payment' => array( - 'name' => __( 'eCommerce', 'formidable' ), + 'name' => __( 'E-Commerce', 'formidable' ), 'icon' => 'frmfont frm_credit_card_alt_icon', 'actions' => array( 'paypal', @@ -187,7 +235,7 @@ public static function form_action_groups() { ), ), 'marketing' => array( - 'name' => __( 'Email Marketing', 'formidable' ), + 'name' => __( 'Marketing', 'formidable' ), 'icon' => 'frmfont frm_mail_bulk_icon', 'actions' => array( 'mailchimp', @@ -197,6 +245,7 @@ public static function form_action_groups() { 'aweber', 'mailpoet', 'convertkit', + 'twilio', ), ), 'crm' => array( @@ -220,6 +269,7 @@ private static function get_crm_actions() { $crm_actions = array( 'salesforce', 'hubspot', + 'zapier', ); // Only include Highrise when the add-on is active. @@ -300,18 +350,40 @@ public static function show_action_icon_link( $action_control, $allowed ) { if ( $requires && 'free' !== $requires ) { $data['data-requires'] = $requires; } + + $learn_more_slug = ! empty( $action_control->action_options['learn-more'] ) + ? $action_control->action_options['learn-more'] + : self::get_learn_more_slug( $action_control->id_base ); + + if ( $learn_more_slug ) { + $data['data-learn-more'] = FrmAppHelper::get_doc_url( + $learn_more_slug, + 'settings-' . $action_control->id_base, + ! str_contains( $learn_more_slug, '/' ) + ); + } }//end if - // HTML to include on the icon. - $icon_atts = array(); + include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_icon.php'; + } - if ( $action_control->action_options['color'] !== 'var(--primary-700)' ) { - $icon_atts = array( + /** + * Get the HTML attributes for the action icon. + * + * @since x.x + * + * @param object $action_control + * + * @return array + */ + public static function get_action_icon_atts( $action_control ) { + if ( 'var(--primary-700)' !== $action_control->action_options['color'] ) { + return array( 'style' => '--primary-700:' . $action_control->action_options['color'], ); } - include FrmAppHelper::plugin_path() . '/classes/views/frm-form-actions/_action_icon.php'; + return array(); } /** @@ -380,6 +452,10 @@ public static function list_actions( $form, $values ) { self::maybe_show_limit_warning( $form->id, $form_actions ); + echo '

' + . esc_html__( 'No actions have been added yet. Select an action above to get started.', 'formidable' ) + . '

'; + foreach ( $form_actions as $action ) { if ( ! isset( $action_map[ $action->post_excerpt ] ) ) { // Don't try and show settings if action no longer exists @@ -448,10 +524,16 @@ public static function add_form_action() { FrmAppHelper::permission_check( 'frm_edit_forms' ); check_ajax_referer( 'frm_ajax', 'nonce' ); + $action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' ); + $lite_actions = array_fill_keys( self::get_lite_actions(), true ); + + if ( ! FrmAppHelper::pro_is_connected() && ! isset( $lite_actions[ $action_type ] ) ) { + wp_die(); + } + global $frm_vars; - $action_key = FrmAppHelper::get_param( 'list_id', '', 'post', 'absint' ); - $action_type = FrmAppHelper::get_param( 'type', '', 'post', 'sanitize_text_field' ); + $action_key = FrmAppHelper::get_param( 'list_id', '', 'post', 'absint' ); /** * @var FrmFormAction @@ -459,8 +541,14 @@ public static function add_form_action() { $action_control = self::get_form_actions( $action_type ); $action_control->_set( $action_key ); - $form_id = FrmAppHelper::get_param( 'form_id', '', 'post', 'absint' ); - $form_action = $action_control->prepare_new( $form_id ); + $form_id = FrmAppHelper::get_param( 'form_id', '', 'post', 'absint' ); + $form_action = $action_control->prepare_new( $form_id ); + $existing_titles = (array) FrmAppHelper::get_post_param( 'existing_titles', array(), 'sanitize_text_field' ); + + if ( $existing_titles ) { + $form_action->post_title = self::get_unique_action_title( $form_action->post_title, $existing_titles ); + } + $use_logging = self::should_show_log_message( $action_type ); $values = array(); $form = self::fields_to_values( $form_id, $values ); @@ -469,6 +557,27 @@ public static function add_form_action() { wp_die(); } + /** + * Returns the first available title not in $existing_titles, appending " (2)", " (3)", etc. if needed. + * + * @since x.x + * + * @param string $base_title Default action title from the action type. + * @param string[] $existing_titles Titles currently visible in the form editor. + * + * @return string + */ + private static function get_unique_action_title( $base_title, array $existing_titles ) { + $taken = array_flip( $existing_titles ); + $title = $base_title; + + for ( $n = 2; isset( $taken[ $title ] ); $n++ ) { + $title = $base_title . ' (' . $n . ')'; + } + + return $title; + } + public static function fill_action() { FrmAppHelper::permission_check( 'frm_edit_forms' ); check_ajax_referer( 'frm_ajax', 'nonce' ); @@ -801,6 +910,121 @@ public static function limit_by_type( $where ) { public static function prevent_wpml_translations( $null, $post_type ) { return self::$action_post_type === $post_type ? false : $null; } + + /** + * If Pro is not connected, hook a filter that will force all non-Lite + * actions to inactive so the upgrade popup is shown instead. + * + * @since x.x + * + * @return void + */ + private static function maybe_setup_unlicensed_action_gate() { + if ( FrmAppHelper::pro_is_connected() ) { + return; + } + + add_filter( 'frm_registered_form_actions', array( self::class, 'disable_unlicensed_actions' ), 100 ); + } + + /** + * For every registered action that is not a Lite action, add a per-action + * options filter that forces it to inactive with the upgrade class. + * + * Runs inside apply_filters('frm_registered_form_actions') at priority 100, + * so the per-key option filters are in place before the class constructors + * run in the foreach loop that follows. + * + * @since x.x + * + * @param array $actions Map of action_key => class_name. + * + * @return array + */ + public static function disable_unlicensed_actions( $actions ) { + $lite_actions = array_fill_keys( self::get_lite_actions(), true ); + + foreach ( array_keys( $actions ) as $key ) { + if ( isset( $lite_actions[ $key ] ) ) { + continue; + } + + add_filter( + 'frm_' . $key . '_action_options', + function ( $options ) { + $options['active'] = false; + + if ( ! str_contains( $options['classes'], 'frm_show_upgrade' ) ) { + $options['classes'] .= ' frm_show_upgrade'; + } + + return $options; + } + ); + }//end foreach + + return $actions; + } + + /** + * Get action keys that are available in Lite without a Pro license. + * + * @since x.x + * + * @return string[] + */ + public static function get_lite_actions() { + return apply_filters( 'frm_lite_form_actions', array( 'on_submit', 'email', 'payment' ) ); + } + + /** + * Single source of truth for learn-more URL slugs used in + * upgrade modals for non-Lite form actions. + * + * Slugs without '/' are KB doc slugs (knowledgebase/ prefix is added). + * Slugs with '/' are direct paths (e.g. features/) used as-is. + * + * @since x.x + * + * @return array Map of action_key => URL slug. + */ + public static function get_action_learn_more_links() { + return array( + 'wppost' => 'features/user-submitted-posts-wordpress-forms', + 'register' => 'user-registration', + 'paypal' => 'features/paypal-wordpress-payments', + 'quiz' => 'quiz-maker-forms', + 'quiz_outcome' => 'quiz-maker-forms', + 'aweber' => 'features/aweber-addon', + 'mailchimp' => 'features/mailchimp-addon', + 'zapier' => 'features/form-entry-routing-with-zapier', + 'twilio' => 'features/twilio-sms-form-notifications', + 'activecampaign' => 'features/entries-to-activecampaign', + 'salesforce' => 'features/form-entries-to-salesforce', + 'constantcontact' => 'features/entries-to-constant-contact', + 'getresponse' => 'features/form-entries-to-getresponse', + 'hubspot' => 'features/form-entries-to-hubspot', + 'mailpoet' => 'features/mailpoet-newsletters-addon', + 'api' => 'features/wordpress-form-api', + 'googlespreadsheet' => 'features/google-sheets', + 'n8n' => 'features/connect-your-forms-to-any-app-with-n8n', + 'convertkit' => 'features/convertkit', + ); + } + + /** + * Look up the learn-more doc slug for a given action key. + * + * @since x.x + * + * @param string $action_key Action identifier (e.g. 'register'). + * + * @return string Doc slug or empty string. + */ + private static function get_learn_more_slug( $action_key ) { + $links = self::get_action_learn_more_links(); + return $links[ $action_key ] ?? ''; + } } class Frm_Form_Action_Factory { diff --git a/classes/controllers/FrmFormsController.php b/classes/controllers/FrmFormsController.php index ff5f5d4eda..d7bf1aa909 100644 --- a/classes/controllers/FrmFormsController.php +++ b/classes/controllers/FrmFormsController.php @@ -1521,17 +1521,17 @@ private static function get_settings_tabs( $values ) { 'name' => __( 'General', 'formidable' ), 'title' => __( 'General Form Settings', 'formidable' ), 'function' => array( self::class, 'advanced_settings' ), - 'icon' => 'frmfont frm_settings_icon', + 'icon' => 'frmfont frm_small_settings_icon', ), 'email' => array( 'name' => __( 'Actions & Notifications', 'formidable' ), 'function' => array( 'FrmFormActionsController', 'email_settings' ), 'id' => 'frm_notification_settings', - 'icon' => 'frmfont frm_mail_bulk_icon', + 'icon' => 'frmfont frm_notification_check_icon', ), 'permissions' => array( 'name' => __( 'Form Permissions', 'formidable' ), - 'icon' => 'frmfont frm_lock_closed_icon', + 'icon' => 'frmfont frm_lock_closed2_icon', 'html_class' => 'frm_show_upgrade_tab frm_noallow', 'data' => array( 'medium' => 'permissions', @@ -1543,7 +1543,7 @@ private static function get_settings_tabs( $values ) { ), 'scheduling' => array( 'name' => __( 'Form Scheduling', 'formidable' ), - 'icon' => 'frmfont frm_calendar_icon', + 'icon' => 'frmfont frm_schedule_icon', 'html_class' => 'frm_show_upgrade_tab frm_noallow', 'data' => array( 'medium' => 'scheduling', @@ -1556,17 +1556,17 @@ private static function get_settings_tabs( $values ) { 'name' => __( 'Buttons', 'formidable' ), 'class' => self::class, 'function' => 'buttons_settings', - 'icon' => 'frmfont frm_button_icon', + 'icon' => 'frmfont frm-buttons-style', ), 'landing' => array( 'name' => __( 'Form Landing Page', 'formidable' ), - 'icon' => 'frmfont frm_file_text_icon', + 'icon' => 'frmfont frm_cross_device_icon', 'html_class' => 'frm_show_upgrade_tab frm_noallow', 'data' => FrmAppHelper::get_landing_page_upgrade_data_params(), ), 'chat' => array( 'name' => __( 'Conversational Forms', 'formidable' ), - 'icon' => 'frmfont frm_chat_forms_icon', + 'icon' => 'frmfont frm_chat_bubbles_icon', 'html_class' => 'frm_show_upgrade_tab frm_noallow', 'data' => FrmAppHelper::get_upgrade_data_params( 'chat', @@ -1596,7 +1596,7 @@ private static function get_settings_tabs( $values ) { 'name' => __( 'Customize HTML', 'formidable' ), 'class' => self::class, 'function' => 'html_settings', - 'icon' => 'frmfont frm_code_icon', + 'icon' => 'frmfont frm_code2_icon', ), ); diff --git a/classes/helpers/FrmAddonsHelper.php b/classes/helpers/FrmAddonsHelper.php index 06f67455f9..5ff9790ab8 100644 --- a/classes/helpers/FrmAddonsHelper.php +++ b/classes/helpers/FrmAddonsHelper.php @@ -232,7 +232,7 @@ public static function add_addon_attributes( $addon ) { * @return string */ private static function prepare_single_addon_classes( $addon ) { - $class_names = array( 'frm-card-item frm-flex-col' ); + $class_names = array( 'frm-card-item frm-card-item--outlined frm-flex-col' ); $class_names[] = 'plugin-card-' . $addon['slug']; $class_names[] = 'frm-addon-' . $addon['status']['type']; diff --git a/classes/helpers/FrmOnSubmitHelper.php b/classes/helpers/FrmOnSubmitHelper.php index 3dcc5b1f54..8557620137 100644 --- a/classes/helpers/FrmOnSubmitHelper.php +++ b/classes/helpers/FrmOnSubmitHelper.php @@ -32,7 +32,7 @@ public static function show_message_settings( $args ) { $id_attr = $args['action_control']->get_field_id( 'success_msg' ); // phpcs:disable Generic.WhiteSpace.ScopeIndent ?> -
+
@@ -54,7 +54,7 @@ public static function show_message_settings( $args ) { $id_attr = $args['action_control']->get_field_id( 'show_form' ); $name_attr = $args['action_control']->get_field_name( 'show_form' ); ?> -
+
get_field_name( 'success_page_id' ); // phpcs:disable Generic.WhiteSpace.ScopeIndent ?> -
-
+
+