AgentStack
SKILL verified MIT Self-run

Je Dynamic Visibility Condition

skill-lonsdale201-wp-agent-skills-je-dynamic-visibility-condition · by Lonsdale201

Register a custom Dynamic Visibility condition for

No reviews yet
0 installs
6 views
0.0% view→install

Install

$ agentstack add skill-lonsdale201-wp-agent-skills-je-dynamic-visibility-condition

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Je Dynamic Visibility Condition? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

JetEngine: register a Dynamic Visibility condition

For developers extending JetEngine's Dynamic Visibility module with custom conditions — "is current page the front page", "is the user past N days since registration", "has the visitor purchased a specific product", "is the LearnDash course completed". The module already ships ~40 built-in conditions; this skill is for the custom-condition extension contract that's been stable across JetEngine 3.x.

Misconception this skill corrects

> "I'll add a pre_get_posts filter or a custom shortcode to gate the visibility — same outcome."

Different layer. JetEngine's Dynamic Visibility runs the condition check INSIDE the rendering pipeline of every JE-aware widget/block (Elementor widgets, Gutenberg blocks, Bricks elements, listing items). A custom condition slots into the SAME UI as the built-in ones — site editors pick it from a dropdown, configure it, and it applies wherever JE Dynamic Visibility is enabled.

pre_get_posts only filters main queries; doesn't help with widget-level visibility. Shortcodes don't get the visibility-condition UI. The verified extension contract is:

add_action( 'jet-engine/modules/dynamic-visibility/conditions/register', function ( $manager ) {
    $manager->register_condition( new MyCondition() );
} );

Where MyCondition extends \Jet_Engine\Modules\Dynamic_Visibility\Conditions\Base. Verified at [wp-content/plugins/jet-engine/includes/modules/dynamic-visibility/inc/conditions/base.php:4](base.php).

Other AI-prone misconceptions:

  • "check( $args ) returns true when the visitor SHOULD see the content." Half-true. The semantics is "should the widget be displayed". $args['type'] carries 'show' (user wants to show when condition is true) or 'hide' (user wants to hide when condition is true). Your check() MUST invert the result based on $args['type'] — see the Equal built-in at [conditions/equal.php:37-41](equal.php).
  • "is_for_fields() controls whether the condition is shown in the UI." Wrong — it controls whether the condition is available in the meta-field context (where the user is comparing meta values). Conditions like "is front page" don't compare values, so they return is_for_fields() => false.
  • "I should hardcode get_post_meta( get_the_ID(), ... ) to read meta." Don't — use $this->get_current_value( $args ) from the Base class. It handles current_listing context automatically (WPPost / WPUser / WPTerm / WPComment / listing-macros).

When to use this skill

Trigger when ANY of the following is true:

  • The diff calls register_condition, hooks jet-engine/modules/dynamic-visibility/conditions/register, or extends \Jet_Engine\Modules\Dynamic_Visibility\Conditions\Base.
  • The user asks "how do I add a custom Dynamic Visibility condition for JetEngine".
  • A companion plugin needs role-aware / membership-aware / LMS-progress-aware visibility logic.
  • Reviewing PR code that adds a condition class.

The Base API surface (verified)

All methods live on \Jet_Engine\Modules\Dynamic_Visibility\Conditions\Base ([base.php:4-212](base.php)):

| Method | Default | Purpose | |---|---|---| | get_id() | abstract | Unique slug — used as the form-control key. Pick a stable slug; renaming breaks saved configurations. | | get_name() | abstract | Display label in the editor dropdown. Translatable. | | check( $args = [] ) | abstract | The actual gate. Return true if the widget should display, false to hide. Honor $args['type']. | | get_group() | false | Optional UI group label. false lands the condition in "Other"; otherwise groups conditions visually. | | is_for_fields() | true | Whether the condition appears in the meta-field value-compare UI. Set false for context-only conditions. | | need_value_detect() | true | Whether the UI prompts for a "value" input. Set false for binary "is X" conditions. | | need_type_detect() | false | Whether the UI prompts for a "datatype" (chars/numeric/datetime/date/list). Set true if your condition does numeric/date comparison. | | get_current_value( $args ) | helper | Reads the current value from listing context (post / user / term / comment / macro). Use INSTEAD of raw get_post_meta. | | checkboxes_to_array( $array ) | helper | Convert JE-checkbox {value: bool} to plain list. | | adjust_values_type( $current, $compare, $data_type ) | helper | Cast both sides for comparison per datatype. | | explode_string( $value ) | helper | CSV → array, trims values. | | get_custom_controls() | false | Optional — return an array of UI controls (like Elementor controls) to add per-condition fields. |

The $args shape (verified)

When check( $args ) runs, $args contains:

[
    'type'      => 'show' | 'hide',          // user's intent — invert your boolean here
    'condition' => 'my-condition-id',         // your get_id() value
    'field'     => 'meta_value_or_macro',     // user-entered, possibly with %macros%
    'field_raw' => 'meta_key',                // raw meta key without macro processing
    'operator'  => 'equal' | 'not-equal' | …, // (for value-compare conditions)
    'value'     => 'value_to_compare',        // user-configured comparison value
    'data_type' => 'chars' | 'numeric' | 'datetime' | 'date' | 'list',
    'context'   => 'default' | 'current_listing',
    // ... plus any keys your get_custom_controls() declared
]

Not all keys are present in every call — depends on the condition's is_for_fields() / need_value_detect() / need_type_detect() flags + get_custom_controls().

Workflow

1. Bootstrap your companion plugin

add_action( 'plugins_loaded', static function (): void {
    if ( ! class_exists( '\Jet_Engine' ) ) {
        return;   // JetEngine not active
    }

    add_action(
        'jet-engine/modules/dynamic-visibility/conditions/register',
        [ \MyPlugin\Visibility\Manager::class, 'register' ]
    );
}, 11 );

Priority 11 so JE's own plugins_loaded:10 has run, making the Conditions\Base class loadable.

Important — do NOT call jet_engine()->modules->is_module_active( 'dynamic-visibility' ) inside this guard. jet_engine()->modules is null until JE's init() callback runs at init priority -999 (verified at [jet-engine.php:164,345](jet-engine.php)) — long after plugins_loaded. A pre-init module-active check fatal-errors with "Call to a member function ismoduleactive() on null". You don't need the gate anyway: the jet-engine/modules/dynamic-visibility/conditions/register action is fired from inside the visibility module's own bootstrap ([conditions/manager.php:77](manager.php)) — if the module is disabled, the action never fires and your callback never runs.

1b. When you DO need to check the module is active

The condition-registration action handles the gating for you, so a module-active check is unnecessary for that path. But there are legitimate uses for is_module_active() — admin notices ("Dynamic Visibility module is off, your conditions won't appear"), conditional asset enqueuing, REST endpoints that depend on visibility state, second registrations on a sibling module. For those, run the check AFTER JE's modules manager exists.

// CORRECT — check on jet-engine/init, fired right after JE finishes its init()
add_action( 'jet-engine/init', static function ( $jet_engine ): void {
    if ( ! $jet_engine->modules->is_module_active( 'dynamic-visibility' ) ) {
        // Module is disabled — surface a notice, skip your asset enqueue, etc.
        add_action( 'admin_notices', static function (): void {
            echo ''
               . esc_html__( 'MyPlugin: enable JetEngine Dynamic Visibility to use the conditions.', 'myplugin' )
               . '';
        } );
        return;
    }

    // Module is active — do whatever needs the module to be on
    MyPlugin\Visibility\AssetLoader::register();
} );

// ALSO CORRECT — same effect via init:-998 (one step after JE's init:-999)
add_action( 'init', static function (): void {
    if ( ! function_exists( 'jet_engine' ) || ! jet_engine()->modules ) {
        return; // JE not loaded at all
    }
    if ( ! jet_engine()->modules->is_module_active( 'dynamic-visibility' ) ) {
        return;
    }
    // ... module-dependent setup
}, -998 );

The jet-engine/init action is the cleanest because the JE instance is passed in and you don't need defensive function_exists() checks. It is fired at [jet-engine.php:375](jet-engine.php).

// WRONG — calling on plugins_loaded (any priority) fatal-errors
add_action( 'plugins_loaded', static function (): void {
    if ( jet_engine()->modules->is_module_active( 'dynamic-visibility' ) ) {  // 🔴 modules is null here
        // ...
    }
}, 11 );

2. Minimal context condition — "Is Front Page"

namespace MyPlugin\Visibility;

use Jet_Engine\Modules\Dynamic_Visibility\Conditions\Base;

class IsFrontPage extends Base {

    public function get_id(): string {
        return 'myplugin-is-front-page';   // namespace-prefix the slug to avoid collisions
    }

    public function get_name(): string {
        return __( 'Is Front Page', 'myplugin' );
    }

    public function get_group(): string {
        return __( 'Page Context', 'myplugin' );   // groups multiple related conditions in the UI
    }

    /**
     * No meta-field comparison — this is a binary context check.
     */
    public function is_for_fields(): bool {
        return false;
    }

    /**
     * No "value" input in the UI — the condition is a yes/no gate.
     */
    public function need_value_detect(): bool {
        return false;
    }

    public function check( $args = [] ): bool {
        $is_front_page = ( (int) get_option( 'page_on_front' ) === (int) get_the_ID() );

        // Honor the user's show/hide intent
        $type = $args['type'] ?? 'show';

        return ( 'hide' === $type ) ? ! $is_front_page : $is_front_page;
    }
}

3. Meta-comparison condition — read listing context correctly

class HighRatedProducts extends Base {

    public function get_id(): string {
        return 'myplugin-high-rated';
    }

    public function get_name(): string {
        return __( 'Product Rating ≥ N', 'myplugin' );
    }

    public function need_type_detect(): bool {
        return true;   // user picks numeric / chars / etc. — we use numeric here
    }

    public function check( $args = [] ): bool {
        // get_current_value() handles the listing context for us:
        // - WP_Post (incl. WC_Product) → get_post_meta
        // - WP_User                     → get_user_meta
        // - WP_Term                     → get_term_meta
        // - WP_Comment                  → get_comment_meta
        // - macros (%dynamic_value%)    → resolved via macros engine
        $current = (float) $this->get_current_value( $args );
        $threshold = (float) ( $args['value'] ?? 0 );

        $type = $args['type'] ?? 'show';
        $matches = $current >= $threshold;

        return ( 'hide' === $type ) ? ! $matches : $matches;
    }
}

4. Custom UI controls — get_custom_controls()

For conditions that need bespoke inputs beyond the default field/value pair (like "select days of week", "select user roles", repeater of items):

class WeekDays extends Base {

    public function get_id(): string {
        return 'myplugin-week-days';
    }

    public function get_name(): string {
        return __( 'Day of Week', 'myplugin' );
    }

    public function is_for_fields(): bool {
        return false;   // not a meta-comparison
    }

    public function need_value_detect(): bool {
        return false;   // value input is custom, declared below
    }

    public function get_custom_controls(): array {
        global $wp_locale;
        return [
            'week_days' => [
                'label'    => __( 'Days of Week', 'myplugin' ),
                'type'     => 'select2',
                'multiple' => true,
                'default'  => [],
                'options'  => $wp_locale->weekday,   // [0 => 'Sunday', 1 => 'Monday', ...]
            ],
        ];
    }

    public function check( $args = [] ): bool {
        $allowed = $args['week_days'] ?? [];   // matches the key from get_custom_controls
        $today = (int) current_time( 'w' );

        $matches = in_array( $today, array_map( 'intval', $allowed ), true );
        $type = $args['type'] ?? 'show';

        return ( 'hide' === $type ) ? ! $matches : $matches;
    }
}

The control type field accepts the same values JE uses elsewhere: text, number, select, select2, checkbox, media, date, time, datetime. Verified usage in WeekDays, TimePeriod, ListingOdd built-ins.

5. Register all conditions

namespace MyPlugin\Visibility;

class Manager {
    public static function register( $manager ): void {
        $manager->register_condition( new IsFrontPage() );
        $manager->register_condition( new HighRatedProducts() );
        $manager->register_condition( new WeekDays() );

        // Conditional — only if dependency plugin is active
        if ( class_exists( '\WooCommerce' ) ) {
            $manager->register_condition( new \MyPlugin\Visibility\Wc\PurchasedProducts() );
        }
        if ( function_exists( 'wc_memberships' ) ) {
            $manager->register_condition( new \MyPlugin\Visibility\Wc\MemberAccess() );
        }
    }
}

Conditional registration based on third-party plugin presence is the canonical pattern — verified in the existing dynamic-elementor-extension plugin's VisibilityManager::register_conditions().

6. Listing context awareness

When the visibility check runs INSIDE a JE listing (a card in a listing grid, for example), $args['context'] is 'current_listing' and get_current_value() reads from the LISTING's current object — NOT the parent page. The Base helper handles this:

// Inside check( $args ):
$value = $this->get_current_value( $args );

// For 'current_listing' context, this resolves to:
// - if listing iterates WP_Post → get_post_meta( $current_post_id, $field_raw, true )
// - if listing iterates WP_User → get_user_meta( $current_user_id, $field_raw, true )
// - …etc.

// Don't manually re-read with get_post_meta( get_the_ID() ) — get_the_ID() in
// listing context returns the LISTING'S parent page, not the iterated item.

Verified at [base.php:69-120](base.php) — the resolver dispatches by get_class( $object ).

7. Group conditions in the UI

public function get_group(): string {
    return __( 'My Plugin', 'myplugin' );
}

All conditions returning the same string land in a labeled section in the editor's condition dropdown. Useful for plugins shipping multiple conditions — keeps them visually clustered. Without get_group(), the condition lands in the default "Other" bucket.

8. Checkbox values — convert before use

JE checkboxes return { value1: true, value2: false } shape (so the user can keep all keys but only some are "on"). Convert to a plain list:

$selected = $this->checkboxes_to_array( $args['my_checkbox_field'] ?? [] );
// Now $selected is ['value1', 'value3', ...]

Critical rules

  • get_id() is stable. Renaming breaks saved visibility configurations on every site that uses the condition.
  • Namespace-prefix the slug (myplugin-is-front-page not is-front-page) to avoid collisions with built-ins or other companion plugins.
  • check() MUST honor $args['type']. Invert the boolean when type === 'hide'. Otherwise the user's "hide if true" UI choice does the opposite of what they expect.
  • Use $this->get_current_value( $args ) for meta reads — handles the listing context automatically. Don't get_post_meta( get_the_ID(), ... ) directly.
  • is_for_fields() => false when the condition isn't comparing a meta field value. Cleaner UI for the editor.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.