@bacano/sdk 1.1.15 → 1.1.18

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/react/forms.js ADDED
@@ -0,0 +1,446 @@
1
+ // libs/sdk/src/react/forms.tsx
2
+ import {
3
+ useCallback as useCallback2,
4
+ useEffect as useEffect2,
5
+ useId,
6
+ useMemo,
7
+ useRef as useRef2,
8
+ useState as useState2
9
+ } from "react";
10
+
11
+ // libs/sdk/src/react/form-field-control.tsx
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ function BacanoFormFieldControl(props) {
14
+ const { field, className, value, onChange } = props;
15
+ const controlId = props.id ?? field.key;
16
+ const common = {
17
+ id: controlId,
18
+ name: field.key,
19
+ required: field.required,
20
+ className
21
+ };
22
+ if (field.type === "textarea") {
23
+ return /* @__PURE__ */ jsx(
24
+ "textarea",
25
+ {
26
+ ...common,
27
+ value: typeof value === "string" ? value : "",
28
+ placeholder: field.placeholder ?? void 0,
29
+ minLength: field.validation?.minLength ?? void 0,
30
+ maxLength: field.validation?.maxLength ?? void 0,
31
+ onChange: (event) => onChange(event.target.value)
32
+ }
33
+ );
34
+ }
35
+ if (field.type === "select" || field.type === "multi_select") {
36
+ const isMultiple = field.type === "multi_select";
37
+ return /* @__PURE__ */ jsxs(
38
+ "select",
39
+ {
40
+ ...common,
41
+ multiple: isMultiple,
42
+ value: isMultiple && Array.isArray(value) ? value : String(value ?? ""),
43
+ onChange: (event) => onChange(
44
+ isMultiple ? Array.from(
45
+ event.target.selectedOptions,
46
+ (option) => option.value
47
+ ) : event.target.value
48
+ ),
49
+ children: [
50
+ !isMultiple ? /* @__PURE__ */ jsx("option", { value: "", children: field.placeholder ?? "Selecciona una opci\xF3n" }) : null,
51
+ (field.options ?? []).map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
52
+ ]
53
+ }
54
+ );
55
+ }
56
+ if (field.type === "radio") {
57
+ return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": `${controlId}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs("label", { children: [
58
+ /* @__PURE__ */ jsx(
59
+ "input",
60
+ {
61
+ type: "radio",
62
+ name: field.key,
63
+ value: option.value,
64
+ checked: value === option.value,
65
+ required: field.required,
66
+ onChange: (event) => onChange(event.target.value)
67
+ }
68
+ ),
69
+ option.label
70
+ ] }, option.value)) });
71
+ }
72
+ if (field.type === "checkbox") {
73
+ return /* @__PURE__ */ jsx(
74
+ "input",
75
+ {
76
+ ...common,
77
+ type: "checkbox",
78
+ checked: value === true,
79
+ onChange: (event) => onChange(event.target.checked)
80
+ }
81
+ );
82
+ }
83
+ return /* @__PURE__ */ jsx(
84
+ "input",
85
+ {
86
+ ...common,
87
+ type: field.type === "phone" ? "tel" : field.type,
88
+ value: typeof value === "string" ? value : "",
89
+ placeholder: field.placeholder ?? void 0,
90
+ min: field.validation?.min ?? void 0,
91
+ max: field.validation?.max ?? void 0,
92
+ minLength: field.validation?.minLength ?? void 0,
93
+ maxLength: field.validation?.maxLength ?? void 0,
94
+ onChange: (event) => onChange(event.target.value)
95
+ }
96
+ );
97
+ }
98
+
99
+ // libs/sdk/src/react/hooks.ts
100
+ import { useCallback, useContext, useEffect, useRef, useState } from "react";
101
+
102
+ // libs/sdk/src/react/context-registry.ts
103
+ import { createContext } from "react";
104
+ var REACT_CONTEXT_REGISTRY = /* @__PURE__ */ Symbol.for(
105
+ "@bacano/sdk/react-context-registry/v1"
106
+ );
107
+ function getReactContextRegistry() {
108
+ const globalRegistry = globalThis;
109
+ const existing = globalRegistry[REACT_CONTEXT_REGISTRY];
110
+ if (existing instanceof Map) {
111
+ return existing;
112
+ }
113
+ const registry = /* @__PURE__ */ new Map();
114
+ globalRegistry[REACT_CONTEXT_REGISTRY] = registry;
115
+ return registry;
116
+ }
117
+ function getOrCreateReactContext(key, defaultValue) {
118
+ const registry = getReactContextRegistry();
119
+ const existing = registry.get(key);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ const context = createContext(defaultValue);
124
+ registry.set(key, context);
125
+ return context;
126
+ }
127
+
128
+ // libs/sdk/src/react/context.ts
129
+ var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
130
+ client: null,
131
+ loading: true,
132
+ error: null
133
+ });
134
+
135
+ // libs/sdk/src/react/hooks.ts
136
+ function useBacanoState() {
137
+ return useContext(BacanoReactContext);
138
+ }
139
+
140
+ // libs/sdk/src/react/forms.tsx
141
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
142
+ var EMPTY_FORM_FIELDS = [];
143
+ var EMPTY_FORM_SECTIONS = [];
144
+ var EMPTY_FORM_STEPS = [];
145
+ function useBacanoForm(formKey, options) {
146
+ const {
147
+ client,
148
+ loading: clientLoading,
149
+ error: clientError
150
+ } = useBacanoState();
151
+ const enabled = options?.enabled ?? true;
152
+ const initialForm = options?.initialForm?.key === formKey ? options.initialForm : null;
153
+ const [form, setForm] = useState2(initialForm);
154
+ const formRef = useRef2(initialForm);
155
+ const [loading, setLoading] = useState2(enabled && !initialForm);
156
+ const [error, setError] = useState2(null);
157
+ const load = useCallback2(
158
+ async (force = false) => {
159
+ if (!enabled || !client) return;
160
+ setLoading(formRef.current === null);
161
+ setError(null);
162
+ try {
163
+ const nextForm = await client.forms.getDefinition(formKey, { force });
164
+ formRef.current = nextForm;
165
+ setForm(nextForm);
166
+ } catch (loadError) {
167
+ if (!formRef.current) setForm(null);
168
+ setError(
169
+ loadError instanceof Error ? loadError : new Error(String(loadError))
170
+ );
171
+ } finally {
172
+ setLoading(false);
173
+ }
174
+ },
175
+ [client, enabled, formKey]
176
+ );
177
+ useEffect2(() => {
178
+ if (!enabled) {
179
+ formRef.current = null;
180
+ setForm(null);
181
+ setLoading(false);
182
+ setError(null);
183
+ return;
184
+ }
185
+ if (client) void load();
186
+ }, [client, enabled, load]);
187
+ return {
188
+ form,
189
+ loading: enabled && form === null && (clientLoading || loading),
190
+ error: clientError ?? error,
191
+ refresh: async () => load(true)
192
+ };
193
+ }
194
+ function FormFields(props) {
195
+ return props.fields.map((field) => {
196
+ const controlId = `${props.idPrefix}-${field.key}`;
197
+ const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
198
+ return /* @__PURE__ */ jsxs2("div", { className: props.classNames?.field, children: [
199
+ /* @__PURE__ */ jsx2(
200
+ "label",
201
+ {
202
+ id: `${controlId}-label`,
203
+ htmlFor: field.type === "radio" ? void 0 : controlId,
204
+ className: props.classNames?.label,
205
+ children: field.label
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsx2(
209
+ BacanoFormFieldControl,
210
+ {
211
+ field,
212
+ id: controlId,
213
+ value,
214
+ className: props.classNames?.input,
215
+ onChange: (nextValue) => props.onChange(field.key, nextValue)
216
+ }
217
+ ),
218
+ field.helpText ? /* @__PURE__ */ jsx2("p", { className: props.classNames?.helpText, children: field.helpText }) : null
219
+ ] }, field.key);
220
+ });
221
+ }
222
+ function FormSections(props) {
223
+ return props.sections.map((section) => /* @__PURE__ */ jsxs2("section", { className: props.classNames?.section, children: [
224
+ /* @__PURE__ */ jsx2("h3", { className: props.classNames?.sectionTitle, children: section.title }),
225
+ section.description ? /* @__PURE__ */ jsx2("p", { className: props.classNames?.sectionDescription, children: section.description }) : null,
226
+ /* @__PURE__ */ jsx2(
227
+ FormFields,
228
+ {
229
+ fields: section.fields,
230
+ idPrefix: props.idPrefix,
231
+ values: props.values,
232
+ classNames: props.classNames,
233
+ onChange: props.onChange
234
+ }
235
+ )
236
+ ] }, section.key));
237
+ }
238
+ function BacanoForm(props) {
239
+ const reactId = useId();
240
+ const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
241
+ const { client } = useBacanoState();
242
+ const { form, loading, error } = useBacanoForm(props.formKey, {
243
+ initialForm: props.initialForm
244
+ });
245
+ const [values, setValues] = useState2({});
246
+ const [step, setStep] = useState2(0);
247
+ const [submitting, setSubmitting] = useState2(false);
248
+ const [submitted, setSubmitted] = useState2(false);
249
+ const [submissionError, setSubmissionError] = useState2(null);
250
+ const [honeypot, setHoneypot] = useState2("");
251
+ const idempotencyKey = useRef2(crypto.randomUUID());
252
+ const fields = form?.definition.fields ?? EMPTY_FORM_FIELDS;
253
+ const sections = form?.definition.sections ?? EMPTY_FORM_SECTIONS;
254
+ const steps = form?.definition.steps ?? EMPTY_FORM_STEPS;
255
+ const fieldKeys = useMemo(
256
+ () => new Set(
257
+ [
258
+ ...fields,
259
+ ...sections.flatMap((section) => section.fields),
260
+ ...steps.flatMap((formStep) => [
261
+ ...formStep.fields,
262
+ ...formStep.sections.flatMap((section) => section.fields)
263
+ ])
264
+ ].map((field) => field.key)
265
+ ),
266
+ [fields, sections, steps]
267
+ );
268
+ useEffect2(() => {
269
+ setStep(0);
270
+ setValues(
271
+ (current) => Object.fromEntries(
272
+ Object.entries(current).filter(([key]) => fieldKeys.has(key))
273
+ )
274
+ );
275
+ setSubmitted(false);
276
+ setSubmissionError(null);
277
+ setHoneypot("");
278
+ idempotencyKey.current = crypto.randomUUID();
279
+ }, [fieldKeys, form?.version]);
280
+ if (loading) return /* @__PURE__ */ jsx2(Fragment, { children: props.loadingFallback ?? null });
281
+ if (error && !form)
282
+ return /* @__PURE__ */ jsx2(Fragment, { children: props.errorFallback?.(error) ?? props.unavailableFallback ?? null });
283
+ if (!form) return /* @__PURE__ */ jsx2(Fragment, { children: props.unavailableFallback ?? null });
284
+ if (form.status === "PAUSED") {
285
+ return /* @__PURE__ */ jsx2("div", { className: props.classNames?.message, "data-bacano-form-unavailable": true, children: props.unavailableFallback ?? form.definition.unavailableMessage });
286
+ }
287
+ if (submitted) {
288
+ return /* @__PURE__ */ jsx2("div", { className: props.classNames?.message, "data-bacano-form-success": true, children: form.definition.successMessage });
289
+ }
290
+ const isMultiStep = steps.length > 0;
291
+ const activeStep = isMultiStep ? steps[step] : null;
292
+ const isLastStep = !isMultiStep || step === steps.length - 1;
293
+ const setFieldValue = (key, value) => setValues((current) => ({ ...current, [key]: value }));
294
+ const submit = async (event) => {
295
+ event.preventDefault();
296
+ if (!isLastStep) {
297
+ setStep((current) => current + 1);
298
+ return;
299
+ }
300
+ setSubmitting(true);
301
+ setSubmissionError(null);
302
+ try {
303
+ let responseId = null;
304
+ if (props.onSubmit) {
305
+ await props.onSubmit(values, form);
306
+ } else {
307
+ if (!client) throw new Error("Bacano client is not ready");
308
+ const result = await client.forms.submitResponse({
309
+ formKey: form.key,
310
+ formVersion: form.version,
311
+ definitionHash: form.definitionHash,
312
+ idempotencyKey: idempotencyKey.current,
313
+ values,
314
+ honeypot
315
+ });
316
+ if (!result.accepted) throw new Error(result.message);
317
+ responseId = result.responseId;
318
+ }
319
+ setSubmitted(true);
320
+ props.onSubmitted?.(responseId);
321
+ } catch (submitError) {
322
+ setSubmissionError(
323
+ submitError instanceof Error ? submitError : new Error(String(submitError))
324
+ );
325
+ } finally {
326
+ setSubmitting(false);
327
+ }
328
+ };
329
+ return /* @__PURE__ */ jsxs2(
330
+ "form",
331
+ {
332
+ className: props.classNames?.root,
333
+ "data-bacano-form": form.key,
334
+ "data-bacano-form-version": form.version,
335
+ onSubmit: (event) => void submit(event),
336
+ children: [
337
+ /* @__PURE__ */ jsxs2(
338
+ "div",
339
+ {
340
+ "aria-hidden": "true",
341
+ style: {
342
+ position: "absolute",
343
+ width: 1,
344
+ height: 1,
345
+ overflow: "hidden",
346
+ clip: "rect(0 0 0 0)"
347
+ },
348
+ children: [
349
+ /* @__PURE__ */ jsx2("label", { htmlFor: `${form.key}-company-website`, children: "Empresa" }),
350
+ /* @__PURE__ */ jsx2(
351
+ "input",
352
+ {
353
+ id: `${form.key}-company-website`,
354
+ name: "company_website",
355
+ tabIndex: -1,
356
+ autoComplete: "off",
357
+ value: honeypot,
358
+ onChange: (event) => setHoneypot(event.target.value)
359
+ }
360
+ )
361
+ ]
362
+ }
363
+ ),
364
+ /* @__PURE__ */ jsx2("h2", { className: props.classNames?.title, children: form.name }),
365
+ form.description ? /* @__PURE__ */ jsx2("p", { className: props.classNames?.description, children: form.description }) : null,
366
+ activeStep ? /* @__PURE__ */ jsxs2("section", { className: props.classNames?.step, "data-bacano-form-step": true, children: [
367
+ /* @__PURE__ */ jsx2("h3", { className: props.classNames?.stepTitle, children: activeStep.title }),
368
+ activeStep.description ? /* @__PURE__ */ jsx2("p", { className: props.classNames?.stepDescription, children: activeStep.description }) : null,
369
+ /* @__PURE__ */ jsx2(
370
+ FormFields,
371
+ {
372
+ fields: activeStep.fields,
373
+ idPrefix,
374
+ values,
375
+ classNames: props.classNames,
376
+ onChange: setFieldValue
377
+ }
378
+ ),
379
+ /* @__PURE__ */ jsx2(
380
+ FormSections,
381
+ {
382
+ sections: activeStep.sections,
383
+ idPrefix,
384
+ values,
385
+ classNames: props.classNames,
386
+ onChange: setFieldValue
387
+ }
388
+ )
389
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
390
+ /* @__PURE__ */ jsx2(
391
+ FormFields,
392
+ {
393
+ fields,
394
+ idPrefix,
395
+ values,
396
+ classNames: props.classNames,
397
+ onChange: setFieldValue
398
+ }
399
+ ),
400
+ /* @__PURE__ */ jsx2(
401
+ FormSections,
402
+ {
403
+ sections,
404
+ idPrefix,
405
+ values,
406
+ classNames: props.classNames,
407
+ onChange: setFieldValue
408
+ }
409
+ )
410
+ ] }),
411
+ /* @__PURE__ */ jsxs2("div", { className: props.classNames?.actions, children: [
412
+ isMultiStep && step > 0 ? /* @__PURE__ */ jsx2(
413
+ "button",
414
+ {
415
+ type: "button",
416
+ onClick: () => setStep((current) => current - 1),
417
+ children: "Anterior"
418
+ }
419
+ ) : null,
420
+ /* @__PURE__ */ jsx2(
421
+ "button",
422
+ {
423
+ type: "submit",
424
+ className: props.classNames?.button,
425
+ disabled: submitting,
426
+ children: isLastStep ? form.definition.submitLabel : "Continuar"
427
+ }
428
+ )
429
+ ] }),
430
+ submissionError ? /* @__PURE__ */ jsx2(
431
+ "div",
432
+ {
433
+ role: "alert",
434
+ className: props.classNames?.message,
435
+ "data-bacano-form-error": true,
436
+ children: submissionError.message
437
+ }
438
+ ) : null
439
+ ]
440
+ }
441
+ );
442
+ }
443
+ export {
444
+ BacanoForm,
445
+ useBacanoForm
446
+ };
package/react.cjs CHANGED
@@ -68,10 +68,10 @@ var WebsiteError = class extends Error {
68
68
  }
69
69
  };
70
70
  var RateLimitError = class extends WebsiteError {
71
- constructor(message, retryAfterSeconds) {
71
+ constructor(message, retryAfterSeconds2) {
72
72
  super(message, "RATE_LIMITED", {
73
73
  status: 429,
74
- retryAfterSeconds
74
+ retryAfterSeconds: retryAfterSeconds2
75
75
  });
76
76
  this.name = "RateLimitError";
77
77
  }
@@ -6497,6 +6497,171 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
6497
6497
  };
6498
6498
  }
6499
6499
 
6500
+ // libs/sdk/src/modules/forms.ts
6501
+ function retryAfterSeconds(response) {
6502
+ const value = Number(response.headers.get("Retry-After") ?? "1");
6503
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : 1;
6504
+ }
6505
+ function isFormKey(value) {
6506
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
6507
+ }
6508
+ function createFormsModule(apiUrl, websiteSlug, getContext) {
6509
+ const cache = /* @__PURE__ */ new Map();
6510
+ function getServerOrigin() {
6511
+ if (typeof window !== "undefined") return null;
6512
+ const domain = getContext().domain?.trim();
6513
+ if (!domain) return null;
6514
+ try {
6515
+ return new URL(domain.includes("://") ? domain : `https://${domain}`).origin;
6516
+ } catch {
6517
+ return null;
6518
+ }
6519
+ }
6520
+ return {
6521
+ async getDefinition(formKey, options) {
6522
+ const normalizedKey = formKey.trim();
6523
+ if (!isFormKey(normalizedKey)) {
6524
+ throw new WebsiteError(
6525
+ "formKey has an invalid format",
6526
+ "INVALID_FORM_KEY"
6527
+ );
6528
+ }
6529
+ const cached = cache.get(normalizedKey);
6530
+ if (!options?.force && cached && cached.expiresAt > Date.now()) {
6531
+ return cached.value;
6532
+ }
6533
+ const url = new URL(
6534
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/forms/definition`
6535
+ );
6536
+ url.searchParams.set("websiteSlug", websiteSlug);
6537
+ url.searchParams.set("formKey", normalizedKey);
6538
+ let response;
6539
+ try {
6540
+ const serverOrigin = getServerOrigin();
6541
+ const headers = new Headers();
6542
+ if (serverOrigin) headers.set("Origin", serverOrigin);
6543
+ if (cached?.etag) headers.set("If-None-Match", cached.etag);
6544
+ response = await fetch(url, {
6545
+ ...serverOrigin || cached?.etag ? { headers } : {},
6546
+ ...options?.force ? { cache: "no-cache" } : {}
6547
+ });
6548
+ } catch (error) {
6549
+ throw new WebsiteError(
6550
+ `Could not load the website form: ${error instanceof Error ? error.message : String(error)}`,
6551
+ "NETWORK_ERROR"
6552
+ );
6553
+ }
6554
+ if (response.status === 304 && cached) {
6555
+ cache.set(normalizedKey, {
6556
+ ...cached,
6557
+ expiresAt: Date.now() + 3e4
6558
+ });
6559
+ return cached.value;
6560
+ }
6561
+ if (response.status === 404) {
6562
+ cache.set(normalizedKey, {
6563
+ etag: null,
6564
+ expiresAt: Date.now() + 15e3,
6565
+ value: null
6566
+ });
6567
+ return null;
6568
+ }
6569
+ if (response.status === 429) {
6570
+ throw new RateLimitError(
6571
+ "Rate limit exceeded. Please try again later.",
6572
+ retryAfterSeconds(response)
6573
+ );
6574
+ }
6575
+ if (!response.ok) {
6576
+ throw new WebsiteError(
6577
+ `Could not load the website form (status ${response.status})`,
6578
+ response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "FORM_DEFINITION_ERROR",
6579
+ { status: response.status }
6580
+ );
6581
+ }
6582
+ const value = await response.json();
6583
+ if (value.schemaVersion !== 1 || value.key !== normalizedKey || !Array.isArray(value.definition?.fields) || !Array.isArray(value.definition?.sections) || !Array.isArray(value.definition?.steps)) {
6584
+ throw new WebsiteError(
6585
+ "Website form returned an invalid definition",
6586
+ "INVALID_FORM_DEFINITION"
6587
+ );
6588
+ }
6589
+ cache.set(normalizedKey, {
6590
+ etag: response.headers.get("ETag"),
6591
+ expiresAt: Date.now() + 3e4,
6592
+ value
6593
+ });
6594
+ return value;
6595
+ },
6596
+ async submitResponse(input) {
6597
+ const normalizedKey = input.formKey.trim();
6598
+ if (!isFormKey(normalizedKey)) {
6599
+ throw new WebsiteError(
6600
+ "formKey has an invalid format",
6601
+ "INVALID_FORM_KEY"
6602
+ );
6603
+ }
6604
+ const context = getContext();
6605
+ const attribution = getWebsiteMarketingAttribution(context.websiteId);
6606
+ const pageUrl = typeof window !== "undefined" ? window.location.href : void 0;
6607
+ const referrerUrl = typeof document !== "undefined" && document.referrer ? document.referrer : void 0;
6608
+ let response;
6609
+ try {
6610
+ response = await fetch(
6611
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/forms/responses`,
6612
+ {
6613
+ method: "POST",
6614
+ headers: { "Content-Type": "application/json" },
6615
+ body: JSON.stringify({
6616
+ websiteSlug,
6617
+ formKey: normalizedKey,
6618
+ formVersion: input.formVersion,
6619
+ definitionHash: input.definitionHash,
6620
+ idempotencyKey: input.idempotencyKey,
6621
+ source: input.source ?? "WEBSITE",
6622
+ values: input.values,
6623
+ pageUrl,
6624
+ referrerUrl,
6625
+ attribution: attribution ? serializeWebsiteMarketingAttribution(attribution) : null,
6626
+ honeypot: input.honeypot ?? ""
6627
+ })
6628
+ }
6629
+ );
6630
+ } catch (error) {
6631
+ throw new WebsiteError(
6632
+ `Could not submit the website form: ${error instanceof Error ? error.message : String(error)}`,
6633
+ "NETWORK_ERROR"
6634
+ );
6635
+ }
6636
+ if (response.status === 429) {
6637
+ throw new RateLimitError(
6638
+ "Rate limit exceeded. Please try again later.",
6639
+ retryAfterSeconds(response)
6640
+ );
6641
+ }
6642
+ const result = await response.json().catch(() => null);
6643
+ if (!response.ok) {
6644
+ throw new WebsiteError(
6645
+ result?.message ?? `Could not submit the website form (status ${response.status})`,
6646
+ result?.code ?? (response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "FORM_RESPONSE_ERROR"),
6647
+ { status: response.status }
6648
+ );
6649
+ }
6650
+ if (typeof result?.accepted !== "boolean" || typeof result.message !== "string") {
6651
+ throw new WebsiteError(
6652
+ "Website form returned an invalid response",
6653
+ "INVALID_RESPONSE"
6654
+ );
6655
+ }
6656
+ return {
6657
+ accepted: result.accepted,
6658
+ responseId: typeof result.responseId === "string" ? result.responseId : null,
6659
+ message: result.message
6660
+ };
6661
+ }
6662
+ };
6663
+ }
6664
+
6500
6665
  // libs/sdk/src/modules/marketing.ts
6501
6666
  function readRetryAfter(response) {
6502
6667
  const value = Number(response.headers.get("Retry-After") ?? "1");
@@ -7023,6 +7188,7 @@ function createBacanoClient(config) {
7023
7188
  config.websiteSlug,
7024
7189
  getContext
7025
7190
  );
7191
+ const formsModule = createFormsModule(apiUrl, config.websiteSlug, getContext);
7026
7192
  return {
7027
7193
  async init() {
7028
7194
  if (context) return;
@@ -7075,6 +7241,10 @@ function createBacanoClient(config) {
7075
7241
  getByTag: contentModule.getByTag.bind(contentModule),
7076
7242
  getForProduct: contentModule.getForProduct.bind(contentModule)
7077
7243
  },
7244
+ forms: {
7245
+ getDefinition: formsModule.getDefinition.bind(formsModule),
7246
+ submitResponse: formsModule.submitResponse.bind(formsModule)
7247
+ },
7078
7248
  cart: {
7079
7249
  getOrCreate: cartModule.getOrCreate.bind(cartModule),
7080
7250
  startCheckout: cartModule.startCheckout.bind(cartModule),