Vai al contenuto
Contenuto principale
plugin-billing

Changelog — Hostwebo Billing

Changelog — Hostwebo Billing Sistema billing custom che ha sostituito Upmind nel maggio 2026. Tieni questo file aggiornato a ogni cambiamento di rilievo (no patch micro-fix). Per la roadmap delle fasi (stato avanzamento …

Aggiornato 18 Mag 2026 min di lettura
# Changelog — Hostwebo Billing Sistema billing custom che ha sostituito Upmind nel maggio 2026. Tieni questo file aggiornato a ogni cambiamento di rilievo (no patch micro-fix). Per la **roadmap delle fasi** (stato avanzamento + prossimi step) vedi [`docs/PROGRESS.md`](./PROGRESS.md). ## 2026-05 — Fase 13 (parte 4/4) · hostwebo-toolkit-pro admin → hwbi-* harmonized · **FASE 13 COMPLETA** ### Step Fase13-d · hostwebo-toolkit-pro admin adopts the unified palette – 🎨 **Diagnosi**: toolkit-pro è il più “esotico” — usa **Tailwind Play CDN** + utility classes (`bg-wp-blue`, `text-wp-success`, `grid-cols-4`, ecc.) + custom `.wp-postbox` semantica. Sotto Tailwind però la **palette è già identica** a `hwbi-*` (entrambi usano `#2271b1` / `#1d2327` / `#f0f0f1` WordPress-native). – 🎯 **Strategia “harmonization not rewrite”**: – **Keep Tailwind CDN** → strappare 1559 LOC di utility classes sarebbe una settimana di lavoro per zero guadagno visivo. – **Co-load `hwbi-admin`** prima di Tailwind → la pagina eredita anche le hwbi-* convenzioni (background body, font tokens) per ridondanza e branding consistency. – **Body class** `hwbi-page hwtk-admin-body` → ulteriore consistency con gli altri 3 plugin Hostwebo backend. – **Cleanup emoji content** (8 occorrenze inline `🩺🔧📝📊🤖💻✍️🎨💳🖼` ecc). – 🛠 **`HWTKAdminAdminPage::enqueue_assets`**: – Aggiunto `wp_enqueue_style(‘hwbi-admin’)` BEFORE Tailwind enqueue. – Su customer-side (dove billing plugin NON è installato), il check `wp_style_is(‘hwbi-admin’, ‘registered’)` fallisce gracefully → Tailwind continua a funzionare standalone. – Aggiunto `add_filter(‘admin_body_class’)` → `hwbi-page hwtk-admin-body`. – 🎨 **Emoji cleanup** in `render()` method: – `🩺` (health notice + accordion item) → `` – `🔧` (wrench list item) → `` – `📝` (file-pen list item) → `` – `📊` (supporto remoto) → `` – `✦` (gradient AI button) → `` – `⏱` (scan throttle string) → rimosso (testo già esplicito) – 8 agenti AI badge (`🤖💻🔧✍️🎨📊💳🖼`) → SVG FA per ognuno con palette indigo coerente. – 🛠 **HWTK_VERSION → 1.8.1-fase13d-hwbi**. ### Fase 13 totale (4 step, ~10k LOC) | Step | Plugin/Theme | LOC scope | Strategia | Status | |—|—|—|—|—| | 13-a | hostwebo-dashboard | 1778 | PHP wrap manuale postbox | ✅ | | 13-b | hostwebo theme | 1032 | CSS bridge `.hostwebo-card` → `.hwbi-postbox` | ✅ | | 13-c | hostwebo-ai | 5671 | Hybrid (body class + opt-in classes + emoji bulk) | ✅ | | 13-d | hostwebo-toolkit-pro | 1559 | Co-load + body class + emoji cleanup | ✅ | **Identità visiva uniforme su TUTTO il backend Hostwebo: raggiunta.** Pattern di migrazione documentato in `PROGRESS.md` per future espansioni (nuovi plugin Hostwebo). ## 2026-05 — Fase 13 (parte 3/4) · hostwebo-ai admin → hwbi-* design system ### Step Fase13-c · hostwebo-ai admin pages adopt the unified design (HARD) – 🎨 **Scope**: 5671 LOC × 11 submenu pages + 9 admin classes. Pattern misto: WP-native `
` con **216 inline `style=”…”` attributes** + emoji ovunque (`📊` `🤖` `⚠️` ecc). – 🎨 **Strategia ibrida**: – **Bridge CSS**: nuovo file `assets/css/hwai-admin.css` (250 LOC) con classi opt-in (`.hwai-card`, `.hwai-alert-*`, `.hwai-pill-*`, `.hwai-stat-grid`) che gli inline-styled markup possono adottare progressivamente. – **Body class** + enqueue automatico di `hwbi-admin` + `hwbi-fontawesome` → full-bleed wrap, palette `#f0f0f1`, font Segoe UI, polish form-table, `widefat striped` arrotondati. – **Palette dedicata AI**: accent fuchsia/purple gradient (`#6b3eb5 → #d946ef`) per differenziare visivamente da dashboard plugin (indigo) e billing (blu). – 🛠 **`HWAIAdminAdminMenu::enqueue_assets`**: – Enqueue `hwbi-admin` + `hwbi-fontawesome` (registered by billing). – Enqueue local `hwai-admin.css` con dep `hwbi-admin`. – `admin_body_class` filter → `hwbi-page hwai-admin-body`. – 🎨 **Page renderers refactorati**: – `render_dashboard()` (main entry `?page=hwai`): h1 con pill versione, icona FA chart-line in accent purple-fuchsia gradient. – `render_perf_alerts_widget()`: inline 2px-border card + `⚠️` emoji → `.hwai-card.is-warning` con icona `fa-triangle-exclamation`. Severity badges colorate inline → `.hwai-pill.is-{danger/warning/info}`. – `render_analytics()`: emoji `📈` su h1 → icona FA `fa-chart-line`. – 🧹 **Emoji cleanup MASSIVO** (delegato a sub-agent, ~70 sostituzioni): – `class-admin-menu.php`: `📊` `🤖` `⚠️` `🖨️` `📈` ecc. (8 ridotti a SVG FA) – `class-admin-packages.php`: 30+ tool icons (`🔍` `📊` `💾` `⚡` `📁` `📄` `✏️` `🎨` `✍️` `⏰` `🏷` `📧` `🔒` `🌐` `🤖`) – `class-user-meta-box.php`: 14 emojis (idem ai tool icons + `🔌`) – `class-admin-monitoring.php`: 14 (`✅×3` `❌` `⏳` `⌛` `⚠️` `🚫` `🛑` `🔐`) – `class-admin-ts-monitoring.php`: 12 (`📦` `✅` `⚙` `❌` `🛒` `💶` `💰` `📈` `💸` `🗂` `🏆` `📒`) – `class-admin-theme-studio.php`: 8 (`⚡` `💾` `🔄` `⚠` `🔍` `💡` `⭐×3` `▶`) – `class-admin-maintenance.php`: 5 (`⚠` `↻` `▶` `✅` `❌`) – 🛡 **`wp_kses`** introdotto sui renderer che ora servono HTML in label arrays (`role_label`, `tool_labels`) — `` allowed con classi/style sicuri. – 🛠 **HWAI_VERSION → 1.0.0-alpha.71-fase13c-hwbi**. ### Roadmap Fase 13 (resto) – 🟡 **Fase 13-d**: hostwebo-toolkit-pro (Tailwind CDN → hwbi-*, full rewrite) ## 2026-05 — Fase 13 (parte 2/4) · hostwebo theme admin → hwbi-* design system ### Step Fase13-b · hostwebo theme admin pages adopt the unified design – 🎨 **Strategia “CSS bridge”**: il theme ha 9 page renderer (~1032 LOC) tutti costruiti sul pattern `

`. Invece di riscrivere i 9 file PHP, ho **riscritto SOLO `assets/css/admin.css`** per far sì che `.hostwebo-card` venga renderizzata come `.hwbi-postbox`. – 🛠 **`hostwebo/inc/admin/admin-init.php`**: – Enqueue `hwbi-admin` + `hwbi-fontawesome` come deps del `hostwebo-admin` locale. – Nuovo `admin_body_class` filter → aggiunge `hwbi-page hostwebo-admin-body` quando lo screen ID contiene “hostwebo”. – Pulito inline `style=”…”` su `.hostwebo-plan-item` / `-plan-header` / `-plan-body` (3 attributi inline rimossi, ora gestiti via CSS). – 🎨 **`hostwebo/assets/css/admin.css`** riscritto (245 → 318 LOC): – **Bridge `.hostwebo-card` → `.hwbi-postbox` look**: bg bianco, bordo `#c3c4c7`, border-top accent indigo `#2271b1` 4px, shadow soft, border-radius 3px. Animazione `hwbiFadeIn` per ingresso elegante. – **`.hostwebo-card > h2`** → header gray `#f6f7f7` con border-bottom + icona FA tramite `::before` (font-awesome content `f0d8`). – **Form-table inside card**: padding/border/focus harmonized to hwbi-page style (focus → border indigo + glow 1px). – **Buttons**: `.button:not(.button-primary)` → outline indigo style come `.hwbi-btn-secondary`. – **Repeater items**: bg `#fafbfc`, bordo `#e0e1e2`, hover border-darker. `.hostwebo-remove-item` styled rosso danger. – **Plan items**: header gray, borders allineati alla palette. – **Matrix selector/table**: bordi tondi, header uppercase 11px, font-size coerente. – **`.hostwebo-switch`** (page-status toggle): completamente restyled per matchare `.hwbi-toggle` (48×24px, indigo `#2271b1` quando checked, knob 20px con shadow). – **Variant accent colors**: `.is-success` / `.is-warning` / `.is-danger` / `.is-purple` su `.hostwebo-card` per accent-top differenziato (opt-in, non breaking). – 🛠 **Zero modifiche ai 9 page renderer** (general/home/cloud-eco/wordpress/ ecommerce/advanced-deploy/about/contact/seo) — il markup esistente è già perfetto, il CSS fa tutto il lavoro. – 🛠 **HOSTWEBO_VERSION → 1.3.0-fase13b-hwbi-admin** (cache-bust + signal). – ⏱ Cache-bust automatico via `hostwebo_asset_ver()` (filemtime) — esistente. ## 2026-05 — Fase 13 (parte 1/4) · hostwebo-dashboard admin → hwbi-* design system ### Step Fase13-a · hostwebo-dashboard admin pages migrate to hwbi-* unified design – 🎨 **Strategia**: il design system `hwbi-*` (1402 LOC, definito in `hostwebo-billing/assets/css/hwbi-admin.css`) diventa il **linguaggio visivo unico** per tutti i plugin Hostwebo. Niente più WP-native sciolto o markup inline `style=”…”` random. – 🛠 **`HWBillingAdminAdminMenu`** refactor: – Aggiunto metodo pubblico `register_admin_styles()` hooked on `admin_enqueue_scripts` priority 5 — registra (NON enqueueing) i due handle `hwbi-admin` + `hwbi-fontawesome` su OGNI admin pageload. – `enqueue_admin_assets()` (priority 10) ora solo enqueueing condizionale sulle pagine `hwbi-*` del billing. – Altri plugin chiamano semplicemente `wp_enqueue_style(‘hwbi-admin’)` nei loro callback — la dependency è già registrata da billing. – 🎨 **`hostwebo-dashboard/includes/admin/class-admin.php`** migrato: – `enqueue_admin()` ora enqueueing `hwbi-admin` + `hwbi-fontawesome` come dipendenze prima del `hwdash-admin` locale. – `admin_body_class` filter aggiunge `hwbi-page hwdash-admin` → tutta la pagina admin di hostwebo-dashboard eredita la palette/font/full-bleed del design system. – **`page_settings()`** (260 righe) refactorato: – `

` → `.hwbi-page-header` con icona FA + page-actions buttons. – 6 sezioni `

+` wrappate in `.hwbi-postbox accent-top` con icone colorate (Plesk/server, WP Toolkit/wordpress purple, Generale/sliders, PayPal/paypal blue, Email/envelope, Sicurezza/shield danger). – Rimossi 5 `
` separator (postbox cards forniscono visual gap). – Bottoni “Test connessione” da `.button` → `.hwbi-btn hwbi-btn-secondary` con icone FA `fa-plug`. – Warning inline rosso “⚠️ Salva URL…” → `.hwbi-notice warning` box con icona `fa-triangle-exclamation` (no emoji). – **`page_mapping()`** refactorato: – Header → `.hwbi-page-header` + page-action “Torna alle impostazioni”. – Sync-massiva bar inline `style=”…”` → classe `.hwdash-sync-bar` (left-accent stripe stile `.hwbi-notice`). – Bottone primary `.button-primary` → `.hwbi-btn hwbi-btn-primary`. – **`page_logs()`** refactorato: – Header → `.hwbi-page-header` + counter eventi in `` pill. – Bottone purge → `.hwbi-btn hwbi-btn-danger` con icona `fa-broom`. – **`page_diag()`** refactorato: – Header → `.hwbi-page-header`. – Form parametri inline `style=”…”` → `.hwbi-postbox` accent-top con `.hwbi-form-grid cols-3` per i campi. – Endpoint buttons → `.hwbi-btn-secondary`. – Risultato JSON → `.hwbi-postbox accent-top success/danger` con icona coerente al success/error state. – 🎨 **`assets/css/admin.css`** riscritto (28 → 100 LOC): – Solo override per `.hwbi-postbox-body .form-table` (allineamento padding/borders/focus state) — niente palette duplicata. – `.hwdash-sync-bar` (left-accent stripe stile notice). – `.hwdash-badge*` aggiornate con palette `hwbi-pill`. – `widefat striped` con bordi arrotondati + header uppercase. – 🛠 **Class-admin-tickets.php** + **class-admin-services.php**: – Bulk replace `class=”wrap hwdash-admin”` → `class=”wrap hwbi-wrap hwdash-admin”` per attivare il full-bleed layout. – 🛠 **HWDASH_VERSION → 1.39.0-fase13-hwbi-admin** (cache-bust). – 🛠 **HWBI_VERSION → 1.0.2-fase13-shared-css** (signal: CSS handle pubblico). ### Roadmap Fase 13 (resto) – 🟡 **Fase 13-b**: hostwebo theme admin pages (Customizer + 9 page renderers) – 🟡 **Fase 13-c**: hostwebo-ai admin pages (5.7k LOC, biggest) – 🟡 **Fase 13-d**: hostwebo-toolkit-pro (Tailwind CDN → hwbi-*, full rewrite) ## 2026-05 — Polish empty/error state WP vista (Step C1c) ### Step C1c — Friendly empty/error states + bugfix ctx wptoolkit – 🐛 **ROOT CAUSE FIX**: `class-rest-controller.php::context()` non includeva la chiave `wptoolkit` ma `class-rest-wordpress.php` la referenziava in 4 punti (`is_configured()`, `installations_list()`, `installation_get()`, ecc.) → `Call to a member function is_configured() on null` PHP fatal → “errore critico” mostrato in dashboard ogni volta che un utente (admin o nuovo cliente senza provisioning) apriva il tab WordPress. – Aggiunta chiave `wptoolkit => new HWDash_WPToolkit_Api()` (con `class_exists()` guard) — stesso pattern di `plesk`. – Defensive null-check in 4 callsites di `class-rest-wordpress.php`: `empty( $ctx[‘wptoolkit’] ) || ! $ctx[‘wptoolkit’]->is_configured()`. – 🎨 **Empty state polish** (`views-wordpress.js::emptyStateHtml()`): – Hero gradient blue/indigo/violet + icona WordPress 36px in quadrato gradient + dual blob blur per profondità visuale. – Titolo “Nessuna installazione WordPress” + body friendly (SSL/backup/1-click admin). – 2 CTA: **Installa WordPress** (indigo button) + **Vedi il mio piano** (white outline + icona zap → naviga a `#billing`). – Footnote: “Hai appena attivato un piano? L’installazione richiede pochi minuti.” → riduce ansia post-checkout. – 🎨 **Error state polish** (`views-wordpress.js::renderErrorState()`): – Sostituisce il banner rosso grezzo `Errore: ` con un layout coerente al list view (header + card amber con icona alertCircle 34px). – Discrimina 4 casi via `err.code` / `err.data.status`: – `hwdash_no_plesk` → “Account hosting non ancora collegato” + CTA “Vedi i piani disponibili” → `#billing` – `hwdash_no_wpt` → “WP Toolkit non disponibile” + CTA “Riprova” – `401` / `rest_forbidden` → “Sessione scaduta” + CTA “Ricarica” – generic → “Impossibile caricare i siti WordPress” + CTA “Riprova” – Secondary CTA universale: **Contatta supporto** → `#tickets`. – Applicato anche a `loadManage()` (single-site error path). – 🧹 **Emoji cleanup residue in views-wordpress.js**: – `🚀` toggle Toolkit Pro row → `I.zap(14)` SVG – `✦ AI · Inclusa` badge → `I.zap(12)` SVG – `🚀 Ottimizza con AI` button → `I.activity(16)` SVG – `⚡` toolkit update banner → `I.zap(18)` SVG – 🛠 **HWDASH_VERSION → 1.38.1-step-c1c-wp-empty** (cache-bust). ## 2026-05 — Cleanup emoji residui v2-fatturazione (Step C1b) ### Step C1b — Tabelle Fatture/Pagamenti → SVG-only – 🎨 **`v2-billing-dashboard.js`** — eliminati gli ultimi caratteri non-SVG dalle tabelle interne del hub Billing: – `⚠` (warning V2 inactive banner) → `alertCircle` SVG dentro tile arrotondato (bg-amber-100 + ring) — coerente con altri stati di servizio nel dashboard. – `Visualizza →` (riga Fatture) → `Visualizza` + `chevronRight` SVG (12px) – `Fattura →` (riga Pagamenti) → `Fattura` + `chevronRight` SVG (12px) – `Gestisci piano →` (header sezione Abbonamenti) → idem – `Cambia piano →` (CTA overview injected card) → idem – `Esplora i piani →` (empty-state CTA) → idem – 🧹 Colonna “Gateway” della tabella Pagamenti ripulita: – Rimosso `var gwIcon = ”` vuoto e il leading-space residuo. – Aggiunto `creditCard` SVG (12px) prima del nome gateway in UPPERCASE, con styling slate-400 — pattern identico a quello già usato negli infoTile e nella KPI card. – 📐 Tutti gli anchor/button CTA ora `inline-flex items-center gap-1` per allineamento icona/testo perfetto, niente più “spazi morti” da carattere `→`. – 🛠 HWBI_VERSION → `1.0.1-step-c1b-emoji-fatt` – ⏱ Cache-bust automatico via `filemtime()` su `v2-billing-dashboard.js` (convenzione progetto già in DashboardExtension). ## 2026-05 — Cleanup finale Upmind admin (Step B4) ### Step B4 — Tombstone totale Upmind in class-admin.php – ❌ Rimosso intero blocco `sanitize_settings()` Upmind (url, brand_id, token, webhook_secret) – ❌ Rimossa sezione UI “Upmind API” da pagina Impostazioni (~50 righe HTML form) – ❌ Rimossa colonna “Upmind Client ID” da pagina Mappatura clienti – ❌ Rimosso input “Upmind Client ID” da WP user profile – ❌ Rimosso handler `save_user_mapping_fields` Upmind – 🧹 Updated label “Plesk/Upmind via email” → “Plesk via email” in 3 punti – 🐛 Risolti potenziali warning PHP per `HWDASH_USER_META_UPMIND` undefined constant – 📊 **Status finale**: 0 callsite Upmind nel codebase, solo 2 commenti tombstone in admin.php ## 2026-05 — Hub Gestione Sito unificato (Step C1) ### Step C1 — WordPress + Domini + Email + Backup → 1 hub – 🎯 Cluster IL MIO SITO: da 4 voci separate a **1 hub “Gestione sito”** con sub-tabs interne. – ➕ Nuovo `views-site-hub.js` orchestratore (~115 righe): – Hero gradient + tab bar 4-tab (WordPress · Domini · Email · Backup) – Hash deep-link `#sito/wp`, `#sito/domini`, `#sito/email`, `#sito/backup` – Ogni tab delega alla view esistente (HWView_WordPress / Domains / Email / Backups) – 🎨 Active-state aggregato: voce sidebar “Gestione sito” highlighted anche quando l’utente è in deep-pages (wp-manage, dns-manage, domain-manage, ecc.) – 🛠 Router: `case ‘sito’` chiama `HWView_SiteHub.load()` – 🛠 Enqueue ordering: views-site-hub.js dipende da wp/domains/email/backups – HWDASH_VERSION → 1.38.0-step-c1-site ## 2026-05 — Hub Account unificato (Step C4) ### Step C4 — Profilo + Affiliazione → 1 hub – 🎯 Cluster ACCOUNT: da 2 voci (Profilo + Programma Affiliazione) a **1 hub “Account”** con sub-tabs interne. – ➕ Nuovo `views-account-hub.js` orchestratore (~115 righe): – Hero gradient + tab bar 2-tab (Profilo · Affiliazione) – Hash deep-link `#account/profilo`, `#account/affiliazione` – Profilo delega a `HWView_Profile` (era HWView_Account, rinominato) – Affiliazione delega a `HWDashExtensions.views[‘v2-affiliazione’]` – 🔁 Legacy redirect `#v2-affiliazione` → `#account` in dashboard.js – 🛠 Rimossa registrazione menu `v2-affiliazione` da v2-affiliate-dashboard.js – 🛠 Router: `case ‘account’` ora chiama `HWView_AccountHub.load()` – HWDASH_VERSION → 1.37.2-step-c4-acc ## 2026-05 — Hub Billing unificato (Step C3) ### Step C3 — 3 voci billing → 1 hub – 🎯 Menu cluster BILLING: da 3 voci separate (Il mio piano + Fatturazione V2 + Affiliazione) a **1 sola voce “Billing”** con sub-tabs interne. – ➕ Nuovo `loadBillingHub` orchestratore in `v2-billing-dashboard.js`: – Hero gradient + tab bar 2-tab (Il mio piano · Storico billing) – Hash deep-link `#billing/piano`, `#billing/storico` – 🔁 Legacy redirects in `dashboard.js`: – `#il-mio-piano` · `#v2-fatturazione` · `#hosting` · `#orders` → `#billing` – 🎨 Affiliazione rimane nel cluster ACCOUNT (sarà integrata in Step C4) – HWDASH_VERSION → 1.36.0-step-c3 ## 2026-05 — Cleanup emoji finale (Welcome / Billing / Overview) ### Icons cleanup completo (post-C2/C1) – 🎨 **`v2-welcome-wizard.js`** — emoji rimosse: – Helper `icon(name)` aggiunto (delega a `window.HWDashIcons`) – Hero “Benvenuto in Hostwebo” (no 🎉) · tile icons: server/creditCard/zap – Step 2 dominio (globe/link/check SVG) + WordPress (wordpress SVG) – Step 3 AI tour: messageCircle/puzzle/refresh/activity al posto di emoji – “Già completato” panel: check SVG (era ✓) – Toast finale: testo puro (no 🚀) – 🎨 **`v2-billing-dashboard.js`** — emoji rimosse: – Helper `icon()` aggiunto · hero zap SVG (era ✦) – infoTile: globe/creditCard/calendar · KPI cards: fileText/creditCard/refresh/calendar – Section headers (Abbonamenti/Fatture/Pagamenti) con icone SVG colorate – Quick actions: activity/fileText/trash SVG – 🎨 **`views-overview.js`** — welcome banner: 👋 rimosso – HWDASH_VERSION → 1.35.0-icons-pro ## 2026-05 — Dashboard: stile professionale no emoji (Step C1 icons) ### Step C1 icons — Zero emoji, solo SVG professionali – 🎨 Rewrite icone in `views-ai-hub.js`: – Tab bar SVG (messageCircle/activity/cart) invece di 💬/📊/🎁 – Hero icona lightning bolt (zap) in quadrato vetro – Accordion categories: dashboard/wordpress/lifeBuoy/zap (al posto di ✦/⚡/🎫/🚀) – Quota tiles: messageCircle/calendar/fileImage al posto di 💬/📅/🖼️/🗓️ – Agent + capability cards: check/lock SVG al posto di ✓/🔒 – Copia buttons: icona copy SVG – Section headers: users (Agenti) + settings (Capabilities) in quadrati colorati – Chevron accordion: chevronDown SVG al posto di ▾ – 📐 Tutte le icone provengono da `HWDashIcons` (lo stesso set usato in tutto il tema) per coerenza visiva totale. – HWDASH_VERSION → 1.34.0-step-c1-icons ## 2026-05 — Dashboard AI Hub polish (Step C2) ### Step C2 — Sezione Assistente AI ridisegnata – 🎨 Rewrite completo di `views-ai-hub.js` (~430 LOC, AUTONOMO, no più delega a HWView_AIAssistant per tutto): – **Tab Chat AI**: card intro “Come usare la chat” + 4 accordion colorati di use cases (Generale/WordPress/Supporto/Avanzato) con prompt cards + copy buttons via Clipboard API. – **Tab Risparmio**: delega pulita a `HWView_AISavings.load()` (vista già buona). – **Tab Il mio pacchetto**: package hero + 4 quota tiles con progress bar + agents grid 3-col (con lock 🔒) + capabilities grid + upgrade CTA. – 🎯 Design coerente con sezioni Domini/Email/Billing (stesso pattern `rounded-3xl border bg-white dark:bg-[#131A2B]`). – ✦ Hero gradient indigo→purple→fuchsia con dot Online pulsante. – HWDASH_VERSION → 1.33.0-step-c2. ## 2026-05 — Dashboard SSR + Prefetch + Upmind cleanup finale (Step B3) ### Step B3 — Performance polish + Upmind tombstone – ➕ **SSR `window.HWInitialData`** server-side bootstrap: – `class-plugin.php::compute_initial_data()` pre-calcola subscription + welcome status – Iniettato via `wp_add_inline_script` prima di `hwdash-app` – `v2-billing-dashboard.js loadMyPlan()` + `views-overview.js` consumano e skipano fetch – ➕ **Prefetch on hover** (core.js + dashboard.js): – `HWApi.prefetch(path)` con sessionStorage cache (60s TTL) – `HWApi.getCached(path)` lookup sincrono – Map `section → endpoint` con `mouseenter` listener su ogni `[data-nav]` – 🎨 **Skeleton shape-matched** in views-overview.js (hero + 4 KPI + 2-col grid) – ❌ **Upmind tombstone** — eliminazione completa dei restanti vestigi: – DELETED: `includes/api/class-upmind-api.php` (stub no-op) – DELETED: `HWDash_Profile_Fields::to_upmind()` method (dead code) – REWRITE: `class-rest-controller.php::context()` con alias `plesk_client_id` – FIX: `class-rest-domains.php` + `class-rest-databases.php` → `get_user_meta()` diretti – FIX: `class-admin.php` test connection: rimosso branch `upmind` – FIX: `class-admin.php handle_save_mapping()` → direct `update_user_meta()` – **ZERO `HWDash_Upmind_Api` callers rimasti nel codebase.** – HWDASH_VERSION → 1.32.0-step-b3 ## 2026-05 — Dashboard cliente: cluster labels + AI Hub (Step B2) ### Step B2 — Menu cluster + AI unification + Theme Studio in evidenza – 🎨 **Menu cluster labels** (stile hwbi backend): IL MIO SITO · AI & STUDIO · BILLING · SERVIZI & SUPPORTO · ACCOUNT. Type `’label’` aggiunto al menu schema. – ✦ **AI Hub unificato**: nuova sezione `id: ‘ai’` con 3 sub-tabs interne: Chat (HWView_AIAssistant) · Risparmio (HWView_AISavings) · Il mio pacchetto (NEW). Sostituisce le voci separate “Hostwebo AI” + “Risparmio AI”. – 🏆 **Theme Studio in evidenza**: badge `PRO` gradient fuchsia→purple + styling accent (text fuchsia, hover bg fuchsia, border ring). Riflette posizione strategica come top service. – ➕ **Sub-tab infrastructure**: `HWStore.setSubTab()` + `HWUtil.tabBar()` + hash parser `#section/subtab` + popstate handling. – ➕ **views-ai-hub.js** (~230 righe): hero gradient + tab bar + “Il mio pacchetto” con KPI quota (msg/giorno/mese, immagini), agenti, modelli premium + upgrade CTA. – 🛠 Enqueue chain ricalibrata: hwdash-v-aihub registrato come dep di hwdash-app + di hwdash-v-aiass/aisave. Eliminati hwdash-v-credit/hosting (defunct). – 🐛 **Bug fix deploy**: deps di `hwdash-app` puntavano a handle defunct → dashboard.js non veniva enqueueato → pagina bianca. Risolto aggiornando deps a handle reali. – HWDASH_VERSION → 1.31.1-step-b2-fix. ## 2026-05 — Dashboard cliente: Upmind purge + cache (Step B1) ### Step B1 — Upmind decommission totale + DashboardCache – ❌ **15 file legacy eliminati** (~4.717 LOC): – `class-rest-billing.php`, `class-rest-hosting.php`, `class-rest-orders.php`, `class-rest-shop.php`, `class-rest-customers.php`, `class-rest-addons.php` – `class-checkout.php`, `class-customer-mapper.php`, `class-migrator.php`, `class-access-lock.php` – `views-hosting.js`, `views-credit.js`, `checkout.js` – `templates/checkout-app.php`, `templates/page-checkout.php` – 🧹 **Inline cleanup** in 14 file (class-plugin/registration/password-reset/tickets/ services/notifications/mailer/rest-controller/rest-account/rest-overview/admin/ admin-tickets) — rimosso ogni callsite `HWDash_Upmind_Api`, `HWDash_Customer_Mapper`, `HWDASH_USER_META_UPMIND`, `hwdash_upmind_webhook`, `hwdash_daily_expiry_check`. – ➕ **Nuova `DashboardCache`** (`hostwebo-billing/includes/cache/class-dashboard-cache.php`): – Transient per-utente con TTL configurabile per-key (60s subs, 5min invoices, 30min affiliate) – `flush_user()` SQL walk del options table – `flush_user_billing()` flush solo slice billing – Hook listener: `hwbi_order_paid`, `hwbi_subscription_*`, `hwbi_domain_registered` – ⚡ **Cache + fast-path** applicati a `SubscriptionController::me()` (60s TTL), `WelcomeController::handle_status()` (60s + 600s per completed/skipped), `class-rest-overview.php` (fast-path empty payload per utenti senza mapping). – 🎨 **Dashboard menu refactor** (16→11 voci core + 3 V2 extension): – Eliminati: “Piani Hosting”, “Fatture & Pagamenti”, “Storico Ordini” – Rinomati: → WordPress, Domini, Email, Backup, Marketplace, Supporto, Account – Legacy hash redirects: `#hosting`→`#il-mio-piano`, `#billing`→`#v2-fatturazione` – HWDASH_VERSION → 1.30.0-step-b1 (cache-bust) – 📊 **Performance live**: avg endpoint latency **1.292ms → 421ms** (-67%). – 🐛 **Notification bridge**: `hwdash_upmind_webhook` listener sostituito da `hwbi_order_paid` + `hwbi_subscription_*` listener nativi V2. ## 2026-05 — PayPal 3-cycle sync (Fase 10) ### Fase 10 — PayPal 3-cycle Billing Plan sync – 🛠 `PayPalProductSync::sync_plan()` esteso con doppio path: – **3-cycle path**: itera `metadata.cycles` (monthly/semestral/annual) creando 1 PayPal Billing Plan per ciclo, persistendo in `cycles[X].paypal_plan_id`. – **Single-cycle fallback**: back-compat per piani senza `metadata.cycles` (one-time o legacy). – Idempotency per ciclo: price + interval + count + status comparison; riusa o deactivate+ricrea. – Flat `paypal_plan_id` root mantenuto puntando al monthly per back-compat. – 🎨 Maintenance hub: rimpiazzato placeholder “In arrivo…” con CTA prominente “Sync tutti i piani su PayPal” (handler `hwbi_sync_paypal` già wired). – 🏗 Audit log strutturato con `cycles_synced` per traceability per-ciclo. ## 2026-05 — Affiliazione & Coupon admin (Fase 9) ### Fase 9 — Affiliazione & Coupon Stripe-native – 🎨 Riscritta `AdminAffiliates` (`includes/admin/class-admin-affiliates.php`): – Sostituito `wp-list-table` legacy con design system `hwbi-*` (hwbi-page-header, hwbi-stat-grid, hwbi-postbox, hwbi-filter-tabs, hwbi-plan-table, hwbi-pill). – Tab “Affiliati”: 4 KPI + tabella + sidebar help. – Tab “Commissioni”: 4 KPI + filter tabs per stato + actions inline. – Nuovo bottone “Auto-approve scadute” che invoca `auto_approve_old_pending(30)`. – ➕ Nuova classe `AdminCoupons` (`includes/admin/class-admin-coupons.php`) Stripe-native per gestire coupon direttamente via API (no tabella locale). – Lista live: KPI + tabella con discount/durata/redemptions/scadenza/stato. – Form creazione con tipo sconto (percent/amount), durata (once/forever/repeating), max_redemptions, redeem_by. JS toggle dinamico tra modi. – Sidebar “Source-of-truth Stripe” + “Best practice” (Launch/Black Friday/Lifetime). – Handlers `hwbi_coupon_create` (POST Stripe API) e `hwbi_coupon_delete`. – 🎨 Menu admin: nuova voce “Coupon Stripe” in Cluster A (Catalogo) dopo Domini. – 🛠 `Plugin::register_hooks()` registra `AdminCoupons`. ## 2026-05 — Welcome wizard onboarding (Fase 8) ### Fase 8 — Welcome wizard cliente post-checkout – ➕ Nuovo `WelcomeController` (`includes/rest/class-welcome-controller.php`) con 4 endpoint cliente: `GET /welcome/status` (status + step + subscription summary + should_show), `POST /welcome/step`, `POST /welcome/skip`, `POST /welcome/complete`. – ➕ Nuovo `WelcomeBridge` (`includes/integrations/class-welcome-bridge.php`) listener su `hwbi_order_paid` → setta `hwbi_welcome_status=’pending’` per primi pagamenti, preserva ‘completed’/’skipped’ (no override). – ➕ Nuovo `v2-welcome-wizard.js` (`assets/js/v2-welcome-wizard.js`): – View `welcome` registrata via `HWDashExtensions.views` – Auto-navigate: al boot dashboard, se `should_show=true` e nessun deep-link, naviga automaticamente alla sezione welcome (rispetta `#tickets` ecc.) – Step 1: hero gradient + badge “Pagamento confermato” + roadmap preview – Step 2: stato dominio + install WordPress (gated) – Step 3: 4-card AI tour (Chat, Theme Studio, Recipe Migrator, Risparmio AI) – Progress bar animata (33% → 67% → 100%) – Footer Skip / Indietro / Continua / Inizia – 🛠 `DashboardExtension` esteso per enqueueare `hwbi-v2-welcome` con filemtime cache-bust e dep di `hwdash-app`. – 🛠 `Plugin::boot()` registra `WelcomeController` + `WelcomeBridge`. – 🎨 State storage: solo user_meta (no schema migration) — `hwbi_welcome_status`, `_step`, `_started_at`, `_finished_at`, `_first_payment_at`. ## 2026-05 — Mono-piano UX + Dashboard polish + AI bridge (Fasi 5–7) ### Fase 5 — Mono-piano UX enforcement – ➕ Banner server-side al checkout V2: utente con sub attiva vede “Hai già un piano attivo” + CTA “Apri dashboard” invece del form di acquisto. – 🛡 REST guard al `/checkout/stripe/create-session`: HTTP 409 + `upgrade_url` se l’utente ha già una sub `active|trial|past_due|suspended` (defense-in-depth). – ➕ Nuovo `SubscriptionController` (`includes/rest/class-subscription-controller.php`) con 4 endpoint cliente: `GET /subscriptions/me`, `GET /subscriptions/upgrade-options`, `POST /subscriptions/{id}/upgrade` (Stripe proration), `POST /subscriptions/{id}/cancel`. – ➕ Metodo `StripeClient::subscription_update()` (era mancante) — usato dal flow di upgrade per `proration_behavior=create_prorations`. – 🎨 Nuova pagina `/v2-upgrade/` (`hostwebo/page-v2-upgrade.php`) — wizard standalone con radio cards piani + cycle picker + sticky confirm bar. Auto-creata da `Installer::ensure_pages_exist()`. ### Fase 6 — Polish dashboard SPA cliente – 🎨 Riscritto completamente `assets/js/v2-billing-dashboard.js`: – Nuova sezione **”Il mio piano”** (id `il-mio-piano`, icona zap, prima di “billing”) con hero card gradient indigo→fuchsia → nome piano + badge stato + prezzo + prossimo rinnovo + dominio + gateway. – CTA prominenti: Cambia piano → `urlUpgrade`, Le mie fatture → `urlFattura`, Cancella piano → modal con select-motivo + POST cancel. – 2-col layout: details card + quick actions card. – History block per piani cancellati. – 🎨 Overview injection: mini-card “Il tuo piano” iniettata dopo il welcome banner se l’utente ha sub attiva V2. – 🛠 Cancel modal polished con `HWModal` (consistent con flow logout cliente) e select-motivo (cambio strategia / prezzo / funzioni / provider / chiusura). – 🛠 `DashboardExtension` esteso: – Cache-busting via `filemtime()` (sostituisce `HWBI_VERSION` statico — la cache CDN viene invalidata automaticamente a ogni deploy) – Localized config esteso con `urlUpgrade`, `urlFattura`, `urlCheckout`, `urlHome` risolti via `get_permalink()` con fallback a `home_url()`. – 🐛 Bug fix critico: usata `HWBI_PATH` (inesistente) invece di `HWBI_DIR` → script tag mancante in DOM, menu vuoto. Risolto in 2 iterazioni live. ### Fase 7 — AI ↔ Billing source-of-truth bridge – ➕ Nuovo `AIBridge` (`includes/integrations/class-ai-bridge.php`): – 6 hook listener: `hwbi_order_paid`, `hwbi_subscription_cancelled` (Stripe), `hwbi_subscription_cancelled_local` (admin/cliente), `hwbi_subscription_suspended`, `hwbi_subscription_reactivated`, `hwbi_subscription_renewed`. – `get_ai_package_slug_for_user( $user_id )` → punto di lettura unico, snapshot da `metadata.ai_package_slug` su sub, fallback su `metadata.ai_package` del piano. – `resolve_ai_package_id( $slug )` con alias map legacy → internal slug (standard→smart_s, pro→smart_m, elite→power_xl). – `sync_hwai_sub_for_user()` upsert su `wp_hwai_user_subscriptions` preservando `custom_overrides` per-utente. – Tutti gli handler `try/catch` con audit log — bridge non rompe webhook billing. – 🛠 `HWAILimitsPackageRegistry::get_for_user()` esteso: read-path prova prima `AIBridge::get_ai_package_slug_for_user`, fonde overrides legacy se trovati; fallback a query originale per utenti senza hwbi_sub (zero impact). – 🎨 Plan admin UI: dropdown “AI Package” non più hardcoded ma popolato leggendo `wp_hwai_packages` via soft-dep. Label dinamica `slug (X msg/g · €Y/mese)`. Fallback a lista legacy se plugin AI off. – 🏗 Auto-include current slug nella lista anche se obsoleto (zero data loss durante migrazione). ## 2026-05 — Pulizia & riorganizzazione backend (Fasi 1–4) ### Fase 1 — Menu + Manutenzione unificata – ❌ Rimosse voci morte: `Coupon` (tabella mai usata, coupon delegati a Stripe), `Importa piani da tema` (handler reale era già il bottone in Piani), `AdminThemeImporter` guard (classe inesistente). – 🎨 Menu raggruppato in 3 cluster con separator CSS-stylati: `— Catalogo —` (Piani · Servizi Extra · Domini) · `— Operatività —` (Ordini · Abbonamenti · Fatture · Affiliazione) · `— Sistema —` (Manutenzione · Impostazioni). – ➕ Nuova `AdminMaintenance` (`hwbi-maintenance`) hub con 4 tabs: Diagnostica (legacy Test Mode wrappato in postbox) · Pagamenti falliti (legacy Dunning) · Audit log (legge `wp_hwai_audit_log` filtrato hwbi_*) · Sync gateway (Stripe/PayPal/Namecheap status + sidebar cron schedules). – 🔁 Legacy redirect `?page=hwbi-test` → `?page=hwbi-maintenance&tab=diagnostics` (hook `init` priority 1). – 🐛 Fix Test Mode: query usava colonna `last_error` ma lo schema definisce `error_message` → query silenziosamente vuota. Sostituito in 2 punti. – 🐛 Rimosso handler morto `hwbi_dunning_force_step` (nessun pulsante UI lo invocava). ### Fase 2 — Sezione Ordini reale – ➕ Nuovo `OrderRepository` (`includes/models/class-order-repository.php`) con 7 stati canonici (pending_payment → paid → provisioning → active; failed/cancelled/refunded), filtri completi su `list()`, `stats()` per KPI, `update_status()` con stamp automatico timestamps + audit log. – ➕ Nuovo `AdminOrders` (list + detail): – **List**: 4 KPI cards (Ordini totali · Fatturato 30g · In attesa pagamento · Falliti 7g) + 8 filter tabs con counter + ricerca + paginazione + sidebar “Distribuzione per stato” + “Azioni rapide”. – **Detail**: postbox accent purple items con totali/sconto/IVA · cliente snapshot completo (CF/P.IVA/SDI/PEC) · provisioning jobs Plesk · sidebar (stato + pagamento con link Stripe/PayPal Dashboard + fattura + abbonamento collegato + azioni admin retry/cancel). – ➕ Action handlers: `hwbi_order_retry_provisioning` (re-inserisce job in coda), `hwbi_order_mark_cancelled`. ### Fase 3 — Sezione Abbonamenti reale – ➕ Nuovo `SubscriptionRepository` (`includes/models/class-subscription-repository.php`) con 5 stati (active/trial/past_due/suspended/cancelled), JOIN con wp_users + wp_hwbi_plans + wp_hwbi_orders, `stats()` con calcolo **MRR** normalizzato al canone mensile (divisori: monthly=1, quarterly=3, semestral=6, annual=12, biennial=24, triennial=36). – ➕ Nuovo `AdminSubscriptions` (list + detail): – **List**: 4 KPI (Sub attivi · MRR · Past due+Sospesi · Churn 30g) + filter tabs + ricerca cross-field (email/nome/piano/Stripe ID/PayPal/Plesk). – **Detail**: postbox piano (accent purple) · binding Stripe (sub_id + customer_id clickable Dashboard test/live) · binding PayPal · binding Plesk (subscription_id + account_id) · **dunning history stepper visivo** (5 step R1/R2/R3/Sospensione/Cancellazione con card colorate per gli step inviati) · fatture collegate · sidebar (stato + ordine origine + azioni admin). – ➕ Action handlers: `hwbi_sub_mark_cancelled` (delega `SubscriptionEngine:: mark_cancelled`), `hwbi_sub_reactivate` (delega `mark_active` + azzera dunning). ### Fase 4 — Dashboard `hwbi` analitica vera – ❌ Rimossa vecchia render_dashboard inline (status banner + roadmap statica). – ➕ Nuovo `AdminDashboard` (`includes/admin/class-admin-dashboard.php`): – **6 KPI cards**: MRR · Sub attivi (+ clienti unici 30g foot) · Ricavi 30g (+ 7g/oggi foot) · Ordini 30g (+ oggi/totali foot) · Fatture 30g · Da seguire (past_due+failed_7d aggregato con pill error/success). – **Grafico trend ricavi 30g**: SVG inline (no Chart.js, no dipendenze esterne), barre con gradient `#6b3eb5 → #2271b1`, polyline overlay opacity 0.5, 5 tick orizzontali con € label, label X-axis ogni 5 giorni, tooltip nativi via `` su ogni barra. – **Top piani 30g** con progress bar in pct e revenue per piano. – **Top clienti 30g** con avatar iniziale colorato + spend + count ordini. – **Ultimi 5 ordini** in tabella mini. – **Pagamenti da gestire** (visible solo se ci sono past_due/suspended). – Sidebar: System Health (Stripe/PayPal/Namecheap pill colorate) + Quick Actions (6 link rapidi) + info box footer con riferimento PROGRESS.md. ## v1.x — Current state (May 2026) ## v1.x — Current state (May 2026) ### Architettura – **Source of truth = `hostwebo-billing`**. Il tema `hostwebo` è solo display. – **Theme cards** leggono prezzi/sconti/feature dal piano billing collegato (auto-discovery via slug match `eco_mini` → `eco-mini`). – **Toggle ciclo** (mensile/semestrale/annuale) in `/cloud-eco/`, `/wordpress-hosting/`, `/ecommerce/` aggiorna prezzi+sconti+sub-caption dinamicamente. – **Checkout standalone** `/v2-checkout/` con 4 step + sidebar sticky + auth inline (registrazione/login popup). – **Upmind decommissionato**: tutte le tracce rimosse o stubbate. Il plugin `hostwebo-dashboard` ha un no-op `HWDash_Upmind_Api`. – **Menu Piani = solo hosting**. Il `product_type` è hardcoded a `hosting` nell’editor (no più select). Domini hanno menu dedicato, addon avranno un menu separato in una fase futura. – **TLD Catalog separato**: nuovo `Hostwebo Billing → Domini` con catalogo TLD source-of-truth (`wp_option hwbi_tld_catalog`). Ogni TLD ha provider, cost, margin, override e sale_price auto-calcolato. Cron giornaliero `hwbi_tld_pricing_sync` (03:00 UTC) pulla i prezzi live da Namecheap. – **Routing per-TLD**: `.com`/`.net`/`.org`/`.io`/`.ai`/… → **NamecheapProvider**; `.it` → **OpenSrsProvider** (stub fino a go-live API). `DomainManager::resolve_provider_for($domain)` legge il catalogo e instrada di conseguenza. – **TLD Catalog dual-price** (May 2026 update): ogni TLD ora ha **2 costi separati** (`cost_register` + `cost_renew`) e **2 prezzi vendita** (`sale_register` + `sale_renew`), con lo stesso `margin_pct` applicato a entrambi. Permette di: – Offrire un prezzo iniziale aggressivo per nuovi clienti / trasferimenti (es. `.online` cost €2,46 → sale €3,20) – Mantenere un prezzo di rinnovo realistico per i clienti esistenti (es. `.online` renew €37,54 → sale €48,80) – Sync Namecheap pulla entrambi i prezzi (REGISTER + RENEW action), via XML attribute parser corretto (`@attributes` nested), con `RegularPrice` (true MSRP) preferito su `YourPrice` (reseller scontato). – FX USD→EUR via Frankfurter ECB (live, cached 12h, fallback exchangerate.host). – Override individuali per registrazione e rinnovo, con fallback all’auto-calcolo se vuoti. – **Servizi Extra (Addons) — menu dedicato** (May 2026): nuovo menu `Hostwebo Billing → Servizi Extra` per gestire un catalogo riutilizzabile di addon. Schema: “`json { “pricing_type”: “recurring | one_time”, “cycles”: { “monthly”:{…}, “semestral”:{…}, “annual”:{…} }, “one_time_price_excl/incl”: 19.00, “compatible_plans”: [“eco-mini”,”wp-starter”], “is_universal”: false, “default_mandatory”: false, “category”: “backup|security|email|performance|ai|other”, “icon”: “fa-shield-halved”, “short_description”: “…” } “` – **AddonRepository** (`includes/models/class-addon-repository.php`) — facade su PlanRepository (product_type=’addon’), con helper `price_for($addon, $plan_cycle, $price_kind)` che restituisce il prezzo dell’addon **allineato al ciclo scelto del piano hosting**. – **AdminAddons** (`includes/admin/class-admin-addons.php`) — lista + edit form con segmented control `Ricorrente / Una tantum`, matrice prezzi M/S/A speculare a quella dei piani hosting, chips compatibilità con piani, toggle universale. – **Plan editor card** “Servizi extra collegati”: elenca solo addon compatibili (universal OR slug in compatible_plans), per ogni addon 2 toggle: **Abilita** (offerto al checkout) + **Obbligatorio** (selezionato di default e non rimovibile). Format storage: `linked_addons: [{slug, mandatory}]` (legacy bare-slug ancora supportato). – **REST API**: – `GET /hostwebo-billing/v1/addons` — catalogo completo attivo – `GET /hostwebo-billing/v1/addons/for-plan/{slug}?cycle=annual` — addon offerti su quel piano, con `mandatory` flag e prezzi promo+std risolti per il ciclo – `GET /hostwebo-billing/v1/addons/{slug}` — singolo addon – **Checkout integration completata** (`page-v2-checkout.php`): – Server-side load via `AddonRepository::normalize_plan_linked()` con i 3 cicli + one-time pre-calcolati nella response (no fetch in più al client). – Step 2 “Dominio + Servizi Extra”: ogni addon è una card con icona categoria, descrizione breve, prezzo dinamico, badge “Incluso” (mandatory) o “Una tantum”. – **Mandatory addons** pre-checked + disabled + bordo verde (non rimovibili dal cliente). – **Pricing dinamico per ciclo**: cambiando il ciclo in Step 1, il prezzo dell’addon ricorrente si ricalcola live sia nella card sia nel side-cart sticky (es. Backup Avanzato: €2,99/mese → €29,88/12 mesi). – One-time addons mantengono prezzo fisso indipendente dal ciclo. – **CheckoutController.create_session** forza i mandatory addons del piano lato server (anche se mancano dal POST), valida compatibilità prima di addebitarli, e aggiunge un `line_item` Stripe ricorrente per ogni addon con `recurring.interval_count = months del piano`. One-time addons vengono “deferred” e salvati nei `metadata.hwbi_addons_one_off` (handling separato in fase futura). – **Stripe Product+Price sync per addon** (`StripeProductSync::sync_addon`): – Ricorrenti: 1 Product + 3 Price (mensile/semestrale/annuale = `recurring.interval=month` × 1/6/12) + 3 Coupon (`duration=once`) per il delta promo→std. – Una tantum: 1 Product + 1 Price one-time. – Idempotent: re-runs aggiornano in place, archiviano price obsoleti su cambio importo. – Admin trigger: bottone **”Crea/Aggiorna in Stripe”** nella sidebar dell’edit form addon, con status pill (Synced / Parziale / —) e contatore cicli sincronizzati (`3 / 3`). – **Checkout usa Stripe Price ID quando disponibile**: invece di inviare `price_data` inline, `CheckoutController` ora preferisce il `stripe_price_id` sincronizzato dei cicli dell’addon. Fallback automatico a `price_data` se l’addon non è ancora synced. – **One-time addons via `subscription_data.add_invoice_items`**: invece di skipparli, il CheckoutController li aggiunge alla **prima fattura** della subscription Stripe. Usa il `stripe_one_time_price_id` synced; fallback inline `price_data` se mancante. – **Side-cart renewal con addon**: il “Rinnovo” mostrato sotto il totale include ora il prezzo std anche degli addon ricorrenti, con breakdown `(piano € X + extra € Y)`. Es: `Rinnovo: € 24,98 / mese (piano € 19,99 + extra € 4,99)`. Addon one-time esclusi dal rinnovo (correttamente, perché sono one-time). ### Modello dati piani Tabella `wp_hwbi_plans` con un record per piano + `metadata_json.cycles` strutturato: “`json { “cycles”: { “monthly”: { “months”: 1, “price_promo_pm”: 4.99, “price_std_pm”: 8.99, “savings_pct”: 44, “stripe_price_id”: “price_xxx”, “stripe_coupon_id”: “hwbi-launch-eco-mini-monthly-xxx” }, “semestral”: { “months”: 6, “price_promo_total”: 27.24, “price_std_total”: 53.94, … }, “annual”: { “months”: 12, “price_promo_total”: 49.00, “price_std_total”: 107.88, … } }, “free_domain”: { “monthly”: { “enabled”: false, “tlds”: [], “duration”: “first_year” }, “semestral”: { “enabled”: true, “tlds”: [“.it”], “duration”: “first_year” }, “annual”: { “enabled”: true, “tlds”: [“.com”, “.net”, “.org”, “.it”], “duration”: “first_year” } }, “linked_addons”: [“backup-extra”, “ssl-premium”], “ai_package”: “free|standard|standard_plus|pro|pro_plus|elite”, “theme_id”: “eco_mini”, “features_struct”: { “sites”: { “count”: 1, “unlimited”: false }, “storage_gb”: { “amount”: 20, “unlimited”: false }, “traffic_gb”: { “amount”: 100,”unlimited”: false }, “email_accounts”: { “count”: 10, “unlimited”: true }, “ssl_free”: true, “backup”: “daily”, // none|hourly|daily|weekly|monthly “ai_assistant”: false } } “` La lista bullet pubblica (`features_json`) viene **derivata automaticamente** da `features_struct` ogni volta che si salva il piano. L’admin non scrive più bullet a mano: configura i campi strutturati nella card “Features del piano” (stepper + toggle “Illimitato” + segmented control per backup e AI). Backfill legacy: i piani con bullet text vecchi vengono inferiti la prima volta che si apre l’editor (regex su storage/traffic/email/backup/SSL). ### Stripe — modello promo+rinnovo 1. **Admin** edita la matrice prezzi in `Hostwebo Billing → Piani → Modifica`. 2. **Click “Crea/Aggiorna in Stripe”** → crea 1 Product + 3 Price (uno per ciclo, intervallo mensile × N) + 3 Coupon (per gli sconti lancio, `duration=once`). 3. Al **checkout**: Stripe Checkout Session viene creata con `line_items[0].price = price_std` + `discounts[0].coupon = launch_coupon`. 4. Stripe processa: prima fattura = prezzo lancio (con coupon), rinnovi successivi = prezzo standard. ### Stato funzioni | Area | Stato | |—|—| | Plan editor matrice prezzi M/S/A × Promo/Std | ✅ | | Plan editor — Features strutturate (stepper + segmented + auto-bullets) | ✅ | | Plan editor — Backend tecnico semplificato (Plesk ID only) | ✅ | | Stripe sync (3 Price + 3 Coupon per piano) | ✅ | | Theme → Billing data flow (auto via slug) | ✅ | | Checkout 4-step + cycle picker + login popup + auto-register | ✅ | | Free domain config UI (per ciclo, TLD multi-select) | ✅ | | Free domain checkout integration (auto-prefill TLD) | ⏳ Prossima | | Addon system — admin + checkout integration | ✅ | | PayPal 3-cycle sync | ⏳ Prossima | | Aruba SDI auto-submission | ⏳ Stub | | Server-side PDF fatture (TCPDF) | ⏳ HTML print-only | | Dashboard cliente cleanup | ⏳ Prossima | ### Pages auto-create at activation – `/v2-checkout/` (template `page-v2-checkout.php` in theme) – `/v2-grazie/` (template `page-v2-grazie.php`) – `/v2-fattura/` (template `page-v2-fattura.php`) Page IDs salvati in `wp_options` (`hwbi_page_v2_checkout`, `hwbi_page_v2_grazie`, `hwbi_page_v2_fattura`) — i redirect Stripe/PayPal usano `get_permalink( $page_id )`. ### REST endpoints (hostwebo-billing/v1) | Path | Method | Auth | Scope | |—|—|—|—| | `/checkout/stripe/create-session` | POST | public | Crea sessione Stripe | | `/checkout/paypal/create-order` | POST | public | Crea ordine PayPal | | `/checkout/paypal/capture` | POST | public | Cattura pagamento PayPal | | `/orders/{id}/status` | GET | public | Polling stato per `/v2-grazie/` | | `/webhooks/stripe` | POST | HMAC | Webhook Stripe | | `/webhooks/paypal` | POST | HMAC | Webhook PayPal | | `/invoices/me` | GET | user | Fatture cliente loggato | | `/invoices/{id}` | GET | user | Singola fattura | | `/users/me/billing-overview` | GET | user | Dashboard tab | | `/users/me/payments` | GET | user | Storico pagamenti | | `/users/me/subscriptions` | GET | user | Abbonamenti attivi | | `/subscriptions/{id}/cancel` | POST | user | Cancella sub | | `/affiliates/me` | GET | user | Programma affiliati | | `/affiliates/signup` | POST | user | Iscriviti | | `/users/me/affiliate/commissions` | GET | user | Commissioni | | `/domains/check` | GET | public | Verifica disponibilità | | `/domains/pricing` | GET | public | Listino TLD | | `/domains/my` | GET | user | Domini cliente | | `/auth/email-check` | POST | public | Real-time email exists | | `/auth/login` | POST | public | Login da popup checkout | | `/auth/register` | POST | public | Auto-register al checkout | | `/auth/validate-field` | POST | public | Validazione live form | ### Admin pages `wp-admin → Hostwebo Billing`: – **Dashboard** — stato sistema + 4 KPI card + roadmap – **Piani** — list view (filtri tipo) + edit view (5 card: Identità · Matrice prezzi · Dominio gratis · Backend · Addon · Features · Sync gateway) – **Ordini** — placeholder (Phase futura) – **Abbonamenti** — placeholder – **Fatture** — lista + visualizza + XML SDI – **Coupon** — placeholder – **Affiliazione** — admin commissioni + auto-approve cron – **Dunning** — past_due/suspended list + force resend + manual reactivate – **Domini** — Namecheap config + lista domini + check – **Test Mode** — diagnostica + webhook URLs + carte test + recent orders/webhooks/jobs/audit – **Impostazioni** — Modalità sistema (attivo/manutenzione) + Stripe + PayPal + Aruba SDI + Dati fornitore ### Cron schedules – `hwbi_provisioning_worker` — every minute (Plesk job queue) – `hwbi_subscription_renewal_check` — daily 04:00 – `hwbi_dunning_check` — daily 09:00 – `hwbi_affiliate_auto_approve` — daily (auto-approve commissioni dopo refund window) ### Pricing 2026 raccomandato | Tier | Mensile (promo→std) | Annuale (promo→std) | |—|—|—| | Eco Mini | €4,99 → €8,99 | €49 → €107,88 | | Eco Starter | €7,99 → €12,99 | €79 → €155,88 | | Eco Plus | €13,99 → €19,99 | €139 → €239,88 | | Eco Pro | €22,99 → €32,99 | €229 → €395,88 | | WP Starter | €12,99 → €19,99 | €129 → €239,88 | | WP Plus | €24,99 → €34,99 | €249 → €419,88 | | WP Pro | €44,99 → €59,99 | €449 → €719,88 | | WP Elite | €79,99 → €99,99 | €799 → €1199,88 | | Ecom Starter | €24,99 → €34,99 | €249 → €419,88 | | Ecom Plus | €49,99 → €64,99 | €499 → €779,88 | | Ecom Pro | €89,99 → €109,99 | €899 → €1319,88 | | Ecom Elite | €179,99 → €199,99 | €1799 → €2399,88 | Modificabili in `Piani → Modifica` per ogni piano. Bottone “Applica prezzi raccomandati” in lista per resettare tutti. — ## Roadmap prossime sessioni 1. **Wiring dominio gratis nel checkout** — quando il piano ha free_domain.enabled, lo step Dominio mostra solo i TLD ammessi + chiama Namecheap API in background dopo pagamento 2. **PayPal 3-cycle sync** — equivalente di Stripe per Billing Plans (1 Product + 3 Billing Plan) 3. **Dashboard cliente cleanup** — rimuovere completamente Upmind dal plugin `hostwebo-dashboard`, raggruppare menu, ridurre sidebar, tab interne 4. **WP-native UI sweep** — applicare hwbi-admin.css a TUTTE le pagine billing (Test Mode, Settings, Dunning, Domains, Affiliation, Invoices) 5. **PDF fatture server-side** (TCPDF) + Aruba SDI auto-submission (SOAP) </div> <!-- Feedback widget --> <section class="mt-12 p-6 rounded-3xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-[#131A2B]" data-hwai-feedback data-doc-id="166"> <h3 class="text-lg font-bold text-slate-900 dark:text-white mb-2 flex items-center gap-2"> <span class="text-indigo-500"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span> Questa guida ti è stata utile? </h3> <p class="text-sm text-slate-600 dark:text-slate-400 mb-4"> Il tuo feedback ci aiuta a tenere la documentazione utile e aggiornata. </p> <div class="flex gap-3" data-fb-buttons> <button type="button" data-vote="up" class="inline-flex items-center gap-2 px-5 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl font-semibold text-slate-700 dark:text-slate-200 hover:bg-emerald-50 dark:hover:bg-emerald-900/30 hover:border-emerald-500 hover:text-emerald-700 dark:hover:text-emerald-300 transition-all"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Sì, utile </button> <button type="button" data-vote="down" class="inline-flex items-center gap-2 px-5 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl font-semibold text-slate-700 dark:text-slate-200 hover:bg-rose-50 dark:hover:bg-rose-900/30 hover:border-rose-500 hover:text-rose-700 dark:hover:text-rose-300 transition-all"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> No, da migliorare </button> </div> <div data-fb-comment class="hidden mt-4"> <textarea data-fb-text rows="3" placeholder="Cosa migliorare? (opzionale)" class="w-full px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 text-sm focus:ring-2 focus:ring-indigo-500 focus:outline-none"></textarea> <button type="button" data-fb-submit class="mt-3 inline-flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl text-sm transition-colors"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-10 5L2 7"/></svg> Invia feedback </button> </div> <div data-fb-thanks class="hidden inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300 font-semibold"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Grazie del feedback! </div> </section> <!-- Related docs --> <section class="mt-10"> <h3 class="text-xl font-extrabold tracking-tight text-slate-900 dark:text-white mb-4"> Continua a leggere </h3> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <a href="https://www.hostwebo.it/documentazione/plesk-customer-panel-lockout-guida-operativa-admin/" class="group block p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#131A2B] hover:shadow-md hover:-translate-y-0.5 transition-all no-underline"> <div class="flex items-start gap-3"> <div class="w-9 h-9 rounded-xl bg-slate-50 dark:bg-slate-800/30 text-slate-600 dark:text-slate-400 flex items-center justify-center ring-1 ring-slate-100 dark:ring-slate-700/40 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> </div> <div class="min-w-0"> <h4 class="font-bold text-slate-900 dark:text-white text-sm mb-1 leading-tight">Plesk customer panel lockout — guida operativa admin</h4> <p class="text-xs text-slate-500 dark:text-slate-400 line-clamp-2">Plesk customer panel lockout — guida operativa admin # Obiettivo: i clienti non devono mai poter loggare nel pannello Plesk diretto. Tutta l’amministrazione hosting passa dalla dashboard Hostwebo (file manager, ema…</p> </div> </div> </a> <a href="https://www.hostwebo.it/documentazione/progress-hostwebo-billing-roadmap/" class="group block p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#131A2B] hover:shadow-md hover:-translate-y-0.5 transition-all no-underline"> <div class="flex items-start gap-3"> <div class="w-9 h-9 rounded-xl bg-slate-50 dark:bg-slate-800/30 text-slate-600 dark:text-slate-400 flex items-center justify-center ring-1 ring-slate-100 dark:ring-slate-700/40 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> </div> <div class="min-w-0"> <h4 class="font-bold text-slate-900 dark:text-white text-sm mb-1 leading-tight">PROGRESS — Hostwebo Billing roadmap</h4> <p class="text-xs text-slate-500 dark:text-slate-400 line-clamp-2">PROGRESS — Hostwebo Billing roadmap Snapshot dello stato di avanzamento della roadmap di pulizia 2026-05 decisa dopo l’audit iniziale del backend hwbi. Aggiornare questo file alla fine di ogni sessione di lavoro co…</p> </div> </div> </a> <a href="https://www.hostwebo.it/documentazione/testing-guide-hostwebo-billing-v2/" class="group block p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#131A2B] hover:shadow-md hover:-translate-y-0.5 transition-all no-underline"> <div class="flex items-start gap-3"> <div class="w-9 h-9 rounded-xl bg-slate-50 dark:bg-slate-800/30 text-slate-600 dark:text-slate-400 flex items-center justify-center ring-1 ring-slate-100 dark:ring-slate-700/40 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> </div> <div class="min-w-0"> <h4 class="font-bold text-slate-900 dark:text-white text-sm mb-1 leading-tight">Testing Guide — Hostwebo Billing V2</h4> <p class="text-xs text-slate-500 dark:text-slate-400 line-clamp-2">Testing Guide — Hostwebo Billing V2 # Procedura concreta per fare il primo ordine test end-to-end sul sistema V2. Tempo richiesto: ~30 minuti la prima volta, 5 minuti le volte successive. Cosa è già pronto (v0.10.0) # Co…</p> </div> </div> </a> <a href="https://www.hostwebo.it/documentazione/paypal-integration-phase-c/" class="group block p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-[#131A2B] hover:shadow-md hover:-translate-y-0.5 transition-all no-underline"> <div class="flex items-start gap-3"> <div class="w-9 h-9 rounded-xl bg-slate-50 dark:bg-slate-800/30 text-slate-600 dark:text-slate-400 flex items-center justify-center ring-1 ring-slate-100 dark:ring-slate-700/40 flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> </div> <div class="min-w-0"> <h4 class="font-bold text-slate-900 dark:text-white text-sm mb-1 leading-tight">PayPal Integration (Phase C)</h4> <p class="text-xs text-slate-500 dark:text-slate-400 line-clamp-2">PayPal Integration (Phase C) # Architettura PayPal — differenze chiave da Stripe # PayPal usa una struttura a 3 livelli: Product (catalogo): descrive cosa vendi (id PROD-xxx)Billing Plan: pricing structure ricorrente (id…</p> </div> </div> </a> </div> </section> </article> <!-- RIGHT: TOC (auto-generated from h2/h3 in content) + Need help card --> <aside class="hidden lg:block"> <div class="sticky top-24 space-y-4"> <nav data-doc-toc class="rounded-2xl bg-slate-50 dark:bg-[#131A2B] border border-slate-200 dark:border-slate-800 p-4 hidden"> <h3 class="text-[11px] font-extrabold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-3 flex items-center gap-1.5"> <span class="text-indigo-500"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg></span> In questa pagina </h3> <ul data-toc-list class="space-y-1 text-sm"></ul> </nav> <!-- Need help? --> <div class="rounded-2xl bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-500 p-5 text-white shadow-lg shadow-fuchsia-500/15"> <div class="w-9 h-9 rounded-xl bg-white/15 ring-1 ring-white/25 flex items-center justify-center mb-3"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg> </div> <h4 class="font-bold text-sm mb-1.5">Hai una domanda specifica?</h4> <p class="text-xs text-white/85 leading-relaxed mb-3"> L'AI Assistant nella dashboard cliente cerca direttamente in questa documentazione. </p> <a href="https://www.hostwebo.it/area-clienti/" class="inline-flex items-center gap-1.5 text-xs font-bold text-white hover:underline no-underline"> Apri Dashboard <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg> </a> </div> </div> </aside> </div> </main> <script> (function(){ /* Auto-generate TOC + slug headings inside the article. ------------ */ var content = document.querySelector('[data-doc-content]'); var tocBox = document.querySelector('[data-doc-toc]'); var tocList = document.querySelector('[data-toc-list]'); if (content && tocBox && tocList) { var headings = content.querySelectorAll('h2, h3'); var built = 0; headings.forEach(function (h) { // Ensure each heading has a stable slug ID. if (!h.id) { var slug = h.textContent.toLowerCase().trim() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); if (slug) h.id = slug; } if (!h.id) return; var li = document.createElement('li'); li.className = h.tagName === 'H3' ? 'pl-4' : ''; var a = document.createElement('a'); a.href = '#' + h.id; a.className = 'block py-1 text-slate-600 dark:text-slate-300 hover:text-indigo-600 dark:hover:text-indigo-300 transition-colors leading-snug no-underline'; a.textContent = h.textContent; li.appendChild(a); tocList.appendChild(li); built++; }); if (built >= 2) { tocBox.classList.remove('hidden'); } } /* Reading-time computed from content word count (~200 wpm). -------- */ if (content) { var words = content.innerText.trim().split(/\s+/).length; var min = Math.max(1, Math.round(words / 200)); var rt = document.querySelector('[data-rt-value]'); if (rt) rt.textContent = min; } /* Feedback widget. ------------------------------------------------- */ var root = document.querySelector('[data-hwai-feedback]'); if (!root) return; var docId = root.getAttribute('data-doc-id'); var btnUp = root.querySelector('[data-vote="up"]'); var btnDown = root.querySelector('[data-vote="down"]'); var commentBox = root.querySelector('[data-fb-comment]'); var commentText = root.querySelector('[data-fb-text]'); var btnSubmit = root.querySelector('[data-fb-submit]'); var thanks = root.querySelector('[data-fb-thanks]'); var buttons = root.querySelector('[data-fb-buttons]'); var chosen = null; function send(vote, comment){ var nonce = (window.wpApiSettings && wpApiSettings.nonce) ? wpApiSettings.nonce : ''; var headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; if (nonce) headers['X-WP-Nonce'] = nonce; fetch('/wp-json/hostwebo-ai/v1/docs/' + docId + '/feedback', { method: 'POST', credentials: 'include', headers: headers, body: JSON.stringify({ vote: vote, comment: comment || '' }) }).catch(function(){ /* ignore network errors silently */ }) .finally(function(){ buttons.classList.add('hidden'); commentBox.classList.add('hidden'); thanks.classList.remove('hidden'); }); } btnUp.addEventListener('click', function () { chosen = 'up'; send('up', ''); }); btnDown.addEventListener('click', function () { chosen = 'down'; buttons.classList.add('hidden'); commentBox.classList.remove('hidden'); commentText.focus(); }); btnSubmit.addEventListener('click', function () { send(chosen || 'down', commentText.value || ''); }); })(); </script> </div><!-- /.flex-grow --> <!-- Footer --> <footer class="bg-white dark:bg-[#0B0F19] border-t border-slate-200 dark:border-slate-800 pt-10 pb-6 text-slate-500 dark:text-slate-400 text-sm mt-auto"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex flex-col md:flex-row justify-between items-center gap-4 mb-6"> <div class="flex items-center gap-2"> <span class="inline-flex shrink-0"><svg width="18" height="21" viewBox="138 118 236 276" style="overflow:visible" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="hw-ft-h" gradientUnits="userSpaceOnUse" x1="150" y1="120" x2="392" y2="396"><stop stop-color="#818CF8"/><stop offset=".5" stop-color="#6366F1"/><stop offset="1" stop-color="#7C3AED"/></linearGradient></defs><g fill="url(#hw-ft-h)"><rect x="150" y="126" width="84" height="260" rx="30"/><rect x="278" y="126" width="84" height="260" rx="30"/><rect x="150" y="224" width="92" height="64" rx="20"/><rect x="270" y="224" width="92" height="64" rx="20"/><circle cx="256" cy="256" r="30"/></g><circle cx="256" cy="256" r="13" fill="#fff"/></svg></span> <span class="font-bold text-slate-800 dark:text-slate-200">Hostwebo Inc.</span> </div> <nav class="flex flex-wrap gap-6 font-medium" aria-label="Footer"> <a href="https://www.hostwebo.it/azienda/" class="hover:text-indigo-500 transition-colors no-underline">Azienda</a> <a href="https://www.hostwebo.it/ai-assistant/" class="inline-flex items-center gap-1 hover:text-fuchsia-500 transition-colors no-underline"> <span class="text-fuchsia-500"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg></span> Hostwebo AI </a> <a href="https://www.hostwebo.it/blog/" class="hover:text-indigo-500 transition-colors no-underline">Blog</a> <a href="https://www.hostwebo.it/contattaci/" class="hover:text-indigo-500 transition-colors no-underline">Contattaci</a> <a href="https://www.hostwebo.it/termini-e-condizioni/" class="hover:text-indigo-500 transition-colors no-underline">Termini di Servizio</a> <a href="https://www.hostwebo.it/privacy-policy/" class="hover:text-indigo-500 transition-colors no-underline">Privacy Policy</a> </nav> </div> <div class="text-center md:text-left flex flex-col md:flex-row justify-between items-center border-t border-slate-200 dark:border-slate-800 pt-6"> <p>© 2026 Hostwebo. Tutti i diritti riservati. Progettato con precisione per il web moderno.</p> <div class="flex flex-wrap gap-4 mt-4 md:mt-0" aria-label="Social"> <a href="https://www.facebook.com/hostwebo/" class="inline-flex items-center justify-center w-9 h-9 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-800 dark:hover:text-slate-200 transition-colors" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.07 2.07 0 1 1 0-4.13 2.07 2.07 0 0 1 0 4.13zm1.78 13.02H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.44c.98 0 1.79-.77 1.79-1.72V1.72C24 .77 23.2 0 22.22 0z"/></svg> </a> <a href="https://www.facebook.com/hostwebo/" class="inline-flex items-center justify-center w-9 h-9 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-800 dark:hover:text-slate-200 transition-colors" target="_blank" rel="noopener noreferrer" aria-label="X"> <svg width="18" height="18" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> </a> <a href="https://www.facebook.com/hostwebo/" class="inline-flex items-center justify-center w-9 h-9 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-800 dark:hover:text-slate-200 transition-colors" target="_blank" rel="noopener noreferrer" aria-label="Facebook"> <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> </a> <a href="https://www.facebook.com/hostwebo/" class="inline-flex items-center justify-center w-9 h-9 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-800 dark:hover:text-slate-200 transition-colors" target="_blank" rel="noopener noreferrer" aria-label="Instagram"> <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg> </a> <a href="https://www.youtube.com/@Hostwebo_ita" class="inline-flex items-center justify-center w-9 h-9 rounded-full hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-800 dark:hover:text-slate-200 transition-colors" target="_blank" rel="noopener noreferrer" aria-label="YouTube"> <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg> </a> </div> </div> </div> </footer> <script type="speculationrules"> {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/hostwebo/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <div id="hwq-modal" class="hwq-modal" hidden aria-hidden="true" role="dialog" aria-labelledby="hwq-title" aria-modal="true"> <div class="hwq-backdrop" data-hwq-close></div> <div class="hwq-dialog"> <button type="button" class="hwq-close" data-hwq-close aria-label="Chiudi">×</button> <div class="hwq-header"> <div class="hwq-badge"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/></svg> Preventivo personalizzato </div> <h2 id="hwq-title">Raccontaci il tuo progetto</h2> <p class="hwq-sub">Compila il form, ricevi una proposta dettagliata via email entro 24 ore. Nessun impegno.</p> </div> <form id="hwq-form" class="hwq-form" novalidate> <input type="hidden" id="hwq_nonce" name="hwq_nonce" value="f8563c8bc0" /><input type="hidden" name="_wp_http_referer" value="/documentazione/changelog-hostwebo-billing/" /> <input type="text" name="hwq_hp" class="hwq-honeypot" tabindex="-1" autocomplete="off" aria-hidden="true"> <div class="hwq-row"> <label> <span class="hwq-label">Nome e cognome <em>*</em></span> <input type="text" name="full_name" required maxlength="120" value=""> </label> <label> <span class="hwq-label">Email <em>*</em></span> <input type="email" name="email" required maxlength="160" value=""> </label> </div> <div class="hwq-row"> <label> <span class="hwq-label">Telefono (facoltativo)</span> <input type="tel" name="phone" maxlength="40"> </label> <label> <span class="hwq-label">Azienda (facoltativo)</span> <input type="text" name="company" maxlength="160"> </label> </div> <label> <span class="hwq-label">Categoria del servizio <em>*</em></span> <select name="category" required> <option value="">— Seleziona una categoria —</option> <option value="wordpress">WordPress</option> <option value="seo">SEO</option> <option value="performance">Performance / ottimizzazione</option> <option value="site_create">Creazione sito da zero</option> <option value="ecommerce">Ecommerce</option> <option value="woocommerce">WooCommerce</option> <option value="ai">AI / automazione</option> <option value="marketing">Marketing digitale</option> <option value="development">Sviluppo custom</option> <option value="design">Design / grafica</option> <option value="migration">Migrazione siti</option> <option value="maintenance">Manutenzione continuativa</option> <option value="other">Altro / non in lista</option> </select> </label> <label> <span class="hwq-label">Sito web di riferimento (se esiste)</span> <input type="url" name="website" placeholder="https://miosito.it" maxlength="200"> </label> <label> <span class="hwq-label">Descrivi il tuo progetto <em>*</em></span> <textarea name="description" rows="5" required minlength="20" maxlength="5000" placeholder="Cosa vorresti realizzare? Quali risultati ti aspetti? Hai vincoli di tempo o budget?"></textarea> </label> <label> <span class="hwq-label">Budget orientativo (facoltativo)</span> <select name="budget"> <option value="">— Preferisco non indicare —</option> <option value="under_500">< € 500</option> <option value="500_2k">€ 500 — 2.000</option> <option value="2k_5k">€ 2.000 — 5.000</option> <option value="5k_10k">€ 5.000 — 10.000</option> <option value="over_10k">> € 10.000</option> </select> </label> <label class="hwq-consent"> <input type="checkbox" name="privacy_ok" required value="1"> <span>Ho letto e accetto la <a href="https://www.hostwebo.it/privacy-policy/" target="_blank" rel="noopener">Privacy Policy</a>. I dati saranno usati solo per rispondere alla richiesta.</span> </label> <div class="hwq-actions"> <button type="button" data-hwq-close class="hwq-btn-ghost">Annulla</button> <button type="submit" class="hwq-btn-primary"> <span class="hwq-submit-label">Invia richiesta</span> <span class="hwq-submit-spin" hidden>⟳</span> </button> </div> <div class="hwq-result" role="status" aria-live="polite" hidden></div> </form> </div> </div> <style> .hwq-modal{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px} .hwq-modal[hidden]{display:none} .hwq-backdrop{position:absolute;inset:0;background:rgba(15,23,42,.65);backdrop-filter:blur(4px)} .hwq-dialog{position:relative;background:#fff;color:#0f172a;border-radius:24px;max-width:600px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 30px 80px rgba(0,0,0,.35);padding:28px;animation:hwq-pop .2s ease-out} .dark .hwq-dialog{background:#131a2b;color:#f1f5f9} @keyframes hwq-pop{from{transform:scale(.96);opacity:0}to{transform:scale(1);opacity:1}} .hwq-close{position:absolute;top:14px;right:14px;width:32px;height:32px;border:0;background:transparent;font-size:24px;color:#64748b;cursor:pointer;border-radius:8px} .hwq-close:hover{background:#f1f5f9;color:#0f172a} .dark .hwq-close:hover{background:#1f2937;color:#f1f5f9} .hwq-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:linear-gradient(90deg,rgba(168,85,247,.12),rgba(217,70,239,.12));color:#a855f7;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;margin-bottom:10px;border:1px solid rgba(168,85,247,.3)} .hwq-header h2{font-size:24px;font-weight:800;margin:0 0 6px;line-height:1.2} .hwq-sub{font-size:13px;color:#64748b;margin:0 0 18px} .dark .hwq-sub{color:#94a3b8} .hwq-form{display:flex;flex-direction:column;gap:14px} .hwq-form label{display:flex;flex-direction:column;gap:6px;font-size:13px} .hwq-row{display:grid;grid-template-columns:1fr 1fr;gap:14px} @media (max-width:560px){.hwq-row{grid-template-columns:1fr}} .hwq-label{font-weight:600;color:#374151} .dark .hwq-label{color:#e5e7eb} .hwq-label em{color:#dc2626;font-style:normal} .hwq-form input[type=text],.hwq-form input[type=email],.hwq-form input[type=tel],.hwq-form input[type=url],.hwq-form select,.hwq-form textarea{width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;color:#0f172a;font-size:14px;font-family:inherit;transition:border-color .15s, box-shadow .15s} .dark .hwq-form input[type=text],.dark .hwq-form input[type=email],.dark .hwq-form input[type=tel],.dark .hwq-form input[type=url],.dark .hwq-form select,.dark .hwq-form textarea{background:#0b0f19;color:#f1f5f9;border-color:#334155} .hwq-form input:focus,.hwq-form select:focus,.hwq-form textarea:focus{outline:0;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)} .hwq-form textarea{resize:vertical;min-height:110px} .hwq-honeypot{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden} .hwq-consent{flex-direction:row !important;align-items:flex-start;gap:10px !important;font-size:12px;color:#64748b;line-height:1.5} .hwq-consent input{margin-top:2px} .hwq-consent a{color:#6366f1;text-decoration:underline} .hwq-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:8px} .hwq-btn-ghost{padding:10px 18px;border:1px solid #e2e8f0;background:transparent;color:#64748b;border-radius:10px;font-weight:600;cursor:pointer;font-size:14px} .hwq-btn-ghost:hover{background:#f8fafc} .dark .hwq-btn-ghost{border-color:#334155;color:#94a3b8} .dark .hwq-btn-ghost:hover{background:#1f2937} .hwq-btn-primary{padding:10px 22px;border:0;background:linear-gradient(90deg,#6366f1,#d946ef);color:#fff;border-radius:10px;font-weight:700;cursor:pointer;font-size:14px;box-shadow:0 4px 12px rgba(99,102,241,.3);min-width:140px} .hwq-btn-primary:hover{filter:brightness(1.05)} .hwq-btn-primary:disabled{opacity:.6;cursor:not-allowed} .hwq-result{padding:12px 14px;border-radius:10px;font-size:13px;line-height:1.5} .hwq-result.success{background:#ecfdf5;color:#065f46;border:1px solid #6ee7b7} .hwq-result.error{background:#fef2f2;color:#991b1b;border:1px solid #fecaca} body.hwq-locked{overflow:hidden} </style> <script> (function(){ const modal = document.getElementById('hwq-modal'); if(!modal) return; const form = document.getElementById('hwq-form'); const result = modal.querySelector('.hwq-result'); const submitLabel = modal.querySelector('.hwq-submit-label'); const submitSpin = modal.querySelector('.hwq-submit-spin'); const submitBtn = form.querySelector('button[type=submit]'); function open() { modal.hidden = false; modal.setAttribute('aria-hidden','false'); document.body.classList.add('hwq-locked'); setTimeout(() => { const f = form.querySelector('input,select,textarea'); if(f) f.focus(); }, 100); } function close() { modal.hidden = true; modal.setAttribute('aria-hidden','true'); document.body.classList.remove('hwq-locked'); } document.addEventListener('click', function(e){ if(e.target.closest('[data-hwq-open]')) { e.preventDefault(); open(); } if(e.target.closest('[data-hwq-close]')) { e.preventDefault(); close(); } }); document.addEventListener('keydown', function(e){ if(e.key === 'Escape' && !modal.hidden) close(); }); form.addEventListener('submit', async function(e){ e.preventDefault(); result.hidden = true; result.className = 'hwq-result'; submitBtn.disabled = true; submitLabel.hidden = true; submitSpin.hidden = false; const fd = new FormData(form); fd.append('action', 'hwq_submit'); fd.append('source_url', window.location.href); try { const r = await fetch('https://www.hostwebo.it/wp-admin/admin-ajax.php', { method: 'POST', body: fd, credentials: 'same-origin' }); const data = await r.json(); if (data && data.success) { result.className = 'hwq-result success'; result.textContent = data.data && data.data.message ? data.data.message : 'Richiesta ricevuta. Ti contatteremo entro 24h.'; result.hidden = false; form.reset(); setTimeout(close, 4000); } else { result.className = 'hwq-result error'; result.textContent = (data && data.data && data.data.message) ? data.data.message : 'Errore: riprova.'; result.hidden = false; } } catch(err) { result.className = 'hwq-result error'; result.textContent = 'Errore di rete. Riprova fra qualche istante.'; result.hidden = false; } finally { submitBtn.disabled = false; submitLabel.hidden = false; submitSpin.hidden = true; } }); })(); </script> <script id="hostwebo-theme-js-extra"> var hostweboData = {"currency":"\u20ac","darkMode":{"mode":"auto","dayStart":7,"nightStart":19},"restRoot":"https://www.hostwebo.it/wp-json/"}; //# sourceURL=hostwebo-theme-js-extra </script> <script id="hostwebo-theme-js" defer src="https://www.hostwebo.it/wp-content/themes/hostwebo/assets/js/theme.js?ver=1779132485"></script> <script id="hwai-widget-js-extra"> var HWAI_CONFIG = {"rest_url":"https://www.hostwebo.it/wp-json/hostwebo-ai/v1/","nonce":"65d4d2a49b","mode":"public_sales","user_logged_in":"","agent_default":"marketing","allowed_agents":[],"public_widget_mode":"both","public_max_chars":"1500","i18n":{"placeholder":"Scrivi qui la tua domanda...","send":"Invia","thinking":"Sto elaborando...","opener_public":"Ciao! \ud83d\udc4b Sono il consulente Hostwebo. Ti aiuto a scegliere il piano giusto?","opener_private":"Ciao! Sono il tuo assistente Hostwebo. Come posso aiutarti?","quota_label":"Utilizzo AI","quota_exhausted":"Hai esaurito le richieste AI per oggi. Continuer\u00f2 ad aiutarti con risposte basate sulla nostra documentazione."}}; //# sourceURL=hwai-widget-js-extra </script> <script id="hwai-widget-js" src="https://www.hostwebo.it/wp-content/plugins/hostwebo-ai/assets/js/widget.js?ver=1.0.0-alpha.131.0-rg7-themestudio"></script> <script id="wp-emoji-settings" type="application/json"> {"baseUrl":"https://s.w.org/images/core/emoji/17.0.2/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/17.0.2/svg/","svgExt":".svg","source":{"wpemoji":"https://www.hostwebo.it/wp-includes/js/wp-emoji.js?ver=7.0","twemoji":"https://www.hostwebo.it/wp-includes/js/twemoji.js?ver=7.0"}} </script> <script type="module"> /** * @output wp-includes/js/wp-emoji-loader.js */ /* eslint-env es6 */ // Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. /** * Emoji Settings as exported in PHP via _print_emoji_detection_script(). * @typedef WPEmojiSettings * @type {object} * @property {?object} source * @property {?string} source.concatemoji * @property {?string} source.twemoji * @property {?string} source.wpemoji */ const settings = /** @type {WPEmojiSettings} */ ( JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) ); // For compatibility with other scripts that read from this global, in particular wp-includes/js/wp-emoji.js (source file: js/_enqueues/wp/emoji.js). window._wpemojiSettings = settings; /** * Support tests. * @typedef SupportTests * @type {object} * @property {?boolean} flag * @property {?boolean} emoji */ const sessionStorageKey = 'wpEmojiSettingsSupports'; const tests = [ 'flag', 'emoji' ]; /** * Checks whether the browser supports offloading to a Worker. * * @since 6.3.0 * * @private * * @returns {boolean} */ function supportsWorkerOffloading() { return ( typeof Worker !== 'undefined' && typeof OffscreenCanvas !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL && typeof Blob !== 'undefined' ); } /** * @typedef SessionSupportTests * @type {object} * @property {number} timestamp * @property {SupportTests} supportTests */ /** * Get support tests from session. * * @since 6.3.0 * * @private * * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. */ function getSessionSupportTests() { try { /** @type {SessionSupportTests} */ const item = JSON.parse( sessionStorage.getItem( sessionStorageKey ) ); if ( typeof item === 'object' && typeof item.timestamp === 'number' && new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. typeof item.supportTests === 'object' ) { return item.supportTests; } } catch ( e ) {} return null; } /** * Persist the supports in session storage. * * @since 6.3.0 * * @private * * @param {SupportTests} supportTests Support tests. */ function setSessionSupportTests( supportTests ) { try { /** @type {SessionSupportTests} */ const item = { supportTests: supportTests, timestamp: new Date().valueOf() }; sessionStorage.setItem( sessionStorageKey, JSON.stringify( item ) ); } catch ( e ) {} } /** * Checks if two sets of Emoji characters render the same visually. * * This is used to determine if the browser is rendering an emoji with multiple data points * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser * does not support the emoji correctly. * * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing * scope. Everything must be passed by parameters. * * @since 4.9.0 * * @private * * @param {CanvasRenderingContext2D} context 2D Context. * @param {string} set1 Set of Emoji to test. * @param {string} set2 Set of Emoji to test. * * @return {boolean} True if the two sets render the same. */ function emojiSetsRenderIdentically( context, set1, set2 ) { // Cleanup from previous test. context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); context.fillText( set1, 0, 0 ); const rendered1 = new Uint32Array( context.getImageData( 0, 0, context.canvas.width, context.canvas.height ).data ); // Cleanup from previous test. context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); context.fillText( set2, 0, 0 ); const rendered2 = new Uint32Array( context.getImageData( 0, 0, context.canvas.width, context.canvas.height ).data ); return rendered1.every( ( rendered2Data, index ) => { return rendered2Data === rendered2[ index ]; } ); } /** * Checks if the center point of a single emoji is empty. * * This is used to determine if the browser is rendering an emoji with a single data point * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly * rendered emoji will have a non-zero value at the center point. * * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing * scope. Everything must be passed by parameters. * * @since 6.8.2 * * @private * * @param {CanvasRenderingContext2D} context 2D Context. * @param {string} emoji Emoji to test. * * @return {boolean} True if the center point is empty. */ function emojiRendersEmptyCenterPoint( context, emoji ) { // Cleanup from previous test. context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); context.fillText( emoji, 0, 0 ); // Test if the center point (16, 16) is empty (0,0,0,0). const centerPoint = context.getImageData(16, 16, 1, 1); for ( let i = 0; i < centerPoint.data.length; i++ ) { if ( centerPoint.data[ i ] !== 0 ) { // Stop checking the moment it's known not to be empty. return false; } } return true; } /** * Determines if the browser properly renders Emoji that Twemoji can supplement. * * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing * scope. Everything must be passed by parameters. * * @since 4.2.0 * * @private * * @param {CanvasRenderingContext2D} context 2D Context. * @param {string} type Whether to test for support of "flag" or "emoji". * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. * * @return {boolean} True if the browser can render emoji, false if it cannot. */ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { let isIdentical; switch ( type ) { case 'flag': /* * Test for Transgender flag compatibility. Added in Unicode 13. * * To test for support, we try to render it, and compare the rendering to how it would look if * the browser doesn't render it correctly (white flag emoji + transgender symbol). */ isIdentical = emojiSetsRenderIdentically( context, '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space ); if ( isIdentical ) { return false; } /* * Test for Sark flag compatibility. This is the least supported of the letter locale flags, * so gives us an easy test for full support. * * To test for support, we try to render it, and compare the rendering to how it would look if * the browser doesn't render it correctly ([C] + [Q]). */ isIdentical = emojiSetsRenderIdentically( context, '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space ); if ( isIdentical ) { return false; } /* * Test for English flag compatibility. England is a country in the United Kingdom, it * does not have a two letter locale code but rather a five letter sub-division code. * * To test for support, we try to render it, and compare the rendering to how it would look if * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). */ isIdentical = emojiSetsRenderIdentically( context, // as the flag sequence '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', // with each code point separated by a zero-width space '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' ); return ! isIdentical; case 'emoji': /* * Is there a large, hairy, humanoid mythical creature living in the browser? * * To test for Emoji 17.0 support, try to render a new emoji: Hairy Creature. * * The hairy creature emoji is a single code point emoji. Testing for browser * support required testing the center point of the emoji to see if it is empty. * * 0xD83E 0x1FAC8 (\uD83E\u1FAC8) == 🫈 Hairy creature. * * When updating this test, please ensure that the emoji is either a single code point * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width * joiner vs a zero-width space. */ const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\u1FAC8' ); return ! notSupported; } return false; } /** * Checks emoji support tests. * * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing * scope. Everything must be passed by parameters. * * @since 6.3.0 * * @private * * @param {string[]} tests Tests. * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. * * @return {SupportTests} Support tests. */ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { let canvas; if ( typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope ) { canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. } else { canvas = document.createElement( 'canvas' ); } const context = canvas.getContext( '2d', { willReadFrequently: true } ); /* * Chrome on OS X added native emoji rendering in M41. Unfortunately, * it doesn't work when the font is bolder than 500 weight. So, we * check for bold rendering support to avoid invisible emoji in Chrome. */ context.textBaseline = 'top'; context.font = '600 32px Arial'; const supports = {}; tests.forEach( ( test ) => { supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); } ); return supports; } /** * Adds a script to the head of the document. * * @ignore * * @since 4.2.0 * * @param {string} src The url where the script is located. * * @return {void} */ function addScript( src ) { const script = document.createElement( 'script' ); script.src = src; script.defer = true; document.head.appendChild( script ); } settings.supports = { everything: true, everythingExceptFlag: true }; // Obtain the emoji support from the browser, asynchronously when possible. new Promise( ( resolve ) => { let supportTests = getSessionSupportTests(); if ( supportTests ) { resolve( supportTests ); return; } if ( supportsWorkerOffloading() ) { try { // Note that the functions are being passed as arguments due to minification. const workerScript = 'postMessage(' + testEmojiSupports.toString() + '(' + [ JSON.stringify( tests ), browserSupportsEmoji.toString(), emojiSetsRenderIdentically.toString(), emojiRendersEmptyCenterPoint.toString() ].join( ',' ) + '));'; const blob = new Blob( [ workerScript ], { type: 'text/javascript' } ); const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); worker.onmessage = ( event ) => { supportTests = event.data; setSessionSupportTests( supportTests ); worker.terminate(); resolve( supportTests ); }; return; } catch ( e ) {} } supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); setSessionSupportTests( supportTests ); resolve( supportTests ); } ) // Once the browser emoji support has been obtained from the session, finalize the settings. .then( ( supportTests ) => { /* * Tests the browser support for flag emojis and other emojis, and adjusts the * support settings accordingly. */ for ( const test in supportTests ) { settings.supports[ test ] = supportTests[ test ]; settings.supports.everything = settings.supports.everything && settings.supports[ test ]; if ( 'flag' !== test ) { settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && settings.supports[ test ]; } } settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && ! settings.supports.flag; // When the browser can not render everything we need to load a polyfill. if ( ! settings.supports.everything ) { const src = settings.source || {}; if ( src.concatemoji ) { addScript( src.concatemoji ); } else if ( src.wpemoji && src.twemoji ) { addScript( src.twemoji ); addScript( src.wpemoji ); } } } ); //# sourceURL=https://www.hostwebo.it/wp-includes/js/wp-emoji-loader.js </script> <hostwebo-ai-widget mode="public_sales"></hostwebo-ai-widget></body> </html>