Storefront modal + email templating: theme extension, redesign, editable receipt

Storefront (theme app extension recesso-storefront):
- App block + app embed che linkano a /apps/recesso; modal via iframe con
  fallback progressive-enhancement; apertura immediata, dimensione fissa
- Redesign form: pattern modal moderno (header/footer fissi, corpo scorrevole),
  modalita' embed, popover info "i", testo ridotto, de-AI, trattini al posto degli em-dash

Ricevuta email:
- Dati per-shop dinamici da Shopify (nome, url, link stato ordine) via getShopInfo + statusPageUrl
- Template editabili VINCOLATI (oggetto/introduzione/nota) con segnaposto; layout
  fisso e conforme (dichiarazione, timestamp, avviso art. 54-bis)
- Pagina admin (Polaris) con anteprima live + ripristina default
- Campi email in Settings (Prisma) + migrazioni

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
This commit is contained in:
2026-07-07 12:17:07 +02:00
parent 8ca4de6c0b
commit d140d90629
18 changed files with 1002 additions and 178 deletions

View File

@@ -0,0 +1,106 @@
/* Stile dei blocchi storefront di recesso. Scoped con prefisso .recesso- per non
interferire col tema del merchant. */
/* App block "Recesso - pulsante" */
.recesso-block { margin: 12px 0; }
.recesso-align-center { text-align: center; }
.recesso-align-right { text-align: right; }
.recesso-link {
color: inherit;
text-decoration: underline;
font-size: 15px;
line-height: 1.4;
}
.recesso-button {
display: inline-block;
padding: 12px 20px;
min-height: 44px;
box-sizing: border-box;
border-radius: 8px;
background: #1a1a1a;
color: #ffffff;
text-decoration: none;
font-size: 15px;
font-weight: 600;
line-height: 1.3;
transition: opacity .15s ease;
}
.recesso-button:hover { opacity: .9; }
.recesso-link:focus-visible,
.recesso-button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* App embed "Recesso - link globale" */
.recesso-embed { padding: 12px 16px; text-align: center; }
.recesso-embed-left { text-align: left; }
.recesso-embed-right { text-align: right; }
.recesso-embed-link {
font-size: 14px;
color: inherit;
text-decoration: underline;
line-height: 1.4;
}
.recesso-embed-link:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Modal (iframe sul flusso /apps/recesso) */
.recesso-modal-overlay {
position: fixed;
inset: 0;
z-index: 2147483000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, .5);
padding: 16px;
}
.recesso-modal-overlay.is-open { display: flex; }
.recesso-modal {
position: relative;
width: 100%;
max-width: 600px;
height: min(760px, 92vh); /* fissa: apertura immediata; header/footer ancorati, solo il corpo scorre */
background: #ffffff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 24px 70px rgba(0, 0, 0, .35);
}
.recesso-modal-frame {
width: 100%;
height: 100%;
border: 0;
display: block;
}
.recesso-modal-close {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, .06);
color: #111111;
font-size: 22px;
line-height: 1;
cursor: pointer;
}
.recesso-modal-close:hover { background: rgba(0, 0, 0, .12); }
.recesso-modal-close:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
@media (max-width: 480px) {
.recesso-modal { height: 92vh; max-height: none; border-radius: 12px; }
}
@media (prefers-color-scheme: dark) {
.recesso-modal { background: #1a1a1a; }
.recesso-modal-close { background: rgba(255, 255, 255, .12); color: #ffffff; }
}

View File

@@ -0,0 +1,81 @@
/* Modal del flusso di recesso.
Progressive enhancement: i link puntano a /apps/recesso (funzionano senza JS
come pagina piena). Se questo script è caricato, intercetta QUALSIASI link a
/apps/recesso - anche una voce di menu o un testo linkato dal merchant - e lo
apre in un modal con iframe sul flusso (stesso dominio → framing consentito).
Opt-out per singolo link: attributo data-recesso-no-modal. */
(function () {
if (window.__recessoModalInit) return; // guard: eseguito una volta sola
window.__recessoModalInit = true;
var PROXY_PATH = "/apps/recesso";
var overlay = null;
var lastFocus = null;
function build() {
var o = document.createElement("div");
o.className = "recesso-modal-overlay";
o.setAttribute("role", "dialog");
o.setAttribute("aria-modal", "true");
o.setAttribute("aria-label", "Recesso dal contratto");
o.innerHTML =
'<div class="recesso-modal">' +
'<button type="button" class="recesso-modal-close" aria-label="Chiudi">×</button>' +
'<iframe class="recesso-modal-frame" title="Recesso dal contratto"></iframe>' +
"</div>";
o.addEventListener("click", function (e) {
if (e.target === o) close();
});
o.querySelector(".recesso-modal-close").addEventListener("click", close);
return o;
}
function onKey(e) {
if (e.key === "Escape" || e.key === "Esc") close();
}
function open(url) {
lastFocus = document.activeElement;
if (!overlay) {
overlay = build();
document.body.appendChild(overlay);
}
overlay.querySelector(".recesso-modal-frame").src = url;
overlay.classList.add("is-open");
document.documentElement.style.overflow = "hidden";
document.addEventListener("keydown", onKey);
var btn = overlay.querySelector(".recesso-modal-close");
if (btn && btn.focus) btn.focus();
}
function close() {
if (!overlay) return;
overlay.classList.remove("is-open");
overlay.querySelector(".recesso-modal-frame").src = "about:blank";
document.documentElement.style.overflow = "";
document.removeEventListener("keydown", onKey);
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
function normalizePath(p) {
return (p || "").replace(/\/+$/, "");
}
// Click delegato: intercetta ogni <a> che punta a /apps/recesso (path match),
// salvo opt-out esplicito. Funziona per blocchi app, app embed e link/menu
// creati dal merchant. Solo click primario, senza modificatori/target.
document.addEventListener("click", function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target.closest ? e.target.closest("a[href]") : null;
if (!a || a.hasAttribute("data-recesso-no-modal")) return;
var path;
try {
path = normalizePath(new URL(a.href, window.location.origin).pathname);
} catch (_) {
return;
}
if (path !== PROXY_PATH) return;
e.preventDefault();
open(a.getAttribute("href") || PROXY_PATH);
});
})();

View File

@@ -0,0 +1,56 @@
{%- comment -%}
App embed "Recesso - link globale". Attivabile site-wide dal theme editor
(Impostazioni tema → App embed). Carica CSS+JS su TUTTE le pagine: questo abilita
il modal su QUALSIASI link a /apps/recesso (anche voci di menu o testi linkati
dal merchant). In più, opzionalmente, mostra un proprio link discreto a fondo pagina.
CSS/JS in assets/.
{%- endcomment -%}
{{ 'recesso-storefront.css' | asset_url | stylesheet_tag }}
{{ 'recesso-storefront.js' | asset_url | script_tag }}
{% if block.settings.show_link %}
<div class="recesso-embed recesso-embed-{{ block.settings.alignment }}">
<a
class="recesso-embed-link"
href="/apps/recesso"
>{{ block.settings.label | default: 'Recedere dal contratto qui' | escape }}</a>
</div>
{% endif %}
{% schema %}
{
"name": "Recesso - link globale",
"target": "body",
"settings": [
{
"type": "paragraph",
"content": "Attivando questo embed, qualsiasi link a /apps/recesso nel tuo tema (voci di menu, testi linkati, ecc.) si apre in un popup. Puoi anche mostrare un link discreto a fondo pagina."
},
{
"type": "checkbox",
"id": "show_link",
"label": "Mostra anche un link a fondo pagina",
"default": true
},
{
"type": "text",
"id": "label",
"label": "Testo del link",
"info": "Per legge deve essere inequivocabile.",
"default": "Recedere dal contratto qui"
},
{
"type": "select",
"id": "alignment",
"label": "Allineamento",
"options": [
{ "value": "left", "label": "Sinistra" },
{ "value": "center", "label": "Centro" },
{ "value": "right", "label": "Destra" }
],
"default": "center"
}
]
}
{% endschema %}

View File

@@ -0,0 +1,87 @@
{%- comment -%}
App block "Recesso - pulsante". Il merchant lo aggiunge in QUALSIASI sezione
dal theme editor e lo personalizza. Linka al flusso via App Proxy /apps/recesso.
Se "Apri in un popup" è attivo, il JS apre un modal con iframe sul flusso
(fallback: pagina piena se JS off). CSS/JS in assets/ (le app block non
ammettono i tag stylesheet/javascript inline).
{%- endcomment -%}
{%- assign rc_label = block.settings.label | default: 'Recedere dal contratto qui' -%}
{{ 'recesso-storefront.css' | asset_url | stylesheet_tag }}
{% if block.settings.use_modal %}{{ 'recesso-storefront.js' | asset_url | script_tag }}{% endif %}
<div class="recesso-block recesso-align-{{ block.settings.alignment }}" {{ block.shopify_attributes }}>
<a
class="recesso-{{ block.settings.style }}"
href="/apps/recesso"
{% unless block.settings.use_modal %}data-recesso-no-modal{% endunless %}
{% if block.settings.open_new_tab and block.settings.use_modal == false %}target="_blank" rel="noopener"{% endif %}
{%- if block.settings.style == 'button' and block.settings.button_bg != blank %} style="background:{{ block.settings.button_bg }};color:{{ block.settings.button_text | default: '#ffffff' }};"{% endif -%}
>{{ rc_label | escape }}</a>
</div>
{% schema %}
{
"name": "Recesso - pulsante",
"target": "section",
"settings": [
{
"type": "text",
"id": "label",
"label": "Testo del link",
"info": "Per legge deve essere inequivocabile (es. \"Recedere dal contratto qui\").",
"default": "Recedere dal contratto qui"
},
{
"type": "select",
"id": "style",
"label": "Stile",
"options": [
{ "value": "link", "label": "Link" },
{ "value": "button", "label": "Bottone" }
],
"default": "button"
},
{
"type": "select",
"id": "alignment",
"label": "Allineamento",
"options": [
{ "value": "left", "label": "Sinistra" },
{ "value": "center", "label": "Centro" },
{ "value": "right", "label": "Destra" }
],
"default": "left"
},
{
"type": "checkbox",
"id": "use_modal",
"label": "Apri in un popup (modal)",
"info": "Se disattivo, apre la pagina intera del flusso.",
"default": true
},
{
"type": "checkbox",
"id": "open_new_tab",
"label": "Apri in una nuova scheda",
"info": "Solo se il popup è disattivo.",
"default": false
},
{
"type": "header",
"content": "Colori (solo stile Bottone)"
},
{
"type": "color",
"id": "button_bg",
"label": "Sfondo bottone"
},
{
"type": "color",
"id": "button_text",
"label": "Testo bottone"
}
]
}
{% endschema %}

View File

@@ -0,0 +1,3 @@
name = "recesso-storefront"
type = "theme"
uid = "32fd4d4f-a884-a3be-0335-9696fbc24efd646eafe9"