Causa reale del mancato invio in prod: Settings aveva porta 587 con 'Connessione sicura diretta' spuntata -> nodemailer apriva subito TLS su una porta che parla in chiaro + STARTTLS -> handshake fallito -> receipt_failed. - buildTransport: secure derivato dalla porta standard (465=true, 587=false), requireTLS su 587. Su porte non standard resta la scelta del merchant. - Checkbox 'Connessione sicura' ora dichiara che e' ignorata sulle porte standard. - Avviso se notifiche attive ma 'Email notifiche' vuota (era null in prod: per questo non arrivava neanche la notifica al merchant).
678 lines
25 KiB
TypeScript
678 lines
25 KiB
TypeScript
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,
|
|
Tabs,
|
|
TextField,
|
|
Button,
|
|
ButtonGroup,
|
|
Banner,
|
|
Text,
|
|
BlockStack,
|
|
InlineStack,
|
|
Badge,
|
|
Checkbox,
|
|
} from "@shopify/polaris";
|
|
import { TitleBar } from "@shopify/app-bridge-react";
|
|
|
|
import { authenticate } from "../shopify.server";
|
|
import db from "../db.server";
|
|
import { decryptSecret, encryptSecret } from "../lib/crypto.server";
|
|
import { sendTestEmail } from "../lib/mailer.server";
|
|
import {
|
|
DEFAULT_INTRO,
|
|
DEFAULT_NOTE,
|
|
DEFAULT_SUBJECT,
|
|
DEFAULT_OP_SHIPPED,
|
|
DEFAULT_OP_UNFULFILLED,
|
|
OP_PLACEHOLDERS,
|
|
SAMPLE_VARS,
|
|
TEXT_PLACEHOLDERS,
|
|
renderOperationalPreview,
|
|
renderReceiptHtml,
|
|
renderSubject,
|
|
} from "../lib/emailTemplate";
|
|
|
|
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
const { session } = await authenticate.admin(request);
|
|
const s = await db.settings.findUnique({ where: { shop: session.shop } });
|
|
return {
|
|
subject: s?.emailSubject ?? DEFAULT_SUBJECT,
|
|
intro: s?.emailIntro ?? DEFAULT_INTRO,
|
|
note: s?.emailNote ?? DEFAULT_NOTE,
|
|
notifyEnabled: s?.notifyEnabled ?? true,
|
|
notifyEmail: s?.notifyEmail ?? "",
|
|
tagEnabled: s?.tagEnabled ?? true,
|
|
enforceWindow: s?.enforceWindow ?? false,
|
|
windowDays: s?.defaultWindowDays ?? 14,
|
|
enforceExclusions: s?.enforceExclusions ?? false,
|
|
stateAwareEmail: s?.stateAwareEmail ?? true,
|
|
autoCancelUnfulfilled: s?.autoCancelUnfulfilled ?? false,
|
|
returnAtCustomerExpense: s?.returnAtCustomerExpense ?? true,
|
|
returnAddress: s?.returnAddress ?? "",
|
|
opTextUnfulfilled: s?.opTextUnfulfilled ?? DEFAULT_OP_UNFULFILLED,
|
|
opTextShipped: s?.opTextShipped ?? DEFAULT_OP_SHIPPED,
|
|
smtpHost: s?.smtpHost ?? "",
|
|
smtpPort: s?.smtpPort != null ? String(s.smtpPort) : "",
|
|
smtpUser: s?.smtpUser ?? "",
|
|
smtpSecure: s?.smtpSecure ?? false,
|
|
smtpFrom: s?.smtpFrom ?? "",
|
|
smtpPassSet: !!s?.smtpPass,
|
|
};
|
|
};
|
|
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const { session } = await authenticate.admin(request);
|
|
const f = await request.formData();
|
|
|
|
// Invio di prova: NON salva, usa i valori correnti del form (password digitata
|
|
// oppure quella gia' salvata, decifrata). Ritorna l'errore SMTP grezzo.
|
|
if (String(f.get("intent") ?? "save") === "test") {
|
|
const to = String(f.get("testTo") ?? "").trim();
|
|
if (!to) {
|
|
return {
|
|
ok: false,
|
|
tested: true,
|
|
error: "Inserisci un destinatario per la prova.",
|
|
};
|
|
}
|
|
const host = String(f.get("smtpHost") ?? "").trim();
|
|
let pass: string | null = String(f.get("smtpPass") ?? "").trim() || null;
|
|
if (!pass) {
|
|
const saved = await db.settings.findUnique({
|
|
where: { shop: session.shop },
|
|
});
|
|
pass = saved?.smtpPass ? decryptSecret(saved.smtpPass) : null;
|
|
}
|
|
const smtp = host
|
|
? {
|
|
host,
|
|
port:
|
|
Number(f.get("smtpPort")) > 0
|
|
? Math.trunc(Number(f.get("smtpPort")))
|
|
: null,
|
|
user: String(f.get("smtpUser") ?? "").trim() || null,
|
|
pass,
|
|
secure: f.get("smtpSecure") === "true",
|
|
from: String(f.get("smtpFrom") ?? "").trim() || null,
|
|
}
|
|
: null;
|
|
const r = await sendTestEmail({ smtp, to });
|
|
return { ok: r.ok, tested: true, error: r.ok ? null : r.error };
|
|
}
|
|
|
|
const subject = String(f.get("subject") ?? "").trim();
|
|
const intro = String(f.get("intro") ?? "").trim();
|
|
const note = String(f.get("note") ?? "").trim();
|
|
const opUnf = String(f.get("opTextUnfulfilled") ?? "").trim();
|
|
const opShip = String(f.get("opTextShipped") ?? "").trim();
|
|
|
|
const data = {
|
|
emailSubject: subject && subject !== DEFAULT_SUBJECT ? subject : null,
|
|
emailIntro: intro && intro !== DEFAULT_INTRO ? intro : null,
|
|
emailNote: note || null,
|
|
notifyEnabled: f.get("notifyEnabled") === "true",
|
|
notifyEmail: String(f.get("notifyEmail") ?? "").trim() || null,
|
|
tagEnabled: f.get("tagEnabled") === "true",
|
|
enforceWindow: f.get("enforceWindow") === "true",
|
|
defaultWindowDays: Math.min(
|
|
365,
|
|
Math.max(1, Number(f.get("windowDays")) || 14),
|
|
),
|
|
enforceExclusions: f.get("enforceExclusions") === "true",
|
|
stateAwareEmail: f.get("stateAwareEmail") === "true",
|
|
autoCancelUnfulfilled: f.get("autoCancelUnfulfilled") === "true",
|
|
returnAtCustomerExpense: f.get("returnAtCustomerExpense") === "true",
|
|
returnAddress: String(f.get("returnAddress") ?? "").trim() || null,
|
|
opTextUnfulfilled:
|
|
opUnf && opUnf !== DEFAULT_OP_UNFULFILLED ? opUnf : null,
|
|
opTextShipped: opShip && opShip !== DEFAULT_OP_SHIPPED ? opShip : null,
|
|
smtpHost: String(f.get("smtpHost") ?? "").trim() || null,
|
|
smtpPort:
|
|
Number(f.get("smtpPort")) > 0 ? Math.trunc(Number(f.get("smtpPort"))) : null,
|
|
smtpUser: String(f.get("smtpUser") ?? "").trim() || null,
|
|
smtpSecure: f.get("smtpSecure") === "true",
|
|
smtpFrom: String(f.get("smtpFrom") ?? "").trim() || null,
|
|
};
|
|
|
|
// Password SMTP: cifrata solo se fornita; vuota = invariata.
|
|
const newPass = String(f.get("smtpPass") ?? "").trim();
|
|
const finalData = newPass
|
|
? { ...data, smtpPass: encryptSecret(newPass) }
|
|
: data;
|
|
|
|
await db.settings.upsert({
|
|
where: { shop: session.shop },
|
|
create: { shop: session.shop, ...finalData },
|
|
update: finalData,
|
|
});
|
|
return { ok: true, tested: false, error: null };
|
|
};
|
|
|
|
function opFrame(html: string, height = 130) {
|
|
const doc = `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">${html}</table>`;
|
|
return (
|
|
<iframe
|
|
title="Anteprima riquadro"
|
|
srcDoc={doc}
|
|
style={{
|
|
width: "100%",
|
|
height: `${height}px`,
|
|
border: "1px solid #e1e1e1",
|
|
borderRadius: "8px",
|
|
background: "#fff",
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
const d = useLoaderData<typeof loader>();
|
|
const actionData = useActionData<typeof action>();
|
|
const nav = useNavigation();
|
|
const submit = useSubmit();
|
|
|
|
const [tab, setTab] = useState(0);
|
|
const [subject, setSubject] = useState(d.subject);
|
|
const [intro, setIntro] = useState(d.intro);
|
|
const [note, setNote] = useState(d.note);
|
|
const [notifyEnabled, setNotifyEnabled] = useState(d.notifyEnabled);
|
|
const [notifyEmail, setNotifyEmail] = useState(d.notifyEmail);
|
|
const [tagEnabled, setTagEnabled] = useState(d.tagEnabled);
|
|
const [enforceWindow, setEnforceWindow] = useState(d.enforceWindow);
|
|
const [windowDays, setWindowDays] = useState(String(d.windowDays));
|
|
const [enforceExclusions, setEnforceExclusions] = useState(d.enforceExclusions);
|
|
const [stateAwareEmail, setStateAwareEmail] = useState(d.stateAwareEmail);
|
|
const [autoCancelUnfulfilled, setAutoCancelUnfulfilled] = useState(
|
|
d.autoCancelUnfulfilled,
|
|
);
|
|
const [returnAtCustomerExpense, setReturnAtCustomerExpense] = useState(
|
|
d.returnAtCustomerExpense,
|
|
);
|
|
const [returnAddress, setReturnAddress] = useState(d.returnAddress);
|
|
const [opTextUnfulfilled, setOpTextUnfulfilled] = useState(d.opTextUnfulfilled);
|
|
const [opTextShipped, setOpTextShipped] = useState(d.opTextShipped);
|
|
const [smtpHost, setSmtpHost] = useState(d.smtpHost);
|
|
const [smtpPort, setSmtpPort] = useState(d.smtpPort);
|
|
const [smtpUser, setSmtpUser] = useState(d.smtpUser);
|
|
const [smtpPass, setSmtpPass] = useState("");
|
|
const [smtpSecure, setSmtpSecure] = useState(d.smtpSecure);
|
|
const [smtpFrom, setSmtpFrom] = useState(d.smtpFrom);
|
|
const [testTo, setTestTo] = useState(d.notifyEmail);
|
|
const [showSaved, setShowSaved] = useState(false);
|
|
|
|
const saving = nav.state === "submitting";
|
|
|
|
useEffect(() => {
|
|
if (actionData?.ok && !actionData.tested) setShowSaved(true);
|
|
}, [actionData]);
|
|
|
|
const previewSubject = useMemo(
|
|
() => renderSubject(subject, SAMPLE_VARS),
|
|
[subject],
|
|
);
|
|
const previewHtml = useMemo(
|
|
() => renderReceiptHtml(SAMPLE_VARS, intro, note),
|
|
[intro, note],
|
|
);
|
|
const previewOpUnf = useMemo(
|
|
() =>
|
|
renderOperationalPreview(
|
|
{
|
|
state: "unfulfilled",
|
|
textUnfulfilled: opTextUnfulfilled,
|
|
textShipped: opTextShipped,
|
|
returnAddress,
|
|
atCustomerExpense: returnAtCustomerExpense,
|
|
},
|
|
SAMPLE_VARS,
|
|
),
|
|
[opTextUnfulfilled, opTextShipped, returnAddress, returnAtCustomerExpense],
|
|
);
|
|
const previewOpShip = useMemo(
|
|
() =>
|
|
renderOperationalPreview(
|
|
{
|
|
state: "shipped",
|
|
textUnfulfilled: opTextUnfulfilled,
|
|
textShipped: opTextShipped,
|
|
returnAddress,
|
|
atCustomerExpense: returnAtCustomerExpense,
|
|
},
|
|
SAMPLE_VARS,
|
|
),
|
|
[opTextShipped, opTextUnfulfilled, returnAddress, returnAtCustomerExpense],
|
|
);
|
|
|
|
const handleTest = () => {
|
|
setShowSaved(false);
|
|
const fd = new FormData();
|
|
fd.set("intent", "test");
|
|
fd.set("testTo", testTo);
|
|
fd.set("smtpHost", smtpHost);
|
|
fd.set("smtpPort", smtpPort);
|
|
fd.set("smtpUser", smtpUser);
|
|
fd.set("smtpPass", smtpPass);
|
|
fd.set("smtpSecure", String(smtpSecure));
|
|
fd.set("smtpFrom", smtpFrom);
|
|
submit(fd, { method: "post" });
|
|
};
|
|
|
|
const handleSave = () => {
|
|
setShowSaved(false);
|
|
const fd = new FormData();
|
|
fd.set("intent", "save");
|
|
fd.set("subject", subject);
|
|
fd.set("intro", intro);
|
|
fd.set("note", note);
|
|
fd.set("notifyEnabled", String(notifyEnabled));
|
|
fd.set("notifyEmail", notifyEmail);
|
|
fd.set("tagEnabled", String(tagEnabled));
|
|
fd.set("enforceWindow", String(enforceWindow));
|
|
fd.set("windowDays", windowDays);
|
|
fd.set("enforceExclusions", String(enforceExclusions));
|
|
fd.set("stateAwareEmail", String(stateAwareEmail));
|
|
fd.set("autoCancelUnfulfilled", String(autoCancelUnfulfilled));
|
|
fd.set("returnAtCustomerExpense", String(returnAtCustomerExpense));
|
|
fd.set("returnAddress", returnAddress);
|
|
fd.set("opTextUnfulfilled", opTextUnfulfilled);
|
|
fd.set("opTextShipped", opTextShipped);
|
|
fd.set("smtpHost", smtpHost);
|
|
fd.set("smtpPort", smtpPort);
|
|
fd.set("smtpUser", smtpUser);
|
|
fd.set("smtpPass", smtpPass);
|
|
fd.set("smtpSecure", String(smtpSecure));
|
|
fd.set("smtpFrom", smtpFrom);
|
|
submit(fd, { method: "post" });
|
|
};
|
|
|
|
const resetEmail = () => {
|
|
setSubject(DEFAULT_SUBJECT);
|
|
setIntro(DEFAULT_INTRO);
|
|
setNote(DEFAULT_NOTE);
|
|
setShowSaved(false);
|
|
};
|
|
|
|
const saveBtn = (
|
|
<div>
|
|
<Button variant="primary" loading={saving} onClick={handleSave}>
|
|
Salva
|
|
</Button>
|
|
</div>
|
|
);
|
|
|
|
const tabs = [
|
|
{ id: "email", content: "Email" },
|
|
{ id: "notifiche", content: "Notifiche" },
|
|
{ id: "regole", content: "Regole recesso" },
|
|
{ id: "reso", content: "Reso e stato ordine" },
|
|
{ id: "smtp", content: "Email (SMTP)" },
|
|
];
|
|
|
|
return (
|
|
<Page>
|
|
<TitleBar title="Impostazioni recesso" />
|
|
<Layout>
|
|
<Layout.Section>
|
|
<BlockStack gap="400">
|
|
{showSaved ? (
|
|
<Banner tone="success" onDismiss={() => setShowSaved(false)}>
|
|
Salvato.
|
|
</Banner>
|
|
) : null}
|
|
|
|
{actionData?.tested ? (
|
|
actionData.ok ? (
|
|
<Banner tone="success">
|
|
Email di prova inviata a {testTo}. Controlla anche lo spam.
|
|
</Banner>
|
|
) : (
|
|
<Banner tone="critical" title="Invio di prova fallito">
|
|
<Text as="p">{actionData.error}</Text>
|
|
</Banner>
|
|
)
|
|
) : null}
|
|
|
|
<Tabs tabs={tabs} selected={tab} onSelect={setTab} />
|
|
|
|
{tab === 0 ? (
|
|
<BlockStack gap="400">
|
|
<Card>
|
|
<BlockStack gap="400">
|
|
<BlockStack gap="100">
|
|
<Text as="h2" variant="headingMd">
|
|
Testi dell'email
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Oggetto e testi. Dettagli ordine, dichiarazione, data/ora,
|
|
avviso di legge e layout sono fissi. Segnaposto:
|
|
</Text>
|
|
<InlineStack gap="200" wrap>
|
|
{TEXT_PLACEHOLDERS.map((p) => (
|
|
<Badge key={p}>{`{{${p}}}`}</Badge>
|
|
))}
|
|
</InlineStack>
|
|
</BlockStack>
|
|
<TextField
|
|
label="Oggetto"
|
|
value={subject}
|
|
onChange={setSubject}
|
|
autoComplete="off"
|
|
/>
|
|
<TextField
|
|
label="Introduzione"
|
|
value={intro}
|
|
onChange={setIntro}
|
|
autoComplete="off"
|
|
multiline={4}
|
|
/>
|
|
<TextField
|
|
label="Nota aggiuntiva (opzionale)"
|
|
value={note}
|
|
onChange={setNote}
|
|
autoComplete="off"
|
|
multiline={3}
|
|
helpText="Riquadro in fondo alla email. Vuoto = nascosto."
|
|
/>
|
|
<ButtonGroup>
|
|
<Button
|
|
variant="primary"
|
|
loading={saving}
|
|
onClick={handleSave}
|
|
>
|
|
Salva
|
|
</Button>
|
|
<Button onClick={resetEmail}>Ripristina default</Button>
|
|
</ButtonGroup>
|
|
</BlockStack>
|
|
</Card>
|
|
<Card>
|
|
<BlockStack gap="200">
|
|
<Text as="h2" variant="headingMd">
|
|
Anteprima
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Oggetto: {previewSubject}
|
|
</Text>
|
|
<iframe
|
|
title="Anteprima email"
|
|
srcDoc={previewHtml}
|
|
style={{
|
|
width: "100%",
|
|
height: "640px",
|
|
border: "1px solid #e1e1e1",
|
|
borderRadius: "8px",
|
|
background: "#fff",
|
|
}}
|
|
/>
|
|
</BlockStack>
|
|
</Card>
|
|
</BlockStack>
|
|
) : null}
|
|
|
|
{tab === 1 ? (
|
|
<Card>
|
|
<BlockStack gap="400">
|
|
<Text as="h2" variant="headingMd">
|
|
Notifiche al merchant
|
|
</Text>
|
|
<Checkbox
|
|
label="Invia email di notifica a ogni recesso"
|
|
checked={notifyEnabled}
|
|
onChange={setNotifyEnabled}
|
|
/>
|
|
<TextField
|
|
label="Email notifiche"
|
|
type="email"
|
|
value={notifyEmail}
|
|
onChange={setNotifyEmail}
|
|
autoComplete="off"
|
|
disabled={!notifyEnabled}
|
|
helpText="Dove ricevere le notifiche. Senza indirizzo l'email non parte."
|
|
placeholder="ordini@tuonegozio.it"
|
|
/>
|
|
{notifyEnabled && !notifyEmail.trim() ? (
|
|
<Banner tone="warning">
|
|
Notifiche attive ma nessun indirizzo: al momento non ricevi
|
|
nulla. Inserisci un'email.
|
|
</Banner>
|
|
) : null}
|
|
<Checkbox
|
|
label="Aggiungi il tag 'Recesso' all'ordine"
|
|
checked={tagEnabled}
|
|
onChange={setTagEnabled}
|
|
helpText="Rende l'ordine filtrabile nella lista ordini."
|
|
/>
|
|
{saveBtn}
|
|
</BlockStack>
|
|
</Card>
|
|
) : null}
|
|
|
|
{tab === 2 ? (
|
|
<Card>
|
|
<BlockStack gap="400">
|
|
<BlockStack gap="100">
|
|
<Text as="h2" variant="headingMd">
|
|
Regole di recesso
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Attiva questi controlli solo dopo aver verificato i dati. Da
|
|
spenti, il recesso è sempre accettato. Le regole di esclusione
|
|
si gestiscono nella pagina Esclusioni.
|
|
</Text>
|
|
</BlockStack>
|
|
<Checkbox
|
|
label="Blocca i recessi oltre il termine"
|
|
checked={enforceWindow}
|
|
onChange={setEnforceWindow}
|
|
helpText="Calcolato dalla data di consegna + i giorni sotto."
|
|
/>
|
|
<TextField
|
|
label="Giorni di recesso"
|
|
type="number"
|
|
value={windowDays}
|
|
onChange={setWindowDays}
|
|
autoComplete="off"
|
|
min={1}
|
|
max={365}
|
|
disabled={!enforceWindow}
|
|
/>
|
|
<Checkbox
|
|
label="Blocca i prodotti esclusi (Art. 59)"
|
|
checked={enforceExclusions}
|
|
onChange={setEnforceExclusions}
|
|
helpText="Blocca se l'intero ordine è escluso. Regole nella pagina Esclusioni."
|
|
/>
|
|
{saveBtn}
|
|
</BlockStack>
|
|
</Card>
|
|
) : null}
|
|
|
|
{tab === 3 ? (
|
|
<BlockStack gap="400">
|
|
<Card>
|
|
<BlockStack gap="400">
|
|
<BlockStack gap="100">
|
|
<Text as="h2" variant="headingMd">
|
|
Reso e stato ordine
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Il riquadro nella ricevuta cambia in base allo stato.
|
|
Segnaposto nei testi:
|
|
</Text>
|
|
<InlineStack gap="200" wrap>
|
|
{OP_PLACEHOLDERS.map((p) => (
|
|
<Badge key={p}>{`{{${p}}}`}</Badge>
|
|
))}
|
|
</InlineStack>
|
|
</BlockStack>
|
|
<Checkbox
|
|
label="Adatta la ricevuta allo stato dell'ordine"
|
|
checked={stateAwareEmail}
|
|
onChange={setStateAwareEmail}
|
|
/>
|
|
<Checkbox
|
|
label="Annulla automaticamente gli ordini non ancora spediti"
|
|
checked={autoCancelUnfulfilled}
|
|
onChange={setAutoCancelUnfulfilled}
|
|
helpText="Al recesso, se l'ordine non è evaso: annullo + rimborso automatici. Irreversibile."
|
|
/>
|
|
<Checkbox
|
|
label="Spese di restituzione a carico del cliente (Art. 57)"
|
|
checked={returnAtCustomerExpense}
|
|
onChange={setReturnAtCustomerExpense}
|
|
helpText="Determina il valore di {{returnCost}}."
|
|
/>
|
|
<TextField
|
|
label="Indirizzo per il reso"
|
|
value={returnAddress}
|
|
onChange={setReturnAddress}
|
|
autoComplete="off"
|
|
multiline={2}
|
|
placeholder="Via ..., CAP Città (PR)"
|
|
helpText="Valore di {{returnAddress}}."
|
|
/>
|
|
<TextField
|
|
label="Testo - ordine non evaso"
|
|
value={opTextUnfulfilled}
|
|
onChange={setOpTextUnfulfilled}
|
|
autoComplete="off"
|
|
multiline={3}
|
|
/>
|
|
<TextField
|
|
label="Testo - ordine spedito/consegnato"
|
|
value={opTextShipped}
|
|
onChange={setOpTextShipped}
|
|
autoComplete="off"
|
|
multiline={4}
|
|
/>
|
|
{saveBtn}
|
|
</BlockStack>
|
|
</Card>
|
|
<Card>
|
|
<BlockStack gap="300">
|
|
<Text as="h2" variant="headingMd">
|
|
Anteprima riquadro
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Ordine non evaso
|
|
</Text>
|
|
{opFrame(previewOpUnf)}
|
|
<Text as="p" tone="subdued">
|
|
Ordine spedito/consegnato
|
|
</Text>
|
|
{opFrame(previewOpShip)}
|
|
</BlockStack>
|
|
</Card>
|
|
</BlockStack>
|
|
) : null}
|
|
|
|
{tab === 4 ? (
|
|
<Card>
|
|
<BlockStack gap="400">
|
|
<BlockStack gap="100">
|
|
<Text as="h2" variant="headingMd">
|
|
Email (SMTP)
|
|
</Text>
|
|
<Text as="p" tone="subdued">
|
|
Vuoto = provider di default dell'app. Compila per inviare dal
|
|
tuo SMTP (email dal tuo dominio). La password è cifrata a
|
|
riposo.
|
|
</Text>
|
|
</BlockStack>
|
|
<TextField
|
|
label="Host SMTP"
|
|
value={smtpHost}
|
|
onChange={setSmtpHost}
|
|
autoComplete="off"
|
|
placeholder="smtp-relay.brevo.com"
|
|
/>
|
|
<TextField
|
|
label="Porta"
|
|
type="number"
|
|
value={smtpPort}
|
|
onChange={setSmtpPort}
|
|
autoComplete="off"
|
|
placeholder="587"
|
|
/>
|
|
<TextField
|
|
label="Utente"
|
|
value={smtpUser}
|
|
onChange={setSmtpUser}
|
|
autoComplete="off"
|
|
/>
|
|
<TextField
|
|
label="Password"
|
|
type="password"
|
|
value={smtpPass}
|
|
onChange={setSmtpPass}
|
|
autoComplete="off"
|
|
helpText={
|
|
d.smtpPassSet
|
|
? "Impostata. Lascia vuoto per non cambiarla."
|
|
: "Non impostata."
|
|
}
|
|
/>
|
|
<Checkbox
|
|
label="Connessione sicura diretta (SSL/TLS)"
|
|
checked={smtpSecure}
|
|
onChange={setSmtpSecure}
|
|
helpText="Ignorato sulle porte standard: 465 usa sempre TLS diretto, 587 usa sempre STARTTLS. Vale solo su porte non standard."
|
|
/>
|
|
<TextField
|
|
label="Mittente (From)"
|
|
value={smtpFrom}
|
|
onChange={setSmtpFrom}
|
|
autoComplete="off"
|
|
placeholder="Il tuo negozio <no-reply@tuodominio.it>"
|
|
/>
|
|
|
|
{smtpHost && !smtpFrom ? (
|
|
<Banner tone="warning">
|
|
Host SMTP impostato ma mittente vuoto. Brevo (come quasi
|
|
tutti i provider) rifiuta un mittente non verificato:
|
|
compila "Mittente (From)" con un indirizzo verificato nel
|
|
tuo account.
|
|
</Banner>
|
|
) : null}
|
|
|
|
<TextField
|
|
label="Destinatario dell'email di prova"
|
|
type="email"
|
|
value={testTo}
|
|
onChange={setTestTo}
|
|
autoComplete="off"
|
|
placeholder="tu@tuodominio.it"
|
|
helpText="La prova usa i valori qui sopra, anche se non ancora salvati."
|
|
/>
|
|
|
|
<ButtonGroup>
|
|
<Button
|
|
variant="primary"
|
|
loading={saving}
|
|
onClick={handleSave}
|
|
>
|
|
Salva
|
|
</Button>
|
|
<Button loading={saving} onClick={handleTest}>
|
|
Invia email di prova
|
|
</Button>
|
|
</ButtonGroup>
|
|
</BlockStack>
|
|
</Card>
|
|
) : null}
|
|
</BlockStack>
|
|
</Layout.Section>
|
|
</Layout>
|
|
</Page>
|
|
);
|
|
}
|