@asteby/storefront-react 0.21.0 → 0.23.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/account.cjs.map +1 -1
- package/dist/account.js +1 -1
- package/dist/checkout.cjs +74 -6
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +9 -2
- package/dist/checkout.d.ts +9 -2
- package/dist/checkout.js +74 -6
- package/dist/checkout.js.map +1 -1
- package/dist/{chunk-Q7UTJIKY.js → chunk-D5L7UZIA.js} +25 -4
- package/dist/chunk-D5L7UZIA.js.map +1 -0
- package/dist/{chunk-PPQEUTXJ.js → chunk-VE7WQPMY.js} +48 -6
- package/dist/chunk-VE7WQPMY.js.map +1 -0
- package/dist/fiscal-server.cjs.map +1 -1
- package/dist/fiscal-server.d.cts +2 -2
- package/dist/fiscal-server.d.ts +2 -2
- package/dist/fiscal-server.js +1 -1
- package/dist/fiscal.cjs +418 -17
- package/dist/fiscal.cjs.map +1 -1
- package/dist/fiscal.css +103 -0
- package/dist/fiscal.d.cts +91 -7
- package/dist/fiscal.d.ts +91 -7
- package/dist/fiscal.js +296 -18
- package/dist/fiscal.js.map +1 -1
- package/dist/{types-BnvSbV6t.d.cts → types-CZ3PleG6.d.cts} +1 -1
- package/dist/{types-BnvSbV6t.d.ts → types-CZ3PleG6.d.ts} +1 -1
- package/package.json +2 -2
- package/dist/chunk-PPQEUTXJ.js.map +0 -1
- package/dist/chunk-Q7UTJIKY.js.map +0 -1
package/dist/fiscal.cjs
CHANGED
|
@@ -146,6 +146,26 @@ function personTypeByLength(lengths) {
|
|
|
146
146
|
return Object.keys(lengths).find((t) => lengths[t] === n) ?? null;
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
|
+
function personTypeFromPatterns(fid, taxId) {
|
|
150
|
+
const patterns = fid?.person_patterns;
|
|
151
|
+
if (!patterns) return null;
|
|
152
|
+
const v = normalizeTaxId(taxId);
|
|
153
|
+
for (const type of Object.keys(patterns).sort()) {
|
|
154
|
+
const re = compile(patterns[type]);
|
|
155
|
+
if (re?.test(v)) return type;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function personTypeResolver(fid, override) {
|
|
160
|
+
return override ?? ((taxId) => personTypeFromPatterns(fid, taxId));
|
|
161
|
+
}
|
|
162
|
+
function suggestInvoiceUse(fid, regime) {
|
|
163
|
+
if (!fid || !regime) return null;
|
|
164
|
+
const allowed = usesFor(fid, regime);
|
|
165
|
+
const preferred = fid.default_invoice_use;
|
|
166
|
+
if (preferred && allowed.some((u) => u.code === preferred)) return preferred;
|
|
167
|
+
return allowed.find((u) => u.suggested_for_regimes?.includes(regime))?.code ?? null;
|
|
168
|
+
}
|
|
149
169
|
function regimesFor(fid, personType) {
|
|
150
170
|
const all = fid?.regimes ?? [];
|
|
151
171
|
if (!personType) return all;
|
|
@@ -156,8 +176,9 @@ function usesFor(fid, regime) {
|
|
|
156
176
|
if (!regime) return all;
|
|
157
177
|
return all.filter((u) => !u.allowed_regimes || u.allowed_regimes.length === 0 || u.allowed_regimes.includes(regime));
|
|
158
178
|
}
|
|
159
|
-
function validateFiscalValues(region, v,
|
|
179
|
+
function validateFiscalValues(region, v, personTypeOverride) {
|
|
160
180
|
const fid = region.fiscal_id;
|
|
181
|
+
const personTypeOf = personTypeResolver(fid, personTypeOverride);
|
|
161
182
|
const out = {};
|
|
162
183
|
const tax = taxIdProblem(fid, v.tax_id ?? "");
|
|
163
184
|
if (tax) out.tax_id = tax;
|
|
@@ -215,6 +236,7 @@ var ES2 = {
|
|
|
215
236
|
useHint: "Solo mostramos los usos que admite tu r\xE9gimen.",
|
|
216
237
|
useChoose: "Elige el uso",
|
|
217
238
|
fromDocument: "De tu {document}",
|
|
239
|
+
suggested: "Sugerido",
|
|
218
240
|
errors: {
|
|
219
241
|
required: "Completa este campo.",
|
|
220
242
|
taxIdFormat: "Revisa el formato: {kind} no v\xE1lido.",
|
|
@@ -247,14 +269,34 @@ var ES2 = {
|
|
|
247
269
|
qrMismatch: "El c\xF3digo QR de tu documento no coincide con los datos le\xEDdos. Revisa con calma tu RFC y tu nombre.",
|
|
248
270
|
stale: "Tu documento tiene {days} d\xEDas de emitido. Si algo cambi\xF3 en tu registro, descarga uno nuevo.",
|
|
249
271
|
regimeMissing: "No pudimos detectar tu r\xE9gimen. El\xEDgelo de la lista.",
|
|
250
|
-
fieldsMissing: "No encontramos algunos datos en el documento. Compl\xE9talos para continuar."
|
|
272
|
+
fieldsMissing: "No encontramos algunos datos en el documento. Compl\xE9talos para continuar.",
|
|
273
|
+
qrPartial: "Del c\xF3digo QR solo le\xEDmos tu identificador fiscal. Completa los dem\xE1s datos o sube el PDF."
|
|
274
|
+
},
|
|
275
|
+
scan: {
|
|
276
|
+
title: "Escanea tu {document}",
|
|
277
|
+
photo: "Tomar foto del QR",
|
|
278
|
+
photoHint: "Enfoca el c\xF3digo QR de tu documento.",
|
|
279
|
+
camera: "Escanear con la c\xE1mara",
|
|
280
|
+
cameraTitle: "Apunta al c\xF3digo QR",
|
|
281
|
+
cameraGuide: "Centra el c\xF3digo QR dentro del marco.",
|
|
282
|
+
cameraStarting: "Abriendo la c\xE1mara\u2026",
|
|
283
|
+
cameraRead: "C\xF3digo le\xEDdo",
|
|
284
|
+
cameraDenied: "No pudimos usar la c\xE1mara. Elige una foto o sube el PDF.",
|
|
285
|
+
cameraUnavailable: "Este dispositivo no permite usar la c\xE1mara aqu\xED. Elige una foto o sube el PDF.",
|
|
286
|
+
closeCamera: "Cerrar la c\xE1mara",
|
|
287
|
+
choosePhoto: "Elegir una foto",
|
|
288
|
+
open: "Escanear {document}",
|
|
289
|
+
manual: "Capturar a mano",
|
|
290
|
+
close: "Cerrar"
|
|
251
291
|
},
|
|
252
292
|
errors: {
|
|
253
293
|
not_pdf: "Ese archivo no es un PDF. Sube el PDF que descargaste o captura tus datos a mano.",
|
|
254
294
|
too_large: "El archivo pesa m\xE1s de {size}. Sube el PDF original o captura tus datos a mano.",
|
|
255
295
|
no_text: "Este PDF parece una imagen escaneada y no podemos leerlo. Captura tus datos a mano.",
|
|
256
296
|
password: "Este PDF tiene contrase\xF1a y no podemos abrirlo. Desc\xE1rgalo de nuevo sin contrase\xF1a o captura tus datos a mano.",
|
|
257
|
-
unreadable: "No pudimos leer este archivo. Int\xE9ntalo con otro o captura tus datos a mano."
|
|
297
|
+
unreadable: "No pudimos leer este archivo. Int\xE9ntalo con otro o captura tus datos a mano.",
|
|
298
|
+
no_qr: "No encontramos el c\xF3digo QR en la foto. Ac\xE9rcate, evita reflejos o sube el PDF.",
|
|
299
|
+
not_image: "Ese archivo no es una foto que podamos leer. Toma otra foto o sube el PDF."
|
|
258
300
|
},
|
|
259
301
|
summaryTitleOne: "Revisa 1 campo para continuar",
|
|
260
302
|
summaryTitleMany: "Revisa {n} campos para continuar"
|
|
@@ -334,6 +376,7 @@ var EN2 = {
|
|
|
334
376
|
useHint: "We only list the uses your regime admits.",
|
|
335
377
|
useChoose: "Choose the use",
|
|
336
378
|
fromDocument: "From your {document}",
|
|
379
|
+
suggested: "Suggested",
|
|
337
380
|
errors: {
|
|
338
381
|
required: "Fill in this field.",
|
|
339
382
|
taxIdFormat: "Check the format: not a valid {kind}.",
|
|
@@ -366,14 +409,34 @@ var EN2 = {
|
|
|
366
409
|
qrMismatch: "The QR code of your document does not match what we read. Check your tax ID and name carefully.",
|
|
367
410
|
stale: "Your document was issued {days} days ago. If something changed in your registration, download a new one.",
|
|
368
411
|
regimeMissing: "We could not detect your regime. Choose it from the list.",
|
|
369
|
-
fieldsMissing: "We did not find some details in the document. Fill them in to continue."
|
|
412
|
+
fieldsMissing: "We did not find some details in the document. Fill them in to continue.",
|
|
413
|
+
qrPartial: "The QR code only gave us your tax ID. Fill in the rest or upload the PDF."
|
|
414
|
+
},
|
|
415
|
+
scan: {
|
|
416
|
+
title: "Scan your {document}",
|
|
417
|
+
photo: "Take a photo of the QR",
|
|
418
|
+
photoHint: "Point at the QR code of your document.",
|
|
419
|
+
camera: "Scan with the camera",
|
|
420
|
+
cameraTitle: "Point at the QR code",
|
|
421
|
+
cameraGuide: "Center the QR code inside the frame.",
|
|
422
|
+
cameraStarting: "Opening the camera\u2026",
|
|
423
|
+
cameraRead: "Code read",
|
|
424
|
+
cameraDenied: "We could not use the camera. Choose a photo or upload the PDF.",
|
|
425
|
+
cameraUnavailable: "This device does not allow the camera here. Choose a photo or upload the PDF.",
|
|
426
|
+
closeCamera: "Close the camera",
|
|
427
|
+
choosePhoto: "Choose a photo",
|
|
428
|
+
open: "Scan {document}",
|
|
429
|
+
manual: "Enter by hand",
|
|
430
|
+
close: "Close"
|
|
370
431
|
},
|
|
371
432
|
errors: {
|
|
372
433
|
not_pdf: "That file is not a PDF. Upload the PDF you downloaded or enter your details by hand.",
|
|
373
434
|
too_large: "The file is larger than {size}. Upload the original PDF or enter your details by hand.",
|
|
374
435
|
no_text: "This PDF looks like a scanned image and we cannot read it. Enter your details by hand.",
|
|
375
436
|
password: "This PDF is password protected and we cannot open it. Download it again without a password or enter your details by hand.",
|
|
376
|
-
unreadable: "We could not read this file. Try another one or enter your details by hand."
|
|
437
|
+
unreadable: "We could not read this file. Try another one or enter your details by hand.",
|
|
438
|
+
no_qr: "We could not find the QR code in the photo. Move closer, avoid glare or upload the PDF.",
|
|
439
|
+
not_image: "That file is not a photo we can read. Take another photo or upload the PDF."
|
|
377
440
|
},
|
|
378
441
|
summaryTitleOne: "Check 1 field to continue",
|
|
379
442
|
summaryTitleMany: "Check {n} fields to continue"
|
|
@@ -480,7 +543,8 @@ function FiscalIdentityFields({
|
|
|
480
543
|
errors = {},
|
|
481
544
|
namePrefix = "invoice",
|
|
482
545
|
idPrefix,
|
|
483
|
-
personTypeOf,
|
|
546
|
+
personTypeOf: personTypeOverride,
|
|
547
|
+
suggestedUse,
|
|
484
548
|
disabled,
|
|
485
549
|
locale: localeProp,
|
|
486
550
|
messages,
|
|
@@ -495,6 +559,8 @@ function FiscalIdentityFields({
|
|
|
495
559
|
const base = idPrefix ?? `sf-fiscal-${reactId.replace(/:/g, "")}`;
|
|
496
560
|
const [v, setV] = react.useState({ ...EMPTY, ...values });
|
|
497
561
|
const [shown, setShown] = react.useState(/* @__PURE__ */ new Set());
|
|
562
|
+
const [autoUse, setAutoUse] = react.useState(null);
|
|
563
|
+
const personTypeOf = react.useMemo(() => personTypeResolver(region.fiscal_id, personTypeOverride), [region.fiscal_id, personTypeOverride]);
|
|
498
564
|
const root = react.useRef(null);
|
|
499
565
|
const valuesKey = JSON.stringify(values ?? null);
|
|
500
566
|
const first2 = react.useRef(true);
|
|
@@ -509,10 +575,17 @@ function FiscalIdentityFields({
|
|
|
509
575
|
const set = (patch) => {
|
|
510
576
|
const next = { ...v, ...patch };
|
|
511
577
|
if (patch.tax_regime !== void 0 && next.use && !usesFor(fid, next.tax_regime).some((u) => u.code === next.use)) next.use = "";
|
|
578
|
+
if (patch.tax_regime !== void 0 && !next.use) {
|
|
579
|
+
const suggestion = suggestInvoiceUse(fid, next.tax_regime);
|
|
580
|
+
if (suggestion) {
|
|
581
|
+
next.use = suggestion;
|
|
582
|
+
setAutoUse(suggestion);
|
|
583
|
+
} else setAutoUse(null);
|
|
584
|
+
} else if (patch.use !== void 0) setAutoUse(null);
|
|
512
585
|
setV(next);
|
|
513
586
|
onChange?.(next);
|
|
514
587
|
};
|
|
515
|
-
const person = personTypeOf
|
|
588
|
+
const person = personTypeOf(v.tax_id);
|
|
516
589
|
const taxIdOk = !validateFiscalValues(region, { tax_id: v.tax_id }, personTypeOf).tax_id;
|
|
517
590
|
const problems = react.useMemo(() => validateFiscalValues(region, v, personTypeOf), [region, v, personTypeOf]);
|
|
518
591
|
const candidates = regimeCandidates?.length ? regimeCandidates : null;
|
|
@@ -680,7 +753,12 @@ function FiscalIdentityFields({
|
|
|
680
753
|
name: name("use"),
|
|
681
754
|
id: `${base}-use`,
|
|
682
755
|
label: m.use,
|
|
683
|
-
hint:
|
|
756
|
+
hint: v.use && (v.use === autoUse || v.use === suggestedUse) ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
757
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.mark, "data-sf-suggested": true, children: m.suggested }),
|
|
758
|
+
" ",
|
|
759
|
+
"\xB7 ",
|
|
760
|
+
m.useHint
|
|
761
|
+
] }) : m.useHint,
|
|
684
762
|
value: v.use,
|
|
685
763
|
onChange: (e) => set({ use: e.target.value }),
|
|
686
764
|
onBlur: () => touch("use"),
|
|
@@ -731,10 +809,93 @@ function summarizeExtraction(x, evidence, validRegimes) {
|
|
|
731
809
|
evidence
|
|
732
810
|
};
|
|
733
811
|
}
|
|
812
|
+
function cameraSupported() {
|
|
813
|
+
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
|
|
814
|
+
}
|
|
815
|
+
function useCameraQr({ active, onCode, intervalMs = 250 }) {
|
|
816
|
+
const videoRef = react.useRef(null);
|
|
817
|
+
const [status, setStatus] = react.useState("idle");
|
|
818
|
+
const streamRef = react.useRef(null);
|
|
819
|
+
const onCodeRef = react.useRef(onCode);
|
|
820
|
+
onCodeRef.current = onCode;
|
|
821
|
+
const stop = react.useCallback(() => {
|
|
822
|
+
for (const track of streamRef.current?.getTracks() ?? []) track.stop();
|
|
823
|
+
streamRef.current = null;
|
|
824
|
+
const v = videoRef.current;
|
|
825
|
+
if (v) v.srcObject = null;
|
|
826
|
+
setStatus((s) => s === "denied" || s === "unsupported" ? s : "idle");
|
|
827
|
+
}, []);
|
|
828
|
+
react.useEffect(() => {
|
|
829
|
+
if (!active) return;
|
|
830
|
+
if (!cameraSupported()) {
|
|
831
|
+
setStatus("unsupported");
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
let cancelled = false;
|
|
835
|
+
let timer;
|
|
836
|
+
let busy = false;
|
|
837
|
+
setStatus("starting");
|
|
838
|
+
const onHidden = () => {
|
|
839
|
+
if (document.visibilityState === "hidden") stop();
|
|
840
|
+
};
|
|
841
|
+
document.addEventListener("visibilitychange", onHidden);
|
|
842
|
+
(async () => {
|
|
843
|
+
let stream;
|
|
844
|
+
try {
|
|
845
|
+
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false });
|
|
846
|
+
} catch {
|
|
847
|
+
if (!cancelled) setStatus("denied");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (cancelled) {
|
|
851
|
+
for (const t of stream.getTracks()) t.stop();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
streamRef.current = stream;
|
|
855
|
+
const video = videoRef.current;
|
|
856
|
+
if (!video) {
|
|
857
|
+
for (const t of stream.getTracks()) t.stop();
|
|
858
|
+
streamRef.current = null;
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
video.srcObject = stream;
|
|
862
|
+
video.muted = true;
|
|
863
|
+
void video.play?.().catch(() => void 0);
|
|
864
|
+
setStatus("scanning");
|
|
865
|
+
const eng = await import('@asteby/storefront/fiscal-document');
|
|
866
|
+
timer = setInterval(async () => {
|
|
867
|
+
if (busy || cancelled || !videoRef.current || videoRef.current.readyState < 2) return;
|
|
868
|
+
busy = true;
|
|
869
|
+
try {
|
|
870
|
+
const text = await eng.decodeImageQr(videoRef.current);
|
|
871
|
+
if (text && !cancelled) {
|
|
872
|
+
cancelled = true;
|
|
873
|
+
clearInterval(timer);
|
|
874
|
+
stop();
|
|
875
|
+
onCodeRef.current(text);
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
} finally {
|
|
879
|
+
busy = false;
|
|
880
|
+
}
|
|
881
|
+
}, intervalMs);
|
|
882
|
+
})();
|
|
883
|
+
return () => {
|
|
884
|
+
cancelled = true;
|
|
885
|
+
if (timer) clearInterval(timer);
|
|
886
|
+
document.removeEventListener("visibilitychange", onHidden);
|
|
887
|
+
for (const t of streamRef.current?.getTracks() ?? []) t.stop();
|
|
888
|
+
streamRef.current = null;
|
|
889
|
+
if (videoRef.current) videoRef.current.srcObject = null;
|
|
890
|
+
};
|
|
891
|
+
}, [active, intervalMs, stop]);
|
|
892
|
+
return { videoRef, status, stop };
|
|
893
|
+
}
|
|
734
894
|
var isPdf = (f, accept) => {
|
|
735
895
|
const okType = f.type ? accept.some((a) => a === f.type) || f.type === "application/pdf" : false;
|
|
736
896
|
return okType || /\.pdf$/i.test(f.name);
|
|
737
897
|
};
|
|
898
|
+
var isImage = (f) => f.type.startsWith("image/") || /\.(jpe?g|png|webp|heic|heif)$/i.test(f.name);
|
|
738
899
|
var mib = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} MB`;
|
|
739
900
|
function IdentityDocumentReader(props) {
|
|
740
901
|
const { region, workerSrc, onConfirm, onManual, initialValues, data, canSave, serverUpload, personTypeOf, now, namePrefix = "invoice", messages, classNames: cx = {}, confirmLabel } = props;
|
|
@@ -766,14 +927,29 @@ function IdentityDocumentReader(props) {
|
|
|
766
927
|
react.useEffect(() => () => abort.current?.abort(), []);
|
|
767
928
|
react.useEffect(() => {
|
|
768
929
|
if (!fromServer) return;
|
|
769
|
-
setValues((cur) =>
|
|
930
|
+
setValues((cur) => {
|
|
931
|
+
const next = { ...cur, ...fromServer.values };
|
|
932
|
+
const suggestion = next.tax_regime && !next.use ? suggestInvoiceUse(region.fiscal_id, next.tax_regime) : null;
|
|
933
|
+
if (suggestion) {
|
|
934
|
+
next.use = suggestion;
|
|
935
|
+
setSuggestedUse(suggestion);
|
|
936
|
+
}
|
|
937
|
+
return next;
|
|
938
|
+
});
|
|
770
939
|
setPhase({ kind: "review", extraction: fromServer, revision: 1 });
|
|
771
940
|
}, [fromServer]);
|
|
772
941
|
react.useEffect(() => {
|
|
773
942
|
if (phase.kind === "error") errorRef.current?.focus();
|
|
774
943
|
if (phase.kind === "review" && phase.revision > 0) reviewRef.current?.focus();
|
|
775
944
|
}, [phase]);
|
|
776
|
-
const accept = react.useMemo(() =>
|
|
945
|
+
const accept = react.useMemo(() => {
|
|
946
|
+
const files = (spec?.accept ?? []).filter((a) => !a.startsWith("image/"));
|
|
947
|
+
return files.length ? files.join(",") : "application/pdf";
|
|
948
|
+
}, [spec]);
|
|
949
|
+
const imageMode = spec?.image_mode && spec.image_mode !== "none" ? spec.image_mode : null;
|
|
950
|
+
const photoInput = react.useRef(null);
|
|
951
|
+
const [suggestedUse, setSuggestedUse] = react.useState(null);
|
|
952
|
+
const cameraActive = phase.kind === "camera";
|
|
777
953
|
const fieldErrors = react.useMemo(() => {
|
|
778
954
|
const out = {};
|
|
779
955
|
for (const [k, v] of Object.entries(data?.fieldErrors ?? {})) out[k] = all.server.errors[v] ?? v;
|
|
@@ -790,9 +966,70 @@ function IdentityDocumentReader(props) {
|
|
|
790
966
|
setPhase({ kind: "review", extraction: null, revision: 1 });
|
|
791
967
|
setLive("");
|
|
792
968
|
}, [onManual]);
|
|
969
|
+
const showExtraction = react.useCallback(
|
|
970
|
+
(sum) => {
|
|
971
|
+
const base = { tax_id: "", legal_name: "", postal_code: "", tax_regime: "", use: "", ...sum.values };
|
|
972
|
+
const suggestion = base.tax_regime ? suggestInvoiceUse(region.fiscal_id, base.tax_regime) : null;
|
|
973
|
+
if (suggestion && !base.use) base.use = suggestion;
|
|
974
|
+
setSuggestedUse(suggestion && base.use === suggestion ? suggestion : null);
|
|
975
|
+
setValues(base);
|
|
976
|
+
setSummary(null);
|
|
977
|
+
setPhase({ kind: "review", extraction: sum, revision: 1 });
|
|
978
|
+
setLive(m.done);
|
|
979
|
+
},
|
|
980
|
+
[region.fiscal_id, m.done]
|
|
981
|
+
);
|
|
982
|
+
const readQr = react.useCallback(
|
|
983
|
+
async (text) => {
|
|
984
|
+
if (!spec) return;
|
|
985
|
+
if (!text) return setPhase({ kind: "error", code: "no_qr" });
|
|
986
|
+
const eng = await import('@asteby/storefront/fiscal-document');
|
|
987
|
+
const x = eng.extractIdentityDocument("", spec, {
|
|
988
|
+
catalogs: region.fiscal_id ? eng.catalogsFromRegion(region.fiscal_id) : {},
|
|
989
|
+
qr: text,
|
|
990
|
+
...now ? { now: now() } : {}
|
|
991
|
+
});
|
|
992
|
+
if (!x.fields.tax_id) return setPhase({ kind: "error", code: "no_qr" });
|
|
993
|
+
const evidence = eng.buildDocumentEvidence(x, { sha256: null, source: "qr" });
|
|
994
|
+
showExtraction(summarizeExtraction(x, evidence, region.fiscal_id?.regimes.map((r) => r.code)));
|
|
995
|
+
},
|
|
996
|
+
[spec, region, now, showExtraction]
|
|
997
|
+
);
|
|
998
|
+
const readImage = react.useCallback(
|
|
999
|
+
async (file) => {
|
|
1000
|
+
if (!spec) return;
|
|
1001
|
+
if (file.size > spec.max_bytes) return setPhase({ kind: "error", code: "too_large", sizeLimit: spec.max_bytes });
|
|
1002
|
+
const ctl = new AbortController();
|
|
1003
|
+
abort.current = ctl;
|
|
1004
|
+
setPhase({ kind: "reading", fileName: file.name });
|
|
1005
|
+
setLive(fill(m.reading, { document: documentName }));
|
|
1006
|
+
try {
|
|
1007
|
+
const eng = await import('@asteby/storefront/fiscal-document');
|
|
1008
|
+
const text = await eng.decodeImageQr(file, { maxBytes: spec.max_bytes, signal: ctl.signal });
|
|
1009
|
+
if (ctl.signal.aborted) return;
|
|
1010
|
+
await readQr(text);
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
if (ctl.signal.aborted) return;
|
|
1013
|
+
const code = err?.code;
|
|
1014
|
+
setLive("");
|
|
1015
|
+
setPhase({ kind: "error", code: code === "too_large" ? "too_large" : code === "not_image" ? "not_image" : "unreadable", sizeLimit: spec.max_bytes });
|
|
1016
|
+
} finally {
|
|
1017
|
+
if (abort.current === ctl) abort.current = null;
|
|
1018
|
+
if (photoInput.current) photoInput.current.value = "";
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
[spec, documentName, m.reading, readQr]
|
|
1022
|
+
);
|
|
1023
|
+
const camera = useCameraQr({
|
|
1024
|
+
active: cameraActive,
|
|
1025
|
+
onCode: (text) => {
|
|
1026
|
+
void readQr(text).then(() => setLive(`${m.scan.cameraRead}. ${m.done}`)).catch(() => setPhase({ kind: "error", code: "unreadable" }));
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
793
1029
|
const read = react.useCallback(
|
|
794
1030
|
async (file) => {
|
|
795
1031
|
if (!spec) return;
|
|
1032
|
+
if (imageMode && isImage(file)) return readImage(file);
|
|
796
1033
|
if (!isPdf(file, spec.accept ?? [])) return setPhase({ kind: "error", code: "not_pdf" });
|
|
797
1034
|
if (file.size > spec.max_bytes) return setPhase({ kind: "error", code: "too_large", sizeLimit: spec.max_bytes });
|
|
798
1035
|
const ctl = new AbortController();
|
|
@@ -817,10 +1054,7 @@ function IdentityDocumentReader(props) {
|
|
|
817
1054
|
});
|
|
818
1055
|
const evidence = eng.buildDocumentEvidence(x, { sha256, source: "pdf_client" });
|
|
819
1056
|
const sum = summarizeExtraction(x, evidence, region.fiscal_id?.regimes.map((r) => r.code));
|
|
820
|
-
|
|
821
|
-
setSummary(null);
|
|
822
|
-
setPhase({ kind: "review", extraction: sum, revision: 1 });
|
|
823
|
-
setLive(m.done);
|
|
1057
|
+
showExtraction(sum);
|
|
824
1058
|
} catch (err) {
|
|
825
1059
|
if (ctl.signal.aborted) return;
|
|
826
1060
|
const code = err?.code;
|
|
@@ -835,7 +1069,7 @@ function IdentityDocumentReader(props) {
|
|
|
835
1069
|
if (fileInput.current) fileInput.current.value = "";
|
|
836
1070
|
}
|
|
837
1071
|
},
|
|
838
|
-
[spec, workerSrc, region, now, documentName, m.reading,
|
|
1072
|
+
[spec, workerSrc, region, now, documentName, m.reading, imageMode, readImage, showExtraction]
|
|
839
1073
|
);
|
|
840
1074
|
if (!spec) return null;
|
|
841
1075
|
const onDrop = (e) => {
|
|
@@ -845,7 +1079,7 @@ function IdentityDocumentReader(props) {
|
|
|
845
1079
|
if (f) void read(f);
|
|
846
1080
|
};
|
|
847
1081
|
const confirm = () => {
|
|
848
|
-
const problems = validateFiscalValues(region, values, personTypeOf);
|
|
1082
|
+
const problems = validateFiscalValues(region, values, personTypeResolver(region.fiscal_id, personTypeOf));
|
|
849
1083
|
const keys = Object.keys(problems);
|
|
850
1084
|
if (keys.length) {
|
|
851
1085
|
const labels = {
|
|
@@ -910,6 +1144,27 @@ function IdentityDocumentReader(props) {
|
|
|
910
1144
|
]
|
|
911
1145
|
}
|
|
912
1146
|
),
|
|
1147
|
+
imageMode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-reader-scan": true, children: [
|
|
1148
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1149
|
+
"input",
|
|
1150
|
+
{
|
|
1151
|
+
ref: photoInput,
|
|
1152
|
+
type: "file",
|
|
1153
|
+
accept: "image/*",
|
|
1154
|
+
capture: "environment",
|
|
1155
|
+
hidden: true,
|
|
1156
|
+
tabIndex: -1,
|
|
1157
|
+
"aria-label": m.scan.photo,
|
|
1158
|
+
"data-sf-reader-photo-input": true,
|
|
1159
|
+
onChange: (e) => {
|
|
1160
|
+
const f = e.target.files?.[0];
|
|
1161
|
+
if (f) void readImage(f);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.secondary, onClick: () => photoInput.current?.click(), "data-sf-reader-photo": true, children: m.scan.photo }),
|
|
1166
|
+
hydrated && cameraSupported() ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.secondary, onClick: () => setPhase({ kind: "camera" }), "data-sf-reader-camera": true, children: m.scan.camera }) : null
|
|
1167
|
+
] }) : null,
|
|
913
1168
|
/* @__PURE__ */ jsxRuntime.jsx("p", { id: helpId, className: cx.privacy, "data-sf-reader-privacy": true, children: privacy }),
|
|
914
1169
|
serverUpload ? /* @__PURE__ */ jsxRuntime.jsx("noscript", { children: /* @__PURE__ */ jsxRuntime.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,
|
|
915
1170
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { "data-sf-reader-links": true, children: [
|
|
@@ -926,6 +1181,48 @@ function IdentityDocumentReader(props) {
|
|
|
926
1181
|
] }),
|
|
927
1182
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.secondary, onClick: cancel, "data-sf-reader-cancel": true, children: m.cancel })
|
|
928
1183
|
] });
|
|
1184
|
+
const cameraView = phase.kind === "camera" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.root, "data-sf-reader": true, "data-state": "camera", children: [
|
|
1185
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { "data-sf-reader-title": true, children: m.scan.cameraTitle }),
|
|
1186
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sf-reader-viewfinder": true, children: [
|
|
1187
|
+
/* @__PURE__ */ jsxRuntime.jsx("video", { ref: camera.videoRef, playsInline: true, muted: true, "aria-label": m.scan.cameraGuide, "data-sf-reader-video": true }),
|
|
1188
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", "data-sf-reader-frame": true })
|
|
1189
|
+
] }),
|
|
1190
|
+
/* @__PURE__ */ jsxRuntime.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 }),
|
|
1191
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.privacy, "data-sf-reader-privacy": true, children: privacy }),
|
|
1192
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-reader-actions": true, children: [
|
|
1193
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.secondary, onClick: () => photoInput.current?.click(), "data-sf-reader-photo": true, children: m.scan.choosePhoto }),
|
|
1194
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1195
|
+
"button",
|
|
1196
|
+
{
|
|
1197
|
+
type: "button",
|
|
1198
|
+
className: cx.primary,
|
|
1199
|
+
onClick: () => {
|
|
1200
|
+
camera.stop();
|
|
1201
|
+
setPhase({ kind: "idle" });
|
|
1202
|
+
},
|
|
1203
|
+
"data-sf-reader-camera-close": true,
|
|
1204
|
+
children: m.scan.closeCamera
|
|
1205
|
+
}
|
|
1206
|
+
)
|
|
1207
|
+
] }),
|
|
1208
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1209
|
+
"input",
|
|
1210
|
+
{
|
|
1211
|
+
ref: photoInput,
|
|
1212
|
+
type: "file",
|
|
1213
|
+
accept: "image/*",
|
|
1214
|
+
capture: "environment",
|
|
1215
|
+
hidden: true,
|
|
1216
|
+
tabIndex: -1,
|
|
1217
|
+
"aria-label": m.scan.choosePhoto,
|
|
1218
|
+
"data-sf-reader-photo-input": true,
|
|
1219
|
+
onChange: (e) => {
|
|
1220
|
+
const f = e.target.files?.[0];
|
|
1221
|
+
if (f) void readImage(f);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
)
|
|
1225
|
+
] });
|
|
929
1226
|
const errored = phase.kind === "error" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.root, "data-sf-reader": true, "data-state": "error", children: [
|
|
930
1227
|
/* @__PURE__ */ jsxRuntime.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) }) }),
|
|
931
1228
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-reader-actions": true, children: [
|
|
@@ -958,6 +1255,7 @@ function IdentityDocumentReader(props) {
|
|
|
958
1255
|
fromDocument: phase.extraction?.from_document ?? [],
|
|
959
1256
|
documentName,
|
|
960
1257
|
regimeCandidates: phase.extraction && phase.extraction.regime_candidates.length > 1 ? phase.extraction.regime_candidates : void 0,
|
|
1258
|
+
suggestedUse,
|
|
961
1259
|
...personTypeOf ? { personTypeOf } : {},
|
|
962
1260
|
locale,
|
|
963
1261
|
...messages ? { messages } : {},
|
|
@@ -985,6 +1283,7 @@ function IdentityDocumentReader(props) {
|
|
|
985
1283
|
/* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", className: cx.status, "data-sf-reader-live": true, style: visuallyHidden, children: live }),
|
|
986
1284
|
phase.kind === "idle" ? idle : null,
|
|
987
1285
|
reading || null,
|
|
1286
|
+
cameraView || null,
|
|
988
1287
|
errored || null,
|
|
989
1288
|
review || null
|
|
990
1289
|
] });
|
|
@@ -994,7 +1293,8 @@ function Warnings({ x, m, cx }) {
|
|
|
994
1293
|
const items = [];
|
|
995
1294
|
if (x.qr_match === false) items.push(m.warnings.qrMismatch);
|
|
996
1295
|
if (x.stale === true && x.age_days !== null) items.push(fill(m.warnings.stale, { days: x.age_days }));
|
|
997
|
-
if (x.
|
|
1296
|
+
if (x.evidence.source === "qr") items.push(m.warnings.qrPartial);
|
|
1297
|
+
else if (x.regime_candidates.length === 0 && !x.values.tax_regime) items.push(m.warnings.regimeMissing);
|
|
998
1298
|
else if (x.missing.some((k) => k !== "tax_regime" && k !== "document_id" && k !== "issued_on")) items.push(m.warnings.fieldsMissing);
|
|
999
1299
|
if (!items.length) return null;
|
|
1000
1300
|
return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: cx.warning, "data-sf-reader-warnings": true, children: items.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("li", { "data-sf-reader-warning": true, children: [
|
|
@@ -1002,6 +1302,101 @@ function Warnings({ x, m, cx }) {
|
|
|
1002
1302
|
t
|
|
1003
1303
|
] }, t)) });
|
|
1004
1304
|
}
|
|
1305
|
+
function useModalDialog({ open, onClose, initialFocus }) {
|
|
1306
|
+
const ref = react.useRef(null);
|
|
1307
|
+
const opener = react.useRef(null);
|
|
1308
|
+
const onCloseRef = react.useRef(onClose);
|
|
1309
|
+
onCloseRef.current = onClose;
|
|
1310
|
+
react.useEffect(() => {
|
|
1311
|
+
const el = ref.current;
|
|
1312
|
+
if (!el || !open) return;
|
|
1313
|
+
opener.current = typeof document !== "undefined" ? document.activeElement : null;
|
|
1314
|
+
const hasModal = typeof el.showModal === "function";
|
|
1315
|
+
if (!el.open) {
|
|
1316
|
+
if (hasModal) {
|
|
1317
|
+
try {
|
|
1318
|
+
el.showModal();
|
|
1319
|
+
} catch {
|
|
1320
|
+
el.setAttribute("open", "");
|
|
1321
|
+
}
|
|
1322
|
+
} else el.setAttribute("open", "");
|
|
1323
|
+
}
|
|
1324
|
+
const target = initialFocus?.current ?? el.querySelector("[data-sf-autofocus]") ?? el;
|
|
1325
|
+
if (!target.hasAttribute("tabindex") && target === el) el.setAttribute("tabindex", "-1");
|
|
1326
|
+
target.focus({ preventScroll: true });
|
|
1327
|
+
const onCancel = (e) => {
|
|
1328
|
+
e.preventDefault();
|
|
1329
|
+
onCloseRef.current("esc");
|
|
1330
|
+
};
|
|
1331
|
+
const onKey = (e) => {
|
|
1332
|
+
if (e.key === "Escape") {
|
|
1333
|
+
e.preventDefault();
|
|
1334
|
+
onCloseRef.current("esc");
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
el.addEventListener("cancel", onCancel);
|
|
1338
|
+
if (!hasModal) el.addEventListener("keydown", onKey);
|
|
1339
|
+
return () => {
|
|
1340
|
+
el.removeEventListener("cancel", onCancel);
|
|
1341
|
+
el.removeEventListener("keydown", onKey);
|
|
1342
|
+
if (el.open) {
|
|
1343
|
+
if (typeof el.close === "function") {
|
|
1344
|
+
try {
|
|
1345
|
+
el.close();
|
|
1346
|
+
} catch {
|
|
1347
|
+
el.removeAttribute("open");
|
|
1348
|
+
}
|
|
1349
|
+
} else el.removeAttribute("open");
|
|
1350
|
+
}
|
|
1351
|
+
const back = opener.current;
|
|
1352
|
+
if (back instanceof HTMLElement && back.isConnected) back.focus({ preventScroll: true });
|
|
1353
|
+
};
|
|
1354
|
+
}, [open]);
|
|
1355
|
+
const onBackdropClick = react.useCallback((e) => {
|
|
1356
|
+
if (e.target === e.currentTarget) onCloseRef.current("backdrop");
|
|
1357
|
+
}, []);
|
|
1358
|
+
return { ref, onBackdropClick };
|
|
1359
|
+
}
|
|
1360
|
+
function ScanTaxDocument(props) {
|
|
1361
|
+
const { onConfirm, onManual, classNames: cx = {}, messages, confirmLabel, ...reader } = props;
|
|
1362
|
+
const locale = useResolvedLocale(props.locale);
|
|
1363
|
+
const m = fiscalMessages(locale, messages).reader;
|
|
1364
|
+
const spec = props.document ?? props.region.fiscal_id?.documents?.[0];
|
|
1365
|
+
const [open, setOpen] = react.useState(false);
|
|
1366
|
+
const titleId = react.useId();
|
|
1367
|
+
const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => setOpen(false) });
|
|
1368
|
+
const opener = react.useRef(null);
|
|
1369
|
+
if (!spec) return null;
|
|
1370
|
+
const name = props.documentName ?? spec.label.toLowerCase();
|
|
1371
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-scan": true, children: [
|
|
1372
|
+
/* @__PURE__ */ jsxRuntime.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 }) }),
|
|
1373
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx.manual ?? cx.secondary, onClick: onManual, "data-sf-scan-manual": true, children: m.scan.manual }),
|
|
1374
|
+
open ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsxs("div", { className: cx.sheetPanel, "data-sf-scan-panel": true, children: [
|
|
1375
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.sheetHeader, "data-sf-scan-header": true, children: [
|
|
1376
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: cx.sheetTitle, "data-sf-scan-title": true, children: fill(m.scan.title, { document: name }) }),
|
|
1377
|
+
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
|
|
1378
|
+
] }),
|
|
1379
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1380
|
+
IdentityDocumentReader,
|
|
1381
|
+
{
|
|
1382
|
+
...reader,
|
|
1383
|
+
locale,
|
|
1384
|
+
...messages ? { messages } : {},
|
|
1385
|
+
...confirmLabel ? { confirmLabel } : {},
|
|
1386
|
+
classNames: cx,
|
|
1387
|
+
onConfirm: (result) => {
|
|
1388
|
+
setOpen(false);
|
|
1389
|
+
onConfirm(result);
|
|
1390
|
+
},
|
|
1391
|
+
onManual: () => {
|
|
1392
|
+
setOpen(false);
|
|
1393
|
+
onManual();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
)
|
|
1397
|
+
] }) }) : null
|
|
1398
|
+
] });
|
|
1399
|
+
}
|
|
1005
1400
|
var profileValues = (p) => ({
|
|
1006
1401
|
tax_id: p.tax_id,
|
|
1007
1402
|
legal_name: p.legal_name,
|
|
@@ -1328,17 +1723,23 @@ exports.FiscalIdentityFields = FiscalIdentityFields;
|
|
|
1328
1723
|
exports.IdentityDocumentReader = IdentityDocumentReader;
|
|
1329
1724
|
exports.InvoiceStatusCard = InvoiceStatusCard;
|
|
1330
1725
|
exports.InvoiceToggle = InvoiceToggle;
|
|
1726
|
+
exports.ScanTaxDocument = ScanTaxDocument;
|
|
1727
|
+
exports.cameraSupported = cameraSupported;
|
|
1331
1728
|
exports.entryLabel = entryLabel;
|
|
1332
1729
|
exports.fiscalMessages = fiscalMessages;
|
|
1333
1730
|
exports.invoiceCardState = invoiceCardState;
|
|
1334
1731
|
exports.maskTaxId = maskTaxId;
|
|
1335
1732
|
exports.normalizeTaxId = normalizeTaxId;
|
|
1336
1733
|
exports.personTypeByLength = personTypeByLength;
|
|
1734
|
+
exports.personTypeFromPatterns = personTypeFromPatterns;
|
|
1735
|
+
exports.personTypeResolver = personTypeResolver;
|
|
1337
1736
|
exports.postalCodeProblem = postalCodeProblem;
|
|
1338
1737
|
exports.regimesFor = regimesFor;
|
|
1738
|
+
exports.suggestInvoiceUse = suggestInvoiceUse;
|
|
1339
1739
|
exports.summarizeExtraction = summarizeExtraction;
|
|
1340
1740
|
exports.taxIdProblem = taxIdProblem;
|
|
1341
1741
|
exports.toInvoiceRequest = toInvoiceRequest;
|
|
1742
|
+
exports.useCameraQr = useCameraQr;
|
|
1342
1743
|
exports.usesFor = usesFor;
|
|
1343
1744
|
exports.validateFiscalValues = validateFiscalValues;
|
|
1344
1745
|
//# sourceMappingURL=fiscal.cjs.map
|