Older SheetLink Forms changelog entries.
The most recent releases are in readme.txt, which is what the plugin
directory displays. Everything below predates those.

= 1.14.7 =
* Security: **the Two-Way Sync page displayed a live credential in plain text.** The page printed the sync secret that authorises incoming sheet updates, so anyone who saw the screen - a screenshot, a support ticket, a screen-share - could post fabricated row updates to your site. It is no longer rendered; an administrator can reveal it on demand instead. **If you have ever shared a screenshot of that page, contact support to reissue your licence key, which changes the secret.**
* Fixed: **"Recent incoming updates" said "No updates received yet" forever if you use the Google connection.** Only the Apps Script route wrote to that table. Sites using the one-click Google connection were syncing edits correctly the whole time, but the page gave no evidence of it, so a working setup looked broken. Entries now record which mechanism delivered them and the table shows it.
* Improved: **the Two-Way Sync page now describes the mechanism your site actually uses.** If Google is connected it reports that the sync is active, how often it checks, when it last read the sheet, and when it will check next - and it is honest that WordPress cron only runs when someone visits the site, so a quiet site checks less often than the interval suggests. The Apps Script instructions are demoted to "not needed on this site" rather than presented as the setup path.
* Improved: **a site with an active licence but no Two-Way Sync add-on now says so.** Previously only a missing licence warned, so an unentitled site looked correctly configured while every incoming edit was rejected.
* Added: a warning against running the Apps Script and the Google connection on the same sheet. Both deliver to the same place, and with both live an edit can be applied twice.

= 1.14.6 =
* Improved: **the setup wizard no longer walks you through an Apps Script you do not need.** If you have already connected Google with one click, the "Google Script" step is gone and the remaining steps renumber - previously the wizard asked you to deploy a script and then told you on the very next screen that there was nothing else to set up.

= 1.14.5 =
* Fixed: **"Fields: 0" again, this time on Send Test.** The 1.14.4 fix only reached the path a real form submission takes; Send Test still reported zero. Since Send Test is how most people confirm a rule works, that was the number most people saw.

= 1.14.4 =
* Fixed: **the Sync Log said "Fields: 0" on deliveries that wrote several columns.** Since 1.14.0 you no longer type column names, so the count had nothing left to count and reported zero. It now reports what was actually written to the row.
* Improved: **Send Test now confirms itself next to the button.** The confirmation only appeared at the top of the page, so on a long list of rules a successful test could look like nothing happened - and a retried test writes another real row to your spreadsheet.

= 1.14.3 =
* Fixed: **Send Test never tested the Excel mirror.** It ran and reported the Airtable and Notion mirrors but skipped Excel entirely - no notice, no Sync Log row, no row in your workbook - so a working Excel mirror and a broken one looked exactly the same. Excel is now included, and reports like the others.
* Fixed: **an Excel mirror that is switched on but not finished being set up now says so.** It used to skip in silence, which looked identical to having no Excel mirror at all.
* Fixed: **the workbook picker could come up empty on a brand-new Microsoft 365 account.** Microsoft's file search needs time to index a newly created OneDrive and returns an error until it has; SheetLink now falls back to listing the files in your OneDrive so you can pick a workbook straight away.

= 1.14.2 =
* Fixed: **checkboxes that could not be switched off.** Unticking a WooCommerce event, or a conditional routing rule, appeared to save and then came back ticked. The browser sent the right value; it was misread on arrival, so the setting was always stored as enabled. Anything you had tried to switch off has been running all along.
* Fixed: opening the WooCommerce screen and pressing Save switched on **all six** event types, whether or not you ticked them. Events left enabled with no Receiver URL are switched off automatically on update, since they could never have sent anything. An enabled event that has a URL is left exactly as it is.

= 1.14.1 =
* Fixed: **the setup wizard could not be opened at all.** Every visit to it returned "Sorry, you are not allowed to access this page", including the automatic redirect a new site is sent through immediately after activating the plugin. The page was registered under the main SheetLink menu and then hidden from the sidebar, which left WordPress unable to resolve it. Present since 1.9.4.
* Fixed: **the one-click Google route could not finish the wizard.** After connecting a Google account, the last step still demanded an Apps Script "Receiver URL" and refused to save without one, so anyone taking the one-click route left the wizard with no sync rule and nothing was ever delivered. That step now recognises the connection, and the Apps Script field is hidden entirely rather than shown and ignored.
* Fixed: an empty Receiver URL on the classic Apps Script route reported that the address "does not look like" a valid URL, when nothing had been typed. It now says what to do instead, including the one-click option.
* Fixed: the wizard's own stylesheet, script and documentation link stopped loading when the page moved.

= 1.14.0 =
* Fixed: **Google Sheets deliveries failed with "No form fields are mapped" for most form plugins.** SheetLink matched your sheet's column headers against each form field's internal key, and several plugins use generated keys - Formidable emits things like `kzn0j`, Ninja Forms `name_1512...` - so a column called "Email" matched nothing and the row was never written. Columns are now matched on the field's **label** as well, which is what you actually name your columns after. Gravity Forms and WPForms were unaffected because they happen to key by the label already.
* Added: **you no longer type column names.** Connect a spreadsheet, point a form at it, and the first submission writes the header row for you, taken from that form's own fields. Existing setups are untouched - if you have already configured columns, they keep being used.
* Added: **each form now gets its own tab.** A tab has a single header row, so two forms with different fields could never share one - the second form's answers landed under the first form's columns. Your first rule keeps using the tab you picked; any additional form gets its own, named after the rule.
* Added: the Google Sheets screen now tells you when a connected spreadsheet has no form pointed at it, and lets you create that rule right there. Connecting a sheet writes its header row, which looked finished while nothing was actually being sent.
* Improvement: seeded column headers use readable labels ("First Name") rather than internal names ("names.first_name", "name-1") for Formidable, Ninja Forms, Forminator, Everest Forms, Fluent Forms, WPForms, WS Form and Divi.


= 1.13.1 =
* Change: **the plugin now contacts `sheetlinkwp.com` for licence activation and entitlement checks**, instead of `siteanswerai.com` - a legacy domain the API happened to be hosted under. Nothing to do on your side, and older installs keep working because the old address forwards to the new one. **If you listed `siteanswerai.com` as a data processor in your privacy notice, update it to `sheetlinkwp.com`.**
* Change: **the Primary Destinations add-on (previously "Excel Primary Destination") now covers Excel Online, Airtable and Notion.** One add-on for all three. If you already own Excel Primary, your existing licence unlocks the other two at no extra cost - nothing to do.
* Removed: a long-unreachable legacy Airtable/Notion delivery path inside the Integrations Bundle. It could not be configured from any shipped rule editor, and Airtable and Notion have been free built-in destinations with proper per-rule field mapping since 1.9.4. If a rule from a very old build still references it, an admin notice now points you to the built-in destination instead of the delivery failing quietly.
* Improvement: corrected pricing and add-on descriptions that implied Airtable and Notion were part of the paid Integrations Bundle. They are free core destinations; only using one as your *sole* destination is paid.

= 1.13.0 =
* Security: **stored API tokens are no longer written into the settings page's HTML.** The Airtable, Notion, Meta CAPI and SMS (Twilio) screens pre-filled their credential field with the saved token. The browser masked it visually, but it sat in the page source in cleartext - readable by anyone who can view source on that screen, by browser extensions, and by anything that captures the page. The field now renders empty with a masked "Currently set" hint: leave it blank to keep the stored token, or tick **Remove the stored value** to delete it. **If you have entered an API token on any of those screens, consider rotating it.**
* New: **Airtable and Notion can now be a rule's primary destination.** Previously they could only mirror a successful Google Sheets delivery, which meant you had to set up a Google Sheet you did not want - and if that Sheets delivery ever failed, the Airtable write never happened either. A rule can now deliver straight to Airtable or Notion with no Google Sheets configuration at all. Pick it from the Primary destination dropdown in the rule editor. Requires the Primary Destinations add-on, which covers all three; **the free Airtable, Notion and Excel mirror modes are unchanged and stay free.**
* New: primary deliveries to Airtable and Notion use the full delivery pipeline - the automatic retry queue (throttling and provider outages retry; a bad token or unknown column does not, because retrying cannot fix those), save-time validation of your credentials, and destination-aware health checks. Mirror deliveries never had retries.
* Fix: **failed deliveries to Excel, Airtable and Notion logged a blank error.** The Sync Log recorded the correct status and HTTP code but discarded the explanation, so a failure showed up with an empty Error column. The provider's actual message is now kept.
* Fix: the Sync Log's Fields count now reflects what was actually written to the destination rather than how many fields arrived from the form - relevant for Airtable and Notion, which only send mapped fields.

= 1.12.4 =
* Fix: **Contact Form 7 submissions now sync even when the notification email fails.** SheetLink captured CF7 submissions on the hook that fires only *after* CF7's email is sent successfully, so on a site with broken or unconfigured SMTP not a single submission ever reached the spreadsheet - precisely the situation a spreadsheet is supposed to protect you from. Capture now happens after CF7 finishes its own validation, acceptance, spam and file checks but before the email is attempted, so delivery no longer depends on your mail server. Spam and aborted submissions are still excluded, exactly as before.
* Improvement: **rows created by the Send Test button are now labelled "Test" in the Sync Log.** Previously a test send and a real form submission produced identical log rows, so a green row proved the destination worked but not that a real submission had ever arrived - awkward when debugging a live site.

= 1.12.3 =
* Fix: **Airtable and Notion no longer reject every submission when your form has more fields than your table has columns.** Airtable refuses the *entire* record if any field name is not a real column, so a five-field form writing to a three-column table failed 100% of the time with "Unknown field name" - losing the lead. When you have set up a field mapping, only the mapped fields are sent. A new **Unmapped fields** checkbox on the Airtable and Notion screens restores the old behavior if every one of your form fields has a matching column. With no mapping configured, all fields are still sent, so a table whose column names match your form field names keeps working with no setup.
* Fix: **an enabled-but-unconfigured mirror now says so.** If Airtable or Notion was switched on but missing its token, Base ID, or table name, the plugin skipped it silently - no Sync Log row - which looked exactly like the feature being broken. It now logs a failed row naming precisely which setting is missing.
* Fix: the Airtable table-name field is now also protected from browser autofill (the token and Base ID fields were covered in 1.12.2).

= 1.12.2 =
* Fix: **Airtable and Notion deliveries no longer fail silently.** When a mirror write was rejected - most often an invalid or mistyped access token - the plugin discarded the error: no entry in the Sync Log, nothing in the retry queue, no notice. That made a broken credential look exactly like a feature that was never running. Every Airtable and Notion attempt is now recorded in the Sync Log, on success and on failure, with the provider's own error message.
* Fix: **the Excel mirror now records successful writes too** (it previously only surfaced failures, as an admin notice).
* Fix: **Send Test now tests what your rule actually does.** It sends through the rule's real destination - so it works for Google Sheets (OAuth) and Excel rules instead of only classic Apps Script rules - and it also exercises your Airtable and Notion mirrors, reporting each one. A mirror failure now fails the test instead of showing a misleading all-clear.
* Fix: **a saved field mapping could be silently reverted.** If a Google account token renewal happened at the same moment you saved a rule's field mapping, the renewal wrote back older settings and discarded the mapping - leaving the rule failing with "No form fields are mapped to the sheet columns". Renewal now only touches the token.
* Fix: saving an empty field mapping now warns that deliveries will fail, instead of reporting a plain "saved" for a rule that cannot deliver.
* Security/UX: the Airtable Base ID and access-token fields (and the Notion token field) now resist browser password-manager autofill, which could quietly overwrite your Base ID with a saved username and break every write.

= 1.12.1 =
* Improvement: **Excel deliveries retry when the workbook is open in the desktop Excel app.** A workbook locked by desktop Excel (the most common real-world Excel failure) previously failed permanently; it now retries automatically with a clear message, and succeeds once the file is closed.
* Improvement: **Excel writes to the same workbook are serialized.** Simultaneous submissions (or a retry batch overlapping a new submission) no longer race each other into Microsoft's same-workbook write conflict.
* Improvement: **Microsoft throttling responses are respected.** When Microsoft asks SheetLink to wait (HTTP 429/503 Retry-After), the retry queue now waits at least that long instead of retrying on its own schedule.
* Improvement: **weekly Microsoft connection keepalive.** Microsoft silently expires connections after ~90 days of inactivity; a weekly background refresh keeps a quiet site's Excel connection alive indefinitely.
* Improvement: Microsoft connections now remember their home tenant, which makes token refresh more reliable for work/school accounts (including guest accounts in another organization).
* Fix: a token refresh finishing at the same moment as a settings save can no longer discard the new Microsoft credentials.

= 1.12.0 =
* New: **One-click Google Sheets connect.** Connect a Google account and write submissions straight to a Google Sheet - no Apps Script, no pasting a script, no webhook URL. Create a new sheet in one click, or pick an existing one with Google's own file picker. SheetLink only ever sees the sheets it creates for you or that you pick - never the rest of your Google Drive. The classic Apps Script method is unchanged and still available.
* New: **Per-rule field mapping for the one-click Google Sheets destination.** Map each form field to a sheet column on the settings page. Columns you leave unmapped are filled automatically when a column header matches a form field name.
* Improvement: newly created sheets get a labeled header row automatically, and rows are appended beneath it.
* Fix: the one-click Google Sheets destination no longer writes a blank row when nothing is mapped - it reports a clear, actionable error instead of silently logging a success.

= 1.11.4 =
* Fix: **queued retries now use the same HTTP transport as first deliveries.** A retried Apps Script delivery previously followed Google's redirect and re-sent the request to the redirect endpoint, which rejects it - logging an HTTP 400 "failure" even though the first hop had already written the row (and each retry could write a duplicate). Retries now treat the redirect as the success it is, and a redirect to a Google sign-in page is a non-retryable configuration error with a clear message.
* Improvement: **the Apps Script setup instructions now explain how to update a deployed script.** Saving code in the Apps Script editor does not change what the web app runs: you must publish it via Deploy > Manage deployments > edit > Version: "New version". The instructions warn that the Version dropdown pre-selects the old version, and that "New deployment" creates a second URL instead of updating the existing one. This distinction has cost real users real hours.

= 1.11.3 =
* Fix: **WS Form submissions now capture field values.** The integration listened with the wrong number of hook arguments, so every WS Form delivery arrived as metadata-only. Fields are captured under their field_N keys, with field labels working as mapping aliases, and the correct form ID is recorded.
* Fix: **Kali Forms integration now fires.** The plugin listened on the wrong hook name (and detection checked a wrong class name), so Kali submissions were never captured. Internal keys (nonce, form id bookkeeping) are excluded from the row.
* Fix: **JetFormBuilder submissions now capture field values and the correct form ID.** The handler read a success flag as the form ID and looked for field data where JetFormBuilder does not store it.
* Fix: **Beaver Builder Contact Form submissions now capture field values** under the module's fixed field keys (name, subject, email, phone, message). The previous capture iterated a settings collection that the stock module does not have.
* Fix: **Gravity Forms multi-input fields are no longer dropped.** Name and Address fields now arrive as dotted sub-fields (name.first, name.last) plus a combined column, and Checkbox/Multiselect/List selections arrive comma-separated. Simple fields are unchanged.
* Fix: **Send Test now uses the same HTTP transport as live deliveries.** It previously followed the Apps Script redirect in a way Google's redirect endpoint rejects, reporting HTTP 400 against healthy deployments.
* Improvement: **clearer diagnosis when an Apps Script deployment requires sign-in.** A redirect to a Google login page is now logged as an explicit failure with the fix ("redeploy with access: Anyone") instead of a success that quietly writes nothing.
* Improvement: Everest Forms fields are now captured under their stable Meta Key (labels still work as mapping aliases); Elementor rules match the form name as well as the element id; Forminator and Everest checkbox/multiselect values arrive comma-separated instead of raw arrays; fields whose labels sanitize to an empty string no longer collapse into one column.
* Internal: add-on form sources dispatch through the shared engine statically, removing a duplicate-delivery risk.

= 1.11.2 =
* Fix: **rules can now be saved with an empty field map**, enabling the documented auto-sync mode (every captured form field becomes a column, no configuration needed). The editor previously required at least one mapping row for Google Sheets rules, which made auto-sync impossible to set up even though the interface described mapping rows as optional.
* Fix: **Divi form IDs are accepted in both spellings.** A rule's Form ID can be the internal divi_0 / divi_1 form number or the et_pb_contact_form_0 identifier visible in Divi's markup - previously the et_pb_... spelling silently matched nothing, so the rule never fired. The Form ID field's help text now documents both.
* Compatibility: the Divi integration (including the 1.11.1 Field ID fix) is verified end to end on both Divi 4 (4.27) and Divi 5.

= 1.11.1 =
* Security: **hardening release - please update.** This release addresses the findings of a full security audit of the plugin and its SheetLink cloud services. Highlights: the Microsoft 365 connection to SheetLink's OAuth broker is now authenticated with your site's activation token; two-way sync payloads support full HMAC-SHA256 verification with a replay window; license entitlement responses support signature verification; stored CRM, Twilio, and AI-key credentials are now encrypted at rest (re-save existing credentials to encrypt them); Airtable and Notion deliveries are pinned to their official API hosts; all outbound destination and webhook URLs get stricter validation (DNS-resolved, private network ranges blocked); the spreadsheet formula-injection guard now catches whitespace and encoding bypasses.
* Privacy: several data captures are now **off by default** and opt-in: consent logging (sheetlink_consent_log_enabled), raw submission payloads in the delivery log (sheetlink_log_payloads), and personal data in WordPress User Sync / ACF Sync exports (sheetlink_user_sync_include_pii, sheetlink_acf_sync_include_pii). If you relied on any of these, re-enable the matching option after updating. Phone numbers used for ad-platform exports are hashed in E.164 form; set sheetlink_default_country_code if your site's local numbers are not +1.
* New: **destination-aware Retry Queue.** Failed Excel Online deliveries now retry through Microsoft Graph just like Sheets deliveries retry through Apps Script; the queue records each entry's destination and the Retry Now buttons in the queue and Delivery Log use the same delivery path as the original attempt. Non-retryable failures (invalid credentials, revoked access) are marked permanently failed instead of retrying pointlessly.
* Fix: **Divi field mapping now matches the documented Field IDs.** Divi Contact Form fields are captured under the Field ID set in the module (e.g. name, email), as the docs describe, instead of a sanitized copy of the field label. Explicit field maps created against the old label-style keys (e.g. email-address) keep working - both key styles now resolve in field maps, conditional routing, and consent capture. Fixes rows arriving with correct headers but blank values when a field map used Field IDs.
* Fix: Divi fields with an empty label no longer collapse into a single unnamed column.

= 1.11.0 =
* New: **Excel Online as primary destination.** Pick Microsoft 365 Excel as the place every submission lands - the Google Sheets delivery becomes optional. The rule editor now has a Primary Destination dropdown at the top of every rule; the existing Sheets fields only appear when Sheets is selected. Licensed under the Agency Growth + Agency Plus bundles, or available standalone as the Excel Primary add-on ($29/mo). Existing Excel-mirror rules continue to work unchanged.
* New: **Pluggable destination registry** (SheetLink_Destination + SheetLink_Destination_Registry). Each destination type is now a registered class with deliver() / validate_rule() / ping() / transform_payload() hooks. Third-party plugins can register their own destinations via the new sheetlink_register_destinations action without touching the core dispatch engine. The Sheets path was extracted into SheetLink_Sheets_Destination and is byte-equivalent to v1.10.0.
* New: **Destination column in Activity Log + Dashboard + Webhook Health.** Each log entry now shows which destination handled the delivery (Google Sheets, Excel Online, etc.). Pre-1.11.0 entries display as "Google Sheets" for backward compatibility.
* New: **Destination-aware webhook health checks.** Excel rules now ping Microsoft Graph /me to verify the OAuth connection; Sheets rules continue to ping the Apps Script /exec URL with the existing tagged payload. Hourly sweep + the on-demand "Test now" button both respect the rule's primary destination.
* Improvement: setup wizard now offers Excel as a destination on the welcome step so new customers can pick Excel without going through a Sheets-first flow.
* Internal: one-time migration backfills primary_destination='sheets' on every rule saved by v1.10.0 or earlier. Idempotent; no-op on subsequent runs. The dispatch engine also defaults to 'sheets' at runtime when the field is missing, so existing customers don't have to wait for the migration to run.

= 1.10.0 =
* New: Excel Online destination. Mirror every form submission into a Microsoft 365 Excel workbook on OneDrive alongside the primary Google Sheets delivery. Per-rule field mapping picks which form fields land in which Excel columns. Connection uses OAuth via a SheetLink-hosted broker so the customer's Microsoft refresh token is encrypted at rest (AES-256-GCM) with no Microsoft client secret in the plugin. The Sheets delivery remains primary; Excel is a non-blocking mirror via the sheetlink_after_delivery hook, same shape as the existing Airtable + Notion destinations.
* New: shared encrypted-tokens helper (SheetLink_Tokens_Encrypted) for at-rest encryption of OAuth refresh tokens. Reusable by future OAuth destinations (HubSpot, Salesforce, etc.). HKDF-SHA256 key derivation from wp_salt('auth') with per-context info string.
* New: Excel Online entry in the Destinations admin tab strip (alongside Airtable and Notion).

= 1.9.5 =
* Tested and confirmed compatible with WordPress 7.0.
* Fix: the retry-queue cron interval's display label is now wrapped in an is_textdomain_loaded() check. Pre-init callers of wp_get_schedules() (WordPress core, other plugins) previously triggered the filter callback's __() call before the SheetLink textdomain had loaded, producing the WP 6.7+ _load_textdomain_just_in_time _doing_it_wrong notice. Filter registration stays at plugins_loaded so the recurrence remains available during the plugin activation hook.

= 1.9.4 =
The largest single release since 1.7.0. 15 new feature classes, 6 industry templates, and a series of internal plumbing changes that let non-form sources dispatch to the same delivery pipeline. Existing rules continue to work unchanged. Customers wanting the new row-upsert behavior on existing rules must regenerate the Apps Script (SheetLink -> [rule] -> Get Script) and redeploy.

**Security**

* New: **Formula injection guard** (`SheetLink -> Features -> Formula Guard`). Sanitises every payload value before dispatch so spreadsheet formula-injection attempts (`=HYPERLINK(...)`, `=cmd|...`, leading `@`/`+`/`-` etc.) cannot be embedded in form input. Defaults ON.

**Ad attribution**

* New: **Meta Ads CSV export** (`SheetLink -> Meta Ads Conversions`). Capture `fbclid` from Meta traffic and export completed conversions as a Meta-compliant CSV (hashed email/phone, SHA-256 normalized).
* New: **Microsoft Ads CSV export** (`SheetLink -> Microsoft Ads Conversions`). Same pattern as Meta for `msclkid` traffic.
* New: **Shared ad-export base** - one Apps Script property (`SHEETLINK_ADS_TOKEN`) now authorizes Google + Meta + Microsoft ads exports. Existing Google Ads token continues to work.
* New: **Meta CAPI server-side** (`SheetLink -> Meta CAPI`). Sends a server-side conversion event to Meta on every successful delivery using a long-lived system-user token. Fires at hook priority 30 on `sheetlink_after_delivery`.

**Operations**

* New: **Email digest cron** (`SheetLink -> Email Digest`). Daily or weekly summary email of submission volume and delivery health. Frequency configurable.
* New: **Webhook health check** (`SheetLink -> Webhook Health`). Hourly cron pings every active rule's destination plus an in-admin "Test now" button. Surfaces a wp-admin notice if a destination has been failing for the last hour. Uses a new `_sheetlink_ping=1` payload marker that newly-generated Apps Scripts handle silently; older deployed scripts ignore it.
* New: **Duplicate prevention** (`SheetLink -> Duplicates`). SHA-1 hash store in transients with configurable key field + window. Modes: suppress (drop the duplicate) or reroute to a `Duplicates` sheet tab.
* New: **Row upsert mode** (per-rule UI on the rule edit page). Per-rule toggle for upsert vs append. When upsert is enabled, the Apps Script scans a chosen key column and updates the matched row in place, falling back to append on no-match. Requires regenerating + redeploying the Apps Script on existing rules.

**New sources**

* New: **User registration sync** (`SheetLink -> User Sync`). New non-form source: `user_registration`. Dispatches a row whenever a WordPress user registers (configurable role filter, configurable fields).
* New: **Form abandonment capture** (`SheetLink -> Form Abandonment`). Frontend partial-capture writes to a transient store on every keystroke; hourly cron sweeps abandoned sessions into a `Abandoned` sheet tab. Defaults OFF (privacy-sensitive).
* New: **ACF + CPT sync** (`SheetLink -> ACF Sync`). Register one source per enabled custom post type (`acf_book`, `acf_event`, ...) via the new `sheetlink_register_dynamic_sources` filter. Fires on post save with ACF field values flattened into the payload.

**New destinations**

* New: **Airtable destination** (`SheetLink -> Airtable`). Free, decoupled from the Integrations Bundle. Mirrors every successful delivery into an Airtable base via the official API. Fires at hook priority 40 on `sheetlink_after_delivery`.
* New: **Notion destination** (`SheetLink -> Notion`). Smart-typed property mapping infers Notion property types from field values (Number, Date, Email, URL, etc.). Fires at priority 45.
* New: **Kit (ConvertKit V4)** added to CRM Fan-Out. Existing HubSpot / Salesforce / Zoho / Pipedrive / Mailchimp / ActiveCampaign destinations continue unchanged.
* New: **SMS / WhatsApp alerts via Twilio** (`SheetLink -> SMS Alerts`). Per-rule condition gating + `{field}` placeholder rendering. Internal-key fields (`_*`) are excluded from rendering. Fires at priority 50 on `sheetlink_after_delivery` so external destinations fan out before notifications.

**Vertical templates**

* New: **Vertical packs** (`SheetLink -> Vertical Packs`). 6 industry-specific rule templates that pre-fill column maps, conditional routing rules, and CRM/destination configuration: real estate, legal intake, healthcare, education, e-commerce quote, agency client intake. One-click apply per pack.

**White-Label hardening**

* New: **Reset to defaults** button on `SheetLink -> White-Label`. Clears the active profile's branding back to "SheetLink" defaults - recovery path when a customer accidentally saves a placeholder string into the menu label.
* Fix: whitespace-only or accidentally-empty values for `plugin_name`, `menu_label`, `menu_icon`, `accent_color`, or `support_email` now fall back to defaults at save time rather than persisting a blank that would render as an invisible sidebar entry. Applies to the legacy single-config and per-client profiles.

**Admin menu de-clutter**

* Renamed: **Multi-CRM Routing** (was "CRM Fan-Out") - matches the sheetlinkwp.com marketing copy. Display label only; the `crm_fanout` slug, option keys, and license entitlement type are unchanged so no data migration is needed.
* New: **Conversions** parent page that groups the four ad-conversion tools (Google Ads, Meta Ads, Microsoft Ads, Meta CAPI) under one sidebar entry with a tab strip at the top of each child page.
* New: **Destinations** parent page that groups the two secondary destinations (Airtable, Notion) under one sidebar entry.
* Tabs: the existing **Dashboard** page now has Overview | User Sync | Duplicates tabs; **Sync Rules** now has Rules | Templates tabs. Each tab links to a child page that keeps its original URL (deep links unchanged).
* Removed from sidebar: User Sync, Duplicates, Templates, Google Ads Conversions, Meta Ads Conversions, Microsoft Ads Conversions, Meta CAPI, Airtable, Notion, SMS Alerts, Upgrade. All ten URLs continue to work; the entries are reachable via their new parent tab strips or via the Features Hub.
* Moved: the **Upgrade** CTA is now a banner at the top of the License page rather than its own submenu entry.
* Result: SheetLink top-level sidebar drops from 21 submenu items to ~12 (or fewer if you've disabled some features via the Features Hub).

**Developer / internal**

* Refactored `SheetLink_Sync_Engine::dispatch()` to a `public static` method. Required so non-form sources (user sync, form abandonment, ACF, vertical packs) can dispatch through the same pipeline without instantiating a second sync engine (which would double-register form hooks). Existing `$instance->dispatch(...)` calls continue to work.
* New filter: `sheetlink_register_dynamic_sources` applied by `SheetLink_Settings::get_all_sources()` so feature classes can register sources at runtime. Static sources cannot be clobbered.
* Features Hub: render code now honors a `default_on` flag per feature, so newly-shipped features can default-enable without a one-shot migration. Used by Formula Guard, Email Digest, Notion, Airtable, Webhook Health, SMS Alerts.
* Hook priority ordering documented on `sheetlink_payload_before_dispatch` (10 ad-conversion columns -> 20 Formula Guard sanitisation -> 50 Duplicate Prevention) and `sheetlink_after_delivery` (30 Meta CAPI -> 40 Airtable -> 45 Notion -> 50 SMS).
* Apps Script generator emits two new payload markers - `_sheetlink_ping=1` (webhook health check) and `_updateMode=1` + `_updateKey=<col>` + `_updateValue=<val>` (upsert). Older deployed scripts ignore both harmlessly; new behavior requires regenerating + redeploying the script.
* 4 new cron events registered, all unscheduled in `sheetlink_deactivate()`: `sheetlink_email_digest_cron`, `sheetlink_form_abandonment_sweep`, `sheetlink_webhook_health_sweep`. Existing `sheetlink_retry_queue` continues unchanged.

= 1.9.3 =
* New: Google Ads offline conversion tracking. Capture GCLID, wbraid, and gbraid from Google Ads traffic; export completed conversions as a formatted CSV for Google Ads offline conversion import. Configure per rule under SheetLink -> Google Ads Conversions.
* Enhancement: wbraid and gbraid click IDs now captured alongside GCLID - required for iOS and in-app Google Ads traffic.
* Enhancement: UTM cookie TTL extended from 30 minutes to 90 days, matching the Google Ads attribution window.
* Developer: new `sheetlink_payload_before_dispatch` filter for extending the outbound payload after data enrichment but before delivery.

= 1.9.2 =
* New: **Auto pass-through field mapping**. Leave the Field Mapping rows empty and SheetLink syncs every form field to your sheet automatically, in form order, header row keyed by field name. Internal WordPress fields (nonces, `_wp_http_referer`, embed post id) are filtered out. Add explicit field-map rows only when you want to pin specific fields to specific spreadsheet columns, skip fields, or reorder.
* New: Nested form-field flattening for Fluent Forms composite fields. The Fluent "Name" field (which emits `names => {first_name, last_name}`) is now addressable as `names.first_name` and `names.last_name` in explicit field maps. The flat `names` key still works and now produces a space-joined display value (was comma-imploded).
* UI: Field Mapping section on the rule setup page now explains the auto-sync default and when explicit mapping is needed.
* Fix: prior to this release, explicit field maps using human-readable labels (e.g. "First Name", "Email") silently dropped values because the dispatcher reads the form's raw payload keys. Existing rules using lowercase payload keys (`email`, `subject`, `message`) continue to work; rules built with the wizard's label keys should be re-saved or simply cleared to use the new pass-through default.

= 1.9.1 =
* New: **Bulk Site Management - Network Template** (Agency / Enterprise / Agency Bundle / Plus). New `Network Template` admin page lets the license owner snapshot one site's rules and settings, publish it as the canonical fleet template via the customer portal, and pull-and-apply it on every other licensed site. Webhook URLs and per-site secrets are deliberately excluded from the template - each site keeps its own destination credentials. Settings are merged; rules are replaced wholesale with local URLs preserved by rule ID.
* New: Backend infrastructure for the **AI Analytics weekly email digest**, **delivery failure alerts**, and **Fan-Out bundle analytics dashboard**. Sites with the corresponding entitlement begin receiving emails on the next cron tick (Mondays 09:00 UTC for digests, every 30 minutes for alerts).
* New: Backend cross-site fleet view at /api/sheetlinkwp/portal/fleet returns the activation roster + 7-day delivery health for fleet-wide reporting.
* Security: SSRF allowlist on the CRM Fan-Out backend - Slack webhooks must end in hooks.slack.com; Salesforce instance URLs must end in salesforce.com / force.com / salesforce.mil; ActiveCampaign API URLs are restricted to *.api-{us1,us2,au,eu}.com. Two-Way Sync inbound relay validates the activation domain before forwarding.
* Security: Zod input validation on the CRM Fan-Out and Two-Way Sync inbound endpoints; malformed requests now return 400 with a precise error message instead of being silently ignored.
* Security: Per-IP rate limit (30 requests/minute) and constant-time secret comparison on the SheetLinkWP admin and scoring-worker endpoints.
* No plugin-side breaking changes. No schema migration on the plugin database (the new fleet-template fields live on the SheetLinkWP backend, not in the WordPress site).

= 1.9.0 =
* New: **Two-Way Sync echo suppression**. When the plugin writes to a Google Sheet that also has the Two-Way Sync onEdit Apps Script wired up, the resulting onEdit echo is now suppressed via a 60-second fingerprint cache. Listeners of `sheetlinkwp_sheet_update` no longer receive the plugin's own writes bouncing back. Eliminates the most common loop-risk scenario on the Two-Way Sync add-on.
* New: **GDPR consent audit log**. Per-submission audit trail of every consent-style field a user ticks. New `wp_sheetlink_consent_log` table captures consent field name, submitted value, form source + ID, anonymized IP, User-Agent, and timestamp. Heuristically detects fields by name (consent / gdpr / privacy / terms / opt-in / accept / agree / newsletter). Surfaced in the WP privacy export and erasure flows so DSAR responses include the consent record alongside the submission data.
* New: **Mailchimp** support in CRM Fan-Out. Add a Mailchimp audience as a fanout destination with a single API key + audience ID. Subscriber status configurable (pending / subscribed / transactional). Default is "pending" for double opt-in - GDPR-friendly out of the box.
* New: **ActiveCampaign** support in CRM Fan-Out. Add an ActiveCampaign account as a fanout destination with API URL + API key. Optional automatic subscription to a numeric list ID. Uses the `contact/sync` endpoint so duplicate emails update the existing contact rather than failing.
* Schema: new `wp_sheetlink_consent_log` table is created on activation and via dbDelta on plugin upgrade. Existing installs auto-migrate on first admin page load.
* Marketing: brings shipped behavior in line with claims in the comparison table on the pricing page (echo suppression, consent logging) and on destination pages (Mailchimp, ActiveCampaign).

= 1.8.4 =
* Fix: Integrations Bundle alt-destinations (Airtable, Notion) now actually fire when configured. Previous releases registered the listener but the sync engine never dispatched the corresponding action - paid alt-destinations were silently inactive. New filterable action `sheetlink_deliver_submission` is dispatched once per rule per submission so add-ons can fan-out alongside the primary Google Sheets delivery.
* Change: Data Enrichment (UTM source/medium/campaign, GCLID, fbclid, msclkid, page URL, submitter IP) now defaults to ON for new installs. Existing sites that previously toggled the option keep their saved preference. The capture is non-blocking and only runs on front-end form submissions.
* New: Delivery logs now include the request payload sent and the response body received (each truncated to 4KB). Failed deliveries previously surfaced only the HTTP code and an error message; admins can now see exactly what was sent and what came back without external tools.
* New: Real exponential-backoff retry schedule (5 minutes, 30 minutes, 2 hours) replaces the previous fixed 15-minute interval. Failed deliveries land in the retry queue with a `next_retry_at` timestamp; the cron handler picks up only rows that are due. Retries now spread over a longer window so customers do not accumulate retry traffic during sustained Google-side outages.
* New: Retry queue table gains `response_body` and `next_retry_at` columns. Existing installs auto-migrate via dbDelta on first admin page load after upgrade - no manual intervention required.
* Developer: Added `do_action( 'sheetlink_deliver_submission', $rule, $payload, $source )` in the sync-engine dispatch path. Listeners receive each rule that matched a submission, the assembled payload (post-enrichment), and the source plugin key. Fires regardless of Sheets primary delivery success so alt destinations are not gated by Google availability.
* Developer: `SheetLink_Logger::log()` and `SheetLink_Retry_Queue::enqueue()` accept an optional payload / response-body parameter respectively; both are stored truncated to 4KB. Backward compatible - existing call sites keep working without the new parameter.

= 1.8.3 =
* New: **Customer portal card** on the License admin screen. Shows account email, two-factor auth status, IP lock mode, and webhook / email notification state. Direct link to sign in to sheetlinkwp.com/account/ via magic link - no WordPress login required to manage billing, cancel subscriptions, or change security settings.
* New: **Agency Overview banner** linking to the portal's cross-site Agency tab. The portal tab rolls up every activated client site across every license on your account with 7-day submission volume, delivery health, and per-site deactivate / reactivate controls.
* New: entitlements response includes a `portal` object with security state (`totpEnabled`, `ipLockMode`, `notifyOnChange`, `hasWebhook`) so the plugin can surface the current setup without a separate API call. Data is cached alongside entitlements (12-hour TTL, auto-refresh).
* Plugin header + Description updated to accurately reflect the 12 core form plugins plus the 5 available through the Integrations Bundle add-on.

= 1.8.2 =
* New: product_created and product_updated event types in WooCommerce Sync. Push product catalogue changes (price, SKU, stock, status) to Google Sheets alongside the existing order and customer events. Enables inventory monitoring, price-change auditing, and catalogue exports.

= 1.8.1 =
* New: Pipedrive destination in CRM Fan-Out. Adds Persons, Leads, or Deals via API token and company domain, alongside existing HubSpot, Salesforce, and Zoho handlers.

= 1.8.0 =
* New: **CRM Fan-Out** admin page (SheetLink → CRM Fan-Out). Adds HubSpot, Salesforce, and Zoho as destinations for every form submission. Credentials stay on your site and are relayed to the CRM API through SheetLink's backend; SheetLink does not retain them. Requires an active CRM Fan-Out add-on.
* New: **Two-Way Sync** admin page (SheetLink → Two-Way Sync). Install a small Apps Script snippet (generated for you) into your Google Sheet and every cell edit flows back to WordPress via a secured REST endpoint. Other plugins can react by listening on the `sheetlinkwp_sheet_update` action. Requires an active Two-Way Sync add-on.
* New: **WooCommerce Sync** admin page (SheetLink → WooCommerce Sync). Push orders (created, completed, refunded) and customers to one or more Google Sheets with a per-event field map. Delivery is non-blocking - checkout is never slowed down. Requires an active WooCommerce Sync add-on.
* New: three matching cards in the Features Hub with lock icons and upgrade links when no license is active.
* Fix: activation of CRM Fan-Out uses the same `fanoutMaxDest` entitlement as Advanced Routing; the pricing distinction is retained for marketing but both unlock the same delivery pathway.

= 1.7.2 =
* New: optional license-activation step in the Setup Wizard. Paste your license key during first-run to unlock premium features immediately. Skip it to finish setup with free-tier features only.
* New: distinguished activation error types. Network / server-reachability failures, bad keys, site-limit exhaustion, and server errors now surface specific messages instead of a generic "activation failed".
* New: post-purchase email with your license key and activation instructions, sent automatically on successful Stripe checkout. Lost keys are still retrievable from the customer portal.

= 1.7.1 =
* Align feature availability with the pricing page. Advanced Routing, Extra Integrations, White-Label, Branded Reports, Agency Dashboard, and Role-Based Menu Visibility now require an active SheetLink license - see [sheetlinkwp.com/pricing](https://sheetlinkwp.com/pricing) for tiers. The core WP-to-Sheets delivery, retry queue, delivery logs, UTM capture, conditional routing, GDPR tools, local analytics, smart mapping, and the guided setup wizard remain free.
* New: **SheetLink → License** admin page for license-key activation. License keys are delivered in your post-purchase email; the customer portal on sheetlinkwp.com can recover a lost key.
* New: Features Hub shows lock icons + "Upgrade to unlock" links on premium features when no license is active.
* New: Backend license system - paste a license key once, the plugin caches your entitlements locally for 12 hours and re-checks automatically.

= 1.7.0 =
* New: Guided Setup Wizard - first-run wizard that walks you from activation to a working Google Sheet connection in three minutes. Generates the Apps Script, sends a test payload, and saves your first sync rule.
* New: Smart Mapping suggestions - local heuristic proposes Google Sheets column headers from your form field names. No external calls.
* New: Advanced Routing - multi-destination fan-out with priority order and optional fallback webhooks.
* New: Local Analytics dashboard - submissions-per-day, peak hours, top forms, duplicate detection, week-over-week trend. All data stays in your own WordPress database.
* New: Branded Reports - generate client-facing HTML reports from your own submission data.
* New: Role-Based Admin View - show or hide plugin sections based on WordPress role (decorative UI gating; WordPress capability checks remain authoritative).
* New: White-Label Admin - replace the plugin menu label and logo for agency installs.
* New: Extra Integrations - JetFormBuilder, Bricks, MetForm, Kali, and HappyForms support.
* New: Agency Dashboard - multisite summary across all network sites.
* New: Features Hub - single control panel to toggle the above features on or off.
* New: Optional Lead Scoring add-on under Analytics. Disabled by default. Requires a SheetLink API token **and** an explicit consent checkbox before any form data is sent to `https://siteanswerai.com/api/sheetlinkwp/lead-score`. Fully disclosed in the External Services section.
* Fix: plugin initialisation moved from `plugins_loaded` to `init` to resolve the "translation loading triggered too early" notice introduced in WordPress 6.7.
* Fix: `uninstall.php` now removes the analytics table and every v1.7 feature flag / config option when the plugin is deleted.

= 1.6.2 and earlier =
* See the full version history in the plugin SVN repository: https://plugins.trac.wordpress.org/log/sheetlink-forms/
