@bacano/sdk 1.1.20 → 1.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.1.20",
3
+ "version": "1.2.2",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -0,0 +1,5 @@
1
+ import type { WebsiteInteractionContext } from "../marketing/interactions";
2
+ /** Invoke inside the existing onClick; never cancels or delays navigation. */
3
+ export declare function useBacanoCta(context: Pick<WebsiteInteractionContext, "formKey" | "placement"> & {
4
+ ctaId: string;
5
+ }): () => void;
@@ -0,0 +1,16 @@
1
+ import type { WebsiteFormField, WebsiteFormSection } from "../types";
2
+ import type { BacanoFormClassNames, BacanoFormValues } from "./forms";
3
+ export declare function FormFields(props: {
4
+ fields: WebsiteFormField[];
5
+ idPrefix: string;
6
+ values: BacanoFormValues;
7
+ classNames?: BacanoFormClassNames;
8
+ onChange(key: string, value: string | string[] | boolean): void;
9
+ }): import("react").JSX.Element[];
10
+ export declare function FormSections(props: {
11
+ sections: WebsiteFormSection[];
12
+ idPrefix: string;
13
+ values: BacanoFormValues;
14
+ classNames?: BacanoFormClassNames;
15
+ onChange(key: string, value: string | string[] | boolean): void;
16
+ }): import("react").JSX.Element[];
@@ -0,0 +1,11 @@
1
+ /** Use with custom renderers; BacanoForm already owns this instrumentation. */
2
+ export declare function useBacanoFormTracking(formKey: string, options?: {
3
+ visible?: boolean;
4
+ }): {
5
+ setVisible(next: boolean): void;
6
+ start(): void;
7
+ stepComplete(stepKey: string): void;
8
+ beginSubmit(): boolean;
9
+ fail(errorCategory: import("../marketing").WebsiteFormErrorCategory): void;
10
+ complete(result: import("..").WebsiteFormResponseResult | void): void;
11
+ };
package/react/forms.cjs CHANGED
@@ -21,10 +21,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var forms_exports = {};
22
22
  __export(forms_exports, {
23
23
  BacanoForm: () => BacanoForm,
24
- useBacanoForm: () => useBacanoForm
24
+ useBacanoForm: () => useBacanoForm,
25
+ useBacanoFormTracking: () => useBacanoFormTracking
25
26
  });
26
27
  module.exports = __toCommonJS(forms_exports);
27
- var import_react3 = require("react");
28
+ var import_react4 = require("react");
29
+
30
+ // libs/sdk/src/errors.ts
31
+ var WebsiteError = class extends Error {
32
+ code;
33
+ status;
34
+ retryAfterSeconds;
35
+ constructor(message, code = "WEBSITE_ERROR", options) {
36
+ super(message);
37
+ this.name = "WebsiteError";
38
+ this.code = code;
39
+ this.status = options?.status ?? null;
40
+ this.retryAfterSeconds = options?.retryAfterSeconds ?? null;
41
+ }
42
+ };
28
43
 
29
44
  // libs/sdk/src/react/form-field-control.tsx
30
45
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -114,9 +129,111 @@ function BacanoFormFieldControl(props) {
114
129
  );
115
130
  }
116
131
 
117
- // libs/sdk/src/react/hooks.ts
132
+ // libs/sdk/src/react/form-sections.tsx
133
+ var import_jsx_runtime2 = require("react/jsx-runtime");
134
+ function FormFields(props) {
135
+ return props.fields.map((field) => {
136
+ const controlId = `${props.idPrefix}-${field.key}`;
137
+ const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
138
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.field, children: [
139
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
140
+ "label",
141
+ {
142
+ id: `${controlId}-label`,
143
+ htmlFor: field.type === "radio" ? void 0 : controlId,
144
+ className: props.classNames?.label,
145
+ children: field.label
146
+ }
147
+ ),
148
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
149
+ BacanoFormFieldControl,
150
+ {
151
+ field,
152
+ id: controlId,
153
+ value,
154
+ className: props.classNames?.input,
155
+ onChange: (nextValue) => props.onChange(field.key, nextValue)
156
+ }
157
+ ),
158
+ field.helpText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.helpText, children: field.helpText }) : null
159
+ ] }, field.key);
160
+ });
161
+ }
162
+ function FormSections(props) {
163
+ return props.sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: props.classNames?.section, children: [
164
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: props.classNames?.sectionTitle, children: section.title }),
165
+ section.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.sectionDescription, children: section.description }) : null,
166
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
167
+ FormFields,
168
+ {
169
+ fields: section.fields,
170
+ idPrefix: props.idPrefix,
171
+ values: props.values,
172
+ classNames: props.classNames,
173
+ onChange: props.onChange
174
+ }
175
+ )
176
+ ] }, section.key));
177
+ }
178
+
179
+ // libs/sdk/src/react/form-tracking.ts
118
180
  var import_react2 = require("react");
119
181
 
182
+ // libs/sdk/src/marketing/interactions.ts
183
+ function newMarketingEventId() {
184
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
185
+ }
186
+
187
+ // libs/sdk/src/marketing/form-interactions.ts
188
+ function createFormInteractionTracker(getRuntime, formKey) {
189
+ let visible = false;
190
+ let started = false;
191
+ let attempt = null;
192
+ const completedSteps = /* @__PURE__ */ new Set();
193
+ function emit(eventName, context = {}) {
194
+ try {
195
+ getRuntime().track({ eventName, formKey, ...context });
196
+ } catch {
197
+ }
198
+ }
199
+ return {
200
+ setVisible(next) {
201
+ if (next && !visible && getRuntime().enabled) emit("form_open");
202
+ visible = next;
203
+ },
204
+ start() {
205
+ if (started || !getRuntime().enabled) return;
206
+ started = true;
207
+ emit("form_start");
208
+ },
209
+ stepComplete(stepKey) {
210
+ if (completedSteps.has(stepKey) || !getRuntime().enabled) return;
211
+ completedSteps.add(stepKey);
212
+ emit("form_step_complete", { stepKey });
213
+ },
214
+ beginSubmit() {
215
+ if (attempt) return false;
216
+ attempt = newMarketingEventId();
217
+ emit("form_submit_attempt", { eventId: attempt });
218
+ return true;
219
+ },
220
+ fail(errorCategory) {
221
+ if (!attempt) return;
222
+ emit("form_submit_error", { eventId: attempt, errorCategory });
223
+ attempt = null;
224
+ },
225
+ complete(result) {
226
+ if (!attempt) return;
227
+ if (result?.accepted && typeof result.responseId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
228
+ result.responseId
229
+ )) {
230
+ emit("form_submit_success", { eventId: result.responseId });
231
+ }
232
+ attempt = null;
233
+ }
234
+ };
235
+ }
236
+
120
237
  // libs/sdk/src/react/context-registry.ts
121
238
  var import_react = require("react");
122
239
  var REACT_CONTEXT_REGISTRY = /* @__PURE__ */ Symbol.for(
@@ -143,6 +260,37 @@ function getOrCreateReactContext(key, defaultValue) {
143
260
  return context;
144
261
  }
145
262
 
263
+ // libs/sdk/src/react/marketing-context.ts
264
+ var NOOP_MARKETING_RUNTIME = {
265
+ enabled: false,
266
+ track: () => void 0,
267
+ captureAttribution: () => null,
268
+ getAttribution: () => null,
269
+ destroy: () => void 0
270
+ };
271
+ var BacanoMarketingContext = getOrCreateReactContext(
272
+ "bacano-marketing-v1",
273
+ NOOP_MARKETING_RUNTIME
274
+ );
275
+
276
+ // libs/sdk/src/react/form-tracking.ts
277
+ function useBacanoFormTracking(formKey, options = {}) {
278
+ const runtime = (0, import_react2.useContext)(BacanoMarketingContext);
279
+ const current = (0, import_react2.useRef)(runtime);
280
+ current.current = runtime;
281
+ const tracker = (0, import_react2.useMemo)(
282
+ () => createFormInteractionTracker(() => current.current, formKey),
283
+ [formKey]
284
+ );
285
+ (0, import_react2.useEffect)(() => {
286
+ tracker.setVisible((options.visible ?? true) && runtime.enabled);
287
+ }, [tracker, options.visible, runtime.enabled]);
288
+ return tracker;
289
+ }
290
+
291
+ // libs/sdk/src/react/hooks.ts
292
+ var import_react3 = require("react");
293
+
146
294
  // libs/sdk/src/react/context.ts
147
295
  var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
148
296
  client: null,
@@ -152,11 +300,11 @@ var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
152
300
 
153
301
  // libs/sdk/src/react/hooks.ts
154
302
  function useBacanoState() {
155
- return (0, import_react2.useContext)(BacanoReactContext);
303
+ return (0, import_react3.useContext)(BacanoReactContext);
156
304
  }
157
305
 
158
306
  // libs/sdk/src/react/forms.tsx
159
- var import_jsx_runtime2 = require("react/jsx-runtime");
307
+ var import_jsx_runtime3 = require("react/jsx-runtime");
160
308
  var EMPTY_FORM_FIELDS = [];
161
309
  var EMPTY_FORM_SECTIONS = [];
162
310
  var EMPTY_FORM_STEPS = [];
@@ -168,31 +316,35 @@ function useBacanoForm(formKey, options) {
168
316
  } = useBacanoState();
169
317
  const enabled = options?.enabled ?? true;
170
318
  const initialForm = options?.initialForm?.key === formKey ? options.initialForm : null;
171
- const [form, setForm] = (0, import_react3.useState)(initialForm);
172
- const formRef = (0, import_react3.useRef)(initialForm);
173
- const [loading, setLoading] = (0, import_react3.useState)(enabled && !initialForm);
174
- const [error, setError] = (0, import_react3.useState)(null);
175
- const load = (0, import_react3.useCallback)(
319
+ const [form, setForm] = (0, import_react4.useState)(initialForm);
320
+ const formRef = (0, import_react4.useRef)(initialForm);
321
+ const requestVersion = (0, import_react4.useRef)(0);
322
+ const [loading, setLoading] = (0, import_react4.useState)(enabled && !initialForm);
323
+ const [error, setError] = (0, import_react4.useState)(null);
324
+ const load = (0, import_react4.useCallback)(
176
325
  async (force = false) => {
177
326
  if (!enabled || !client) return;
327
+ const version = ++requestVersion.current;
178
328
  setLoading(formRef.current === null);
179
329
  setError(null);
180
330
  try {
181
331
  const nextForm = await client.forms.getDefinition(formKey, { force });
332
+ if (version !== requestVersion.current) return;
182
333
  formRef.current = nextForm;
183
334
  setForm(nextForm);
184
335
  } catch (loadError) {
336
+ if (version !== requestVersion.current) return;
185
337
  if (!formRef.current) setForm(null);
186
338
  setError(
187
339
  loadError instanceof Error ? loadError : new Error(String(loadError))
188
340
  );
189
341
  } finally {
190
- setLoading(false);
342
+ if (version === requestVersion.current) setLoading(false);
191
343
  }
192
344
  },
193
345
  [client, enabled, formKey]
194
346
  );
195
- (0, import_react3.useEffect)(() => {
347
+ (0, import_react4.useEffect)(() => {
196
348
  if (!enabled) {
197
349
  formRef.current = null;
198
350
  setForm(null);
@@ -201,76 +353,38 @@ function useBacanoForm(formKey, options) {
201
353
  return;
202
354
  }
203
355
  if (client) void load();
356
+ return () => {
357
+ requestVersion.current += 1;
358
+ };
204
359
  }, [client, enabled, load]);
205
360
  return {
206
- form,
207
- loading: enabled && form === null && (clientLoading || loading),
361
+ form: enabled ? form?.key === formKey ? form : initialForm : null,
362
+ loading: enabled && form?.key !== formKey && !initialForm && (clientLoading || loading),
208
363
  error: clientError ?? error,
209
364
  refresh: async () => load(true)
210
365
  };
211
366
  }
212
- function FormFields(props) {
213
- return props.fields.map((field) => {
214
- const controlId = `${props.idPrefix}-${field.key}`;
215
- const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
216
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.field, children: [
217
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
218
- "label",
219
- {
220
- id: `${controlId}-label`,
221
- htmlFor: field.type === "radio" ? void 0 : controlId,
222
- className: props.classNames?.label,
223
- children: field.label
224
- }
225
- ),
226
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
227
- BacanoFormFieldControl,
228
- {
229
- field,
230
- id: controlId,
231
- value,
232
- className: props.classNames?.input,
233
- onChange: (nextValue) => props.onChange(field.key, nextValue)
234
- }
235
- ),
236
- field.helpText ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.helpText, children: field.helpText }) : null
237
- ] }, field.key);
238
- });
239
- }
240
- function FormSections(props) {
241
- return props.sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: props.classNames?.section, children: [
242
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: props.classNames?.sectionTitle, children: section.title }),
243
- section.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.sectionDescription, children: section.description }) : null,
244
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
245
- FormFields,
246
- {
247
- fields: section.fields,
248
- idPrefix: props.idPrefix,
249
- values: props.values,
250
- classNames: props.classNames,
251
- onChange: props.onChange
252
- }
253
- )
254
- ] }, section.key));
255
- }
256
- function BacanoForm(props) {
257
- const reactId = (0, import_react3.useId)();
367
+ function BacanoFormInstance(props) {
368
+ const reactId = (0, import_react4.useId)();
258
369
  const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
259
370
  const { client } = useBacanoState();
260
371
  const { form, loading, error } = useBacanoForm(props.formKey, {
261
372
  initialForm: props.initialForm
262
373
  });
263
- const [internalValues, setValues] = (0, import_react3.useState)({});
264
- const [step, setStep] = (0, import_react3.useState)(0);
265
- const [submitting, setSubmitting] = (0, import_react3.useState)(false);
266
- const [submitted, setSubmitted] = (0, import_react3.useState)(false);
267
- const [submissionError, setSubmissionError] = (0, import_react3.useState)(null);
268
- const [honeypot, setHoneypot] = (0, import_react3.useState)("");
269
- const idempotencyKey = (0, import_react3.useRef)(crypto.randomUUID());
374
+ const [internalValues, setValues] = (0, import_react4.useState)({});
375
+ const [step, setStep] = (0, import_react4.useState)(0);
376
+ const [submitting, setSubmitting] = (0, import_react4.useState)(false);
377
+ const [submitted, setSubmitted] = (0, import_react4.useState)(false);
378
+ const [submissionError, setSubmissionError] = (0, import_react4.useState)(null);
379
+ const [honeypot, setHoneypot] = (0, import_react4.useState)("");
380
+ const tracking = useBacanoFormTracking(props.formKey, {
381
+ visible: (props.visible ?? true) && !loading && form?.status === "ACTIVE" && !submitted
382
+ });
383
+ const idempotencyKey = (0, import_react4.useRef)(crypto.randomUUID());
270
384
  const fields = form?.definition.fields ?? EMPTY_FORM_FIELDS;
271
385
  const sections = form?.definition.sections ?? EMPTY_FORM_SECTIONS;
272
386
  const steps = form?.definition.steps ?? EMPTY_FORM_STEPS;
273
- const fieldKeys = (0, import_react3.useMemo)(
387
+ const fieldKeys = (0, import_react4.useMemo)(
274
388
  () => new Set(
275
389
  [
276
390
  ...fields,
@@ -288,7 +402,7 @@ function BacanoForm(props) {
288
402
  ([key]) => fieldKeys.has(key)
289
403
  )
290
404
  );
291
- (0, import_react3.useEffect)(() => {
405
+ (0, import_react4.useEffect)(() => {
292
406
  setStep(0);
293
407
  setValues(
294
408
  (current) => Object.fromEntries(
@@ -300,27 +414,37 @@ function BacanoForm(props) {
300
414
  setHoneypot("");
301
415
  idempotencyKey.current = crypto.randomUUID();
302
416
  }, [fieldKeys, form?.version]);
303
- if (loading) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.loadingFallback ?? null });
417
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.loadingFallback ?? null });
304
418
  if (error && !form)
305
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.errorFallback?.(error) ?? props.unavailableFallback ?? null });
306
- if (!form) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.unavailableFallback ?? null });
419
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.errorFallback?.(error) ?? props.unavailableFallback ?? null });
420
+ if (!form) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.unavailableFallback ?? null });
307
421
  if (form.status === "PAUSED") {
308
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: props.classNames?.message, "data-bacano-form-unavailable": true, children: props.unavailableFallback ?? form.definition.unavailableMessage });
422
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: props.classNames?.message, "data-bacano-form-unavailable": true, children: props.unavailableFallback ?? form.definition.unavailableMessage });
309
423
  }
310
424
  if (submitted) {
311
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: props.classNames?.message, "data-bacano-form-success": true, children: form.definition.successMessage });
425
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: props.classNames?.message, "data-bacano-form-success": true, children: form.definition.successMessage });
312
426
  }
313
427
  const isMultiStep = steps.length > 0;
314
428
  const activeStep = isMultiStep ? steps[step] : null;
315
429
  const isLastStep = !isMultiStep || step === steps.length - 1;
316
430
  const setFieldValue = (key, value) => {
431
+ if (value !== "" && value !== false && (!Array.isArray(value) || value.length > 0))
432
+ tracking.start();
317
433
  const nextValues = { ...values, [key]: value };
318
434
  setValues(nextValues);
319
435
  props.onValuesChange?.(nextValues);
320
436
  };
321
437
  const submit = async (event) => {
322
438
  event.preventDefault();
439
+ if (submitting || submitted) return;
440
+ if (isLastStep && !tracking.beginSubmit()) return;
441
+ if (!event.currentTarget.checkValidity()) {
442
+ if (isLastStep) tracking.fail("validation");
443
+ event.currentTarget.reportValidity();
444
+ return;
445
+ }
323
446
  if (!isLastStep) {
447
+ if (activeStep) tracking.stepComplete(activeStep.key);
324
448
  setStep((current) => current + 1);
325
449
  return;
326
450
  }
@@ -328,8 +452,12 @@ function BacanoForm(props) {
328
452
  setSubmissionError(null);
329
453
  try {
330
454
  let responseId = null;
455
+ let officialResult;
331
456
  if (props.onSubmit) {
332
- await props.onSubmit(values, form);
457
+ officialResult = await props.onSubmit(values, form);
458
+ if (officialResult && !officialResult.accepted)
459
+ throw new Error(officialResult.message);
460
+ responseId = officialResult?.responseId ?? null;
333
461
  } else {
334
462
  if (!client) throw new Error("Bacano client is not ready");
335
463
  const result = await client.forms.submitResponse({
@@ -341,11 +469,16 @@ function BacanoForm(props) {
341
469
  honeypot
342
470
  });
343
471
  if (!result.accepted) throw new Error(result.message);
472
+ officialResult = result;
344
473
  responseId = result.responseId;
345
474
  }
475
+ tracking.complete(officialResult);
346
476
  setSubmitted(true);
347
477
  props.onSubmitted?.(responseId);
348
478
  } catch (submitError) {
479
+ tracking.fail(
480
+ submitError instanceof WebsiteError && submitError.code === "NETWORK_ERROR" ? "network" : "rejected"
481
+ );
349
482
  setSubmissionError(
350
483
  submitError instanceof Error ? submitError : new Error(String(submitError))
351
484
  );
@@ -353,15 +486,16 @@ function BacanoForm(props) {
353
486
  setSubmitting(false);
354
487
  }
355
488
  };
356
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
489
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
357
490
  "form",
358
491
  {
492
+ noValidate: true,
359
493
  className: props.classNames?.root,
360
494
  "data-bacano-form": form.key,
361
495
  "data-bacano-form-version": form.version,
362
496
  onSubmit: (event) => void submit(event),
363
497
  children: [
364
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
498
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
365
499
  "div",
366
500
  {
367
501
  "aria-hidden": "true",
@@ -373,8 +507,8 @@ function BacanoForm(props) {
373
507
  clip: "rect(0 0 0 0)"
374
508
  },
375
509
  children: [
376
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: `${form.key}-company-website`, children: "Empresa" }),
377
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
510
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: `${form.key}-company-website`, children: "Empresa" }),
511
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
378
512
  "input",
379
513
  {
380
514
  id: `${form.key}-company-website`,
@@ -388,12 +522,12 @@ function BacanoForm(props) {
388
522
  ]
389
523
  }
390
524
  ),
391
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: props.classNames?.title, children: form.name }),
392
- form.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.description, children: form.description }) : null,
393
- activeStep ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: props.classNames?.step, "data-bacano-form-step": true, children: [
394
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: props.classNames?.stepTitle, children: activeStep.title }),
395
- activeStep.description ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: props.classNames?.stepDescription, children: activeStep.description }) : null,
396
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
525
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: props.classNames?.title, children: form.name }),
526
+ form.description ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: props.classNames?.description, children: form.description }) : null,
527
+ activeStep ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { className: props.classNames?.step, "data-bacano-form-step": true, children: [
528
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: props.classNames?.stepTitle, children: activeStep.title }),
529
+ activeStep.description ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: props.classNames?.stepDescription, children: activeStep.description }) : null,
530
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
397
531
  FormFields,
398
532
  {
399
533
  fields: activeStep.fields,
@@ -403,7 +537,7 @@ function BacanoForm(props) {
403
537
  onChange: setFieldValue
404
538
  }
405
539
  ),
406
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
540
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
407
541
  FormSections,
408
542
  {
409
543
  sections: activeStep.sections,
@@ -413,8 +547,8 @@ function BacanoForm(props) {
413
547
  onChange: setFieldValue
414
548
  }
415
549
  )
416
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
417
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
550
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
551
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
418
552
  FormFields,
419
553
  {
420
554
  fields,
@@ -424,7 +558,7 @@ function BacanoForm(props) {
424
558
  onChange: setFieldValue
425
559
  }
426
560
  ),
427
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
561
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
428
562
  FormSections,
429
563
  {
430
564
  sections,
@@ -435,8 +569,8 @@ function BacanoForm(props) {
435
569
  }
436
570
  )
437
571
  ] }),
438
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.actions, children: [
439
- isMultiStep && step > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
572
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: props.classNames?.actions, children: [
573
+ isMultiStep && step > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
440
574
  "button",
441
575
  {
442
576
  type: "button",
@@ -444,7 +578,7 @@ function BacanoForm(props) {
444
578
  children: "Anterior"
445
579
  }
446
580
  ) : null,
447
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
581
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
448
582
  "button",
449
583
  {
450
584
  type: "submit",
@@ -454,7 +588,7 @@ function BacanoForm(props) {
454
588
  }
455
589
  )
456
590
  ] }),
457
- submissionError ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
591
+ submissionError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
458
592
  "div",
459
593
  {
460
594
  role: "alert",
@@ -467,3 +601,6 @@ function BacanoForm(props) {
467
601
  }
468
602
  );
469
603
  }
604
+ function BacanoForm(props) {
605
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BacanoFormInstance, { ...props }, props.formKey);
606
+ }
package/react/forms.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
- import type { WebsitePublishedForm } from "../types";
2
+ import type { WebsiteFormResponseResult, WebsitePublishedForm } from "../types";
3
+ export { useBacanoFormTracking } from "./form-tracking";
3
4
  export type BacanoFormValues = Record<string, string | string[] | boolean>;
4
5
  export interface UseBacanoFormOptions {
5
6
  enabled?: boolean;
@@ -33,6 +34,8 @@ export interface BacanoFormClassNames {
33
34
  }
34
35
  export interface BacanoFormProps {
35
36
  formKey: string;
37
+ /** Pass the actual modal/drawer visibility when it stays mounted while closed. */
38
+ visible?: boolean;
36
39
  /** Controlled values. Use together with onValuesChange; option values, not labels. */
37
40
  values?: BacanoFormValues;
38
41
  /** Receives the complete next values after a visitor edits a field. */
@@ -43,11 +46,12 @@ export interface BacanoFormProps {
43
46
  * Optional custom submission. When omitted, the official Bacano response
44
47
  * endpoint is used. Existing storefronts may keep a custom handler.
45
48
  */
46
- onSubmit?(values: BacanoFormValues, form: WebsitePublishedForm): void | Promise<void>;
49
+ onSubmit?(values: BacanoFormValues, form: WebsitePublishedForm): void | WebsiteFormResponseResult | Promise<void | WebsiteFormResponseResult>;
47
50
  onSubmitted?(responseId: string | null): void;
48
51
  classNames?: BacanoFormClassNames;
49
52
  loadingFallback?: ReactNode;
50
53
  unavailableFallback?: ReactNode;
51
54
  errorFallback?: (error: Error) => ReactNode;
52
55
  }
56
+ /** A different form cannot inherit pending submission state from the old key. */
53
57
  export declare function BacanoForm(props: BacanoFormProps): import("react").JSX.Element;