@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/README.md +14 -0
- package/STOREFRONT_CONTENT_LOCATOR.md +111 -0
- package/STOREFRONT_FORMS.md +211 -0
- package/client.d.ts +8 -1
- package/content-entry-locator.d.ts +16 -0
- package/index.cjs +195 -2
- package/index.d.ts +3 -1
- package/index.js +195 -2
- package/modules/forms.d.ts +9 -0
- package/package.json +6 -1
- package/react/form-field-control.d.ts +11 -0
- package/react/forms.cjs +460 -0
- package/react/forms.d.ts +49 -0
- package/react/forms.js +446 -0
- package/react.cjs +172 -2
- package/react.js +172 -2
- package/skills/bacano-figma-web/SKILL.md +8 -0
- package/skills/bacano-figma-web/references/bacano-mapping.md +5 -3
- package/skills/bacano-figma-web/references/content-workflow.md +11 -1
- package/skills/bacano-figma-web/references/implementation-quality.md +12 -0
- package/skills/bacano-web/SKILL.md +7 -1
- package/skills/bacano-web/references/common.md +7 -0
- package/skills/bacano-web/references/forms.md +67 -0
- package/skills/bacano-web/references/informational.md +3 -0
- package/types.d.ts +87 -0
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,
|
|
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),
|