# Embeddare il widget

# Widget di prenotazione embeddabile

Il widget di prenotazione è un iframe da incollare in qualsiasi sito esterno. Renderizza un calendario localizzato degli eventi **pubblicati** imminenti dell'azienda e gestisce l'intero flusso di prenotazione — inclusa waitlist e handoff widget-auth.

## Tre modi di integrazione

Apri **Impostazioni azienda → tab Widget prenotazioni** (`/companies/{company}/edit?tab=widget` — owner/admin). Scegli il percorso adatto alla piattaforma:

- **Condividi un link** — il link diretto di prenotazione (`/book/{company}`) funziona ovunque: pulsanti di navigazione, bio, email. I link per canale pre-taggano sorgente/medium UTM. Zero codice.
- **Script di una riga** *(predefinito)* — un solo tag `<script src="…/api/embed/snippet.js?public_key=bw_pk_…">`. Inserisce il pulsante di prenotazione dove lo incolli e carica il runtime da solo. Il clic apre il calendario come overlay. Richiede una pagina che esegua JavaScript — la maggior parte delle piattaforme lo fa.
- **Frame incorporato** — un tag `<iframe src="…/w/bw_pk_…">`. `/w/{public_key}` crea la sessione embed lato server e reindirizza a `/embed?token=…`, quindi non serve alcun JS nella pagina ospite — il formato che sopravvive ai CMS che rimuovono gli script (blocchi embed di Wix e Squarespace, piani gratuiti wordpress.com). Compromesso: il calendario appare incorporato nella pagina invece che come overlay. Con **Domini consentiti** configurati, l'URL `/w/` funziona solo dentro pagine di quei domini — aperto come link diretto (email, chat, host con `referrerpolicy="no-referrer"`) risponde "Widget non disponibile".
- **HTML avanzato** — il blocco originale `<button>` + `<script>` inline. Identico a ciò che le installazioni precedenti hanno già pubblicato, ma incorpora la selezione corrente nel markup: dopo aver cambiato filtri o stile va ricopiato (lo script di una riga serve sempre le opzioni salvate).

Le **guide per piattaforma** nella tab danno i passi per CMS (WordPress, Wix/Squarespace, Shopify, HTML personalizzato), e **Verifica il tuo embed** scarica l'URL di pagina che incolli e riporta se trova la tua public key, i runtime del widget o un link `/book/` — riservato ai gestori widget, con rate limit e difese contro richieste verso reti private.

Gli host che creano sessioni embed vengono ricordati (più recenti prima, max 20) e proposti come **suggerimenti** con un clic sopra la textarea dei domini auth consentiti — finiscono solo nella textarea; nulla persiste finché non salvi. Fidarsi automaticamente di un host visto non sarebbe sicuro, quindi l'aggiunta è sempre esplicita.

`generateWidgetSnippet()` crea la `WidgetIntegration` aziendale al primo uso (public key `bw_pk_…`, secret `bw_sk_…` — il secret non è mai usato lato client). I campi bottone si risolvono per-campo: evento → tipo evento → azienda → default `config/booking-widget.php`. Il livello azienda si modifica sotto **Avanzate → Stile predefinito bottone** — alimenta ogni bottone widget (prenotazioni, richieste, voucher). La pagina welcome della dashboard renderizza il bottone live quando l'azienda non ha ancora prenotazioni.

## oEmbed

Le pagine `/book/{company}` e `/book/{company}/{eventType}` pubblicizzano `GET /oembed?url=…` via `<link rel="alternate" type="application/json+oembed">`. I CMS compatibili con oEmbed trasformano automaticamente un link di prenotazione incollato in un iframe del widget incorporato. Il provider risponde solo per URL sull'host di questa app (`/book/*` e `/w/*`), restituisce HTML `type: rich` che punta a `/w/{key}`, e limita `maxwidth`/`maxheight` a 1200/720. Nota: i CMS che isolano i provider oEmbed non fidati (WordPress avvolge i provider non in whitelist in un frame sandbox dove cookie e form sono bloccati) — usa lì lo snippet script o l'URL iframe `/w/`.

## Filtri e stile

Sotto **Avanzate** puoi scegliere **tipi evento** e/o **eventi** per limitare ciò che il widget mostra (vuoto = tutto). Un singolo evento selezionato apre il widget direttamente su di esso. I filtri confluiscono in ogni formato — URL dello script, URL iframe `/w/` e HTML grezzo — quindi genera una volta e poi copia. Sovrascrizione di **testo / classi / stile del bottone**; l'anteprima live renderizza il bottone reale.

## Come funziona l'embed

1. Il bottone dello snippet chiama `POST /api/embed/session` con la `public_key` → restituisce un token `bw_et_…` da 15 minuti e l'URL iframe `/embed?token=…`.
2. L'iframe carica `/embed` attraverso il middleware `widget.locale`; `ConfigureEmbedSession` imposta `SameSite=None` su HTTPS così il cookie di sessione sopravvive negli iframe cross-site.
3. Se l'integrazione ha `allowed_domains`, l'header `Origin`/`Referer` deve combaciare — il parametro `origin` non è mai considerato fidato. L'origine dell'app stessa passa sempre: la allowlist blocca gli embedder esterni, mai l'host che serve la pagina.
4. Quando l'iframe non può portare il cookie di sessione (un embed cross-site su host non-HTTPS non può emettere `SameSite=None`), le pagine `booking/*` accettano il fallback `embed` + `embed_parent_origin` — validato contro `allowed_domains` dell'integrazione (o l'origine dell'app) prima di rilassare `frame-ancestors`.

Ogni render di `/embed` riuscito marca anche l'origine host validata nella sessione con un timestamp. Le pagine `booking/*` e gli header di sicurezza si fidano di quel marcatore solo finché è fresco — `booking-widget.embed_marker_ttl` secondi, sei ore di default (il token iframe `bw_et_…` scade comunque dopo 15 minuti). Un marcatore mancante, malformato, nel futuro o scaduto fallisce chiuso: la pagina torna all'origine dell'app, `?embed=1` da solo può solo rinfrescare un marcatore già fresco — mai resuscitarne uno morto — e resta il fallback allowlist `embed_parent_origin`.

Prima di imporre una allowlist in produzione, esegui `php artisan widget:allowlist-audit`: elenca ogni integrazione attiva con funzionalità widget e `allowed_domains` ancora vuota (exit 1 se ne esistono, sola lettura), così puoi popolare la lista in anticipo invece di bloccare gli embed attivi.

## Link diretti di prenotazione

- `/api/{company}/direct-booking` — apre il widget dritto sugli eventi di un'azienda.
- `/api/{eventType}/event-type-booking` — preseleziona un tipo evento.
- Aggiungi `?open=1` a qualsiasi URL `/book/...` — il widget si apre al caricamento della pagina, senza clic (il "Link di prenotazione immediata" mostrato nelle impostazioni widget).
- `data-area-id` sul bottone dello snippet (o `area_id` su `POST /api/embed/session`) — deep-link diretto a un'area per le aziende che offrono la prenotazione aree.
- `data-event-type-ids` / `data-event-ids` sul bottone dello snippet (o array `event_type_ids` / `event_ids` su `POST /api/embed/session`, CSV su `GET /api/embed/snippet`) — whitelist separate da virgola: il calendario del widget mostra solo i tipi/eventi elencati. Gli attributi singolari `data-event-type-id` / `data-event-id` restano supportati; `data-event-id` inoltre fissa e apre quell'evento.
- `data-cal-display` sul bottone dello snippet (o oggetto `cal_display` su `POST /api/embed/session`) — overlay di visualizzazione per-embed, vedi sotto.

## Avvisami quando aprono le date

La pagina pubblica di un tipo evento (`/book/{company}/{eventType}`) senza date aperte mostra il modulo **Avvisami**: email più una casella di consenso marketing esplicita e non preselezionata. L'invio crea o aggiorna l'ospite (ricevuta di consenso registrata, tag `event-interest`) e salva una riga in `event_type_interests`; la risposta è identica sia che l'email fosse già nota sia di no. Quando una data di quel tipo passa a *Pubblicato* ed è prenotabile, ogni ospite in attesa riceve **una sola** email di annuncio con il link per prenotare (consenso ricontrollato all'invio, disiscrizione con un clic). L'invio viene reclamato in modo atomico tramite `notified_at`, quindi pubblicazioni ripetute non scrivono mai due volte allo stesso ospite; solo un nuovo consenso lo riarma. L'editor del tipo evento mostra quanti ospiti sono in attesa. La cancellazione dell'ospite elimina i suoi interessi.

## Attribuzione

`POST /api/embed/session` riceve già l'URL della pagina ospite (`page_url`, inviato da `widget.js`). Alla creazione della sessione i cinque parametri `utm_*` di quell'URL vengono filtrati in whitelist, troncati e salvati sulla sessione embed; le prenotazioni create dentro l'iframe li ereditano in `bookings.utm` (`{source, medium, campaign, term, content}`). Parametri `utm_*` espliciti sull'URL `/booking/confirm` vincono sulla copia di sessione — sono il salto più fresco. Le prenotazioni senza nessuno dei due portano `utm = null`. La sezione dashboard **Per fonte** riporta le prenotazioni per `bookings.source` (web/widget/staff/walkin…) più le principali `utm_campaign`, scopate per azienda.

## Regole di visualizzazione

**Impostazioni azienda → Visualizzazione calendario** (`/companies/{company}/edit?tab=display` — owner/admin) decide cosa il calendario pubblico elenca e come ogni elemento si legge, per condizione. Condizioni evento: `available`, `sold_out`, `suspended`, `closed`, `draft`, `unavailable` (limiti/figli), `upcoming` (vendite non aperte), `sales_expired` (finestra vendite chiusa), `ended`. Posti/orari accettano `available`, `sold_out`, `closed`, `unavailable`; le aree le stesse più il toggle `closed_day` per i giorni la cui programmazione è stata interamente rimossa dalle chiusure.

Ogni regola sceglie **mostra/nascondi**, **apre il pannello** (`auto` riproduce il comportamento precedente — i prenotabili si aprono, i sold-out si aprono solo con la lista d'attesa attiva, i fallimenti per soli figli aprono un pannello in sola lettura, tutto il resto resta chiuso — `always`/`never` lo sovrascrivono), e un **badge**: etichetta di stato, posti rimasti, totale prenotati, prenotati/capacità, percentuale prenotata, testo personalizzato (token `{availables}` `{booked}` `{capacity}` `{from}` `{to}`), oppure nessuno. `capacity.threshold` nasconde i badge "posti rimasti" finché restano più di N posti; il `show_bookable_threshold` del singolo prenotabile ha comunque priorità.

Le regole si ereditano a cascata azienda → tipo evento → evento (ogni livello sovrascrive per condizione) e il salvataggio emette `venue.updated`, così i widget aperti si ridipingono live. I vincoli sono solo di visualizzazione: gli elementi nascosti non sono mai serializzati né prenotabili, e nulla qui allenta autorizzazione, capacità o controlli sugli archiviati.

Un singolo embed può adattare la policy con `data-cal-display='{"events":{"closed":{"visible":false}}}'` — l'overlay si fonde per ultimo nella cascata, quindi vince per ogni chiave che imposta. È sanificato con whitelist: sezioni, condizioni e campi sconosciuti vengono scartati, `visible` non si può forzare su `available`, e non può mai aggirare i controlli su tenant, archiviati o capacità.

## Modalità aree

Quale calendario renderizza l'iframe dipende dal modello di prenotazione dell'azienda:

- Le aziende **`areas`** ottengono sempre il calendario aree (`/widget/areas`) — scegli un giorno → un'area → una finestra `[from, to)`.
- Le aziende **ibride** (`events` + *Consenti prenotazioni senza evento*) renderizzano il calendario eventi unificato, che offre anche la scelta area "senza evento"; un deep-link `data-area-id` apre il calendario aree direttamente su quell'area.
- Quando almeno un'area ha una forma disegnata, entrambi i widget mostrano la mappa del locale come selettore delle aree (forme toccabili, aree non disegnate su una riga di chip); i tavoli sul floor sono dipinti come decorazione. Senza nulla di disegnato, i clienti vedono la lista semplice. Dentro le prenotazioni di evento con posti la stessa mappa funge anche da selettore delle posizioni — i posti prendono la forma della loro area madre — incluse le aziende solo-eventi che attivano `area_map_picker`.

Vedi [Prenotazione aree](/docs/venue/area-booking) per le impostazioni a livello di area che lo governano.

## Voucher vendibili

Le aziende possono vendere voucher regalo dal proprio sito — **Impostazioni azienda → tab Voucher** (`?tab=vouchers`) configura gli importi predefiniti, il range dell'importo libero, i giorni di validità, il link alle condizioni e mostra lo storico acquisti. Abilitare lo shop sblocca la modalità voucher nel builder embed unificato (**tab Widget prenotazioni**, selettore → *Voucher regalo*): un bottone `data-promo-voucher` che apre lo shop voucher nello stesso overlay (`POST /api/embed/session` con `mode: 'voucher'`). Testo/classi/stile del bottone persistono nel meta dell'integrazione sotto `buttons.voucher` — i campi vuoti ereditano lo stile predefinito aziendale del bottone.

Il compratore sceglie l'importo, inserisce la propria email, opzionalmente lo invia come regalo (email destinatario + messaggio), e paga tramite l'account Stripe connesso dell'azienda — stessa meccanica destination-charge delle prenotazioni. Su `checkout.session.completed` il sistema crea un `PromoCode` whitelist (`source='purchase'`, `balance` = importo) e lo invia via email al compratore o al destinatario regalo.

I codici voucher sono **multi-uso**: ogni riscatto decrementa `balance` sotto lock della riga promo, quindi il valore residuo continua a funzionare su prenotazioni successive fino a esaurimento. Acquisto scaduto/rimborsato → `status='expired'`/`'refunded'`; un rimborso Stripe completo disattiva il codice creato.

**Voucher mirati**: selezionare eventi o tipi di evento nel builder in modalità voucher incorpora `data-event-id`/`data-event-ids` nel bottone voucher — ogni voucher acquistato tramite esso resta vincolato a quella selezione. Lo shop mostra la limitazione prima del pagamento, il codice creato rifiuta qualsiasi altra prenotazione (errore di validazione per i clienti E per le approvazioni staff), e la mail di consegna dichiara l'ambito. `data-area-id` viene ignorato ai fini del targeting. Gli stessi selettori esistono nell'editor codici promo per i codici manuali.

**Reinvio + card stampabile**: ogni riga pagata dello storico acquisti ha un'azione **Reinvia** che rispedisce la mail del codice al titolare (destinatario regalo se presente, altrimenti acquirente). La mail di consegna linka anche una gift card stampabile brandizzata su `GET /voucher/{code}/card` — logo, importo, saldo residuo, destinazione e scadenza, con bottone stampa/"salva come PDF". Il codice stesso è la credenziale della pagina (viaggia già nella mail); la rotta è rate-limited e solo i voucher acquistati si risolvono — i codici promo staff danno 404.

## Widget lead-gen: richieste e raccolta

Il selettore nella **tab Widget prenotazioni** genera anche le superfici lead-gen — stessi formati embed (script / iframe / HTML), stessa pipeline di sessione, stessa allowlist di origini:

- **Richiesta evento privato** (`mode: 'inquiry'`, trigger `data-inquiry`) apre il form lead del locale dentro `/embed` con `source=widget` marcato al mount. I parametri calendario (`event_id`, `area_id`, `cal_display`, …) sono ignorati. I lead arrivano nell'[inbox lead](/docs/venue/leads).
- **Raccolta email** (`mode: 'capture'`, trigger `data-capture`) apre il form iscrizione con consenso dentro `/embed`. Le iscrizioni fanno upsert nel guestbook con semantica primo-consenso-vince.

Entrambi i form mantengono gli URL pubblici diretti — `/inquiry/{company}` e `/capture/{company}` — per i link diretti. Una volta configurati gli `allowed_domains`, quelle pagine possono essere *incorporate* solo dai siti consentiti (`frame-ancestors`); senza lista configurata restano liberamente incorporabili come prima.

## Interruttori per superficie

In **Avanzate → Superfici widget** ogni modalità ha un toggle. Spegnendola la si scrive in `meta.disabled_modes` dell'integrazione e ogni entrypoint la rifiuta all'istante — generazione sessioni, `snippet`/`snippet.js`, redirect `/w/`, render `/embed` già attivi e le pagine pubbliche dirette. Le altre modalità non vengono toccate; riattivandola il servizio riprende subito.

## Link di prenotazione per canale

La tab widget prenotazioni mostra anche i **deep link per canale**: URL di prenotazione pronti da incollare già taggati con sorgente/medium UTM, così l'[attribuzione sorgente](/docs/bookings/manage-bookings) su ogni prenotazione divide il traffico per canale — Google Business Profile, Instagram, Facebook, più un campo sorgente libera che normalizza ciò che digiti. Un tag campagna opzionale si applica a ogni riga. Copia il link nel campo "Prenota online" del Profilo dell'attività, nella bio di Instagram o nel pulsante "Prenota ora" di Facebook e le prenotazioni portano la sorgente automaticamente — nessun embed necessario.

## Dentro il widget

I clienti vedono gli eventi pubblicati, dentro la finestra di prenotazione e non sold-out (quelli sold-out offrono la waitlist quando abilitata). Lingua e tema si cambiano dentro l'iframe. Gli eventi di aziende **private** sono prenotabili solo dai membri azienda.

## Permessi

La gestione dello snippet è owner/admin via impostazioni azienda. Il widget stesso è pubblico — chiunque abbia l'URL embed può navigare e prenotare (salvo il flag `private` dell'azienda). Un utente loggato a un'azienda **diversa** vede il widget esattamente come un visitatore anonimo — nessun permesso staff, nessuna prenotazione forzata — e la sua azienda attiva non cambia mai.


---
> Source: https://placeful-app.com/docs/widget/embed-widget
