@bacano/sdk 1.1.13 → 1.1.17

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.
@@ -0,0 +1,450 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // libs/sdk/src/react/forms.tsx
21
+ var forms_exports = {};
22
+ __export(forms_exports, {
23
+ BacanoForm: () => BacanoForm,
24
+ useBacanoForm: () => useBacanoForm
25
+ });
26
+ module.exports = __toCommonJS(forms_exports);
27
+ var import_react3 = require("react");
28
+
29
+ // libs/sdk/src/react/form-field-control.tsx
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ function BacanoFormFieldControl(props) {
32
+ const { field, className, value, onChange } = props;
33
+ const common = {
34
+ id: field.key,
35
+ name: field.key,
36
+ required: field.required,
37
+ className
38
+ };
39
+ if (field.type === "textarea") {
40
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
41
+ "textarea",
42
+ {
43
+ ...common,
44
+ value: typeof value === "string" ? value : "",
45
+ placeholder: field.placeholder ?? void 0,
46
+ minLength: field.validation?.minLength ?? void 0,
47
+ maxLength: field.validation?.maxLength ?? void 0,
48
+ onChange: (event) => onChange(event.target.value)
49
+ }
50
+ );
51
+ }
52
+ if (field.type === "select" || field.type === "multi_select") {
53
+ const isMultiple = field.type === "multi_select";
54
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
55
+ "select",
56
+ {
57
+ ...common,
58
+ multiple: isMultiple,
59
+ value: isMultiple && Array.isArray(value) ? value : String(value ?? ""),
60
+ onChange: (event) => onChange(
61
+ isMultiple ? Array.from(
62
+ event.target.selectedOptions,
63
+ (option) => option.value
64
+ ) : event.target.value
65
+ ),
66
+ children: [
67
+ !isMultiple ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: field.placeholder ?? "Selecciona una opci\xF3n" }) : null,
68
+ (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value))
69
+ ]
70
+ }
71
+ );
72
+ }
73
+ if (field.type === "radio") {
74
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-labelledby": `${field.key}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
75
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
76
+ "input",
77
+ {
78
+ type: "radio",
79
+ name: field.key,
80
+ value: option.value,
81
+ checked: value === option.value,
82
+ required: field.required,
83
+ onChange: (event) => onChange(event.target.value)
84
+ }
85
+ ),
86
+ option.label
87
+ ] }, option.value)) });
88
+ }
89
+ if (field.type === "checkbox") {
90
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
91
+ "input",
92
+ {
93
+ ...common,
94
+ type: "checkbox",
95
+ checked: value === true,
96
+ onChange: (event) => onChange(event.target.checked)
97
+ }
98
+ );
99
+ }
100
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
101
+ "input",
102
+ {
103
+ ...common,
104
+ type: field.type === "phone" ? "tel" : field.type,
105
+ value: typeof value === "string" ? value : "",
106
+ placeholder: field.placeholder ?? void 0,
107
+ min: field.validation?.min ?? void 0,
108
+ max: field.validation?.max ?? void 0,
109
+ minLength: field.validation?.minLength ?? void 0,
110
+ maxLength: field.validation?.maxLength ?? void 0,
111
+ onChange: (event) => onChange(event.target.value)
112
+ }
113
+ );
114
+ }
115
+
116
+ // libs/sdk/src/react/hooks.ts
117
+ var import_react2 = require("react");
118
+
119
+ // libs/sdk/src/react/context-registry.ts
120
+ var import_react = require("react");
121
+ var REACT_CONTEXT_REGISTRY = /* @__PURE__ */ Symbol.for(
122
+ "@bacano/sdk/react-context-registry/v1"
123
+ );
124
+ function getReactContextRegistry() {
125
+ const globalRegistry = globalThis;
126
+ const existing = globalRegistry[REACT_CONTEXT_REGISTRY];
127
+ if (existing instanceof Map) {
128
+ return existing;
129
+ }
130
+ const registry = /* @__PURE__ */ new Map();
131
+ globalRegistry[REACT_CONTEXT_REGISTRY] = registry;
132
+ return registry;
133
+ }
134
+ function getOrCreateReactContext(key, defaultValue) {
135
+ const registry = getReactContextRegistry();
136
+ const existing = registry.get(key);
137
+ if (existing) {
138
+ return existing;
139
+ }
140
+ const context = (0, import_react.createContext)(defaultValue);
141
+ registry.set(key, context);
142
+ return context;
143
+ }
144
+
145
+ // libs/sdk/src/react/context.ts
146
+ var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
147
+ client: null,
148
+ loading: true,
149
+ error: null
150
+ });
151
+
152
+ // libs/sdk/src/react/hooks.ts
153
+ function useBacanoState() {
154
+ return (0, import_react2.useContext)(BacanoReactContext);
155
+ }
156
+
157
+ // libs/sdk/src/react/forms.tsx
158
+ var import_jsx_runtime2 = require("react/jsx-runtime");
159
+ var EMPTY_FORM_FIELDS = [];
160
+ var EMPTY_FORM_SECTIONS = [];
161
+ var EMPTY_FORM_STEPS = [];
162
+ function useBacanoForm(formKey, options) {
163
+ const {
164
+ client,
165
+ loading: clientLoading,
166
+ error: clientError
167
+ } = useBacanoState();
168
+ const enabled = options?.enabled ?? true;
169
+ const initialForm = options?.initialForm?.key === formKey ? options.initialForm : null;
170
+ const [form, setForm] = (0, import_react3.useState)(initialForm);
171
+ const formRef = (0, import_react3.useRef)(initialForm);
172
+ const [loading, setLoading] = (0, import_react3.useState)(enabled && !initialForm);
173
+ const [error, setError] = (0, import_react3.useState)(null);
174
+ const load = (0, import_react3.useCallback)(
175
+ async (force = false) => {
176
+ if (!enabled || !client) return;
177
+ setLoading(formRef.current === null);
178
+ setError(null);
179
+ try {
180
+ const nextForm = await client.forms.getDefinition(formKey, { force });
181
+ formRef.current = nextForm;
182
+ setForm(nextForm);
183
+ } catch (loadError) {
184
+ if (!formRef.current) setForm(null);
185
+ setError(
186
+ loadError instanceof Error ? loadError : new Error(String(loadError))
187
+ );
188
+ } finally {
189
+ setLoading(false);
190
+ }
191
+ },
192
+ [client, enabled, formKey]
193
+ );
194
+ (0, import_react3.useEffect)(() => {
195
+ if (!enabled) {
196
+ formRef.current = null;
197
+ setForm(null);
198
+ setLoading(false);
199
+ setError(null);
200
+ return;
201
+ }
202
+ if (client) void load();
203
+ }, [client, enabled, load]);
204
+ return {
205
+ form,
206
+ loading: enabled && form === null && (clientLoading || loading),
207
+ error: clientError ?? error,
208
+ refresh: async () => load(true)
209
+ };
210
+ }
211
+ function FormFields(props) {
212
+ return props.fields.map((field) => {
213
+ const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
214
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.field, children: [
215
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
216
+ "label",
217
+ {
218
+ id: `${field.key}-label`,
219
+ htmlFor: field.type === "radio" ? void 0 : field.key,
220
+ className: props.classNames?.label,
221
+ children: field.label
222
+ }
223
+ ),
224
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
225
+ BacanoFormFieldControl,
226
+ {
227
+ field,
228
+ value,
229
+ className: props.classNames?.input,
230
+ onChange: (nextValue) => props.onChange(field.key, nextValue)
231
+ }
232
+ ),
233
+ field.helpText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.helpText, children: field.helpText }) : null
234
+ ] }, field.key);
235
+ });
236
+ }
237
+ function FormSections(props) {
238
+ return props.sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: props.classNames?.section, children: [
239
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: props.classNames?.sectionTitle, children: section.title }),
240
+ section.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.sectionDescription, children: section.description }) : null,
241
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
242
+ FormFields,
243
+ {
244
+ fields: section.fields,
245
+ values: props.values,
246
+ classNames: props.classNames,
247
+ onChange: props.onChange
248
+ }
249
+ )
250
+ ] }, section.key));
251
+ }
252
+ function BacanoForm(props) {
253
+ const { client } = useBacanoState();
254
+ const { form, loading, error } = useBacanoForm(props.formKey, {
255
+ initialForm: props.initialForm
256
+ });
257
+ const [values, setValues] = (0, import_react3.useState)({});
258
+ const [step, setStep] = (0, import_react3.useState)(0);
259
+ const [submitting, setSubmitting] = (0, import_react3.useState)(false);
260
+ const [submitted, setSubmitted] = (0, import_react3.useState)(false);
261
+ const [submissionError, setSubmissionError] = (0, import_react3.useState)(null);
262
+ const [honeypot, setHoneypot] = (0, import_react3.useState)("");
263
+ const idempotencyKey = (0, import_react3.useRef)(crypto.randomUUID());
264
+ const fields = form?.definition.fields ?? EMPTY_FORM_FIELDS;
265
+ const sections = form?.definition.sections ?? EMPTY_FORM_SECTIONS;
266
+ const steps = form?.definition.steps ?? EMPTY_FORM_STEPS;
267
+ const fieldKeys = (0, import_react3.useMemo)(
268
+ () => new Set(
269
+ [
270
+ ...fields,
271
+ ...sections.flatMap((section) => section.fields),
272
+ ...steps.flatMap((formStep) => [
273
+ ...formStep.fields,
274
+ ...formStep.sections.flatMap((section) => section.fields)
275
+ ])
276
+ ].map((field) => field.key)
277
+ ),
278
+ [fields, sections, steps]
279
+ );
280
+ (0, import_react3.useEffect)(() => {
281
+ setStep(0);
282
+ setValues(
283
+ (current) => Object.fromEntries(
284
+ Object.entries(current).filter(([key]) => fieldKeys.has(key))
285
+ )
286
+ );
287
+ setSubmitted(false);
288
+ setSubmissionError(null);
289
+ setHoneypot("");
290
+ idempotencyKey.current = crypto.randomUUID();
291
+ }, [fieldKeys, form?.version]);
292
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.loadingFallback ?? null });
293
+ if (error && !form)
294
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.errorFallback?.(error) ?? props.unavailableFallback ?? null });
295
+ if (!form) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.unavailableFallback ?? null });
296
+ if (form.status === "PAUSED") {
297
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: props.classNames?.message, "data-bacano-form-unavailable": true, children: props.unavailableFallback ?? form.definition.unavailableMessage });
298
+ }
299
+ if (submitted) {
300
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: props.classNames?.message, "data-bacano-form-success": true, children: form.definition.successMessage });
301
+ }
302
+ const isMultiStep = steps.length > 0;
303
+ const activeStep = isMultiStep ? steps[step] : null;
304
+ const isLastStep = !isMultiStep || step === steps.length - 1;
305
+ const setFieldValue = (key, value) => setValues((current) => ({ ...current, [key]: value }));
306
+ const submit = async (event) => {
307
+ event.preventDefault();
308
+ if (!isLastStep) {
309
+ setStep((current) => current + 1);
310
+ return;
311
+ }
312
+ setSubmitting(true);
313
+ setSubmissionError(null);
314
+ try {
315
+ let responseId = null;
316
+ if (props.onSubmit) {
317
+ await props.onSubmit(values, form);
318
+ } else {
319
+ if (!client) throw new Error("Bacano client is not ready");
320
+ const result = await client.forms.submitResponse({
321
+ formKey: form.key,
322
+ formVersion: form.version,
323
+ definitionHash: form.definitionHash,
324
+ idempotencyKey: idempotencyKey.current,
325
+ values,
326
+ honeypot
327
+ });
328
+ if (!result.accepted) throw new Error(result.message);
329
+ responseId = result.responseId;
330
+ }
331
+ setSubmitted(true);
332
+ props.onSubmitted?.(responseId);
333
+ } catch (submitError) {
334
+ setSubmissionError(
335
+ submitError instanceof Error ? submitError : new Error(String(submitError))
336
+ );
337
+ } finally {
338
+ setSubmitting(false);
339
+ }
340
+ };
341
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
342
+ "form",
343
+ {
344
+ className: props.classNames?.root,
345
+ "data-bacano-form": form.key,
346
+ "data-bacano-form-version": form.version,
347
+ onSubmit: (event) => void submit(event),
348
+ children: [
349
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
350
+ "div",
351
+ {
352
+ "aria-hidden": "true",
353
+ style: {
354
+ position: "absolute",
355
+ width: 1,
356
+ height: 1,
357
+ overflow: "hidden",
358
+ clip: "rect(0 0 0 0)"
359
+ },
360
+ children: [
361
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: `${form.key}-company-website`, children: "Empresa" }),
362
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
363
+ "input",
364
+ {
365
+ id: `${form.key}-company-website`,
366
+ name: "company_website",
367
+ tabIndex: -1,
368
+ autoComplete: "off",
369
+ value: honeypot,
370
+ onChange: (event) => setHoneypot(event.target.value)
371
+ }
372
+ )
373
+ ]
374
+ }
375
+ ),
376
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: props.classNames?.title, children: form.name }),
377
+ form.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.description, children: form.description }) : null,
378
+ activeStep ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: props.classNames?.step, "data-bacano-form-step": true, children: [
379
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: props.classNames?.stepTitle, children: activeStep.title }),
380
+ activeStep.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.stepDescription, children: activeStep.description }) : null,
381
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
382
+ FormFields,
383
+ {
384
+ fields: activeStep.fields,
385
+ values,
386
+ classNames: props.classNames,
387
+ onChange: setFieldValue
388
+ }
389
+ ),
390
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
391
+ FormSections,
392
+ {
393
+ sections: activeStep.sections,
394
+ values,
395
+ classNames: props.classNames,
396
+ onChange: setFieldValue
397
+ }
398
+ )
399
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
400
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
401
+ FormFields,
402
+ {
403
+ fields,
404
+ values,
405
+ classNames: props.classNames,
406
+ onChange: setFieldValue
407
+ }
408
+ ),
409
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
410
+ FormSections,
411
+ {
412
+ sections,
413
+ values,
414
+ classNames: props.classNames,
415
+ onChange: setFieldValue
416
+ }
417
+ )
418
+ ] }),
419
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.actions, children: [
420
+ isMultiStep && step > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
421
+ "button",
422
+ {
423
+ type: "button",
424
+ onClick: () => setStep((current) => current - 1),
425
+ children: "Anterior"
426
+ }
427
+ ) : null,
428
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
429
+ "button",
430
+ {
431
+ type: "submit",
432
+ className: props.classNames?.button,
433
+ disabled: submitting,
434
+ children: isLastStep ? form.definition.submitLabel : "Continuar"
435
+ }
436
+ )
437
+ ] }),
438
+ submissionError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
439
+ "div",
440
+ {
441
+ role: "alert",
442
+ className: props.classNames?.message,
443
+ "data-bacano-form-error": true,
444
+ children: submissionError.message
445
+ }
446
+ ) : null
447
+ ]
448
+ }
449
+ );
450
+ }
@@ -0,0 +1,49 @@
1
+ import { type ReactNode } from "react";
2
+ import type { WebsitePublishedForm } from "../types";
3
+ export type BacanoFormValues = Record<string, string | string[] | boolean>;
4
+ export interface UseBacanoFormOptions {
5
+ enabled?: boolean;
6
+ /** Definition embedded by a server/build to preserve Static First rendering. */
7
+ initialForm?: WebsitePublishedForm | null;
8
+ }
9
+ export interface UseBacanoFormResult {
10
+ form: WebsitePublishedForm | null;
11
+ loading: boolean;
12
+ error: Error | null;
13
+ refresh(): Promise<void>;
14
+ }
15
+ export declare function useBacanoForm(formKey: string, options?: UseBacanoFormOptions): UseBacanoFormResult;
16
+ export interface BacanoFormClassNames {
17
+ root?: string;
18
+ title?: string;
19
+ description?: string;
20
+ section?: string;
21
+ sectionTitle?: string;
22
+ sectionDescription?: string;
23
+ step?: string;
24
+ stepTitle?: string;
25
+ stepDescription?: string;
26
+ field?: string;
27
+ label?: string;
28
+ input?: string;
29
+ helpText?: string;
30
+ actions?: string;
31
+ button?: string;
32
+ message?: string;
33
+ }
34
+ export interface BacanoFormProps {
35
+ formKey: string;
36
+ /** Definition embedded by a server/build; it is revalidated in runtime. */
37
+ initialForm?: WebsitePublishedForm | null;
38
+ /**
39
+ * Optional custom submission. When omitted, the official Bacano response
40
+ * endpoint is used. Existing storefronts may keep a custom handler.
41
+ */
42
+ onSubmit?(values: BacanoFormValues, form: WebsitePublishedForm): void | Promise<void>;
43
+ onSubmitted?(responseId: string | null): void;
44
+ classNames?: BacanoFormClassNames;
45
+ loadingFallback?: ReactNode;
46
+ unavailableFallback?: ReactNode;
47
+ errorFallback?: (error: Error) => ReactNode;
48
+ }
49
+ export declare function BacanoForm(props: BacanoFormProps): import("react").JSX.Element;