== Changelog ==

The most recent releases are listed in readme.txt. This file keeps the full history.

= 3.9.26 =
* COMPLIANCE (WordPress.org plugin directory): the display name was changed from "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart" to "GoUltra for WooCommerce". The Plugins Team notified us that a third-party trademark (WhatsApp) may not appear in a plugin's display name, and that the previous name was long and keyword-heavy. The new name is the one the reviewer suggested: our own brand first, "for WooCommerce" at the end, which is the directory's approved non-affiliation pattern. The name is updated in the plugin header, in readme.txt (byte-identical) and in the he/ar/es/fr translations. Plugin slug, options, tables and behavior are unchanged, so this is a drop-in update.
* COMPLIANCE: readme tags no longer contain third-party trademark terms (now: woocommerce, abandoned-cart, order-notifications, chat-button, marketing-automation).
* COMPLIANCE: the directory banner artwork no longer contains a third-party trademark word. The plugin icon is our own brand mark and is unchanged.
* REST API: documented explicitly why the single public route (GET /wp-json/goultra/v1/info) uses permission_callback __return_true. It is read-only and returns only data that is already public on the storefront (site title, site URL, shop currency and symbol, locale) plus a boolean connection flag; no personal data, settings, secrets or version numbers. It must be reachable unauthenticated because the currency and language are read to pre-fill the connection screen before any credential exists. The other two routes (/webhook, /sync) authenticate with the per-site shared secret via hash_equals in their permission_callback.
* Descriptive references to WhatsApp remain in the description, features, FAQ, admin screens, screenshots and the whatsapp-notification tag, where they factually state what the plugin integrates with. The directory guidelines permit descriptive trademark use; only the plugin NAME and brand artwork had to change.
* Added an explicit independence statement to readme.txt: GoUltra is an independent software service and is not affiliated with, endorsed by, or operated by Meta or WhatsApp.
* The public /info route no longer returns the connection-status flag. It now returns only the site title, site URL, shop currency and symbol, and locale, which are already public on the storefront. NOTE for the GoUltra backend: if any dashboard code read `connected` from this route, it must use the verify response instead.

= 3.9.25 =
* CHAT BUTTON COLORS: a color picker in the chat-button settings lets the store choose any button color (background of the floating, shortcode and product buttons + the icon accent), with a one-click "Reset to WhatsApp green". Colors are validated hex only (sanitize_hex_color) and rendered as an escaped inline style; the default green keeps using the stylesheet.
* COLLAPSIBLE SETTINGS: every card on the Settings screen can be collapsed/expanded from its header (chevron, RTL-aware); the open/closed state of each card is remembered per browser (localStorage), making the long settings page much easier to scan and work with.
* TEMPLATE PICKERS: the status-to-template and abandoned-cart pickers are now fully localized - template type group labels, language names (21 languages) and style names (Detailed, Direct, Friendly, Sale, Flash Sale, Holiday, New Product Launch, Back in Stock and more) all follow the plugin language instead of raw English. NEW: an eye ("view") button next to every template picker opens a WhatsApp-style preview of the CURRENTLY selected template with highlighted variables, so it is always clear exactly which template was chosen; the Auto option explains what will be sent.
* QUIET HOURS DEFAULT-OFF FIX: the Quiet Hours toggle rendered as ON by default on a fresh install (render fallback 'yes'), so the first Save silently enabled it even though the sending engine default was off. The toggle now defaults to OFF like every other feature (nothing is on until the admin chooses it), and the card text now explains clearly that Quiet Hours only delays abandoned-cart MARKETING reminders: order and shipping confirmations are transactional and are ALWAYS sent immediately, at any hour (a customer buying after midnight still gets the confirmation instantly). Stores that already saved settings may have it stored as on; turn it off once in Settings if unwanted.
* ADMIN NUMBER UNIFIED: the OTP-verified store-manager number and the admin-alert number are one flow now. Verifying fills the admin number automatically (when empty, both in the UI and server-side), a green "Verified" badge shows next to the admin number field whenever its first number matches the verified one, and admin alerts fall back to the verified number when the field is empty. The verified block no longer repeats the same number twice.

= 3.9.24 =
* NEW FEATURE: a free WhatsApp chat button. Adds a floating one-tap "chat with us" button, a [goultra_whatsapp_button] shortcode, and an optional product-page button, so store visitors can message the store on WhatsApp. It is a standard WhatsApp click-to-chat link (wa.me) to the store's own number, works with NO GoUltra account and sends no data to any service (pure front-end), gives every fresh install day-one value, and is fully self-contained (no remote assets, no WhatsApp/Meta logo - a neutral chat glyph in the GoUltra brand). Configurable in GoUltra -> Settings: enable, WhatsApp number, greeting text, button label, screen position (bottom-right/left), where to show (all pages or shop/product), and an optional per-product button that prefills the product name. HPOS-safe, i18n he/ar/es/fr.
* POSITIONING: the plugin listing (title, tags, description, FAQ, screenshot order) and in-plugin wording now LEAD with WhatsApp marketing campaigns (the flagship in-plugin send capability), while keeping the transactional order/shipping and abandoned-cart terms. Title -> "GoUltra for WooCommerce - WhatsApp Marketing, Notifications & Abandoned Cart"; tag "order-notifications" swapped for "whatsapp-marketing"; new campaign + chat-button FAQ entries; campaign composer screenshot moved to position 1. ZERO forbidden marketing/trademark claims.
* Refreshed all 7 wp.org screenshots from the current UI.

= 3.9.23 =
* FINAL wp.org RELEASE-READINESS pass before the SVN publish. Improved the plugin listing DESCRIPTION (header Description + readme short + long description) to a clearer, benefit-led, compliant version: "Automated WhatsApp order, shipping and abandoned-cart messages for WooCommerce, plus opt-in marketing campaigns, powered by GoUltra." (132 chars < 150), with a stronger == Description == intro that keeps the correct "WhatsApp Cloud API from Meta" framing + the transactional-vs-opt-in distinction, and NO forbidden marketing/trademark claims. The header Description is translated in all 4 languages (he/ar/es/fr, .mo recompiled 501x4 plural-safe).
* Verified via a 10-agent release-gate workflow + direct checks: PCP escaping/SQL/remote-assets/error_log clean (no unescaped output, every $wpdb prepared or prefix-literal, no Google Fonts/CDN, error_log gated behind goultra_wc_log/WP_DEBUG, no eval/exec family); readme + headers valid (Stable tag==Version==3.9.23, title byte-matches Plugin Name, 5 tags, 7 screenshots, <10KB); ZERO forbidden claims; SECURITY clean (30/30 admin-AJAX subscriber refusals, 8/8 secret-leak probes, nopriv + REST + new campaign paths all green - no reopened hole); runtime health (php -l 24 files + node --check 4 JS clean, all 6 admin screens render en_US + he_IL with 0 notices, clean activation = 4 tables + 12 seeded templates + 192-bit webhook secret, plurals load both forms, 0 em-dash across all 46 shipped files incl the .mo binaries).

= 3.9.22 =
* SAFETY (owner: "after a successful send all fields stayed selected so you can send again and again - dangerous"). After a confirmed launch/schedule the composer now RESETS via a new resetComposer(): SELECTED={}, template select cleared + selectTemplate('') (hides vars/preview/header), name + header + search cleared, audience back to "all", every recipient row unchecked, counts refreshed. The launch handler's `if(!current)` guard then blocks any further click (no template selected), so the SAME campaign can never be re-fired by accident. This is on top of the existing server guards (2-min transient lock + client_ref idempotency 24h + backend 120s name+template+count dedup + the shared send engine's opt-out/quota/per-phone brakes). VERIFIED end to end in headless: select template + recipient -> launch -> exactly 1 send, template value cleared, 0 recipients selected, 0 checkboxes, success message shown.
* RTL: campaign HISTORY table cells used `<td dir="ltr">` for the date + "sent / total", which left-aligned those cells while the headers were right-aligned (start) - so the values did not line up under their headers in Hebrew/Arabic. Now the LTR text is isolated in a `<bdi dir="ltr">` and the `<td>` keeps the table's start-alignment, so every column lines up under its header. Verified in a he_IL render.
* RTL: the self-test tip under the recipient list was squeezed to the side and left-aligned (text-align:end); now it wraps to its own full-width line and reads in the document direction (right-aligned in he/ar). No i18n changes.

= 3.9.21 =
* CAMPAIGN SENDING FULLY ENABLED end to end. The GoUltra backend (worker v2.8.975, 2026-07-19) shipped the number-aware `/api/v1/wc/campaigns/*` family (queue-send / scheduled / status / pause / cancel), authed via the SAME `_wooAuth` resolver as `/api/v1/wc/subscribers` + `/api/v1/wc/templates`, so the store's existing per-number `gk_` key (scope `plugin:woo`) now works for campaigns. LIVE-CONFIRMED: all 5 routes return 401 (exist, need auth) instead of 404. No plugin routing change needed - v3.9.19's number-aware-first logic hits them directly; the worker's response shape (campaign_id/id + total/accepted/queued) matches what ajax_send already reads, and it forces the send onto the key's bound number (a store can never send from the wrong number). client_ref idempotency (already sent) is honored by the backend (24h) with a 120s name+template+count backstop.
* Clearer errors for the real send: friendly_error now also maps the `plugin:woo` scope gate ("...WooCommerce / Shopify plugin service enabled") to the connection-help message, and `MARKETING_DISABLED` (403) to a plain "marketing sending is turned off for this number, enable it in the GoUltra dashboard" message. i18n he/ar/es/fr (+1). Verified: friendly_error mappings + plurals load, php -l clean, 0 em-dash.
* NOTE: a store must be on v3.9.19+ for campaigns to work (that is the version that calls the new number-aware route); the worker did NOT change the legacy `/api/campaigns/*` (still account-key only). So the owner installs 3.9.21 once and campaigns work - BOTH sides are ready now.

= 3.9.20 =
* DEEP PRE-INSTALL VERIFICATION round (owner: "verify deeply before I install, each install wastes my time"). Ran a LIVE unauthenticated route-existence probe of go.goultra.ai (safe: no key, rejected at the auth gate) - DECISIVE: /api/v1/wc/verify + /subscribers + /templates/submit = 401 (exist, accept gk_), /api/v1/wc/campaigns/* = 404 (NOT built yet), /api/campaigns/* = 401 (exist, reject gk_). So campaign send is blocked ONLY on the backend route (worker action), not a plugin bug; the plugin's v1-first/404-fallback is correct and forward-compatible. Then ran a 6-dimension multi-agent verification (campaign contract, regressions, send-safety, templates, i18n, security) + adversarial confirm. Security posture HOLDS (14/14 subscriber-refusal runtime probe, no reopened holes, no new SQL/XSS/secret leak); i18n/render/plurals/em-dash all clean. Fixed the 2 real findings:
* i18n: the campaign header-image picker alert "JPEG or PNG images only." (guCamp jpegOnly) was missing from all 4 .po -> shown in raw English on he/ar/es/fr stores. Added translations (he תמונות JPEG או PNG בלבד. etc.), .mo recompiled (500 entries x4, plural-safe).
* REGRESSION fix: ajax_disconnect wiped api_key BEFORE calling clear_verified(), so the store-manager unsubscribe HTTP had no valid key and silently failed - the number was cleared locally but left on the backend marketing list. Now clear_verified() runs FIRST (while the key is valid); verified the unsubscribe fires with the old number then the key is wiped.
* Not fixed (deferred, not an install blocker): the Logs admin screen renders LTR under he/ar (pre-existing partial-i18n gap, 0 notices, functions fine). NOTE: 4 of the 6 verification agents returned placeholder output / one errored; those areas (campaign payload contract, send-safety, templates) are re-covered by prior probes this project (ac-flow 14/14, campaign one-POST, param_count 5/5, custom-area 13/13) which all still pass on this build.

= 3.9.19 =
* CAMPAIGN "Missing API key" on send (owner live-test; the store IS connected and order messages send fine). ROOT CAUSE: same class as the old template-build gap. Campaign routes were called under /api/campaigns/* which authenticate with the ACCOUNT sk_live_ key only (validateApiKey) and reject the store's PER-NUMBER gk_live_ key with "Missing API key" - whereas order sends (/webhook/woo) and the subscriber list / templates (/api/v1/wc/*) accept the per-number key, which is why only campaigns failed. FIX: GoUltra_API::campaign_request() now calls the number-aware /api/v1/wc/campaigns/* route FIRST (queue-send / scheduled / status / pause / delete), and falls back to the legacy /api/campaigns/* route ONLY on a 404 (route not present) - a 404 means nothing was processed, so this can never double-send. Once the backend accepts the per-number key on either route, campaigns work with no further plugin change. Verified in the harness: queue_campaign hits /api/v1/wc/campaigns/queue-send, exactly 1 POST, no fallback on success.
* Clearer error: a "Missing/invalid API key" from a feature route (not the connection itself, which is verified separately) now shows a plain message - the connection is fine, campaign sending for this number may not be enabled yet on the GoUltra account, contact GoUltra support - instead of the raw "Missing API key" string, and it never implies the stored key is wrong. i18n he/ar/es/fr (+1).
* NOTE on testing honesty: the local harness intercepts outbound HTTP and returns a canned success, so it validates the plugin's SEND LOGIC (dedup, counting, no empty-Bearer, one POST) but CANNOT detect the real backend rejecting the key on a specific route - that is a live-only signal. This build routes to the number-aware surface; if a store still sees the message, the GoUltra backend must enable the per-number key on the campaign route (same fix the worker applied to /api/v1/wc/templates/submit).

= 3.9.18 =
* CAMPAIGN VARIABLE MAPPING bug (serious - owner could not choose what to insert for a template variable). ajax_templates computed param_count by scanning the cached body ONLY for NAMED vars ({{first_name}}) via /\{\{\s*([a-z_]+)\s*\}\}/i. But after a status sync the cached body_text holds the NUMBERED form ({{1}}) that Meta returns, which that regex never matches -> param_count=0 -> the composer rendered ZERO variable rows (nothing to choose) and the preview kept the raw "{{1}}". FIX: after the named->numbered pass, also preg_match_all the final body for numbered placeholders and take the highest index, so param_count is correct whether the body is named OR already-numbered. Verified 5/5 (post-sync numbered=1, named single=1, named double=2, numbered-gap {{1}}..{{3}}=3, no-vars=0) + a live headless drive (select template -> the "Variables" row with the name/full/fixed select appears, preview replaces {{1}} with the sample name).
* CAMPAIGN compose layout: the preview was pinned too far to the side with unused space. Widened the grid cap 900px->1080px, preview column 320px->340px, gap 28px->40px so the live preview uses more of the space and sits comfortably beside the fields (responsive collapse unchanged).

= 3.9.17 =
* CUSTOM TEMPLATES area: added a search box (filters by name / body text / language / status) and a per-row "View" button that opens a read-only WhatsApp-style preview modal (store avatar + header media placeholder or header text + body with sample values + footer + button + template name; ESC / backdrop / x to close). Owner: "why can't I view or search a custom template?".
* CAMPAIGNS layout: the compose grid was minmax(0,1fr) 300px with no cap, so on a wide screen the form column stretched to the full page width and shoved the 300px preview far to the side. Now the grid is capped (max-width:900px, preview column 320px) so fields keep a readable width and the live preview sits right beside them. Owner: "the campaign name field is too long, pushing the preview too far left".
* MARKETING LIST: a verified store-manager number that is no longer the current verified admin is now removed automatically. On re-verifying a DIFFERENT number, ajax_check first unsubscribes the previous admin_phone_verified number (remove_store_manager -> unsubscribe_subscriber); ajax_disconnect calls the new GoUltra_Admin_Verify::clear_verified() which unsubscribes the manager number and clears admin_phone_verified/_at. Owner: "in the marketing list the admin is no longer verified; in that state it should be deleted automatically".
* i18n he/ar/es/fr (+5: View, rejected, search placeholder, no-matches, Close). Verified: 5/5 store-manager probes (clear_verified + re-verify unsubscribe old number + no-op guards), node --check on the real rendered wizard JS, headless screenshots (custom list with View modal in he_IL, balanced campaigns layout), plural-safe .mo (498 x4), php -l clean, 0 em-dash.

= 3.9.16 =
* SECURITY HARDENING from a full security audit (12-agent Workflow: unauth surface / authz+CSRF / injection / secrets+PII / compromised-account blast radius / 6 runtime probes, then adversarial verify per finding; ~2.0M tokens). HEADLINE: the plugin is NOT a site-compromise vector and creates NO cross-tenant path - no SQLi (all $wpdb->prepare / array APIs, table names are prefix constants), no XSS (backend/inbound values sanitized on write AND escaped at every sink), no auth bypass, no CSRF (every admin AJAX = check_ajax_referer('goultra_admin_nonce') + current_user_can('manage_woocommerce'); CSV export adds check_admin_referer), no SSRF/open-redirect/RCE/file-include, no privilege escalation (a subscriber/customer reaches nothing privileged), no hardcoded secrets, and one store's per-site key/webhook_secret cannot address another tenant. Fixed the confirmed findings:
* [MEDIUM] Unauthenticated marketing-consent FORGERY: the nopriv ghost-capture (goultra_capture_checkout) let anyone submit a victim's phone with optin=1, and schedule_sequence() then called push_cart_grant() which DURABLY added that number to the warm list as state='granted' (source woo_abandoned_cart) - a permanent, unbounded campaign subscription for a number the sender never proved they own (forged GDPR/ePrivacy consent + future charge/harassment). FIX: removed the abandoned-cart warm-list grant entirely. Durable campaign eligibility now comes ONLY from a VERIFIED signal - a completed order with _goultra_whatsapp_optin=yes (maybe_push_order_consent, source woo_checkout, still works) or the OTP-verified store-manager number. The bounded 3-step recovery still runs for a genuine abandoner; it just never mints a durable subscriber.
* [LOW] webhook_secret was written verbatim into wp_goultra_webhook_logs.request_body on every send (log_webhook masked only customer_phone) - queryable/exportable/viewable secret sprawl. FIX: log_webhook redacts wc_webhook_secret to '[redacted]' before insert (the real body still goes to the backend).
* [DEFENSE] The Settings API-key field echoed the stored key into the page HTML (value="..."), so it could leak via a screenshot / shared screen / view-source (this owner screenshots the admin constantly). FIX: the field never pre-fills the key (value=""); the stored key is no longer read into any page variable. It stays in wp_options + is used server-side only.
* REJECTED by adversarial verify (NOT vulns, left as-is): a compromised manage_woocommerce admin doing admin things (standard WP; a shop_manager can already export customer PII natively, an admin can install a plugin = RCE) - the plugin adds no amplification and applies least-privilege; the nopriv goultra_run_queue endpoint (only runs already-due Action Scheduler actions behind a 10s lock, exactly like a wp-cron tick, no attacker input, no amplification).
* Verified: 7/7 new probe-sec-3916.php asserts (AC schedules but grants NO durable subscriber; a real consented order STILL grants; secret redacted in logs; stored key absent from settings HTML), 14/14 ac-flow asserts still pass, php -l clean, 0 em-dash. No i18n changes (all three fixes are silent/internal). Owner-facing note: to protect a store whose WP is compromised, the real security boundary is server-side (per-tenant authorization + the ability to REGENERATE the per-number key in the GoUltra dashboard, which the plugin already surfaces via the connection-problem notice); optional future defense-in-depth = a dedicated manage_goultra capability so plugin access can be revoked independently of manage_woocommerce.

= 3.9.15 =
* SCROLL: the Templates screen always opens pinned to the TOP. Root cause of the "blank white page" the owner hit: reloads (Sync Status button, post-submit reload) made the browser RESTORE the previous deep scroll offset onto the short step-1 view, so the viewport landed in empty space below the content. Fix: history.scrollRestoration='manual' + scrollTo(0,0) on load, guTop() on EVERY step transition (type/language/back/custom), and the language-search focus uses focus({preventScroll:true}) so it cannot yank the viewport.
* RTL: the forward chevron on the template type cards now points LEFT in Hebrew/Arabic (transform:scaleX(-1) when $gu_rtl; verified 8 cards flipped in he render, 0 flipped in en render).

= 3.9.14 =
* CUSTOM TEMPLATE AREA fixes (owner live-test round on 3.9.13):
* Status banner in the custom builder showed an UNRELATED marketing template ("approved 1 woo_marketing_offer_he_...") - the banner was computed only in the standard-styles branch and the custom branch kept whatever was rendered last. Now a shared showStatusBanner() renders per-area: the custom branch computes it from the CUSTOM templates of the selected language (approved/pending counts + names, words translated via guT.approved/pending instead of hardcoded English).
* Custom templates screen showed "no local custom templates" after building a MARKETING-category custom template (the row is template_key=marketing_offer, so the old custom%-only filter missed it; it also matched no preset card) - the owner could not tell the submission went through. The list now includes builder-made marketing rows: style_key='custom' rows, plus legacy style-less rows whose body does NOT equal any preset (preset-built rows stay on their style cards). Rich submit now sends style_key='custom' for builder MARKETING templates so the backend names them woo_marketing_offer_custom_{lang}_{ts}; sync's known_styles includes 'custom' so the identity survives every status sync.
* Smart starter text: picking a product in the builder now pre-fills the message (only when empty) with the localized "new product" starter + the product title, on top of the image header, "View product" button and name pre-fill.
* i18n: he "Footer"/"Footer text (recommended)" were transliterated ("פוטר") - now proper Hebrew ("טקסט תחתון"); msgid "Image" was missing in ALL 4 languages (the header dropdown showed English "Image" between Hebrew options) - added he/ar/es/fr.
* Verified: 13/13 harness asserts (_localtest/test-custom-area.php: list include/exclude per row origin, banner per-area + translated, starter-text hook, sync extraction of 'custom', Image translated, zero "פוטר", 0 render notices), node --check on the real rendered wizard JS, php -l clean, .mo recompiled plural-safe (493 entries x4), 0 em-dash.

= 3.9.13 =
* SEND-SAFETY HARDENING for the two sensitive areas (campaigns + abandoned cart), from a 28-agent adversarial audit (5 dimensions x deep read + adversarial verify + 6 runtime probes on the real WP harness). Fixes the confirmed findings:
* Abandoned-cart KILL SWITCH now matches capture-stored phones. The capture layer stores '+E164' (leading '+') but the kill switch built only plus-less candidates, so a paying customer whose email did not match kept getting the 3-step "you left items in your cart" sequence + a coupon. Now matches a de-duplicated candidate set (digits, '+'digits, raw) - a completed purchase reliably stops the sequence by phone OR email.
* Campaign AMBIGUOUS-TIMEOUT no longer invites a duplicate blast. A transport timeout (code 0) is treated as UNKNOWN, not a plain failure: the double-launch lock is kept, a provisional "unknown - verify before resending" history row is recorded, an idempotency key (client_ref UUID) is sent for backend de-dup, and the admin is told to check before relaunching. Definite backend rejections still release the lock and let the admin retry.
* Campaign HISTORY count = backend-ACCEPTED recipients (reads total/accepted/queued from the response), not the raw submitted list, so opt-out-filtered recipients no longer read as delivery failures.
* API-CONNECTION VALIDITY (owner: "no possibility of an invalid API connection"): (a) send_event bails when disconnected/empty key, so a disconnected store never POSTs an empty "Bearer " carrying the webhook secret (was reproduced in probe P4); (b) consecutive auth failures (401/403/"missing/invalid api key") raise a visible "connection problem - reconnect" admin notice after 3 in a row, so a regenerated/revoked per-number key can no longer show a green "Connected" while silently dropping every message; the flag clears on the next good send or on reconnect, and NEVER auto-disconnects (a transient outage must not clear the connection).
* ABANDONED-CART CONCURRENCY: scan_and_schedule now ATOMICALLY claims each cart (UPDATE ... WHERE sent_to_goultra=0, proceed only if 1 row changed), and the recurring scanner is scheduled with $unique=true - so overlapping runners (wp-cron + the instant queue kick) can never give one cart two step sequences.
* RATE LIMIT reworked: fixed per-minute window keyed by (endpoint, event, minute) with a per-event cap of 200 - the old shared 60/min bucket renewed its TTL on every hit (so it never reset under load) and let marketing bursts starve order/shipping. Each event type now gets its own headroom and the window cannot slide.
* AC COUNTING: a step is marked sent + counted only when a message was really sent - if the backend explicitly reports no message (meta_sent/sent/skipped false), the step is not stamped (so it can still go out once the template is approved) and the count is not inflated. No explicit signal => 2xx still counts as sent (safe default).
* Recovered-cart REVENUE is attributed to at most ONE analytics event per order (coupon OR restore-link, not both).
* DELIVERY-STATUS webhook: replay de-dup by (message id + status) so a retried/replayed receipt cannot add duplicate order notes; the logged phone is masked; inbound rows now respect the 500-row cap (they previously bypassed it).
* Anti-flood: ghost-capture caps active (unrecovered) rows per phone at 3 across sessions, so a bot rotating WooCommerce sessions cannot aim repeated marketing at a victim number.
* Verified: 14/14 abandoned-cart flow asserts + 11/11 new hardening probes (kill-switch +E164 match, disconnected 0-HTTP + not_connected, atomic scan claim = exactly 1 sequence, separate rate buckets, response-not-sent gate, per-phone cap), campaign launch probe = one queue-send POST, all 4 admin screens render he_IL with 0 notices, plurals preserved in the recompiled .mo, php -l + node --check clean, 0 em-dash. i18n he/ar/es/fr (+5). Rejected (not real, per adversarial verify): 'selected'-mode phone trust (admin already holds the key), non-atomic launch-lock race (backend 120s dedup covers), 'all'-mode cap on missing summary (backend always sends it).

= 3.9.12 =
* Marketing templates behave per-STYLE, exactly like the standard types (owner: "same approved template showed on every design card; order confirmation does it right, everything must work the same"). Root cause: the marketing submit never sent style_key, so the backend named templates woo_marketing_offer_{lang}_{ts} (no style segment) and the 3.9.11 fallback blanket-marked ALL style cards Active. Fix: (1) the marketing .gu-pick submit now sends style_key (sale/flash_sale/holiday/launch/back_in_stock); ajax_submit_rich_template forwards it in the payload - the SAME field the standard saved-row submit sends, and the backend embeds it in the name (nameParts.push(styleKey)) -> woo_marketing_offer_{style}_{lang}_{ts}; the local cache row stores the real style_key too. (2) sync_remote_statuses style extraction now knows the marketing styles and matches the LONGEST style prefix of the name remainder (flash_sale/back_in_stock carry underscores; the old explode('_')[0] could never match them). (3) JS per-card matching: name fallback anchored to the full woo_{type}_{style}_ prefix (a bare indexOf('_sale_') would also hit flash_sale names); pre-3.9.12 style-less marketing templates are assigned to their style card by comparing the stored body against the preset text (variables, bidi marks and whitespace stripped), so exactly ONE card shows Active/Pending and the other designs stay buildable.
* Card matching uses status+recency priority (approved > pending > rejected, newest wins) across BOTH matchers - found by the adversarial review: a reject->resubmit cycle leaves TWO rows for one style (Meta keeps rejected templates, sync mirrors both), and the old first-match would keep showing the stale rejected row over the newer pending/approved resubmission (also improves the standard types in the same scenario). Accepted trade-off (low, by design): a rich-custom-builder MARKETING template whose body is byte-identical to a preset claims that preset card - body equality is the only signal for legacy style-less rows, and blocking a content-duplicate is exactly the owner's requested behavior.
* uninstall.php completeness: also unschedules goultra_async_consent/goultra_async_redact, drops wp_goultra_campaigns, and deletes the _goultra_av_token user meta.
* Verified: 8/8 PHP style-extraction asserts + 10/10 node asserts running the REAL rendered wizard JS (legacy body-match incl. RTL-mark noise, styled match, resubmit priority, anchored prefix no-collision, standard regression), 0 render notices he_IL, headless screenshot: sale=Active(legacy name), flash_sale=Pending, others buildable.

= 3.9.11 =
* Marketing (marketing_offer) preset cards now reuse the EXISTING per-template active/pending/rejected state instead of a confirm dialog. Because a marketing template's backend name (woo_marketing_offer_{lang}_{ts}) carries no style segment and sync always nulls its style_key, per-style matching cannot work; so when any marketing template of the language is approved/pending/rejected, every preset card shows that state (Active template / pending / rejected) at the type level, preventing accidental duplicates. Reverted the 3.9.10 confirm() guard + its guT.dupWarn string (owner: do not reinvent the wheel; reuse what works in the standard types).

= 3.9.10 =
* RTL fix: the masked verification phone number in the admin-verify status message is wrapped in a bidi isolate (U+2066..U+2069) so it renders LTR inside Hebrew/Arabic. * Duplicate guard: the template builder's .gu-pick submit (marketing_offer + standard) now confirms before creating another template when an approved/pending one of that type+language already exists.

= 3.9.9 =
* New: product marketing template builder (owner's design, simpler than a dynamic send-time button). In the custom/marketing template builder a "Building a template for a product?" picker (reuses the campaigns `goultra_camp_products` AJAX, which returns image + permalink) lets you pick a store product: it sets an IMAGE header and adds a "View product" URL button whose value is the product's static permalink (baked into the template), and pre-fills the template name. Pick a language, submit, wait for Meta approval, then send a campaign for that product. The product link is STATIC (per-product template), so it needs only that the backend creates a URL button from the button the plugin already sends (button_text/button_url + buttons JSON) - NO dynamic {{1}} suffix, no send-time button param. i18n he/ar/es/fr (+4, 482/482). php -l + real rendered wizard JS node --check clean, 0 em-dash, templates render he_IL 0 notices.
* NOTE: whether the "View product" button actually appears depends on the DEPLOYED backend forwarding the button on /v1/wc/templates/submit (the reference src/index.js drops it - the "B1" gap in the relay). The owner's live test of any URL button in the builder confirms it; if buttons are dropped, it is a one-line backend fix (forward button_text/button_url -> a Meta URL button), unblocking this + the abandoned_cart {{checkout_url}} button too.

= 3.9.8 =
* New: admin-number WhatsApp verification (OTP). New GoUltra_Admin_Verify class + API methods (submit_auth_template / fetch_auth_templates / send_auth_code over /v1/auth/*, which accept the store's per-number gk_ key via its OTP scope). On connect, the plugin auto-builds the Meta AUTH-category template so it is ready. In Settings -> Admin Notifications the owner enters their number, receives a 6-digit code on WhatsApp, and confirms it. Security mirrors the GoUltra Auth plugin (Mode A): CSPRNG code, HMAC-hashed (peppered), single-use, 5-minute TTL, hash_equals, capped attempts, rate-limited; the code is generated and verified in the plugin, GoUltra only delivers it. A verified number is stored (admin_phone_verified) and registered as a "store manager" granted subscriber so it is selectable as a campaign recipient for the owner's own tests. If the AUTH template is still pending Meta approval, the owner gets a clear "try again shortly" message. i18n he/ar/es/fr (+23). NOTE: depends on the deployed backend accepting the gk_ key on /v1/auth/* (the owner's key has the OTP scope; the Auth plugin proves these endpoints work).

= 3.9.7 =
* Campaign recipient selection rebuilt as a fully JS-controlled instant toggle (div rows, synchronous click/keyboard, ARIA checkbox) instead of native checkboxes in a wrapping label. On a heavy site (page-cache / many plugins) the old label-to-checkbox forwarding lagged; the new toggle marks a contact immediately and never affects another row. Selection persists across search and Select-all/Clear.
* New "Campaign name (optional)" field; stored in wp_goultra_campaigns (idempotent column migration) and shown in the history so campaigns are easy to recognize.
* Header image picker: chosen image shows as a thumbnail with Remove; Upload / Media Library and Pick a product are the primary path; the raw URL field is hidden behind an "Or paste an image URL" link.
* Cleaner WhatsApp-style media placeholder (SVG, not emoji/broken-image) in the campaign and custom-template previews for image, video and document headers; a broken image URL falls back to the placeholder instead of a broken-image icon.
* Connection screen: API-key placeholder is now gk_live_xxx and the hint notes the key starts with gk_ (per-number keys). i18n he/ar/es/fr.

= 3.9.6 =
* Fixed campaign recipient selection. The chosen recipients are now held in a persistent selection model instead of only in the visible list, so: searching or filtering the recipient list no longer clears prior selections; a contact you selected but then hid with a search is still included in the send; and selecting one contact never affects another. Select all adds every currently-shown contact; Clear drops the whole selection.
* Note on template building: since 3.9.5 custom and marketing templates submit through the same dedicated WooCommerce template endpoint that standard order and shipping templates already use, so a store that can build those can build marketing templates the same way. If a submission is still rejected, the plugin shows the exact reason from GoUltra.

= 3.9.5 =
* Fixed custom and marketing template submission that could report "saved locally" with a missing-key error: templates are now submitted through the dedicated WooCommerce template endpoint (POST /api/v1/wc/templates/submit), which accepts the store's connected key and resolves the linked WhatsApp number, instead of the account-only rich endpoint.
* The template and campaign preview now shows your live WhatsApp business profile picture, refreshed automatically (the signed URL expires after a short time), with the site icon as a fallback and the initial letter as a last resort.
* Editing a subscriber's name now tells you clearly when the number is not on your marketing list, instead of appearing to save.
* If a submission is rejected, the exact reason from GoUltra is shown.

= 3.9.4 =
* Rebuilt the custom and marketing template builder to match the GoUltra system, after studying the actual template pipeline:
* Header now supports Text, Image, Video and Document (was Image only). For media you pick the actual file when sending a campaign.
* Marketing templates now always get the required opt-out, in your language: a locked footer line (or an unsubscribe button), matching the wording the service recognizes.
* Full button builder: up to 10 buttons, each a Quick reply, URL link or Phone number.
* A real WhatsApp-style live preview with your business name and profile picture, and the correct right-to-left layout.
* Templates are now submitted through the full template endpoint (header types, buttons and opt-out), with a fallback, and the exact reason is shown if Meta rejects a template.

= 3.9.3 =
* Reports (Analytics) tab is now labeled in your language, and the top navigation is consistent (no odd single icon).
* Campaign recipient list redesigned: each contact shows a clean avatar with the name and number stacked together (no more large gap). Selecting a contact is clear.
* Fixed the template dropdown arrow on right-to-left screens (it now shows on the correct side).
* Scheduling improved: the send button changes to "Schedule campaign" when you pick a later time, with a clear line showing when it will go out; tidier date and time fields.
* Marketing List: slimmer search field, and you can now edit a subscriber's name inline (name only); the change is sent to your GoUltra contacts.
* When a template submission is rejected, the plugin now shows the exact reason from GoUltra instead of a generic message.

= 3.9.2 =
* Campaign preview redesigned to look like a real WhatsApp conversation: your store's profile picture (the site icon) and WhatsApp display name in the header, a date chip, a message bubble with a tail, read checkmarks and a proper button row.
* Recipient picker redesigned: clean rows with an avatar initial, the name and the number in separate aligned columns, a selected counter, and select-all/clear.
* New on the Marketing List: an "Add my number" button that adds your own number to the list as a subscribed contact. To test a campaign on yourself, select only your number on the Campaigns screen. The separate self-test checkbox was removed.

= 3.9.1 =
* Marketing templates now have a dedicated "Marketing Offer" type in the Templates tab: 5 ready-made promotional styles in 21 languages, with the required opt-out footer added automatically. Approved ones appear in the campaign picker.
* Campaigns: choose to send to all subscribers or pick specific contacts, and optionally send a test to your own admin number. The separate test-send box was removed. The campaign template picker now reads from the local template cache, so it no longer shows a connection error.
* Nothing is enabled automatically on install. Order, shipping, admin and abandoned-cart notifications now start OFF until you turn them on.
* The plugin name and description now display in your site language (Hebrew, Arabic, Spanish, French) on the Plugins screen.
* Cleaner top navigation, a simpler "delivery receipts and product sync" section (these work automatically, nothing to paste), and a fixed store icon in the campaign preview.

= 3.9.0 =
* New: a Campaigns screen (GoUltra menu). Compose a WhatsApp marketing campaign for your Marketing List subscribers right from WordPress: pick an approved marketing template, map its variables (customer name, full name or a fixed value), attach a header image from the media library, a product or a link, see a live WhatsApp-style preview, send a test to your own phone, and send now or schedule for later.
* Campaign history with live progress (sent and failed counts), plus pause and cancel controls.
* New: create marketing templates from a built-in library of 5 promotional styles in 21 languages, with the Meta-required opt-out footer added automatically in the template's language. Templates go to Meta for approval and appear in the campaign picker once approved.
* Every campaign send is filtered against unsubscribes twice on the GoUltra service: once when the campaign is submitted and once more per message right before delivery.
* Listing and readme refreshed for clarity and discoverability. No functional change to existing workflows.

= 3.8.3 =
* Removed the test-send bar from the Settings footer (it required an approved template to be useful and confused more than it helped).
* The page header now shows the full menu (Settings, Templates, Marketing List, Analytics, Logs, Docs, Support) on both the Settings and Marketing List screens.
* All dropdown selects now always show a visible arrow, in both LTR and RTL, even when the site's theme or another plugin strips the native one.
* Marketing List: the Date column now shows a real date (the service reports the consent time as a timestamp; it was displayed raw).

= 3.8.2 =
* Marketing List: full right-to-left layout for Hebrew and Arabic and a redesigned screen (summary cards, a cleaner table and clearer actions) matching the rest of the plugin.
* New list settings: choose whether the contact name sent to your GoUltra list is the full name, the first name only or the last name only, and whether the email address is included. Applies to shoppers added from now on.
* Settings: a new "Active now" card summarizes exactly which notifications are currently enabled (customer messages per status, admin alerts, abandoned-cart steps, quiet hours and the marketing list).
* The test-send bar is now clearly labeled as optional.

= 3.8.1 =
* Clarified wording on the listing: describes the WhatsApp Business Platform (the WhatsApp Cloud API from Meta) accurately, and states that abandoned-cart recovery requires an explicit marketing opt-in at checkout.

= 3.8.0 =
* New: a Marketing List screen (GoUltra menu). Shoppers who give explicit WhatsApp marketing consent at checkout are now collected into a warm subscriber list you can search, export to CSV, and unsubscribe from. This list is your per-store marketing audience for campaigns.
* Consenting shoppers are recorded automatically: buyers who tick the marketing opt-in, and shoppers who consent but abandon their cart. Everything runs in the background so checkout is never slowed.
* Unsubscribe removes a contact from marketing only. Order and shipping (utility) updates still reach them, in line with WhatsApp and consent rules.
* Privacy: a WordPress "erase personal data" request now also removes the contact from the GoUltra marketing list, not just the local record.

= 3.7.16 =
* Fix: order and shipping WhatsApp messages now go out within seconds of the order. The queued send is kicked immediately by a non-blocking loopback instead of waiting for the next traffic-dependent wp-cron tick (which could take several minutes on a quiet store).

= 3.7.15 =
* Fixed: the WhatsApp template chosen for an order status or admin alert could be lost after a template sync, so the notification stayed enabled but silently did not send and you had to re-select it. Template ids are now kept stable across syncs, so your saved selections persist.
* Fixed: admin notifications could be silently skipped when no specific template was mapped. Admin alerts now fire on the toggle alone (the backend picks the admin template), consistent with customer notifications.
* Added a warning on the Settings screen when a notification is enabled but you have no approved WhatsApp templates yet, so it is clear why messages are not sending (create and approve a template in the Templates tab).
* The connection now shows a clear message when the GoUltra subscription is not active, instead of a raw error code.
* Security and compliance hardening: the REST webhook secret is accepted only via the Authorization header (no longer as a URL query parameter), plus minor WordPress.org Plugin Check cleanups.
* Updated the API key field hint to match the current GoUltra key format.

= 3.7.13 =
* Fixed the submission error "Your plugin and author URIs are the same": the Plugin URI now points to the plugin's WordPress.org directory page and the Author URI to the author site, so the two headers are distinct as required.

= 3.7.12 =
* WordPress.org review compliance: the admin CSS and JavaScript are now enqueued instead of printed inline (no inline style or script tags). Removed the "Powered by GoUltra" line from the message footers, so it is no longer added to customer messages by default. Added the "Requires Plugins: woocommerce" header and corrected the Plugin URI.

= 3.7.11 =
* Fixed: the Templates screen could reload itself a few seconds after opening (the background status sync refreshed the page), which sent you back to the start if you had already begun creating a template. The status sync now runs quietly in the background and never reloads the page; use the "Sync Status" button for an immediate refresh.
* Fixed: in a right-to-left admin (Arabic / Hebrew) the back arrow in the WhatsApp template preview pointed the wrong way; it now points correctly.
* Hebrew starter templates now use the proper gershayim character in the "total" abbreviation.

= 3.7.10 =
* Made the plugin display name in readme.txt match the plugin header exactly (WordPress.org submission consistency). No functional change.

= 3.7.9 =
* WordPress.org readiness + privacy: removed all remote Google Fonts from the admin screens (the UI now uses your system font stack), so the plugin makes no third-party requests for assets. This is faster, fully private (no data sent to Google), and resolves a common GDPR concern.
* Code-quality / Plugin Check hardening: tightened output escaping in the logs and templates screens, added input unslashing on a couple of admin save paths, gated internal debug logging behind WP_DEBUG, sanitize incoming template data on write (defense in depth), and added capability checks directly on the admin page renderers. No behavior change for store owners.

= 3.7.8 =
* Security hardening (from an adversarial penetration-test style review of the unauthenticated attack surface): the public abandoned-cart capture endpoint now rejects empty or zero-value carts, reducing automated abuse. Added defense-in-depth capability checks on the admin page renderers and extra output escaping in the Templates wizard. No vulnerability that could expose or compromise the host site was found; these are preventative hardening measures.

= 3.7.7 =
* Abandoned cart compliance (GDPR + ePrivacy): the plugin now stores a shopper's contact details for abandoned-cart recovery ONLY when they give marketing consent at checkout. Without consent, no personal data is kept for marketing. Consent is also re-checked at send time, so withdrawing consent stops any pending messages.
* Quiet hours now actually work: abandoned-cart marketing messages that would fire inside your quiet window are automatically rescheduled to when the window ends (transactional order/shipping messages are never delayed).
* GDPR data-subject tools: the plugin now integrates with WordPress Tools > Export/Erase Personal Data, so a customer's stored abandoned-cart data can be exported or erased on request, and it adds suggested privacy-policy text.
* All abandoned-cart logic verified end to end (consent gating, scheduling, sending, coupon generation, kill switch, data export/erase).

= 3.7.6 =
* Security hardening: the public store-info endpoint no longer exposes the WooCommerce or plugin version (reduces version fingerprinting). The backend still receives the plugin version via the authenticated request header.

= 3.7.5 =
* Hardening (from a full code + runtime audit): fixed a case where the delivery-status webhook could fail on PHP 7.4 (it used a PHP 8 only function); the plugin is now compatible with PHP 7.4 and up again.
* Performance: the analytics table is no longer re-created on every analytics write (one-time, gated), and the store-policies / status-template-map options are no longer autoloaded on every page request.
* Robustness: the abandoned-cart kill switch now catches all error types when cancelling scheduled actions, so it can never interrupt order processing.
* Audited: zero blocking HTTP on checkout (all sends are async), full HPOS compatibility, all admin AJAX protected by nonce + capability, all SQL uses prepared statements, REST endpoints use timing-safe secret checks. No customer-facing errors in testing.

= 3.7.4 =
* Analytics: the Analytics dashboard is now fully translated (Hebrew, Arabic, French, Spanish) and renders in right-to-left for Arabic and Hebrew (KPI cards, the stats row, and the recent-activity table all flip correctly). The month name now follows your WordPress language too.

= 3.7.3 =
* Templates: the built-in starter templates are now translated into 21 major world languages (English, Hebrew, Arabic, Spanish, French, German, Italian, Portuguese, Russian, Turkish, Dutch, Polish, Ukrainian, Indonesian, Malay, Vietnamese, Thai, Japanese, Korean, Chinese, Hindi). Choosing a template language now shows ready-to-use content in that language instead of English. Other languages still fall back to English.
* Language: removed the in-plugin interface-language switcher. The admin now simply follows your WordPress language (the cleaner, standard approach). This resolves the admin layout/right-to-left issues that the manual switcher could cause. To use the plugin in another language, set your WordPress language under Settings > General.
* Admin notifications: status toggles and template pickers are now disabled until an admin WhatsApp number is entered (you cannot enable an admin alert without a destination number).
* UX: the bottom test-send bar is clearer (neutral phone placeholder, tooltips, and a clearer "awaiting approval" label for templates pending Meta approval).
* RTL: fixed the "Back" arrow direction in the Templates wizard for Arabic and Hebrew (now points the correct way).

= 3.7.2 =
* Fix (critical): switching the GoUltra interface language to a non-default language (e.g. Arabic or Hebrew) could make GoUltra admin pages return HTTP 500. The locale filter no longer calls a translation function while WordPress is determining the locale (which caused infinite recursion). GoUltra admin pages now load correctly in every language.
* Text: removed the long-dash character from all plugin text for a cleaner, consistent style.

= 3.7.1 =
* Translations: the Templates screen is now fully localized (Hebrew, Arabic, French, Spanish) - message types, the template wizard, the custom-template editor, and all status messages. Arabic and Hebrew render in polished right-to-left.
* Convenience: connecting your API key now automatically pulls your existing approved templates in one step - no need to open the Templates screen and click Sync. The connect button confirms how many templates were imported.

= 3.7.0 =
* Translations: the plugin admin is now translatable and ships with Hebrew, Arabic, French and Spanish (English default; unsupported languages fall back to English). It auto-detects your WordPress language, and a new "GoUltra Interface Language" selector lets you force a specific language for the GoUltra screens.
* Right-to-left: full, polished RTL layout for Arabic and Hebrew on the Settings screen.
* (First i18n phase - Settings screen. Templates/Analytics screens are being localized next.)

= 3.6.0 =
* Consent: separated MARKETING from UTILITY messaging (Meta + GDPR best practice). Utility messages (order confirmation, shipping) are transactional and are now sent automatically to anyone who provides a phone - no opt-in required. Marketing messages (offers, abandoned-cart recovery) now require an EXPLICIT opt-in: an unchecked-by-default checkbox the customer must actively tick at checkout (classic + block checkout). Abandoned-cart recovery is sent only to customers who opted in to marketing.
* Removed the old microcopy/checkbox opt-in toggle (superseded by the consent separation above).

= 3.5.3 =
* UX: the template pickers (Order Status + Admin notifications) are now grouped by template type with a clear "Language, Style - preview" label, so multiple approved templates are easy to tell apart. Removed the confusing inline checkmark and fixed right-to-left rendering.

= 3.5.2 =
* Branding: the GoUltra logo now appears in the Settings, Templates, and Analytics page headers.
* Fix: corrected the API key field placeholder to match the real key format (sk_live_...).

= 3.5.1 =
* Block checkout: added a native WhatsApp opt-in checkbox (WooCommerce Additional Checkout Fields API) for "checkbox" opt-in mode, saved to the order. The block checkout is the default since WooCommerce 8.3, where the classic opt-in hooks do not fire. Microcopy mode keeps working via implicit consent.

= 3.5.0 =
* Performance: all outbound WhatsApp/API calls on order events now run as Action Scheduler async jobs, so checkout and order status changes are never blocked or slowed by the GoUltra API (even if it is slow or unreachable).
* Performance: removed redundant per-request database queries (schema check, scheduler check, analytics table check are now gated).
* Fix: duplicate order-status hooks no longer cause duplicate sends (per-status + per-order de-duplication).
* Fix: shipping updates now normalise the customer phone to E.164 (local-format numbers were sent unnormalised).
* Fix: consistent opt-in handling across order confirmation and shipping.
* Fix: product catalog sync now returns the correct total count and avoids a heavy full-catalog query.
* Fix: corrected a PHP notice on the settings page (status to template option list).
* Fix: PHP 8.x hardening (null-safe phone normalisation and string handling).
* Fix: deactivation/uninstall now unschedule the correct Action Scheduler hooks; uninstall also removes the analytics table.
* Compatibility: declared Cart & Checkout Blocks compatibility; ghost-capture now works on the block checkout; abandoned-cart coupons now use the WC_Coupon CRUD object. Tested up to WooCommerce 10.8 and WordPress 7.0.

= 1.0.0 =
* Initial release.
* Order confirmation notifications.
* Shipping update notifications with tracking.
* Abandoned cart recovery (3-step sequence).
* Admin new order alerts.
* Template builder with 20+ languages.
* Store policies sync for AI support.
* Webhook logs viewer.
* HPOS compatibility.
* Consent-based checkout opt-in.
