${inner}
@@ -487,8 +582,24 @@ function errorBanner(message?: string): string {
`;
}
-function coexistenceBlock(): string {
- return `${escapeHtml(COEXISTENCE_NOTE)}
`;
+/**
+ * Pulsante info "i" + popover nativo (mini-modal, zero JS via Popover API).
+ * Il popover vive nel top-layer; chiusura con Esc, click fuori o pulsante.
+ */
+function infoWidget(id: string, title: string, body: string): string {
+ return `i
+
+
${escapeHtml(title)} ×
+
${escapeHtml(body)}
+
`;
+}
+
+/** Header comune: step + titolo con pulsante info. */
+function stepHead(step: number, subtitle?: string): string {
+ return `${stepIndicator(step)}
+
${escapeHtml(PAGE_TITLE)} ${infoWidget("rcinfo", INFO_TITLE, INFO_BODY)}${
+ subtitle ? `\n${escapeHtml(subtitle)}
` : ""
+ }`;
}
/**
@@ -503,22 +614,31 @@ function stepIndicator(current: number): string {
3: "Conferma",
4: "Fatto",
};
- const segs = [1, 2, 3]
- .map((i) => {
- const cls =
- current >= 4 || current > i
- ? "stepper__seg is-done"
- : current === i
- ? "stepper__seg is-active"
- : "stepper__seg";
- return ` `;
- })
- .join("");
const label = labels[current] ?? "";
- return `
- ${segs}
- ${escapeHtml(label)}
-
`;
+ return `${escapeHtml(label)}
`;
+}
+
+/**
+ * Layout a 3 fasce: header fisso (contesto + step), corpo scorrevole, footer con
+ * la CTA. In pagina piena è flusso normale; in modalità embed (modal) diventa una
+ * colonna flex a tutta altezza con header/footer ancorati e solo il corpo che scorre
+ * (pattern dei modal moderni). I bottoni stanno nel footer e referenziano la form
+ * via attributo `form=` (HTML5), così restano sempre visibili.
+ */
+function stepLayout(parts: { head: string; body: string; foot?: string }): string {
+ return `
+${parts.head}
+
+
+${parts.body}
+
${
+ parts.foot
+ ? `
+`
+ : ""
+ }`;
}
// --- Step 1: lookup guest -------------------------------------------------
@@ -529,21 +649,21 @@ export function renderStep1(opts?: {
}): string {
const orderName = opts?.orderName ?? "";
const email = opts?.email ?? "";
- return renderShell(`
-${stepIndicator(1)}
-${escapeHtml(PAGE_TITLE)}
-Inserisci il numero dell'ordine e l'email usata per l'acquisto per iniziare. Non è necessario alcun account.
+ return renderShell(
+ stepLayout({
+ head: stepHead(1),
+ body: `Inserisci numero dell'ordine ed email dell'acquisto. Non serve un account.
${errorBanner(opts?.error)}
-
-${coexistenceBlock()}
-`);
+`,
+ foot: `Continua `,
+ }),
+ );
}
// --- Step 2: form dati + dichiarazione ------------------------------------
@@ -556,13 +676,11 @@ export function renderStep2(data: {
error?: string;
}): string {
const customerName = data.customerName ?? "";
- return renderShell(`
-${stepIndicator(2)}
-${escapeHtml(PAGE_TITLE)}
-Ordine ${escapeHtml(data.orderName)}
-${escapeHtml(ART49_INFO)}
-${errorBanner(data.error)}
-
-${coexistenceBlock()}
-`);
+ ${escapeHtml(FIELD.email.label)}
+
+ Ti invieremo qui la ricevuta.
+`,
+ foot: `Continua `,
+ }),
+ );
}
// --- Step 3: riepilogo + conferma dedicata --------------------------------
@@ -595,11 +709,10 @@ export function renderStep3(data: {
statementText: string;
error?: string;
}): string {
- return renderShell(`
-${stepIndicator(3)}
-${escapeHtml(PAGE_TITLE)}
-Controlla i dati. Il recesso sarà trasmesso solo quando premi «${escapeHtml(CONFIRM_LABEL)}».
-${errorBanner(data.error)}
+ return renderShell(
+ stepLayout({
+ head: stepHead(3, "Controlla i dati prima di confermare."),
+ body: `${errorBanner(data.error)}
${escapeHtml(FIELD.orderName.label)}
${escapeHtml(data.orderName)}
@@ -610,30 +723,28 @@ ${errorBanner(data.error)}
${escapeHtml(FIELD.statement.label)}
${escapeHtml(data.statementText)}
-
-
-
-
-
-
-
-
-
- Torna indietro
-
-
-
-
-
-
-
-
-
- ${escapeHtml(CONFIRM_LABEL)}
-
-
-${coexistenceBlock()}
-`);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ foot: `
+ Torna indietro
+ ${escapeHtml(CONFIRM_LABEL)}
+
`,
+ }),
+ );
}
// --- Step 4: successo -----------------------------------------------------
@@ -642,16 +753,17 @@ export function renderStep4(data: {
line2: string;
line3: string;
}): string {
- return renderShell(`
-${stepIndicator(4)}
-
+ return renderShell(
+ stepLayout({
+ head: `${stepIndicator(4)}`,
+ body: `
${escapeHtml(data.line1)}
${escapeHtml(data.line2)}
${escapeHtml(data.line3)}
-
-${coexistenceBlock()}
-`);
+
`,
+ }),
+ );
}
/** Helper: Response HTML standalone (status 200 di default per non leakare via status). */
diff --git a/app/app/routes/app.settings.tsx b/app/app/routes/app.settings.tsx
new file mode 100644
index 0000000..16aa286
--- /dev/null
+++ b/app/app/routes/app.settings.tsx
@@ -0,0 +1,202 @@
+import { useEffect, useMemo, useState } from "react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import {
+ useActionData,
+ useLoaderData,
+ useNavigation,
+ useSubmit,
+} from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ Card,
+ TextField,
+ Button,
+ ButtonGroup,
+ Banner,
+ Text,
+ BlockStack,
+ InlineStack,
+ Badge,
+} from "@shopify/polaris";
+import { TitleBar } from "@shopify/app-bridge-react";
+
+import { authenticate } from "../shopify.server";
+import db from "../db.server";
+import {
+ DEFAULT_INTRO,
+ DEFAULT_NOTE,
+ DEFAULT_SUBJECT,
+ SAMPLE_VARS,
+ TEXT_PLACEHOLDERS,
+ renderReceiptHtml,
+ renderSubject,
+} from "../lib/emailTemplate";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const { session } = await authenticate.admin(request);
+ const settings = await db.settings.findUnique({ where: { shop: session.shop } });
+ return {
+ subject: settings?.emailSubject ?? DEFAULT_SUBJECT,
+ intro: settings?.emailIntro ?? DEFAULT_INTRO,
+ note: settings?.emailNote ?? DEFAULT_NOTE,
+ };
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { session } = await authenticate.admin(request);
+ const form = await request.formData();
+ const subject = String(form.get("subject") ?? "").trim();
+ const intro = String(form.get("intro") ?? "").trim();
+ const note = String(form.get("note") ?? "").trim();
+
+ await db.settings.upsert({
+ where: { shop: session.shop },
+ create: {
+ shop: session.shop,
+ emailSubject: subject && subject !== DEFAULT_SUBJECT ? subject : null,
+ emailIntro: intro && intro !== DEFAULT_INTRO ? intro : null,
+ emailNote: note || null,
+ },
+ update: {
+ emailSubject: subject && subject !== DEFAULT_SUBJECT ? subject : null,
+ emailIntro: intro && intro !== DEFAULT_INTRO ? intro : null,
+ emailNote: note || null,
+ },
+ });
+
+ return { ok: true };
+};
+
+export default function SettingsPage() {
+ const data = useLoaderData();
+ const actionData = useActionData();
+ const nav = useNavigation();
+ const submit = useSubmit();
+
+ const [subject, setSubject] = useState(data.subject);
+ const [intro, setIntro] = useState(data.intro);
+ const [note, setNote] = useState(data.note);
+ const [showSaved, setShowSaved] = useState(false);
+
+ const saving = nav.state === "submitting";
+
+ useEffect(() => {
+ if (actionData?.ok) setShowSaved(true);
+ }, [actionData]);
+
+ const previewSubject = useMemo(
+ () => renderSubject(subject, SAMPLE_VARS),
+ [subject],
+ );
+ const previewHtml = useMemo(
+ () => renderReceiptHtml(SAMPLE_VARS, intro, note),
+ [intro, note],
+ );
+
+ const handleSave = () => {
+ setShowSaved(false);
+ const fd = new FormData();
+ fd.set("subject", subject);
+ fd.set("intro", intro);
+ fd.set("note", note);
+ submit(fd, { method: "post" });
+ };
+
+ const handleReset = () => {
+ setSubject(DEFAULT_SUBJECT);
+ setIntro(DEFAULT_INTRO);
+ setNote(DEFAULT_NOTE);
+ setShowSaved(false);
+ };
+
+ return (
+
+
+
+
+
+ {showSaved ? (
+ setShowSaved(false)}>
+ Salvato.
+
+ ) : null}
+
+
+
+
+
+ Testi dell'email
+
+
+ Personalizza oggetto e testi. Il resto (dettagli ordine,
+ dichiarazione, data/ora, avviso di legge, layout) è fisso e
+ sempre conforme. Segnaposto disponibili:
+
+
+ {TEXT_PLACEHOLDERS.map((p) => (
+ {`{{${p}}}`}
+ ))}
+
+
+
+
+
+
+
+
+
+ Salva
+
+ Ripristina default
+
+
+
+
+
+
+
+ Anteprima
+
+
+ Oggetto: {previewSubject}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/app/routes/app.tsx b/app/app/routes/app.tsx
index bdcf116..bcc6a06 100644
--- a/app/app/routes/app.tsx
+++ b/app/app/routes/app.tsx
@@ -24,7 +24,7 @@ export default function App() {
Home
- Additional page
+ Impostazioni
diff --git a/app/app/routes/proxy.tsx b/app/app/routes/proxy.tsx
index 9f6b3bf..685c1c9 100644
--- a/app/app/routes/proxy.tsx
+++ b/app/app/routes/proxy.tsx
@@ -24,6 +24,7 @@ import {
checkRateLimit,
clientIp,
formatTransmittedAt,
+ getShopInfo,
htmlResponse,
isValidEmail,
lookupOrder,
@@ -282,13 +283,25 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// A4 — ricevuta su supporto durevole, senza ritardo. Il recesso è GIÀ
// persistito e valido: un invio email fallito NON deve invalidarlo.
+ const [settings, shopInfo] = await Promise.all([
+ db.settings.findUnique({ where: { shop } }).catch(() => null),
+ getShopInfo(admin),
+ ]);
+ const shopName = shopInfo.name || shop.replace(/\.myshopify\.com$/, "");
const receipt = await sendWithdrawalReceipt({
to: email,
- orderName: match.orderName,
- customerName,
- statementText,
- transmittedAt: transmittedLabel,
- shopName: shop,
+ vars: {
+ shopName,
+ shopUrl: shopInfo.url,
+ orderName: match.orderName,
+ orderUrl: match.orderUrl,
+ customerName,
+ transmittedAt: transmittedLabel,
+ statementText,
+ },
+ subject: settings?.emailSubject,
+ intro: settings?.emailIntro,
+ note: settings?.emailNote,
});
try {
if (receipt.ok) {
diff --git a/app/extensions/recesso-storefront/assets/recesso-storefront.css b/app/extensions/recesso-storefront/assets/recesso-storefront.css
new file mode 100644
index 0000000..6027df1
--- /dev/null
+++ b/app/extensions/recesso-storefront/assets/recesso-storefront.css
@@ -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; }
+}
diff --git a/app/extensions/recesso-storefront/assets/recesso-storefront.js b/app/extensions/recesso-storefront/assets/recesso-storefront.js
new file mode 100644
index 0000000..87681f0
--- /dev/null
+++ b/app/extensions/recesso-storefront/assets/recesso-storefront.js
@@ -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 =
+ '' +
+ '× ' +
+ '' +
+ "
";
+ 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 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);
+ });
+})();
diff --git a/app/extensions/recesso-storefront/blocks/withdrawal_embed.liquid b/app/extensions/recesso-storefront/blocks/withdrawal_embed.liquid
new file mode 100644
index 0000000..98bb4e7
--- /dev/null
+++ b/app/extensions/recesso-storefront/blocks/withdrawal_embed.liquid
@@ -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 %}
+
+{% 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 %}
diff --git a/app/extensions/recesso-storefront/blocks/withdrawal_link.liquid b/app/extensions/recesso-storefront/blocks/withdrawal_link.liquid
new file mode 100644
index 0000000..f592b18
--- /dev/null
+++ b/app/extensions/recesso-storefront/blocks/withdrawal_link.liquid
@@ -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 %}
+
+
+
+{% 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 %}
diff --git a/app/extensions/recesso-storefront/shopify.extension.toml b/app/extensions/recesso-storefront/shopify.extension.toml
new file mode 100644
index 0000000..5bcbb6c
--- /dev/null
+++ b/app/extensions/recesso-storefront/shopify.extension.toml
@@ -0,0 +1,3 @@
+name = "recesso-storefront"
+type = "theme"
+uid = "32fd4d4f-a884-a3be-0335-9696fbc24efd646eafe9"
diff --git a/app/prisma/migrations/20260707092205_email_settings/migration.sql b/app/prisma/migrations/20260707092205_email_settings/migration.sql
new file mode 100644
index 0000000..92588f2
--- /dev/null
+++ b/app/prisma/migrations/20260707092205_email_settings/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Settings" ADD COLUMN "emailCustomMessage" TEXT,
+ADD COLUMN "emailSenderName" TEXT;
diff --git a/app/prisma/migrations/20260707093246_email_logo/migration.sql b/app/prisma/migrations/20260707093246_email_logo/migration.sql
new file mode 100644
index 0000000..6c710b6
--- /dev/null
+++ b/app/prisma/migrations/20260707093246_email_logo/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Settings" ADD COLUMN "emailLogoUrl" TEXT;
diff --git a/app/prisma/migrations/20260707094749_remove_email_logo/migration.sql b/app/prisma/migrations/20260707094749_remove_email_logo/migration.sql
new file mode 100644
index 0000000..c34cf93
--- /dev/null
+++ b/app/prisma/migrations/20260707094749_remove_email_logo/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `emailLogoUrl` on the `Settings` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Settings" DROP COLUMN "emailLogoUrl";
diff --git a/app/prisma/migrations/20260707095341_email_templates/migration.sql b/app/prisma/migrations/20260707095341_email_templates/migration.sql
new file mode 100644
index 0000000..0e333aa
--- /dev/null
+++ b/app/prisma/migrations/20260707095341_email_templates/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `emailCustomMessage` on the `Settings` table. All the data in the column will be lost.
+ - You are about to drop the column `emailSenderName` on the `Settings` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Settings" DROP COLUMN "emailCustomMessage",
+DROP COLUMN "emailSenderName",
+ADD COLUMN "emailBodyTemplate" TEXT,
+ADD COLUMN "emailSubjectTemplate" TEXT;
diff --git a/app/prisma/migrations/20260707100338_email_text_fields/migration.sql b/app/prisma/migrations/20260707100338_email_text_fields/migration.sql
new file mode 100644
index 0000000..9ccb47a
--- /dev/null
+++ b/app/prisma/migrations/20260707100338_email_text_fields/migration.sql
@@ -0,0 +1,13 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `emailBodyTemplate` on the `Settings` table. All the data in the column will be lost.
+ - You are about to drop the column `emailSubjectTemplate` on the `Settings` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Settings" DROP COLUMN "emailBodyTemplate",
+DROP COLUMN "emailSubjectTemplate",
+ADD COLUMN "emailIntro" TEXT,
+ADD COLUMN "emailNote" TEXT,
+ADD COLUMN "emailSubject" TEXT;
diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma
index db9eb74..a2b8f20 100644
--- a/app/prisma/schema.prisma
+++ b/app/prisma/schema.prisma
@@ -49,6 +49,9 @@ model Settings {
returnAddress String?
defaultWindowDays Int @default(14)
withdrawalInfoText String?
+ emailSubject String?
+ emailIntro String?
+ emailNote String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt