@asteby/storefront-react 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fiscal.js CHANGED
@@ -1,11 +1,12 @@
1
- import { validateFiscalValues, regimesFor, usesFor, normalizeTaxId, entryLabel, summarizeExtraction, toInvoiceRequest, maskTaxId } from './chunk-Q7UTJIKY.js';
2
- export { entryLabel, maskTaxId, normalizeTaxId, personTypeByLength, postalCodeProblem, regimesFor, summarizeExtraction, taxIdProblem, toInvoiceRequest, usesFor, validateFiscalValues } from './chunk-Q7UTJIKY.js';
3
- import { fiscalMessages, fill } from './chunk-PPQEUTXJ.js';
4
- export { fiscalMessages } from './chunk-PPQEUTXJ.js';
1
+ import { personTypeResolver, validateFiscalValues, regimesFor, usesFor, normalizeTaxId, entryLabel, suggestInvoiceUse, summarizeExtraction, toInvoiceRequest, maskTaxId } from './chunk-D5L7UZIA.js';
2
+ export { entryLabel, maskTaxId, normalizeTaxId, personTypeByLength, personTypeFromPatterns, personTypeResolver, postalCodeProblem, regimesFor, suggestInvoiceUse, summarizeExtraction, taxIdProblem, toInvoiceRequest, usesFor, validateFiscalValues } from './chunk-D5L7UZIA.js';
3
+ import { useModalDialog } from './chunk-ZBH7JYU4.js';
4
+ import { fiscalMessages, fill } from './chunk-5XYQLOE3.js';
5
+ export { fiscalMessages } from './chunk-5XYQLOE3.js';
5
6
  import { CheckoutField, ErrorSummary } from './chunk-DFGLCIEG.js';
6
7
  import { useResolvedLocale } from './chunk-T52H5HL3.js';
7
8
  import './chunk-DQBL2DFJ.js';
8
- import { useId, useState, useRef, useEffect, useMemo, useCallback } from 'react';
9
+ import { useId, useState, useMemo, useRef, useEffect, useCallback } from 'react';
9
10
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
11
 
11
12
  var FIELDS = ["tax_id", "legal_name", "postal_code", "tax_regime", "use"];
@@ -35,7 +36,8 @@ function FiscalIdentityFields({
35
36
  errors = {},
36
37
  namePrefix = "invoice",
37
38
  idPrefix,
38
- personTypeOf,
39
+ personTypeOf: personTypeOverride,
40
+ suggestedUse,
39
41
  disabled,
40
42
  locale: localeProp,
41
43
  messages,
@@ -50,6 +52,8 @@ function FiscalIdentityFields({
50
52
  const base = idPrefix ?? `sf-fiscal-${reactId.replace(/:/g, "")}`;
51
53
  const [v, setV] = useState({ ...EMPTY, ...values });
52
54
  const [shown, setShown] = useState(/* @__PURE__ */ new Set());
55
+ const [autoUse, setAutoUse] = useState(null);
56
+ const personTypeOf = useMemo(() => personTypeResolver(region.fiscal_id, personTypeOverride), [region.fiscal_id, personTypeOverride]);
53
57
  const root = useRef(null);
54
58
  const valuesKey = JSON.stringify(values ?? null);
55
59
  const first = useRef(true);
@@ -64,10 +68,17 @@ function FiscalIdentityFields({
64
68
  const set = (patch) => {
65
69
  const next = { ...v, ...patch };
66
70
  if (patch.tax_regime !== void 0 && next.use && !usesFor(fid, next.tax_regime).some((u) => u.code === next.use)) next.use = "";
71
+ if (patch.tax_regime !== void 0 && !next.use) {
72
+ const suggestion = suggestInvoiceUse(fid, next.tax_regime);
73
+ if (suggestion) {
74
+ next.use = suggestion;
75
+ setAutoUse(suggestion);
76
+ } else setAutoUse(null);
77
+ } else if (patch.use !== void 0) setAutoUse(null);
67
78
  setV(next);
68
79
  onChange?.(next);
69
80
  };
70
- const person = personTypeOf?.(v.tax_id) ?? null;
81
+ const person = personTypeOf(v.tax_id);
71
82
  const taxIdOk = !validateFiscalValues(region, { tax_id: v.tax_id }, personTypeOf).tax_id;
72
83
  const problems = useMemo(() => validateFiscalValues(region, v, personTypeOf), [region, v, personTypeOf]);
73
84
  const candidates = regimeCandidates?.length ? regimeCandidates : null;
@@ -235,7 +246,12 @@ function FiscalIdentityFields({
235
246
  name: name("use"),
236
247
  id: `${base}-use`,
237
248
  label: m.use,
238
- hint: m.useHint,
249
+ hint: v.use && (v.use === autoUse || v.use === suggestedUse) ? /* @__PURE__ */ jsxs(Fragment, { children: [
250
+ /* @__PURE__ */ jsx("span", { className: cx.mark, "data-sf-suggested": true, children: m.suggested }),
251
+ " ",
252
+ "\xB7 ",
253
+ m.useHint
254
+ ] }) : m.useHint,
239
255
  value: v.use,
240
256
  onChange: (e) => set({ use: e.target.value }),
241
257
  onBlur: () => touch("use"),
@@ -254,10 +270,93 @@ function FiscalIdentityFields({
254
270
  )
255
271
  ] });
256
272
  }
273
+ function cameraSupported() {
274
+ return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
275
+ }
276
+ function useCameraQr({ active, onCode, intervalMs = 250 }) {
277
+ const videoRef = useRef(null);
278
+ const [status, setStatus] = useState("idle");
279
+ const streamRef = useRef(null);
280
+ const onCodeRef = useRef(onCode);
281
+ onCodeRef.current = onCode;
282
+ const stop = useCallback(() => {
283
+ for (const track of streamRef.current?.getTracks() ?? []) track.stop();
284
+ streamRef.current = null;
285
+ const v = videoRef.current;
286
+ if (v) v.srcObject = null;
287
+ setStatus((s) => s === "denied" || s === "unsupported" ? s : "idle");
288
+ }, []);
289
+ useEffect(() => {
290
+ if (!active) return;
291
+ if (!cameraSupported()) {
292
+ setStatus("unsupported");
293
+ return;
294
+ }
295
+ let cancelled = false;
296
+ let timer;
297
+ let busy = false;
298
+ setStatus("starting");
299
+ const onHidden = () => {
300
+ if (document.visibilityState === "hidden") stop();
301
+ };
302
+ document.addEventListener("visibilitychange", onHidden);
303
+ (async () => {
304
+ let stream;
305
+ try {
306
+ stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false });
307
+ } catch {
308
+ if (!cancelled) setStatus("denied");
309
+ return;
310
+ }
311
+ if (cancelled) {
312
+ for (const t of stream.getTracks()) t.stop();
313
+ return;
314
+ }
315
+ streamRef.current = stream;
316
+ const video = videoRef.current;
317
+ if (!video) {
318
+ for (const t of stream.getTracks()) t.stop();
319
+ streamRef.current = null;
320
+ return;
321
+ }
322
+ video.srcObject = stream;
323
+ video.muted = true;
324
+ void video.play?.().catch(() => void 0);
325
+ setStatus("scanning");
326
+ const eng = await import('@asteby/storefront/fiscal-document');
327
+ timer = setInterval(async () => {
328
+ if (busy || cancelled || !videoRef.current || videoRef.current.readyState < 2) return;
329
+ busy = true;
330
+ try {
331
+ const text = await eng.decodeImageQr(videoRef.current);
332
+ if (text && !cancelled) {
333
+ cancelled = true;
334
+ clearInterval(timer);
335
+ stop();
336
+ onCodeRef.current(text);
337
+ }
338
+ } catch {
339
+ } finally {
340
+ busy = false;
341
+ }
342
+ }, intervalMs);
343
+ })();
344
+ return () => {
345
+ cancelled = true;
346
+ if (timer) clearInterval(timer);
347
+ document.removeEventListener("visibilitychange", onHidden);
348
+ for (const t of streamRef.current?.getTracks() ?? []) t.stop();
349
+ streamRef.current = null;
350
+ if (videoRef.current) videoRef.current.srcObject = null;
351
+ };
352
+ }, [active, intervalMs, stop]);
353
+ return { videoRef, status, stop };
354
+ }
257
355
  var isPdf = (f, accept) => {
258
356
  const okType = f.type ? accept.some((a) => a === f.type) || f.type === "application/pdf" : false;
259
357
  return okType || /\.pdf$/i.test(f.name);
260
358
  };
359
+ var isImage = (f) => f.type.startsWith("image/") || /\.(jpe?g|png|webp|heic|heif)$/i.test(f.name);
261
360
  var mib = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} MB`;
262
361
  function IdentityDocumentReader(props) {
263
362
  const { region, workerSrc, onConfirm, onManual, initialValues, data, canSave, serverUpload, personTypeOf, now, namePrefix = "invoice", messages, classNames: cx = {}, confirmLabel } = props;
@@ -289,14 +388,29 @@ function IdentityDocumentReader(props) {
289
388
  useEffect(() => () => abort.current?.abort(), []);
290
389
  useEffect(() => {
291
390
  if (!fromServer) return;
292
- setValues((cur) => ({ ...cur, ...fromServer.values }));
391
+ setValues((cur) => {
392
+ const next = { ...cur, ...fromServer.values };
393
+ const suggestion = next.tax_regime && !next.use ? suggestInvoiceUse(region.fiscal_id, next.tax_regime) : null;
394
+ if (suggestion) {
395
+ next.use = suggestion;
396
+ setSuggestedUse(suggestion);
397
+ }
398
+ return next;
399
+ });
293
400
  setPhase({ kind: "review", extraction: fromServer, revision: 1 });
294
401
  }, [fromServer]);
295
402
  useEffect(() => {
296
403
  if (phase.kind === "error") errorRef.current?.focus();
297
404
  if (phase.kind === "review" && phase.revision > 0) reviewRef.current?.focus();
298
405
  }, [phase]);
299
- const accept = useMemo(() => spec?.accept?.length ? spec.accept.join(",") : "application/pdf", [spec]);
406
+ const accept = useMemo(() => {
407
+ const files = (spec?.accept ?? []).filter((a) => !a.startsWith("image/"));
408
+ return files.length ? files.join(",") : "application/pdf";
409
+ }, [spec]);
410
+ const imageMode = spec?.image_mode && spec.image_mode !== "none" ? spec.image_mode : null;
411
+ const photoInput = useRef(null);
412
+ const [suggestedUse, setSuggestedUse] = useState(null);
413
+ const cameraActive = phase.kind === "camera";
300
414
  const fieldErrors = useMemo(() => {
301
415
  const out = {};
302
416
  for (const [k, v] of Object.entries(data?.fieldErrors ?? {})) out[k] = all.server.errors[v] ?? v;
@@ -313,9 +427,70 @@ function IdentityDocumentReader(props) {
313
427
  setPhase({ kind: "review", extraction: null, revision: 1 });
314
428
  setLive("");
315
429
  }, [onManual]);
430
+ const showExtraction = useCallback(
431
+ (sum) => {
432
+ const base = { tax_id: "", legal_name: "", postal_code: "", tax_regime: "", use: "", ...sum.values };
433
+ const suggestion = base.tax_regime ? suggestInvoiceUse(region.fiscal_id, base.tax_regime) : null;
434
+ if (suggestion && !base.use) base.use = suggestion;
435
+ setSuggestedUse(suggestion && base.use === suggestion ? suggestion : null);
436
+ setValues(base);
437
+ setSummary(null);
438
+ setPhase({ kind: "review", extraction: sum, revision: 1 });
439
+ setLive(m.done);
440
+ },
441
+ [region.fiscal_id, m.done]
442
+ );
443
+ const readQr = useCallback(
444
+ async (text) => {
445
+ if (!spec) return;
446
+ if (!text) return setPhase({ kind: "error", code: "no_qr" });
447
+ const eng = await import('@asteby/storefront/fiscal-document');
448
+ const x = eng.extractIdentityDocument("", spec, {
449
+ catalogs: region.fiscal_id ? eng.catalogsFromRegion(region.fiscal_id) : {},
450
+ qr: text,
451
+ ...now ? { now: now() } : {}
452
+ });
453
+ if (!x.fields.tax_id) return setPhase({ kind: "error", code: "no_qr" });
454
+ const evidence = eng.buildDocumentEvidence(x, { sha256: null, source: "qr" });
455
+ showExtraction(summarizeExtraction(x, evidence, region.fiscal_id?.regimes.map((r) => r.code)));
456
+ },
457
+ [spec, region, now, showExtraction]
458
+ );
459
+ const readImage = useCallback(
460
+ async (file) => {
461
+ if (!spec) return;
462
+ if (file.size > spec.max_bytes) return setPhase({ kind: "error", code: "too_large", sizeLimit: spec.max_bytes });
463
+ const ctl = new AbortController();
464
+ abort.current = ctl;
465
+ setPhase({ kind: "reading", fileName: file.name });
466
+ setLive(fill(m.reading, { document: documentName }));
467
+ try {
468
+ const eng = await import('@asteby/storefront/fiscal-document');
469
+ const text = await eng.decodeImageQr(file, { maxBytes: spec.max_bytes, signal: ctl.signal });
470
+ if (ctl.signal.aborted) return;
471
+ await readQr(text);
472
+ } catch (err) {
473
+ if (ctl.signal.aborted) return;
474
+ const code = err?.code;
475
+ setLive("");
476
+ setPhase({ kind: "error", code: code === "too_large" ? "too_large" : code === "not_image" ? "not_image" : "unreadable", sizeLimit: spec.max_bytes });
477
+ } finally {
478
+ if (abort.current === ctl) abort.current = null;
479
+ if (photoInput.current) photoInput.current.value = "";
480
+ }
481
+ },
482
+ [spec, documentName, m.reading, readQr]
483
+ );
484
+ const camera = useCameraQr({
485
+ active: cameraActive,
486
+ onCode: (text) => {
487
+ void readQr(text).then(() => setLive(`${m.scan.cameraRead}. ${m.done}`)).catch(() => setPhase({ kind: "error", code: "unreadable" }));
488
+ }
489
+ });
316
490
  const read = useCallback(
317
491
  async (file) => {
318
492
  if (!spec) return;
493
+ if (imageMode && isImage(file)) return readImage(file);
319
494
  if (!isPdf(file, spec.accept ?? [])) return setPhase({ kind: "error", code: "not_pdf" });
320
495
  if (file.size > spec.max_bytes) return setPhase({ kind: "error", code: "too_large", sizeLimit: spec.max_bytes });
321
496
  const ctl = new AbortController();
@@ -340,10 +515,7 @@ function IdentityDocumentReader(props) {
340
515
  });
341
516
  const evidence = eng.buildDocumentEvidence(x, { sha256, source: "pdf_client" });
342
517
  const sum = summarizeExtraction(x, evidence, region.fiscal_id?.regimes.map((r) => r.code));
343
- setValues({ tax_id: "", legal_name: "", postal_code: "", tax_regime: "", use: "", ...sum.values });
344
- setSummary(null);
345
- setPhase({ kind: "review", extraction: sum, revision: 1 });
346
- setLive(m.done);
518
+ showExtraction(sum);
347
519
  } catch (err) {
348
520
  if (ctl.signal.aborted) return;
349
521
  const code = err?.code;
@@ -358,7 +530,7 @@ function IdentityDocumentReader(props) {
358
530
  if (fileInput.current) fileInput.current.value = "";
359
531
  }
360
532
  },
361
- [spec, workerSrc, region, now, documentName, m.reading, m.done]
533
+ [spec, workerSrc, region, now, documentName, m.reading, imageMode, readImage, showExtraction]
362
534
  );
363
535
  if (!spec) return null;
364
536
  const onDrop = (e) => {
@@ -368,7 +540,7 @@ function IdentityDocumentReader(props) {
368
540
  if (f) void read(f);
369
541
  };
370
542
  const confirm = () => {
371
- const problems = validateFiscalValues(region, values, personTypeOf);
543
+ const problems = validateFiscalValues(region, values, personTypeResolver(region.fiscal_id, personTypeOf));
372
544
  const keys = Object.keys(problems);
373
545
  if (keys.length) {
374
546
  const labels = {
@@ -433,6 +605,27 @@ function IdentityDocumentReader(props) {
433
605
  ]
434
606
  }
435
607
  ),
608
+ imageMode ? /* @__PURE__ */ jsxs("div", { className: cx.actions, "data-sf-reader-scan": true, children: [
609
+ /* @__PURE__ */ jsx(
610
+ "input",
611
+ {
612
+ ref: photoInput,
613
+ type: "file",
614
+ accept: "image/*",
615
+ capture: "environment",
616
+ hidden: true,
617
+ tabIndex: -1,
618
+ "aria-label": m.scan.photo,
619
+ "data-sf-reader-photo-input": true,
620
+ onChange: (e) => {
621
+ const f = e.target.files?.[0];
622
+ if (f) void readImage(f);
623
+ }
624
+ }
625
+ ),
626
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx.secondary, onClick: () => photoInput.current?.click(), "data-sf-reader-photo": true, children: m.scan.photo }),
627
+ hydrated && cameraSupported() ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.secondary, onClick: () => setPhase({ kind: "camera" }), "data-sf-reader-camera": true, children: m.scan.camera }) : null
628
+ ] }) : null,
436
629
  /* @__PURE__ */ jsx("p", { id: helpId, className: cx.privacy, "data-sf-reader-privacy": true, children: privacy }),
437
630
  serverUpload ? /* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx("button", { type: "submit", name: "intent", value: "extract-document", formEncType: "multipart/form-data", formMethod: "post", formNoValidate: true, className: cx.secondary, "data-sf-reader-upload": true, children: m.uploadServer }) }) : null,
438
631
  /* @__PURE__ */ jsxs("p", { "data-sf-reader-links": true, children: [
@@ -449,6 +642,48 @@ function IdentityDocumentReader(props) {
449
642
  ] }),
450
643
  /* @__PURE__ */ jsx("button", { type: "button", className: cx.secondary, onClick: cancel, "data-sf-reader-cancel": true, children: m.cancel })
451
644
  ] });
645
+ const cameraView = phase.kind === "camera" && /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-reader": true, "data-state": "camera", children: [
646
+ /* @__PURE__ */ jsx("h3", { "data-sf-reader-title": true, children: m.scan.cameraTitle }),
647
+ /* @__PURE__ */ jsxs("div", { "data-sf-reader-viewfinder": true, children: [
648
+ /* @__PURE__ */ jsx("video", { ref: camera.videoRef, playsInline: true, muted: true, "aria-label": m.scan.cameraGuide, "data-sf-reader-video": true }),
649
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-sf-reader-frame": true })
650
+ ] }),
651
+ /* @__PURE__ */ jsx("p", { className: cx.status, "data-sf-reader-camera-status": camera.status, children: camera.status === "denied" ? m.scan.cameraDenied : camera.status === "unsupported" ? m.scan.cameraUnavailable : camera.status === "starting" ? m.scan.cameraStarting : m.scan.cameraGuide }),
652
+ /* @__PURE__ */ jsx("p", { className: cx.privacy, "data-sf-reader-privacy": true, children: privacy }),
653
+ /* @__PURE__ */ jsxs("div", { className: cx.actions, "data-sf-reader-actions": true, children: [
654
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx.secondary, onClick: () => photoInput.current?.click(), "data-sf-reader-photo": true, children: m.scan.choosePhoto }),
655
+ /* @__PURE__ */ jsx(
656
+ "button",
657
+ {
658
+ type: "button",
659
+ className: cx.primary,
660
+ onClick: () => {
661
+ camera.stop();
662
+ setPhase({ kind: "idle" });
663
+ },
664
+ "data-sf-reader-camera-close": true,
665
+ children: m.scan.closeCamera
666
+ }
667
+ )
668
+ ] }),
669
+ /* @__PURE__ */ jsx(
670
+ "input",
671
+ {
672
+ ref: photoInput,
673
+ type: "file",
674
+ accept: "image/*",
675
+ capture: "environment",
676
+ hidden: true,
677
+ tabIndex: -1,
678
+ "aria-label": m.scan.choosePhoto,
679
+ "data-sf-reader-photo-input": true,
680
+ onChange: (e) => {
681
+ const f = e.target.files?.[0];
682
+ if (f) void readImage(f);
683
+ }
684
+ }
685
+ )
686
+ ] });
452
687
  const errored = phase.kind === "error" && /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-reader": true, "data-state": "error", children: [
453
688
  /* @__PURE__ */ jsx("div", { ref: errorRef, role: "alert", tabIndex: -1, className: cx.error, "data-sf-reader-error": phase.code, children: fill(m.errors[phase.code], { size: mib(phase.sizeLimit ?? spec.max_bytes) }) }),
454
689
  /* @__PURE__ */ jsxs("div", { className: cx.actions, "data-sf-reader-actions": true, children: [
@@ -481,6 +716,7 @@ function IdentityDocumentReader(props) {
481
716
  fromDocument: phase.extraction?.from_document ?? [],
482
717
  documentName,
483
718
  regimeCandidates: phase.extraction && phase.extraction.regime_candidates.length > 1 ? phase.extraction.regime_candidates : void 0,
719
+ suggestedUse,
484
720
  ...personTypeOf ? { personTypeOf } : {},
485
721
  locale,
486
722
  ...messages ? { messages } : {},
@@ -508,6 +744,7 @@ function IdentityDocumentReader(props) {
508
744
  /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: cx.status, "data-sf-reader-live": true, style: visuallyHidden, children: live }),
509
745
  phase.kind === "idle" ? idle : null,
510
746
  reading || null,
747
+ cameraView || null,
511
748
  errored || null,
512
749
  review || null
513
750
  ] });
@@ -517,7 +754,8 @@ function Warnings({ x, m, cx }) {
517
754
  const items = [];
518
755
  if (x.qr_match === false) items.push(m.warnings.qrMismatch);
519
756
  if (x.stale === true && x.age_days !== null) items.push(fill(m.warnings.stale, { days: x.age_days }));
520
- if (x.regime_candidates.length === 0 && !x.values.tax_regime) items.push(m.warnings.regimeMissing);
757
+ if (x.evidence.source === "qr") items.push(m.warnings.qrPartial);
758
+ else if (x.regime_candidates.length === 0 && !x.values.tax_regime) items.push(m.warnings.regimeMissing);
521
759
  else if (x.missing.some((k) => k !== "tax_regime" && k !== "document_id" && k !== "issued_on")) items.push(m.warnings.fieldsMissing);
522
760
  if (!items.length) return null;
523
761
  return /* @__PURE__ */ jsx("ul", { className: cx.warning, "data-sf-reader-warnings": true, children: items.map((t) => /* @__PURE__ */ jsxs("li", { "data-sf-reader-warning": true, children: [
@@ -525,6 +763,46 @@ function Warnings({ x, m, cx }) {
525
763
  t
526
764
  ] }, t)) });
527
765
  }
766
+ function ScanTaxDocument(props) {
767
+ const { onConfirm, onManual, classNames: cx = {}, messages, confirmLabel, ...reader } = props;
768
+ const locale = useResolvedLocale(props.locale);
769
+ const m = fiscalMessages(locale, messages).reader;
770
+ const spec = props.document ?? props.region.fiscal_id?.documents?.[0];
771
+ const [open, setOpen] = useState(false);
772
+ const titleId = useId();
773
+ const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => setOpen(false) });
774
+ const opener = useRef(null);
775
+ if (!spec) return null;
776
+ const name = props.documentName ?? spec.label.toLowerCase();
777
+ return /* @__PURE__ */ jsxs("div", { className: cx.actions, "data-sf-scan": true, children: [
778
+ /* @__PURE__ */ jsx("button", { ref: opener, type: "button", className: cx.trigger ?? cx.primary, onClick: () => setOpen(true), "aria-haspopup": "dialog", "data-sf-scan-open": true, children: fill(m.scan.open, { document: spec.label }) }),
779
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx.manual ?? cx.secondary, onClick: onManual, "data-sf-scan-manual": true, children: m.scan.manual }),
780
+ open ? /* @__PURE__ */ jsx("dialog", { ref, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, onClick: onBackdropClick, onClose: () => setOpen(false), className: cx.sheet, "data-sf-scan-sheet": true, children: /* @__PURE__ */ jsxs("div", { className: cx.sheetPanel, "data-sf-scan-panel": true, children: [
781
+ /* @__PURE__ */ jsxs("div", { className: cx.sheetHeader, "data-sf-scan-header": true, children: [
782
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: cx.sheetTitle, "data-sf-scan-title": true, children: fill(m.scan.title, { document: name }) }),
783
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx.sheetClose, onClick: () => setOpen(false), "aria-label": m.scan.close, "data-sf-scan-close": true, "data-sf-autofocus": true, children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
784
+ ] }),
785
+ /* @__PURE__ */ jsx(
786
+ IdentityDocumentReader,
787
+ {
788
+ ...reader,
789
+ locale,
790
+ ...messages ? { messages } : {},
791
+ ...confirmLabel ? { confirmLabel } : {},
792
+ classNames: cx,
793
+ onConfirm: (result) => {
794
+ setOpen(false);
795
+ onConfirm(result);
796
+ },
797
+ onManual: () => {
798
+ setOpen(false);
799
+ onManual();
800
+ }
801
+ }
802
+ )
803
+ ] }) }) : null
804
+ ] });
805
+ }
528
806
  var profileValues = (p) => ({
529
807
  tax_id: p.tax_id,
530
808
  legal_name: p.legal_name,
@@ -710,8 +988,7 @@ function labelOf(list, code, locale) {
710
988
  const e = list?.find((x) => x.code === code);
711
989
  return e ? `${e.code} \xB7 ${entryLabel(e, locale)}` : code;
712
990
  }
713
- function invoiceCardState(invoice, available = true) {
714
- if (!available) return "unavailable";
991
+ function orderState(invoice) {
715
992
  if (!invoice) return "not_requested";
716
993
  const stage = invoice.stage ?? "";
717
994
  const blocked = invoice.blocked_reason ?? "";
@@ -726,11 +1003,18 @@ function invoiceCardState(invoice, available = true) {
726
1003
  if (invoice.can_request === false && blocked) return "unavailable";
727
1004
  return "not_requested";
728
1005
  }
1006
+ function invoiceCardState(invoice, available = true, reason) {
1007
+ if (available) return orderState(invoice);
1008
+ if (reason === "off" || reason === "no_region") return "not_enabled";
1009
+ const own = orderState(invoice);
1010
+ if (invoice && (own !== "not_requested" || invoice.can_request === true || invoice.action === "request")) return own;
1011
+ return reason === "unreachable" ? "unreachable" : "unknown";
1012
+ }
729
1013
  function InvoiceStatusCard(props) {
730
- const { invoice, available = true, onRequest, requestHref, onCorrect, correctHref, onRefresh, refreshMs = 1e4, refreshForMs = 6e4, contact, messages, classNames: cx = {}, headingLevel = 3 } = props;
1014
+ const { invoice, available = true, availability, testMode, windowDays, onRequest, requestHref, onCorrect, correctHref, onRefresh, refreshMs = 1e4, refreshForMs = 6e4, contact, messages, classNames: cx = {}, headingLevel = 3 } = props;
731
1015
  const locale = useResolvedLocale(props.locale);
732
1016
  const m = fiscalMessages(locale, messages).status;
733
- const state = invoiceCardState(invoice, available);
1017
+ const state = invoiceCardState(invoice, available, availability);
734
1018
  const titleId = `sf-invoice-card-${useId().replace(/:/g, "")}`;
735
1019
  const [tired, setTired] = useState(false);
736
1020
  const refresh = useRef(onRefresh);
@@ -763,7 +1047,10 @@ function InvoiceStatusCard(props) {
763
1047
  switch (state) {
764
1048
  case "not_requested": {
765
1049
  title = m.title;
766
- body = /* @__PURE__ */ jsx("p", { className: cx.body, children: m.requestHelp });
1050
+ body = /* @__PURE__ */ jsxs(Fragment, { children: [
1051
+ /* @__PURE__ */ jsx("p", { className: cx.body, children: m.requestHelp }),
1052
+ windowDays && windowDays > 0 ? /* @__PURE__ */ jsx("p", { className: cx.meta, "data-sf-invoice-window": true, children: fill(m.windowNote, { days: windowDays }) }) : null
1053
+ ] });
767
1054
  const can = invoice?.can_request !== false;
768
1055
  if (can && (onRequest || requestHref)) {
769
1056
  actions = onRequest ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.primary, onClick: onRequest, "data-sf-invoice-request": true, children: m.request }) : /* @__PURE__ */ jsx("a", { href: requestHref, className: cx.primary, "data-sf-invoice-request": true, children: m.request });
@@ -821,6 +1108,18 @@ function InvoiceStatusCard(props) {
821
1108
  title = m.cancelledTitle;
822
1109
  body = /* @__PURE__ */ jsx("p", { className: cx.body, children: m.cancelledBody });
823
1110
  break;
1111
+ case "not_enabled":
1112
+ case "unknown":
1113
+ case "unreachable": {
1114
+ title = state === "not_enabled" ? m.notEnabledTitle : m.unknownTitle;
1115
+ body = /* @__PURE__ */ jsx("p", { className: cx.body, children: state === "not_enabled" ? m.notEnabledBody : state === "unreachable" ? m.unreachableBody : m.unknownBody });
1116
+ const mail = contact?.url ?? (contact?.email ? `mailto:${contact.email}` : null);
1117
+ actions = /* @__PURE__ */ jsxs(Fragment, { children: [
1118
+ state === "unreachable" && onRefresh ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.secondary, onClick: manualRefresh, "data-sf-invoice-refresh": true, children: m.refresh }) : null,
1119
+ mail ? /* @__PURE__ */ jsx("a", { href: mail, className: cx.secondary, "data-sf-invoice-contact": true, children: m.contact }) : null
1120
+ ] });
1121
+ break;
1122
+ }
824
1123
  default:
825
1124
  title = m.unavailableTitle;
826
1125
  body = /* @__PURE__ */ jsx("p", { className: cx.body, children: m.unavailableBody });
@@ -832,6 +1131,7 @@ function InvoiceStatusCard(props) {
832
1131
  title
833
1132
  ] }),
834
1133
  /* @__PURE__ */ jsx("div", { "aria-live": live || state === "issued" ? "polite" : void 0, "data-sf-invoice-live": true, children: body }),
1134
+ testMode ? /* @__PURE__ */ jsx("p", { className: cx.meta, "data-sf-invoice-sandbox": true, children: m.sandbox }) : null,
835
1135
  actions ? /* @__PURE__ */ jsx("div", { className: cx.actions, "data-sf-invoice-actions": true, children: actions }) : null
836
1136
  ] });
837
1137
  }
@@ -844,9 +1144,12 @@ var ICON = {
844
1144
  needs_review: "\u25D0",
845
1145
  window_closed: "\u2013",
846
1146
  unavailable: "\u2013",
1147
+ not_enabled: "\u2013",
1148
+ unknown: "?",
1149
+ unreachable: "?",
847
1150
  cancelled: "\xD7"
848
1151
  };
849
1152
 
850
- export { FiscalIdentityFields, IdentityDocumentReader, InvoiceStatusCard, InvoiceToggle, invoiceCardState };
1153
+ export { FiscalIdentityFields, IdentityDocumentReader, InvoiceStatusCard, InvoiceToggle, ScanTaxDocument, cameraSupported, invoiceCardState, useCameraQr };
851
1154
  //# sourceMappingURL=fiscal.js.map
852
1155
  //# sourceMappingURL=fiscal.js.map