Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5d70a3e
Add virtual field to upsell field types array
shervElmi Jan 20, 2026
e111899
Shorten virtual field upsell message for better readability
shervElmi Jan 20, 2026
23f2825
Update virtual field upsell with new icon, message, and preview image
shervElmi Jan 21, 2026
1c59d20
Add phpcs:ignore comment for long line in virtual field upsell message
shervElmi Jan 21, 2026
2fd2f44
Move virtual field to end of upsell field types array and update lear…
shervElmi Jan 23, 2026
5c90545
Merge remote-tracking branch 'origin/master' into rock/virtual-field
shervElmi Jan 29, 2026
e2f67e0
Merge remote-tracking branch 'origin/HEAD' into rock/virtual-field
shervElmi Feb 9, 2026
9be89b0
Merge remote-tracking branch 'origin/master' into rock/virtual-field
shervElmi Feb 17, 2026
17fcc70
Add empty string check to strip_most_html before processing
shervElmi Feb 19, 2026
0a26cec
Change get_posted_meta visibility from private to public
shervElmi Feb 19, 2026
8c300e8
Merge remote-tracking branch 'origin/HEAD' into rock/virtual-field
shervElmi Feb 20, 2026
4d4deda
Change Virtual Field name to Virtual in field types array
shervElmi Feb 20, 2026
b9f1b57
Merge remote-tracking branch 'origin/master' into rock/virtual-field
shervElmi Mar 3, 2026
7b04b27
Merge branch 'master' into rock/virtual-field
Crabcyborg Mar 5, 2026
618734a
Merge remote-tracking branch 'origin/HEAD' into rock/virtual-field
shervElmi Mar 6, 2026
130ce0c
Update @since version tag to x.x for FrmEntriesHelper method
shervElmi Mar 6, 2026
fe75454
Remove outdated Pro test class reference from testing documentation
shervElmi Mar 6, 2026
b56cff1
Update @since version tag to 4.02.04 for FrmEntriesHelper method
shervElmi Mar 6, 2026
06ce6ab
Merge remote-tracking branch 'origin/master' into rock/virtual-field
shervElmi Mar 6, 2026
e6e35e4
Merge branch 'master' into rock/virtual-field
Crabcyborg Mar 6, 2026
457b636
Add filter to flag Pro field types requiring newer Pro version
shervElmi Mar 9, 2026
afb90c1
Add support for frm_show_update class in field type rendering
shervElmi Mar 9, 2026
48a6cc0
Customize upgrade modal text and links for plugin update prompts
shervElmi Mar 9, 2026
bd91ac9
Add cursor pointer style for upgrade-required field types in builder
shervElmi Mar 9, 2026
a2e827b
Add frm_show_update class support to insert fields styling
shervElmi Mar 9, 2026
70e611b
Add get_update_install_data method to retrieve plugin update install …
shervElmi Mar 9, 2026
f4d4f29
Extract field upgrade state logic into separate method and add update…
shervElmi Mar 9, 2026
a46f69c
Add overwrite_package parameter to plugin installer to allow updates
shervElmi Mar 9, 2026
904ae7d
Update virtual field class check to use FrmProVirtualFieldController
shervElmi Mar 9, 2026
9e5c985
Add frm_show_update class support to field dragging styles and hide u…
shervElmi Mar 9, 2026
0d6ae7e
Exclude frm_show_update fields from draggable field types in builder
shervElmi Mar 9, 2026
0e68f4b
Simplify update modal overrides by hiding elements instead of changin…
shervElmi Mar 9, 2026
a6420e3
npm run build
shervElmi Mar 9, 2026
ec2d124
Remove unnecessary frm-upgrade-modal-title-prefix hidden class select…
shervElmi Mar 9, 2026
d4d68f8
Reorder return array in get_field_upgrade_state to match list() destr…
shervElmi Mar 9, 2026
73a70a4
Add data-default attribute to upgrade modal "are not available" span …
shervElmi Mar 9, 2026
75f93b7
Reset not installed text to default when opening upgrade modal and se…
shervElmi Mar 9, 2026
8575ff3
Merge branch 'master' into rock/virtual-field
shervElmi Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .windsurf/rules/formidable/frm-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ Every fix/feature **MUST** be tested with these scenarios before completion:
## Test Classes

- **Lite tests:** Extend `FrmUnitTest`
- **Pro tests:** Extend `FrmProUnitTest`
- **AJAX tests:** Extend `FrmAjaxUnitTest`

```php
Expand Down
41 changes: 40 additions & 1 deletion classes/controllers/FrmAddonsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,45 @@ public static function install_link( $plugin ) {
return $link;
}

/**
* Get the JSON-encoded install data for a plugin update.
*
* @since x.x
*
* @param string $addon_slug The addon slug (e.g. 'pro', 'dates').
*
* @return string JSON-encoded install data, or empty string if no URL is available.
*/
public static function get_update_install_data( $addon_slug ) {
$upgrading = self::install_link( $addon_slug );

if ( isset( $upgrading['class'] ) && 'frm-install-addon' === $upgrading['class'] ) {
return (string) json_encode( $upgrading );
}

if ( 'pro' === $addon_slug ) {
$download_url = self::get_pro_download_url();
$plugin_file = 'formidable-pro/formidable-pro.php';
} else {
$addon_data = self::get_addon( $addon_slug );
$download_url = $addon_data && ! empty( $addon_data['url'] ) ? $addon_data['url'] : '';
$plugin_file = $addon_data && ! empty( $addon_data['plugin'] ) ? $addon_data['plugin'] : 'formidable-' . $addon_slug . '/formidable-' . $addon_slug . '.php';
}

if ( ! $download_url ) {
$update_plugins = get_site_transient( 'update_plugins' );
$plugin_update = $update_plugins->response[ $plugin_file ] ?? null;
$download_url = $plugin_update && ! empty( $plugin_update->package ) ? $plugin_update->package : '';
}

return $download_url ? (string) json_encode(
array(
'url' => $download_url,
'class' => 'frm-install-addon',
)
) : '';
}

/**
* @since 4.09
*
Expand Down Expand Up @@ -1107,7 +1146,7 @@ protected static function install_addon() {

// Create the plugin upgrader with our custom skin.
$installer = new Plugin_Upgrader( new FrmInstallerSkin() );
$installer->install( $download_url );
$installer->install( $download_url, array( 'overwrite_package' => true ) );

// Flush the cache and return the newly installed plugin basename.
wp_cache_flush();
Expand Down
3 changes: 3 additions & 0 deletions classes/controllers/FrmHooksController.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ public static function load_admin_hooks() {

add_action( 'frm_after_duplicate_form', 'FrmFormActionsController::duplicate_form_actions', 20, 3 );

// Fields Model.
add_filter( 'frm_pro_available_fields', 'FrmField::show_update_for_pro_fields' );

// Forms Controller.
add_action( 'admin_menu', 'FrmFormsController::menu', 10 );
add_action( 'admin_head-toplevel_page_formidable', 'FrmFormsController::head' );
Expand Down
4 changes: 4 additions & 0 deletions classes/helpers/FrmAppHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,10 @@ public static function sanitize_with_html( &$value ) {
* @param string $value
*/
public static function strip_most_html( $value ) {
if ( '' === $value ) {
return $value;
}

$allowed_html = array(
'b' => array(),
'br' => array(),
Expand Down
3 changes: 2 additions & 1 deletion classes/helpers/FrmEntriesHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,14 @@ public static function get_posted_value( $field, &$value, $args ) {

/**
* @since 4.02.04
* @since x.x This is public.
*
* @param int|string $field_id Field ID.
* @param array $args Additional arguments.
*
* @return mixed
*/
private static function get_posted_meta( $field_id, $args ) {
public static function get_posted_meta( $field_id, $args ) {
Comment thread
Crabcyborg marked this conversation as resolved.
if ( empty( $args['parent_field_id'] ) ) {
// Sanitizing is done next.
$value = isset( $_POST['item_meta'][ $field_id ] ) ? wp_unslash( $_POST['item_meta'][ $field_id ] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing, SlevomatCodingStandard.Files.LineLength.LineTooLong
Expand Down
119 changes: 92 additions & 27 deletions classes/helpers/FrmFieldsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2383,32 +2383,20 @@ public static function show_add_field_buttons( $args ) {
}

// If the individual field isn't allowed, disable it.
$run_filter = true;
$single_no_allow = ' ';
$install_data = '';
$requires = '';
$link = isset( $field_type['link'] ) ? esc_url_raw( $field_type['link'] ) : '';
$has_show_upgrade_class = isset( $field_type['icon'] ) && str_contains( $field_type['icon'], ' frm_show_upgrade' );
$show_upgrade = $has_show_upgrade_class || str_contains( $args['no_allow_class'], 'frm_show_upgrade' );

if ( $has_show_upgrade_class ) {
$single_no_allow .= 'frm_show_upgrade';
$field_type['icon'] = str_replace( ' frm_show_upgrade', '', $field_type['icon'] );
$run_filter = false;

if ( isset( $field_type['addon'] ) ) {
$upgrading = FrmAddonsController::install_link( $field_type['addon'] );

if ( isset( $upgrading['url'] ) ) {
$install_data = json_encode( $upgrading );
}

$requires = FrmFormsHelper::get_plan_required( $upgrading );
} elseif ( isset( $field_type['require'] ) ) {
$requires = $field_type['require'];
}
}

$link = isset( $field_type['link'] ) ? esc_url_raw( $field_type['link'] ) : '';

list(
$run_filter,
$single_no_allow,
$install_data,
$requires,
$has_show_upgrade_class,
$has_show_update_class,
$upgrading,
$update_addon_name
) = self::get_field_upgrade_state( $field_type );

$show_upgrade = $has_show_upgrade_class || $has_show_update_class || str_contains( $args['no_allow_class'], 'frm_show_upgrade' );
$upgrade_label = '';
$upgrade_message = '';

Expand Down Expand Up @@ -2452,7 +2440,14 @@ public static function show_add_field_buttons( $args ) {
);
}

if ( isset( $upgrading['url'] ) ) {
if ( $has_show_update_class ) {
$li_params['data-message'] = sprintf(
// translators: %1$s: Add-on name, %2$s: Field type name.
esc_html__( 'You need a newer version of %1$s for %2$s fields.', 'formidable' ),
$update_addon_name,
$field_name
);
} elseif ( isset( $upgrading['url'] ) ) {
$li_params['data-message'] = sprintf(
// translators: %s: Field name
esc_html__( 'You already have access to %s fields, you\'ll just need to activate to start using them.', 'formidable' ),
Expand All @@ -2476,6 +2471,76 @@ public static function show_add_field_buttons( $args ) {
// phpcs:enable Generic.WhiteSpace.ScopeIndent
}

/**
* Parse the upgrade and update flags from a field type's icon class.
*
* @since x.x
*
* @param array $field_type Field type configuration, modified in place to strip icon flag classes.
*
* @return array Upgrade and update state values for the field button, in positional order.
*/
private static function get_field_upgrade_state( &$field_type ) {
$single_no_allow = ' ';
$run_filter = true;
$has_show_upgrade_class = false;
$has_show_update_class = false;
$install_data = '';
$requires = '';
$upgrading = array();
$update_addon_name = '';

if ( isset( $field_type['icon'] ) ) {
$has_show_upgrade_class = str_contains( $field_type['icon'], ' frm_show_upgrade' );
$has_show_update_class = str_contains( $field_type['icon'], ' frm_show_update' );
}

if ( $has_show_upgrade_class ) {
$single_no_allow .= 'frm_show_upgrade';
$field_type['icon'] = str_replace( ' frm_show_upgrade', '', $field_type['icon'] );
$run_filter = false;

if ( isset( $field_type['addon'] ) ) {
$upgrading = FrmAddonsController::install_link( $field_type['addon'] );

if ( isset( $upgrading['url'] ) ) {
$install_data = json_encode( $upgrading );
}

$requires = FrmFormsHelper::get_plan_required( $upgrading );
} elseif ( isset( $field_type['require'] ) ) {
$requires = $field_type['require'];
}
}

if ( $has_show_update_class ) {
$single_no_allow .= ' frm_show_update';
$field_type['icon'] = str_replace( ' frm_show_update', '', $field_type['icon'] );
$run_filter = false;
$addon_slug = $field_type['addon'] ?? 'pro';

if ( 'pro' === $addon_slug ) {
$update_addon_name = __( 'Formidable Pro', 'formidable' );
} else {
$addon_data = FrmAddonsController::get_addon( $addon_slug );
$update_addon_name = $addon_data && ! empty( $addon_data['title'] ) ? $addon_data['title'] : ucfirst( $addon_slug );
}

$install_data = FrmAddonsController::get_update_install_data( $addon_slug );
}

return array(
$run_filter,
$single_no_allow,
$install_data,
$requires,
$has_show_upgrade_class,
$has_show_update_class,
$upgrading,
$update_addon_name,
);
}

/**
* Updates the params with limit data (the data-limit attribute, and possibly the frm_at_limit class).
* Some field types are limited to a certain number per form, including coupon fields.
Expand Down
24 changes: 24 additions & 0 deletions classes/models/FrmField.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ public static function pro_field_selection() {
'upsell_image' => $upsell_images_url . 'appointment-field-preview.webp',
'learn-more' => 'simply-schedule-appointments-forms',
),
'virtual' => array(
'name' => __( 'Virtual', 'formidable' ),
'icon' => 'frmfont frm-virtual-field-icon',
'message' => esc_html__( 'Protect sensitive data by storing field values server-side only, preventing users from viewing or manipulating them in their browser.', 'formidable' ), // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
'upsell_image' => $upsell_images_url . 'virtual-field-preview.webp',
'learn-more' => '/virtual',
),
Comment thread
shervElmi marked this conversation as resolved.
'product' => array(
'name' => __( 'Product', 'formidable' ),
'icon' => 'frmfont frm_product2_icon',
Expand Down Expand Up @@ -346,6 +353,23 @@ public static function pro_field_selection() {
return apply_filters( 'frm_pro_available_fields', $fields );
}

/**
* Flag Pro field types that require a newer Pro version with frm_show_update.
*
* @since x.x
*
* @param array $fields Available Pro field types.
*
* @return array
*/
public static function show_update_for_pro_fields( $fields ) {
if ( FrmAppHelper::pro_is_installed() && ! class_exists( 'FrmProVirtualFieldController', false ) ) {
$fields['virtual']['icon'] .= ' frm_show_update';
}

return $fields;
}

/**
* Consider a field new for 90 days after the release date.
*
Expand Down
2 changes: 1 addition & 1 deletion classes/views/shared/upgrade_overlay.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/* translators: %$1s: Feature name, %$2s: open span tag, %$3s: close span tag, %$4s: open prefix span tag, %$5s: close prefix span tag, %$6s: open suffix span tag, %$7s: close suffix span tag. */
esc_html__( '%4$sActivate the %5$s%1$s %2$sare not available%3$s%6$s are now activated%7$s', 'formidable' ),
'<span class="frm_feature_label"></span>',
'<span class="frm_are_not_installed">',
'<span class="frm_are_not_installed" data-default="' . esc_attr_x( 'are not available', 'upgrade modal', 'formidable' ) . '">',
'</span>',
'<span class="frm-upgrade-modal-title-prefix">',
'</span>',
Expand Down
2 changes: 1 addition & 1 deletion css/frm_admin.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions images/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/upsell/virtual-field-preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion js/formidable-web-components.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/formidable_admin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/frm_testing_mode.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10442,7 +10442,7 @@ window.frmAdminBuildJS = function() {

setupSortable( 'ul.frm_sorting' );

document.querySelectorAll( '.field_type_list > li:not(.frm_show_upgrade)' ).forEach( makeDraggable );
document.querySelectorAll( '.field_type_list > li:not(.frm_show_upgrade):not(.frm_show_update)' ).forEach( makeDraggable );

jQuery( 'ul.field_type_list, .field_type_list li, ul.frm_code_list, .frm_code_list li, .frm_code_list li a, #frm_adv_info #category-tabs li, #frm_adv_info #category-tabs li a' ).disableSelection();

Expand Down
39 changes: 38 additions & 1 deletion js/src/admin/upgrade-popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export function initUpgradeModal() {
event.preventDefault();

const modal = $info.get( 0 );
modal.classList.remove( 'frm-success' );
const lockIcon = modal.querySelector( '.frm_lock_icon' );

if ( lockIcon ) {
Expand Down Expand Up @@ -249,7 +250,9 @@ export function initUpgradeModal() {
// If one click upgrade, hide other content
addOneClick( element, 'modal', upgradeLabel );

modal.querySelector( '.frm_are_not_installed' ).style.display = element.dataset.image || element.dataset.oneclick ? 'none' : 'inline-block';
const notInstalled = modal.querySelector( '.frm_are_not_installed' );
notInstalled.style.display = element.dataset.image || element.dataset.oneclick ? 'none' : 'inline-block';
notInstalled.textContent = notInstalled.dataset.default;
modal.querySelector( '.frm-upgrade-modal-title-prefix' ).style.display = element.dataset.oneclick ? 'inline' : 'none';
modal.querySelector( '.frm_feature_label' ).textContent = upgradeLabel;
modal.querySelector( '.frm-upgrade-modal-title-suffix' ).style.display = 'none';
Expand All @@ -266,6 +269,40 @@ export function initUpgradeModal() {
}
link = link.replace( /(content=)[a-z_-]+/ig, `$1${ content }` );
button.setAttribute( 'href', link );

if ( element.classList.contains( 'frm_show_update' ) ) {
applyUpdateModalOverrides( modal );
}
}
}

/**
* Override upgrade modal content for update prompts.
*
* @since x.x
*
* @param {Element} modal The upgrade modal element.
*/
function applyUpdateModalOverrides( modal ) {
const titlePrefix = modal.querySelector( '.frm-upgrade-modal-title-prefix' );
if ( titlePrefix ) {
titlePrefix.style.display = 'none';
}

const notInstalled = modal.querySelector( '.frm_are_not_installed' );
if ( notInstalled ) {
notInstalled.textContent = __( 'require an update', 'formidable' );
notInstalled.style.display = ''; // Clear inline style, span defaults to display:inline.
}

const oneclickMsg = modal.querySelector( '.frm-oneclick' );
if ( oneclickMsg ) {
oneclickMsg.style.display = 'none';
}

const button = modal.querySelector( '.frm-oneclick-button' );
if ( button ) {
button.textContent = __( 'Update Now', 'formidable' );
}
}

Expand Down
6 changes: 4 additions & 2 deletions resources/scss/admin/components/builder/_field-dragging.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ li.frm_noallow.button,
}

.frm_actions_list a.frm_show_upgrade.frm_inactive_action::before,
li.frm_noallow.button.frm_show_upgrade {
li.frm_noallow.button.frm_show_upgrade,
li.frm_noallow.button.frm_show_update {
cursor: pointer;
}

.field_type_list li.frm_noallow.button.frm_show_upgrade:hover {
.field_type_list li.frm_noallow.button.frm_show_upgrade:hover,
.field_type_list li.frm_noallow.button.frm_show_update:hover {
border-color: inherit;
}

Expand Down
Loading
Loading