# Conditional Display

> Show or hide any element (widget / section / column / container) based on a rich set of conditions: login status, user role, post type, browser, OS, date/time, recurring day, query string, visit count, URL pattern, archive page, geolocation (country/city), WooCommerce cart/products/orders.

**Class file:** `includes/Extensions/Conditional_Display.php` (2,936 lines)
**Slug:** `conditional-display` (`config.php` `extensions` key)
**Public docs:** <https://essential-addons.com/elementor/docs/conditional-display/>
**Pro-shared:** ✅ Pro-only (Lite has Promotion teaser)

## Overview

The largest Pro extension. Adds a "Conditional Display" panel to every Section, Column, Container, and widget. Each element gets:

- An Enable switch
- A Visibility Action (Show / Hide / Forcefully Hide)
- A repeater of conditions
- A relation (AND / OR) between conditions

At frontend-render time, the extension hooks `elementor/frontend/{widget,column,section,container}/should_render` and returns `true` or `false` based on evaluated conditions.

## Features

- **Login status** — show only to logged-in / logged-out users
- **User role** — multi-select role allowlist/blocklist
- **Post type / archive** — only on specific post types or archive pages
- **Browser / OS** — UA-based detection
- **Date/time** — date range, specific date, recurring day-of-week, recurring hour
- **Dynamic field** — match against an ACF / custom field on the current post
- **Query string** — show only when `?param=value` is present
- **Visit count** — first visit, Nth visit (cookie-tracked)
- **URL contains** — substring match on current URL
- **Geolocation** — country, city (via `ipinfo.io`)
- **WooCommerce cart** — products in cart, cart total, cart items count
- **WooCommerce products** — user has purchased / has not purchased
- **WooCommerce orders** — recent order count, order status

## Pro vs Lite

| Capability | Lite | Pro |
| --- | --- | --- |
| Panel visible in editor | Teaser only | ✅ Full panel |
| Per-element should_render gating | ❌ | ✅ |
| Geolocation lookup | ❌ | ✅ (ipinfo.io) |
| WooCommerce conditions | ❌ | ✅ |

## File Map

| File | Role |
| --- | --- |
| `includes/Extensions/Conditional_Display.php` | Entire extension (controls + condition evaluator + ipinfo integration) |
| `config.php` (extensions key) | `'conditional-display' => [ 'class' => '\Essential_Addons_Elementor\Pro\Extensions\Conditional_Display' ]` — **no `dependency` block, no assets** |

The extension has no CSS/JS source. All logic is server-side render-time gating.

## Architecture

Constructor wires 8 hooks (`Conditional_Display.php:22-29`):

```php
add_action( 'elementor/element/common/_section_style/after_section_end',    [ $this, 'register_controls' ] );
add_action( 'elementor/element/column/section_advanced/after_section_end', [ $this, 'register_controls' ] );
add_action( 'elementor/element/section/section_advanced/after_section_end', [ $this, 'register_controls' ] );
add_action( 'elementor/element/container/section_layout/after_section_end', [ $this, 'register_controls' ] );
add_filter( 'elementor/frontend/widget/should_render',    [ $this, 'content_render' ], 10, 2 );
add_filter( 'elementor/frontend/column/should_render',    [ $this, 'content_render' ], 10, 2 );
add_filter( 'elementor/frontend/section/should_render',   [ $this, 'content_render' ], 10, 2 );
add_filter( 'elementor/frontend/container/should_render', [ $this, 'content_render' ], 10, 2 );
```

The four `should_render` filters share one handler. The handler reads the element's settings, evaluates each condition, applies the relation (AND/OR), and returns the boolean.

## Controls Reference (high-value)

| Control id | Purpose |
| --- | --- |
| `eael_cl_enable` | Master switch — until `'yes'`, all other controls are conditioned off |
| `eael_cl_visibility_action` | `'show' \| 'hide' \| 'forcefully_hide'` — see "Visibility actions" below |
| `eael_cl_condition_relation` | `'and' \| 'or'` — combines repeater rows |
| `eael_cl_conditions` (repeater) | Each row defines one condition |
| Per-row `eael_cl_condition_type` | Selects the condition kind — drives a `switch` in the evaluator |
| `eael_cl_notice` | Editor-only info notice: "Conditional Display will take effect only on preview or live page, and not while editing in Elementor." |

### Visibility actions

- **`show`** — element is rendered only if all conditions evaluate true (per relation)
- **`hide`** — element is rendered unless conditions evaluate true
- **`forcefully_hide`** — element is **never** rendered, regardless of conditions. Useful for temporarily disabling without deleting.

## Condition evaluator — switch cases

`content_render( $should_render, $element )` reads `$settings['eael_cl_conditions']` (repeater) and walks each row through a `switch` on the row's `eael_cl_condition_type`. Cases observed in code:

| Type | Line | Behaviour |
| --- | --- | --- |
| `login_status` | `:648` | `is_user_logged_in()` check |
| `post_type` | `:673` | `get_post_type()` against settings allowlist |
| `browser` | `:694` | UA string sniff |
| `operating_system` | `:708` | UA string sniff |
| `date_time` | `:722` | `time()` against configured range |
| `recurring_day` | `:737` | day-of-week / day-of-month match |
| `dynamic` | `:773` | reads a saved dynamic value against current post field |
| `query_string` | `:802` | `$_GET[key]` value match |
| `visit_count` | `:817` | cookie-tracked visit count |
| `url_contains` | `:835` | `strpos` on current URL |
| `archive` | `:855` | `is_archive()` / specific archive checks |
| `country` | `:966` | `get_geolocation_data()['country']` |
| `city` | `:989` | `get_geolocation_data()['city']` |
| `woo_cart` | `:1020` | WooCommerce cart inspection |
| `woo_products` | `:1094` | Purchased / not-purchased check |
| `woo_orders` | `:1169` | Order count / status check |

## Geolocation flow — ipinfo.io

`get_geolocation_data()` (`Conditional_Display.php:561`):

```php
$ip = $this->get_user_ip();
if ( empty( $ip ) || $ip === '127.0.0.1' || $ip === '::1' ) { return false; }

$response = wp_remote_get( "https://ipinfo.io/{$ip}/json", [
    'timeout' => 5,
    'headers' => [ 'Accept' => 'application/json' ],
] );
```

**Strong points:**

- Loopback IPs bail (no useless API call)
- Explicit 5s timeout
- Caches result via transient: `eael_cl_geolocation_<md5($ip)>`

**Gaps (already in [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md)):**

- `get_user_ip()` may trust `X-Forwarded-For` without validation — spoofable
- `ipinfo.io` free tier rate-limited; high-traffic sites get throttled
- IP geolocation is **PII under GDPR**. Pair with privacy policy when enabled.

## Behavior Flow

1. **Editor:** user opens Conditional Display panel on a section. Enables, picks visibility action, adds repeater rows.
2. **Save:** settings persisted in the element's `_elementor_data`.
3. **Frontend render:** Elementor's render pipeline calls `apply_filters( 'elementor/frontend/section/should_render', true, $section )`. Pro's `content_render` evaluates conditions, returns `true` (render) or `false` (skip).
4. **Editor preview is unaffected** — the editor renders all elements regardless. The `eael_cl_notice` info notice tells the user this explicitly.

## Hooks & Filters

### Elementor hooks consumed

| Hook | Method |
| --- | --- |
| `elementor/element/{common,column,section,container}/.../after_section_end` | `register_controls` (4 hooks) |
| `elementor/frontend/{widget,column,section,container}/should_render` | `content_render` (4 hooks) |

### Pro / EA hooks emitted

None — pure consumer.

## Behavior Quirks

- **Editor never hides elements.** Users sometimes report "conditional display not working in the editor" — it's by design (see `eael_cl_notice`).
- **`forcefully_hide` bypasses condition evaluation.** Faster than evaluating, useful for "kill switch" scenarios.
- **Cookie-based visit count** persists per-browser; cleared on cookie clear. Not user-bound.
- **WooCommerce conditions** require WooCommerce to be active — verify before assuming.

## Known Limitations / Gaps

1. `get_user_ip()` likely doesn't validate `X-Forwarded-For` — flagged in external-api-integrations.md
2. ipinfo.io rate limits + privacy footprint — document in privacy policy when extension is enabled
3. No per-user cache for geolocation — only per-IP; users sharing an IP share a cache entry
4. Repeater can have unbounded rows — perf risk if user adds 100+ conditions
5. Condition evaluator is a single 600+ line function — refactor candidate (one method per condition type)

## Code Pointers

- Constructor + hook wiring: `Conditional_Display.php:22-29`
- Main controls registration: `:50-625`
- `content_render` (the gate): `:626+`
- Geolocation: `:557-595`
- Condition switch cases: `:648-1230`

## Cross-References

- Architecture: [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md) — ipinfo.io rules
- Rule: [`.claude/rules/external-api-integrations.md`](../../.claude/rules/external-api-integrations.md)
- Shared patterns: [`_patterns.md`](_patterns.md)
