# Mailchimp Widget

> Mailchimp email signup form with AJAX subscribe flow, double-opt-in support, redirect-after-subscribe, and Mailchimp tag assignment.

**Class file:** `includes/Elements/Mailchimp.php` (1,044 lines)
**Slug:** `mailchimp` (widget id `eael-mailchimp`)
**Public docs:** <https://essential-addons.com/elementor/docs/mailchimp/>
**Pro-shared:** Pro-only. **External integration: Mailchimp API v3**.

## Overview

Renders a signup form (email + optional first/last name) that POSTs via AJAX to a Mailchimp List. Datacenter prefix derived from the API key suffix (`-usN`). Supports double-opt-in (sets `status: 'pending'` instead of `'subscribed'`), tag assignment, and post-subscribe redirect. Two layouts: stacked / inline.

## Pro vs Lite

Pro-only. No Lite counterpart.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/Mailchimp.php` | Widget class — controls + render |
| `includes/Traits/Helper.php` (line 31+) | `mailchimp_subscribe_with_ajax()` — AJAX handler |
| `includes/Traits/Helper.php` (line 75+) | `wp_safe_remote_post` to Mailchimp API |
| `includes/Classes/Helper.php` (line 174+) | `mailchimp_lists()` — fetches user's list catalogue |
| `includes/Classes/Bootstrap.php` (lines 216–217) | Registers `wp_ajax_mailchimp_subscribe` AND `wp_ajax_nopriv_mailchimp_subscribe` |
| `src/css/view/mailchimp.scss` → `assets/front-end/css/view/mailchimp.min.css` | Form styling |
| `src/js/view/mailchimp.js` → `assets/front-end/js/view/mailchimp.min.js` | Form submit handler |
| `config.php` entry `'mailchimp'` | Self CSS + self JS |

## Architecture

- **API key + list_id stored separately** — API key in site-wide `eael_save_mailchimp_api` option (EA settings page), list id selected per-widget via `eael_mailchimp_lists` control (Pro fetches the catalogue from `mailchimp_lists()` helper).
- **Datacenter prefix derivation** — Mailchimp API keys have the format `<32-hex>-us<N>`. The DC prefix (`us10`, `us19`, etc.) is the substring after the dash. Used as subdomain: `https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members/{md5(email)}`. `Helper.php:75-83` derives it via `substr( $api_key, strpos( $api_key, '-' ) + 1 )`.
- **API key validation regex** — `Helper.php:48`: `'/^[0-9a-z]{32}(-us)(0?[1-9]|[1-9][0-9])?$/'`. Subscribe is rejected if the key doesn't match.
- **PUT not POST** — `wp_safe_remote_post` with `'method' => 'PUT'`. Mailchimp's `/lists/{id}/members/{md5(email)}` endpoint is idempotent: PUT creates-or-updates by email hash.
- **Basic auth header** — `Authorization: Basic ' . base64_encode( 'user:' . $api_key )`. API key in plaintext in the encoded header. Standard Mailchimp pattern.
- **AJAX subscribe nonce** — `wp_verify_nonce` with nonce `essential-addons-elementor`. Standard EA nonce, generated globally for any EA AJAX. Note: this nonce **is public** in rendered Lite/Pro pages — see [`.claude/skills/nopriv-ajax-hardening/SKILL.md`](../../.claude/skills/nopriv-ajax-hardening/SKILL.md) for the "nonce in nopriv-reachable context is not auth" rule.
- **No timeout set** on the Mailchimp call — uses WP default (5s). Acceptable for an AJAX context.
- **Double-opt-in flag pre-fetched** — Pro calls `ClassesHelper::mailchimp_lists('mailchimp', true)` to map list_id → double_optin boolean. AJAX handler reads this map at subscribe time.

## Render Output

```html
<div class="eael-mailchimp-wrap eael-mailchimp-stacked"
     data-mailchimp-id="{element-id}"
     data-list-id="{list_id}"
     data-button-text="Subscribe"
     data-success-text="Thanks!"
     data-pending-text="Check your email to confirm."
     data-loading-text="..."
     [?] data-redirect-enabled="yes"
     [?] data-redirect-url="..." >

  <form id="eael-mailchimp-form-{element-id}" method="POST">
    <div class="eael-form-fields-wrapper eael-mailchimp-fields-wrapper eael-mailchimp-btn-block">
      <div class="eael-field-group eael-mailchimp-email">
        <label>Email</label>
        <input type="email" name="eael_mailchimp_email" class="eael-mailchimp-input" required />
      </div>
      [?] <div class="eael-field-group eael-mailchimp-fname">
        <label>First Name</label>
        <input type="text" name="eael_mailchimp_firstname" />
      </div>
      [?] <div class="eael-field-group eael-mailchimp-lname">
        <label>Last Name</label>
        <input type="text" name="eael_mailchimp_lastname" />
      </div>
      <button type="submit" class="eael-mailchimp-submit-btn">Subscribe</button>
    </div>
  </form>
</div>
```

If API key is not set, the entire `<div>` is suppressed (`<?php if (!empty($api_key)): ?>`).

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `eael_mailchimp_lists` | Content → Settings | SELECT | List ID (populated by API fetch) |
| `eael_mailchimp_layout` | Content → Settings | CHOOSE | `stacked` / `inline` |
| `eael_section_mailchimp_button_text` | Content → Form | TEXT | Submit button label |
| `eael_section_mailchimp_success_text` | Content → Form | TEXT | Success message |
| `eael_section_mailchimp_pending_text` | Content → Form | TEXT | Pending (double-opt-in) message |
| `eael_section_mailchimp_loading_text` | Content → Form | TEXT | Loading text |
| `eael_mailchimp_fname_show` / `_lname_show` | Content → Fields | SWITCHER | Toggle first/last name fields |
| `eael_mailchimp_email_label_text` / `_placeholder_text` | Content → Fields | TEXT | Email field labels |
| `redirect_for_subscribe_user` | Content → Settings | SWITCHER | Redirect after success |
| `redirect_url_for_subscribe_user` | Content → Settings | URL | Redirect URL (when enabled) |
| `eael_mailchimp_tags` | Content → Settings | TEXT | Comma-separated Mailchimp tags |
| `eael_mailchimp_subscribe_btn_display` | Content → Form | CHOOSE | `block` / `inline` |

## Conditional Dependencies

```text
eael_mailchimp_fname_show = 'yes'
  └── shows first-name input field

eael_mailchimp_lname_show = 'yes'
  └── shows last-name input field

redirect_for_subscribe_user = 'yes'
  └── shows redirect_url_for_subscribe_user URL field
```

API key requirement is hard — if `eael_save_mailchimp_api` is empty, the entire widget render produces no `<form>`. List dropdown will also be empty.

## JavaScript Lifecycle

`src/js/view/mailchimp.js`:

```js
var MailchimpHandler = function( $scope, $ ) {
    var $form = $scope.find( '.eael-mailchimp-wrap form' );
    if ( ! $form.length ) return;

    $form.on( 'submit', function( e ) {
        e.preventDefault();
        var $wrap = $scope.find( '.eael-mailchimp-wrap' );
        var data = {
            action: 'mailchimp_subscribe',
            nonce: eael.nonce,
            listId: $wrap.data( 'list-id' ),
            fields: $form.serialize(),
        };
        $.post( eael.ajaxurl, data, function( resp ) {
            if ( resp.status === 'subscribed' ) {
                // show success
                if ( $wrap.data( 'redirect-enabled' ) === 'yes' ) {
                    window.location = $wrap.data( 'redirect-url' );
                }
            } else if ( resp.status === 'pending' ) {
                // show pending (double-opt-in)
            } else {
                // show error
            }
        } );
    } );
};
```

## Hooks & Filters

### AJAX endpoints

| Action | Handler | Auth |
| --- | --- | --- |
| `wp_ajax_mailchimp_subscribe` | `mailchimp_subscribe_with_ajax` | logged-in |
| `wp_ajax_nopriv_mailchimp_subscribe` | same | **public** |

### Pro / EA hooks emitted

None visible at the widget level. `login_register_mailchimp_integration_subscribe` (Helper.php:114+) is invoked by Login/Register Pro extender, not by this widget.

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| List dropdown empty | API key not set or invalid format | Check `eael_save_mailchimp_api` option + regex match | Re-enter key; ensure format `<32hex>-usN` |
| "An error occurred" on subscribe | Mailchimp returned non-success | Network: inspect response body | Common reasons: invalid email, member already subscribed, list paused |
| Double-opt-in pending message never confirms | User didn't click Mailchimp confirmation email | — | Out of scope — Mailchimp's flow |
| Form submits but nothing visible | JS error or wrong success-text | DevTools console | Verify `eael.nonce` is present (`eael` global localized) |
| Multiple widgets on a page conflict | List id collision | Check `data-list-id` per wrapper | Each widget instance is keyed by element id; should not conflict |

## Known Limitations

- **No reCAPTCHA** — form is publicly POSTable; spam-prone unless throttled at network layer
- **No HTTP timeout set** on the Mailchimp PUT — uses 5s default
- **API key in `wp_safe_remote_post` body via header** — basic auth, plaintext in the request (sent over HTTPS, so OK in transit; sensitive in logs)
- **No tag validation** — user-supplied tag strings are passed straight to Mailchimp; typos create new tags silently
- **No retry / queue on failure** — if Mailchimp is down or rate-limits, the subscribe just fails
- **`nopriv` endpoint trusts the form data** — `parse_str` on `_POST['fields']`. The handler doesn't widen visibility (no `WP_Query`), so the standard nopriv-hardening pattern doesn't apply. But: an attacker with the nonce can submit arbitrary first/last names against arbitrary list ids configured in the same site. Verify `list_id` is whitelisted against `mailchimp_lists()` output before subscribe.
- **Mailchimp's PUT-as-upsert** means re-subscribing the same email updates merge fields. Intentional but worth knowing.

## Cross-References

- Architecture: [`docs/architecture/external-api-integrations.md`](../architecture/external-api-integrations.md)
- Shared patterns: [`_patterns.md`](_patterns.md) — External-API widget pattern
- Skill: [`/nopriv-ajax-hardening`](../../.claude/skills/nopriv-ajax-hardening/SKILL.md) — relevant for the nopriv subscribe endpoint
