Skip to content

Commit 06e1fa8

Browse files
committed
feat(ui): show Labelary privacy notice on first Print, not just Preview
Print routed through Labelary was previously hidden until the user had acknowledged the notice via Preview. Surface Print whenever the gate is enabled and open the notice on first click instead, matching the Preview flow. Disclaimer is extracted into a shared LabelaryNoticeModal so wording stays in lockstep, locale body now mentions both features, and a new env test pins the build-time gate behavior.
1 parent 2c37ac7 commit 06e1fa8

38 files changed

Lines changed: 187 additions & 82 deletions

‎src/components/AppShell.tsx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
SunIcon,
3131
MoonIcon,
3232
} from "@heroicons/react/16/solid";
33-
import { useLabelStore, useHistory, canCallLabelary } from "../store/labelStore";
33+
import { useLabelStore, useHistory } from "../store/labelStore";
34+
import { LabelaryNoticeModal } from "./Output/LabelaryNoticeModal";
3435
import { localeNames } from "../locales";
3536
import type { LocaleCode } from "../locales";
3637
import { mmToUnit } from "../lib/units";
@@ -50,7 +51,9 @@ export function AppShell() {
5051
const setLocale = useLabelStore((s) => s.setLocale);
5152
const theme = useLabelStore((s) => s.theme);
5253
const setTheme = useLabelStore((s) => s.setTheme);
53-
const labelaryReady = useLabelStore(canCallLabelary);
54+
const labelaryEnabled = useLabelStore((s) => s.thirdParty.labelary);
55+
const noticeAcknowledged = useLabelStore((s) => s.labelaryNoticeAcknowledged);
56+
const [showPrintNotice, setShowPrintNotice] = useState(false);
5457

5558
// Bridge the theme preference to <html data-theme> so the CSS variables in
5659
// index.css pick it up.
@@ -202,14 +205,13 @@ export function AppShell() {
202205
{t.app.saveDesign}
203206
</DropdownItem>
204207
<DropdownSeparator />
205-
{/* Print also routes through Labelary. Until the user has seen
206-
the privacy notice (shown only via the Preview modal), Print
207-
is hidden so the very first Labelary call cannot bypass the
208-
disclosure. After acknowledgement Print stays available. */}
209-
{labelaryReady && (
208+
{/* Print routes through Labelary. The button is shown whenever
209+
the Labelary gate is on; clicking it before the notice has
210+
been acknowledged opens the disclosure first, then prints. */}
211+
{labelaryEnabled && (
210212
<DropdownItem
211213
icon={PrinterIcon}
212-
onClick={handlePrint}
214+
onClick={() => (noticeAcknowledged ? handlePrint() : setShowPrintNotice(true))}
213215
disabled={!hasObjects}
214216
>
215217
{t.app.print}
@@ -344,6 +346,15 @@ export function AppShell() {
344346
{showZebraPrint && (
345347
<PrintToZebraDialog zpl={currentZpl()} onClose={closeZebraPrint} />
346348
)}
349+
{showPrintNotice && (
350+
<LabelaryNoticeModal
351+
onClose={() => setShowPrintNotice(false)}
352+
onContinue={() => {
353+
setShowPrintNotice(false);
354+
handlePrint();
355+
}}
356+
/>
357+
)}
347358
</div>
348359
);
349360
}

‎src/components/Output/LabelPreview.tsx‎

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useRef, useState } from 'react';
22
import { XMarkIcon, ArrowDownTrayIcon } from '@heroicons/react/16/solid';
3-
import { useLabelStore, useCurrentObjects, canCallLabelary } from '../../store/labelStore';
3+
import { useLabelStore, useCurrentObjects } from '../../store/labelStore';
44
import { generateZPL } from '../../lib/zplGenerator';
55
import { fetchPreview, labelaryErrorMessage } from '../../lib/labelary';
66
import { triggerDownload } from '../../lib/triggerDownload';
@@ -10,28 +10,22 @@ interface Props {
1010
onClose: () => void;
1111
}
1212

13-
13+
/** Preview modal — assumes the privacy notice has already been
14+
* acknowledged. Callers (ZPLOutput) gate the modal behind
15+
* LabelaryNoticeModal so this component never has to handle the
16+
* pre-ack state. */
1417
export function LabelPreviewModal({ onClose }: Props) {
1518
const t = useT();
1619
const label = useLabelStore((s) => s.label);
1720
const objects = useCurrentObjects();
18-
const noticeAcknowledged = useLabelStore((s) => s.labelaryNoticeAcknowledged);
19-
const acknowledgeLabelaryNotice = useLabelStore((s) => s.acknowledgeLabelaryNotice);
20-
// Same gate as every other Labelary consumer (Print, …) — single source
21-
// of truth in the store. The modal opens only when the gate is on, so in
22-
// practice this resolves to noticeAcknowledged here, but we route through
23-
// the shared selector to stay in lockstep with future call sites.
24-
const canFetch = useLabelStore(canCallLabelary);
2521

2622
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
2723
const [error, setError] = useState<string | null>(null);
2824
const urlRef = useRef<string | null>(null);
2925
const zplRef = useRef<string>(generateZPL(label, objects));
30-
// Derived: fetch is in flight while we have neither result nor error yet.
31-
const loading = canFetch && !previewUrl && !error;
26+
const loading = !previewUrl && !error;
3227

3328
useEffect(() => {
34-
if (!canFetch) return;
3529
let cancelled = false;
3630
fetchPreview(zplRef.current, label)
3731
.then((url) => {
@@ -50,7 +44,7 @@ export function LabelPreviewModal({ onClose }: Props) {
5044
// `label` and the generated ZPL are intentionally captured once at mount
5145
// (via zplRef): the preview should reflect the snapshot the user saw when
5246
// they opened the modal, not refetch when the canvas changes underneath.
53-
}, [canFetch]); // eslint-disable-line react-hooks/exhaustive-deps
47+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
5448

5549
const handleDownloadFallback = () => {
5650
triggerDownload(new Blob([zplRef.current], { type: 'text/plain' }), 'label.zpl');
@@ -83,30 +77,10 @@ export function LabelPreviewModal({ onClose }: Props) {
8377
viewport so small previews are still centered. */}
8478
<div className="flex-1 overflow-auto bg-bg min-h-24 min-w-48">
8579
<div className="min-h-full min-w-full flex items-center justify-center p-4">
86-
{!noticeAcknowledged && (
87-
<div className="flex flex-col gap-3 max-w-80 text-center font-mono text-[10px] text-muted leading-relaxed">
88-
<span className="text-text uppercase tracking-widest">{t.output.previewNoticeTitle}</span>
89-
<span>{t.output.previewNoticeBody}</span>
90-
<a
91-
href="https://labelary.com/privacy.html"
92-
target="_blank"
93-
rel="noreferrer"
94-
className="text-accent hover:underline"
95-
>
96-
{t.output.previewNoticePrivacyLink}
97-
</a>
98-
<button
99-
onClick={acknowledgeLabelaryNotice}
100-
className="self-center mt-1 px-3 py-1.5 rounded text-[10px] font-mono bg-surface-2 border border-border text-text hover:border-accent transition-colors"
101-
>
102-
{t.output.previewNoticeAcknowledge}
103-
</button>
104-
</div>
105-
)}
106-
{canFetch && loading && (
80+
{loading && (
10781
<span className="font-mono text-[10px] text-muted animate-pulse">{t.output.loading}</span>
10882
)}
109-
{canFetch && !loading && error && (
83+
{!loading && error && (
11084
<div className="flex flex-col items-center gap-3 max-w-64 text-center">
11185
<span className="font-mono text-[10px] text-amber-400 leading-relaxed">{error}</span>
11286
<button
@@ -118,7 +92,7 @@ export function LabelPreviewModal({ onClose }: Props) {
11892
</button>
11993
</div>
12094
)}
121-
{canFetch && !loading && !error && previewUrl && (
95+
{!loading && !error && previewUrl && (
12296
<img
12397
src={previewUrl}
12498
alt="Label preview"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { XMarkIcon } from '@heroicons/react/16/solid';
2+
import { useLabelStore } from '../../store/labelStore';
3+
import { useT } from '../../lib/useT';
4+
5+
interface Props {
6+
/** Called after the modal flipped the store flag, so callers only need
7+
* to react (start the fetch / open the next modal). Acknowledgement
8+
* itself is the modal's responsibility. */
9+
onContinue: () => void;
10+
onClose: () => void;
11+
}
12+
13+
/** Privacy notice shown the first time the user invokes a Labelary-backed
14+
* feature (Preview, Print). After acknowledgement the gate
15+
* (`canCallLabelary`) opens permanently and this modal is no longer
16+
* rendered. Shared between Preview and Print so the disclosure wording
17+
* stays in lockstep. */
18+
export function LabelaryNoticeModal({ onContinue, onClose }: Props) {
19+
const t = useT();
20+
const acknowledgeLabelaryNotice = useLabelStore((s) => s.acknowledgeLabelaryNotice);
21+
22+
const handleContinue = () => {
23+
acknowledgeLabelaryNotice();
24+
onContinue();
25+
};
26+
27+
return (
28+
<div
29+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
30+
onClick={onClose}
31+
>
32+
<div
33+
className="bg-surface border border-border-2 rounded shadow-lg flex flex-col overflow-hidden max-w-[90vw]"
34+
onClick={(e) => e.stopPropagation()}
35+
>
36+
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border-2 shrink-0">
37+
<span className="font-mono text-[10px] text-muted uppercase tracking-widest">
38+
{t.output.previewNoticeTitle}
39+
</span>
40+
<button
41+
onClick={onClose}
42+
className="p-0.5 rounded text-muted hover:text-text hover:bg-surface-2 transition-colors ml-6"
43+
>
44+
<XMarkIcon className="w-4 h-4" />
45+
</button>
46+
</div>
47+
48+
<div className="flex flex-col gap-3 p-6 max-w-80 text-center font-mono text-[10px] text-muted leading-relaxed">
49+
<span>{t.output.previewNoticeBody}</span>
50+
<a
51+
href="https://labelary.com/privacy.html"
52+
target="_blank"
53+
rel="noreferrer"
54+
className="text-accent hover:underline"
55+
>
56+
{t.output.previewNoticePrivacyLink}
57+
</a>
58+
<button
59+
onClick={handleContinue}
60+
className="self-center mt-1 px-3 py-1.5 rounded text-[10px] font-mono bg-surface-2 border border-border text-text hover:border-accent transition-colors"
61+
>
62+
{t.output.previewNoticeAcknowledge}
63+
</button>
64+
</div>
65+
</div>
66+
</div>
67+
);
68+
}

‎src/components/Output/ZPLOutput.tsx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useLabelStore } from '../../store/labelStore';
44
import { generateMultiPageZPL } from '../../lib/zplGenerator';
55
import { useT } from '../../lib/useT';
66
import { LabelPreviewModal } from './LabelPreview';
7+
import { LabelaryNoticeModal } from './LabelaryNoticeModal';
78

89
interface Props {
910
collapsed?: boolean;
@@ -15,12 +16,11 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
1516
const t = useT();
1617
const label = useLabelStore((s) => s.label);
1718
const pages = useLabelStore((s) => s.pages);
18-
// Direct gate check — Preview is the path to the privacy notice, so the
19-
// button must be reachable before acknowledgement. Other Labelary callers
20-
// (AppShell.Print, LabelPreview.fetch) use the stricter canCallLabelary.
2119
const labelaryEnabled = useLabelStore((s) => s.thirdParty.labelary);
20+
const noticeAcknowledged = useLabelStore((s) => s.labelaryNoticeAcknowledged);
2221
const [copied, setCopied] = useState(false);
2322
const [showPreview, setShowPreview] = useState(false);
23+
const [showNotice, setShowNotice] = useState(false);
2424

2525
const hasObjects = pages.some((p) => p.objects.length > 0);
2626
const zpl = hasObjects ? generateMultiPageZPL(label, pages) : '';
@@ -49,7 +49,7 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
4949
<div className="flex items-center gap-3">
5050
{labelaryEnabled && (
5151
<button
52-
onClick={() => setShowPreview(true)}
52+
onClick={() => (noticeAcknowledged ? setShowPreview(true) : setShowNotice(true))}
5353
disabled={!zpl}
5454
title={t.output.previewHeading}
5555
className="flex items-center gap-1 font-mono text-[10px] text-muted hover:text-accent disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
@@ -79,6 +79,15 @@ export function ZPLOutput({ collapsed, onCollapse, onExpand }: Props) {
7979
: <span className="text-muted">{t.output.noObjects}</span>
8080
}
8181
</pre>}
82+
{showNotice && (
83+
<LabelaryNoticeModal
84+
onClose={() => setShowNotice(false)}
85+
onContinue={() => {
86+
setShowNotice(false);
87+
setShowPreview(true);
88+
}}
89+
/>
90+
)}
8291
{showPreview && <LabelPreviewModal onClose={() => setShowPreview(false)} />}
8392
</div>
8493
);

‎src/locales/ar.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const ar = {
122122
previewEmpty: 'تظهر المعاينة\nبعد إجراء تغييرات',
123123
previewProvider: 'معاينة عبر api.labelary.com',
124124
previewNoticeTitle: 'إشعار الخصوصية',
125-
previewNoticeBody: 'يتم إنشاء المعاينة بواسطة الخدمة الخارجية api.labelary.com. يتم إرسال ZPL الكامل للملصق، بما في ذلك أي بيانات حساسة، عبر الشبكة.',
125+
previewNoticeBody: 'يتم إنشاء المعاينة والطباعة بواسطة الخدمة الخارجية api.labelary.com. يتم إرسال ZPL الكامل للملصق، بما في ذلك أي بيانات حساسة، عبر الشبكة.',
126126
previewNoticePrivacyLink: 'معلومات خصوصية Labelary',
127127
previewNoticeAcknowledge: 'فهمت، تابع',
128128
},

‎src/locales/bg.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const bg = {
122122
previewEmpty: 'Преглед се появява\nслед промени',
123123
previewProvider: 'Преглед чрез api.labelary.com',
124124
previewNoticeTitle: 'Известие за поверителност',
125-
previewNoticeBody: 'Прегледът се изобразява от външната услуга api.labelary.com. Целият ZPL на етикета, включително чувствителните данни, се изпраща през мрежата.',
125+
previewNoticeBody: 'Прегледът и отпечатването се изобразяват от външната услуга api.labelary.com. Целият ZPL на етикета, включително чувствителните данни, се изпраща през мрежата.',
126126
previewNoticePrivacyLink: 'Информация за поверителност на Labelary',
127127
previewNoticeAcknowledge: 'Разбрах, продължи',
128128
},

‎src/locales/cs.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const cs = {
122122
previewEmpty: 'Náhled se zobrazí\npo úpravách',
123123
previewProvider: 'Náhled přes api.labelary.com',
124124
previewNoticeTitle: 'Upozornění na ochranu údajů',
125-
previewNoticeBody: 'Náhled se generuje pomocí externí služby api.labelary.com. Celý ZPL štítku, včetně citlivých údajů, je odesílán přes síť.',
125+
previewNoticeBody: 'Náhled a tisk se generují pomocí externí služby api.labelary.com. Celý ZPL štítku, včetně citlivých údajů, je odesílán přes síť.',
126126
previewNoticePrivacyLink: 'Informace o ochraně údajů Labelary',
127127
previewNoticeAcknowledge: 'Rozumím, pokračovat',
128128
},

‎src/locales/da.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const da = {
122122
previewEmpty: 'Forhåndsvisning vises\nefter ændringer',
123123
previewProvider: 'Forhåndsvisning via api.labelary.com',
124124
previewNoticeTitle: 'Privatlivsmeddelelse',
125-
previewNoticeBody: 'Forhåndsvisningen genereres af den eksterne tjeneste api.labelary.com. Hele etikettens ZPL, inklusive følsomme data, sendes over netværket.',
125+
previewNoticeBody: 'Forhåndsvisning og udskrivning genereres af den eksterne tjeneste api.labelary.com. Hele etikettens ZPL, inklusive følsomme data, sendes over netværket.',
126126
previewNoticePrivacyLink: 'Labelary-privatlivsoplysninger',
127127
previewNoticeAcknowledge: 'Forstået, fortsæt',
128128
},

‎src/locales/de.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const de = {
142142
previewEmpty: 'Vorschau erscheint\nnach Änderungen',
143143
previewProvider: 'Vorschau über api.labelary.com',
144144
previewNoticeTitle: 'Datenschutzhinweis',
145-
previewNoticeBody: 'Die Vorschau wird vom externen Dienst api.labelary.com erstellt. Dabei wird das vollständige Label-ZPL inklusive sensibler Daten übertragen.',
145+
previewNoticeBody: 'Vorschau und Druck werden vom externen Dienst api.labelary.com erstellt. Dabei wird das vollständige Label-ZPL inklusive sensibler Daten übertragen.',
146146
previewNoticePrivacyLink: 'Labelary-Datenschutzhinweise',
147147
previewNoticeAcknowledge: 'Verstanden, fortfahren',
148148
},

‎src/locales/el.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ const el = {
122122
previewEmpty: 'Η προεπισκόπηση εμφανίζεται\nμετά τις αλλαγές',
123123
previewProvider: 'Προεπισκόπηση μέσω api.labelary.com',
124124
previewNoticeTitle: 'Ειδοποίηση απορρήτου',
125-
previewNoticeBody: 'Η προεπισκόπηση δημιουργείται από την εξωτερική υπηρεσία api.labelary.com. Ολόκληρο το ZPL της ετικέτας, συμπεριλαμβανομένων ευαίσθητων δεδομένων, αποστέλλεται μέσω δικτύου.',
125+
previewNoticeBody: 'Η προεπισκόπηση και η εκτύπωση δημιουργούνται από την εξωτερική υπηρεσία api.labelary.com. Ολόκληρο το ZPL της ετικέτας, συμπεριλαμβανομένων ευαίσθητων δεδομένων, αποστέλλεται μέσω δικτύου.',
126126
previewNoticePrivacyLink: 'Πληροφορίες απορρήτου Labelary',
127127
previewNoticeAcknowledge: 'Κατάλαβα, συνέχεια',
128128
},

0 commit comments

Comments
 (0)