diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 0737359a..2848b8e6 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -30,7 +30,7 @@ import { SunIcon, MoonIcon, } from "@heroicons/react/16/solid"; -import { useLabelStore, useHistory } from "../store/labelStore"; +import { useLabelStore, useHistory, canCallLabelary } from "../store/labelStore"; import { localeNames } from "../locales"; import type { LocaleCode } from "../locales"; import { mmToUnit } from "../lib/units"; @@ -50,6 +50,7 @@ export function AppShell() { const setLocale = useLabelStore((s) => s.setLocale); const theme = useLabelStore((s) => s.theme); const setTheme = useLabelStore((s) => s.setTheme); + const labelaryReady = useLabelStore(canCallLabelary); // Bridge the theme preference to so the CSS variables in // index.css pick it up. @@ -201,13 +202,19 @@ export function AppShell() { {t.app.saveDesign} - - {t.app.print} - + {/* Print also routes through Labelary. Until the user has seen + the privacy notice (shown only via the Preview modal), Print + is hidden so the very first Labelary call cannot bypass the + disclosure. After acknowledgement Print stays available. */} + {labelaryReady && ( + + {t.app.print} + + )} s.label); const objects = useCurrentObjects(); + const noticeAcknowledged = useLabelStore((s) => s.labelaryNoticeAcknowledged); + const acknowledgeLabelaryNotice = useLabelStore((s) => s.acknowledgeLabelaryNotice); + // Same gate as every other Labelary consumer (Print, …) — single source + // of truth in the store. The modal opens only when the gate is on, so in + // practice this resolves to noticeAcknowledged here, but we route through + // the shared selector to stay in lockstep with future call sites. + const canFetch = useLabelStore(canCallLabelary); + const [previewUrl, setPreviewUrl] = useState(null); - const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const urlRef = useRef(null); const zplRef = useRef(generateZPL(label, objects)); + // Derived: fetch is in flight while we have neither result nor error yet. + const loading = canFetch && !previewUrl && !error; useEffect(() => { + if (!canFetch) return; let cancelled = false; fetchPreview(zplRef.current, label) .then((url) => { if (cancelled) { URL.revokeObjectURL(url); return; } urlRef.current = url; setPreviewUrl(url); - setLoading(false); }) .catch((e: unknown) => { if (cancelled) return; setError(labelaryErrorMessage(e)); - setLoading(false); }); return () => { cancelled = true; if (urlRef.current) URL.revokeObjectURL(urlRef.current); }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps + // `label` and the generated ZPL are intentionally captured once at mount + // (via zplRef): the preview should reflect the snapshot the user saw when + // they opened the modal, not refetch when the canvas changes underneath. + }, [canFetch]); // eslint-disable-line react-hooks/exhaustive-deps const handleDownloadFallback = () => { triggerDownload(new Blob([zplRef.current], { type: 'text/plain' }), 'label.zpl'); @@ -72,10 +83,30 @@ export function LabelPreviewModal({ onClose }: Props) { viewport so small previews are still centered. */}
- {loading && ( + {!noticeAcknowledged && ( +
+ {t.output.previewNoticeTitle} + {t.output.previewNoticeBody} + + {t.output.previewNoticePrivacyLink} + + +
+ )} + {canFetch && loading && ( {t.output.loading} )} - {!loading && error && ( + {canFetch && !loading && error && (
{error}
)} - {!loading && !error && previewUrl && ( + {canFetch && !loading && !error && previewUrl && ( Label preview
+ +
); diff --git a/src/components/Output/ZPLOutput.tsx b/src/components/Output/ZPLOutput.tsx index 80adf041..381608b3 100644 --- a/src/components/Output/ZPLOutput.tsx +++ b/src/components/Output/ZPLOutput.tsx @@ -15,6 +15,10 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) { const t = useT(); const label = useLabelStore((s) => s.label); const pages = useLabelStore((s) => s.pages); + // Direct gate check — Preview is the path to the privacy notice, so the + // button must be reachable before acknowledgement. Other Labelary callers + // (AppShell.Print, LabelPreview.fetch) use the stricter canCallLabelary. + const labelaryEnabled = useLabelStore((s) => s.thirdParty.labelary); const [copied, setCopied] = useState(false); const [showPreview, setShowPreview] = useState(false); @@ -43,15 +47,17 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) { {t.output.zplHeading}
- + {labelaryEnabled && ( + + )}