# Shared Widget Patterns (Pro)

> Cross-widget patterns documented once. Per-widget docs reference relevant sections instead of re-explaining mechanics each time.

## How to use this doc

In a per-widget doc:

```markdown
**Skin registration** — see [_patterns.md § Skin registration](_patterns.md#skin-registration).
This widget registers 8 skins (`Skin_Default` through `Skin_Seven`).
```

The widget doc only says **what's different** (skin count, skin names, control namespace). The shared doc handles the **how**.

---

## Skin registration

Some Pro widgets compose multiple visual variants via Elementor's `Skin_Base` system rather than render-branching in one class.

**Pattern:**

```php
protected function register_skins() {
    $this->add_skin( new Skin_Default( $this ) );
    $this->add_skin( new Skin_One( $this ) );
    // ...
}
```

The skin classes live in `includes/Skins/`. Each skin gets its own control sections registered via `add_action('elementor/element/{widget-id}/{section-id}/before_section_end', [$this, 'method'])`.

**Rules:**

- Skin id (`get_id()`) is kebab-case and **persisted in user content** — never rename or delete an existing skin id
- Skin controls should be skin-namespaced: `eael_{widget}_{skin}_{field}`
- Shared controls (those that exist across all skins) live in the parent widget class, not in skin classes

Full rules: [`.claude/rules/skins-system.md`](../../.claude/rules/skins-system.md). Architecture: [`docs/architecture/extensions.md`](../architecture/extensions.md) (covers skin concept), [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md).

**Today's only consumer:** `Advanced_Menu` (8 skins).

---

## External-API widget pattern

Pro widgets that fetch data from third-party APIs (Google Sheets, Mailchimp, Instagram, Twitter, Figma) follow a common shape:

1. **Controls** for API credentials (key / token / OAuth client id) — stored in widget settings
2. **Server-side fetch** in `render()` (or AJAX handler for paginated/refresh)
3. **Transient cache** keyed by `md5( implode( '', $auth_args ) )` with user-configurable TTL
4. **Fallback rendering** when API fails — empty state, last-good cache, or error message

**Per-API gotchas:**

| API | Endpoint | Notes |
| --- | --- | --- |
| Google Sheets | `https://sheets.googleapis.com/v4/spreadsheets/{id}?key={key}` | 70s timeout in current code — flagged as too long in `external-api-integrations.md` |
| Google Maps JS | `https://maps.googleapis.com/maps/api/js?key={key}` | Loaded via `Enqueue::before_enqueue_scripts` (runtime enqueue), not via `config.php` deps |
| Mailchimp | `https://<dc>.api.mailchimp.com/3.0/lists/{id}/members` | Datacenter prefix from API key suffix |
| Instagram Graph | `https://graph.instagram.com/...` | OAuth user-token; refresh-token rotation handled (verify) |
| Twitter | `https://api.twitter.com/2/...` | OAuth2 bearer token; rate limits aggressive |
| Figma | Figma image CDN | Host-allowlist required (SSRF risk) — handled by `FigmaImageHandler` |

**Security rules:** see [`.claude/rules/external-api-integrations.md`](../../.claude/rules/external-api-integrations.md):

- Allowlist hosts
- Explicit timeout ≤ 15s
- Never echo upstream error bodies
- Sanitize user values before URL concat (`add_query_arg`, `urlencode`)
- Cache responses
- No `unserialize()` on HTTP body

**Architecture context:** [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md).

---

## Pro-extends-Lite hook contract

Several Pro widgets are not their own classes — they're Pro features injected into Lite widgets via `Traits/Extender.php`. The Pro plugin doesn't "own" the widget; it owns hooks.

**Examples:**

- Login_Register (Lite widget) — Pro adds social login, GSAP animations, Mailchimp via `Login_Register_Extender` trait
- Progress Bar (Lite widget) — Pro adds line rainbow / circle fill / half circle / box layouts
- Pricing Table (Lite widget) — Pro adds 5 additional styles
- Advanced Data Table (Lite widget) — Pro adds Google Sheets / database / TablePress data sources
- Business Reviews (Lite widget) — Pro adds Google Business Profile integration

For these, **no Pro-side widget doc exists** in this folder. Instead, the Pro extension is documented in:

- [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md) — bridge contract
- Lite's `docs/widgets/<widget>.md` — primary widget doc, with a "Pro additions" section
- Pro's `Extender` trait callbacks — code-level reference

---

## License gating

Pro widgets are **implicitly license-gated** by Pro being active. There is no per-widget license check inside `render()` or `register_controls()` — the Lite-side dashboard hides "Pro Required" widgets and the Pro plugin instantiation gate (in `eael/before_init`) prevents Pro from loading at all when the license is invalid (today: license-state is read by License/* admin code only; front-end render does not consult the license status, see [`docs/architecture/license-system.md`](../architecture/license-system.md)).

**A future paid-tier escalation** (e.g. "advanced features within Pro require an Agency license") would need explicit gating in the widget. None today.

---

## `set_default_values()` registration

Every Pro widget slug must appear in `Pro\Traits\Core::set_default_values()` `$defaults` array. Without it, the widget is OFF on fresh Pro installs.

Pair with Lite's `Admin.php` `$ea_dashboard['widgets']` entry (`'is_pro' => true`). Both are required.

Full checklist: [`.claude/rules/widget-development.md`](../../.claude/rules/widget-development.md) § "Dashboard registration".

---

## Asset declaration in `config.php`

Pro widgets follow the standard `config.php` `'elements'` shape:

```php
'{slug}' => [
    'class'      => '\Essential_Addons_Elementor\Pro\Elements\{ClassName}',  // optional if matches default pattern
    'dependency' => [
        'css' => [
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/css/view/{slug}.min.css', 'type' => 'self', 'context' => 'view' ],
        ],
        'js' => [
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/js/lib-view/{vendor}/{vendor}.min.js', 'type' => 'lib', 'context' => 'view' ],
            [ 'file' => EAEL_PRO_PLUGIN_PATH . 'assets/front-end/js/view/{slug}.min.js', 'type' => 'self', 'context' => 'view' ],
        ],
    ],
],
```

**Runtime-only enqueues** (e.g. Google Maps JS, Google Identity Services SDK) live in `Traits/Enqueue::before_enqueue_scripts()` — not in `config.php` — because they need a runtime API key from a WP option to construct the URL.

---

## Settings storage

Per-widget settings live inside Elementor's `_elementor_data` post meta. Pro widgets don't own option keys for per-widget settings (one exception: `Google_Map` uses `eael_save_google_map_api` site-wide option for the JS API key).

Per-account / per-site secrets (OAuth client_id/secret, API keys, refresh tokens) live in dedicated `wp_options` rows — see per-widget doc.

---

## Helper imports

Pro widgets import helpers via:

```php
use \Essential_Addons_Elementor\Pro\Classes\Helper;                        // Pro-specific helpers
use \Essential_Addons_Elementor\Classes\Helper as ClassesHelper;           // Lite helpers
use Essential_Addons_Elementor\Traits\Helper as HelperTrait;               // Lite helper trait (rare)
```

When Lite refactors `Classes/Helper`, all Pro widgets importing it can break together. Pair Lite refactors with `/pro-lite-sync`.

---

## `has_widget_inner_wrapper`

Newer Elementor versions optionally wrap widget output with an extra `<div>`. Pro widgets that want to opt out implement:

```php
public function has_widget_inner_wrapper(): bool {
    return ! Helper::eael_e_optimized_markup();
}
```

`Advanced_Menu` does this. Helps with strict-CSS layouts.

---

## Optimized markup

`Helper::eael_e_optimized_markup()` returns true when the user has enabled "Optimize markup" in EA settings. Pro widgets that respect this skip wrapper divs.

This is a Lite setting Pro inherits. If you author a new widget, default to honouring it.

---

## WooCommerce widget conventions

Pro ships 5 WooCommerce widgets (woo-product-slider, woo-collections, woo-cross-sells, woo-account-dashboard, woo-thank-you). Shared conventions:

- **WC presence gate.** Two patterns observed:
  - `apply_filters('eael/is_plugin_active', 'woocommerce/woocommerce.php')` — used by Woo_Collections at line 78. Returns boolean; widget renders a "WooCommerce is not installed" RAW_HTML notice when false.
  - **No direct gate.** Other Woo widgets rely on Lite's EA dashboard to hide the widget when WC is missing. Risk: if a user enables the widget via filter but WC is deactivated, `WC()` calls fatal.
- **`WC()` global** for cart/customer/payment access — `WC()->cart`, `WC()->customer`, `WC()->payment_gateways`, `WC()->query`. Always available when WC is loaded; null otherwise.
- **`wc_get_product`, `wc_products_array_orderby`, `wc_get_template`** — standard WC helpers used directly.
- **Template-include pattern** — Woo widgets ship per-preset templates under `includes/Template/Woo-{Widget-Name}/` (3–4 files each). Same dispatch pattern as Post_Block / Post_List. Note Woo_Collections has **no** templates (inline render).
- **`get_categories()` returns 2 categories** — typically `['essential-addons-elementor', 'woocommerce-elements']` so the widget appears in both EA and WC widget panels.
- **`wc_get_template` override** (Woo_Thank_You only) — Pro intercepts WC's template loader to substitute its own. Constructor-time `add_filter`. Other Woo widgets don't override; they render in their own DOM scope.
- **Composes Lite's `Helper` trait** — except Woo_Collections (uses Pro's `Helper` because of `get_terms_list`). Consistent with general-pattern `Helper imports` section above.
- **Per-preset control bloat** — Woo widgets average ~2,000+ lines because of repetitive per-preset styling controls. When refactoring, extract control registration into per-preset methods.
- **Cart fragments dependency** — widgets with Add-to-Cart buttons rely on WC's `wc-cart-fragments` script being enqueued (Pro doesn't enqueue it; WC does on cart-aware pages).
- **HPOS compatibility** declared in Pro's entry file (`before_woocommerce_init` action) — Pro is compatible with WooCommerce's High-Performance Order Storage feature.

When authoring a new Woo widget:

1. Add the WC presence gate (`apply_filters('eael/is_plugin_active', ...)`) at the top of `register_controls()` with a RAW_HTML warning
2. Return `['essential-addons-elementor', 'woocommerce-elements']` from `get_categories()`
3. Use `wc_get_template` for any WC subsystem rendering (My Account, checkout) rather than reimplementing
4. Place per-preset templates under `includes/Template/Woo-{Widget-Name}/`
