@bacano/sdk 1.1.19 → 1.2.1
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/STOREFRONT_FORMS.md +58 -0
- package/STOREFRONT_MARKETING.md +103 -4
- package/index.cjs +253 -84
- package/index.js +253 -84
- package/marketing/browser-adapters.d.ts +30 -0
- package/marketing/clarity.d.ts +1 -1
- package/marketing/form-interactions.d.ts +12 -0
- package/marketing/index.d.ts +2 -0
- package/marketing/interactions.d.ts +19 -0
- package/marketing/types.d.ts +5 -2
- package/package.json +1 -1
- package/react/cta-tracking.d.ts +5 -0
- package/react/form-sections.d.ts +16 -0
- package/react/form-tracking.d.ts +11 -0
- package/react/forms.cjs +240 -94
- package/react/forms.d.ts +10 -2
- package/react/forms.js +233 -87
- package/react/marketing-provider.d.ts +2 -1
- package/react.cjs +342 -134
- package/react.d.ts +1 -0
- package/react.js +325 -117
- package/skills/bacano-web/SKILL.md +5 -0
- package/skills/bacano-web/references/forms.md +5 -0
- package/skills/bacano-web/references/interactions.md +147 -0
- package/skills/bacano-web/references/report.md +5 -0
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
|
|
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/
|
|
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,
|
|
303
|
+
return (0, import_react3.useContext)(BacanoReactContext);
|
|
156
304
|
}
|
|
157
305
|
|
|
158
306
|
// libs/sdk/src/react/forms.tsx
|
|
159
|
-
var
|
|
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,
|
|
172
|
-
const formRef = (0,
|
|
173
|
-
const
|
|
174
|
-
const [
|
|
175
|
-
const
|
|
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,
|
|
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
|
|
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
|
|
213
|
-
|
|
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 [
|
|
264
|
-
const [step, setStep] = (0,
|
|
265
|
-
const [submitting, setSubmitting] = (0,
|
|
266
|
-
const [submitted, setSubmitted] = (0,
|
|
267
|
-
const [submissionError, setSubmissionError] = (0,
|
|
268
|
-
const [honeypot, setHoneypot] = (0,
|
|
269
|
-
const
|
|
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,
|
|
387
|
+
const fieldKeys = (0, import_react4.useMemo)(
|
|
274
388
|
() => new Set(
|
|
275
389
|
[
|
|
276
390
|
...fields,
|
|
@@ -283,7 +397,12 @@ function BacanoForm(props) {
|
|
|
283
397
|
),
|
|
284
398
|
[fields, sections, steps]
|
|
285
399
|
);
|
|
286
|
-
|
|
400
|
+
const values = Object.fromEntries(
|
|
401
|
+
Object.entries(props.values ?? internalValues).filter(
|
|
402
|
+
([key]) => fieldKeys.has(key)
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
(0, import_react4.useEffect)(() => {
|
|
287
406
|
setStep(0);
|
|
288
407
|
setValues(
|
|
289
408
|
(current) => Object.fromEntries(
|
|
@@ -295,23 +414,37 @@ function BacanoForm(props) {
|
|
|
295
414
|
setHoneypot("");
|
|
296
415
|
idempotencyKey.current = crypto.randomUUID();
|
|
297
416
|
}, [fieldKeys, form?.version]);
|
|
298
|
-
if (loading) return /* @__PURE__ */ (0,
|
|
417
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: props.loadingFallback ?? null });
|
|
299
418
|
if (error && !form)
|
|
300
|
-
return /* @__PURE__ */ (0,
|
|
301
|
-
if (!form) return /* @__PURE__ */ (0,
|
|
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 });
|
|
302
421
|
if (form.status === "PAUSED") {
|
|
303
|
-
return /* @__PURE__ */ (0,
|
|
422
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: props.classNames?.message, "data-bacano-form-unavailable": true, children: props.unavailableFallback ?? form.definition.unavailableMessage });
|
|
304
423
|
}
|
|
305
424
|
if (submitted) {
|
|
306
|
-
return /* @__PURE__ */ (0,
|
|
425
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: props.classNames?.message, "data-bacano-form-success": true, children: form.definition.successMessage });
|
|
307
426
|
}
|
|
308
427
|
const isMultiStep = steps.length > 0;
|
|
309
428
|
const activeStep = isMultiStep ? steps[step] : null;
|
|
310
429
|
const isLastStep = !isMultiStep || step === steps.length - 1;
|
|
311
|
-
const setFieldValue = (key, value) =>
|
|
430
|
+
const setFieldValue = (key, value) => {
|
|
431
|
+
if (value !== "" && value !== false && (!Array.isArray(value) || value.length > 0))
|
|
432
|
+
tracking.start();
|
|
433
|
+
const nextValues = { ...values, [key]: value };
|
|
434
|
+
setValues(nextValues);
|
|
435
|
+
props.onValuesChange?.(nextValues);
|
|
436
|
+
};
|
|
312
437
|
const submit = async (event) => {
|
|
313
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
|
+
}
|
|
314
446
|
if (!isLastStep) {
|
|
447
|
+
if (activeStep) tracking.stepComplete(activeStep.key);
|
|
315
448
|
setStep((current) => current + 1);
|
|
316
449
|
return;
|
|
317
450
|
}
|
|
@@ -319,8 +452,12 @@ function BacanoForm(props) {
|
|
|
319
452
|
setSubmissionError(null);
|
|
320
453
|
try {
|
|
321
454
|
let responseId = null;
|
|
455
|
+
let officialResult;
|
|
322
456
|
if (props.onSubmit) {
|
|
323
|
-
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;
|
|
324
461
|
} else {
|
|
325
462
|
if (!client) throw new Error("Bacano client is not ready");
|
|
326
463
|
const result = await client.forms.submitResponse({
|
|
@@ -332,11 +469,16 @@ function BacanoForm(props) {
|
|
|
332
469
|
honeypot
|
|
333
470
|
});
|
|
334
471
|
if (!result.accepted) throw new Error(result.message);
|
|
472
|
+
officialResult = result;
|
|
335
473
|
responseId = result.responseId;
|
|
336
474
|
}
|
|
475
|
+
tracking.complete(officialResult);
|
|
337
476
|
setSubmitted(true);
|
|
338
477
|
props.onSubmitted?.(responseId);
|
|
339
478
|
} catch (submitError) {
|
|
479
|
+
tracking.fail(
|
|
480
|
+
submitError instanceof WebsiteError && submitError.code === "NETWORK_ERROR" ? "network" : "rejected"
|
|
481
|
+
);
|
|
340
482
|
setSubmissionError(
|
|
341
483
|
submitError instanceof Error ? submitError : new Error(String(submitError))
|
|
342
484
|
);
|
|
@@ -344,15 +486,16 @@ function BacanoForm(props) {
|
|
|
344
486
|
setSubmitting(false);
|
|
345
487
|
}
|
|
346
488
|
};
|
|
347
|
-
return /* @__PURE__ */ (0,
|
|
489
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
348
490
|
"form",
|
|
349
491
|
{
|
|
492
|
+
noValidate: true,
|
|
350
493
|
className: props.classNames?.root,
|
|
351
494
|
"data-bacano-form": form.key,
|
|
352
495
|
"data-bacano-form-version": form.version,
|
|
353
496
|
onSubmit: (event) => void submit(event),
|
|
354
497
|
children: [
|
|
355
|
-
/* @__PURE__ */ (0,
|
|
498
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
356
499
|
"div",
|
|
357
500
|
{
|
|
358
501
|
"aria-hidden": "true",
|
|
@@ -364,8 +507,8 @@ function BacanoForm(props) {
|
|
|
364
507
|
clip: "rect(0 0 0 0)"
|
|
365
508
|
},
|
|
366
509
|
children: [
|
|
367
|
-
/* @__PURE__ */ (0,
|
|
368
|
-
/* @__PURE__ */ (0,
|
|
510
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { htmlFor: `${form.key}-company-website`, children: "Empresa" }),
|
|
511
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
369
512
|
"input",
|
|
370
513
|
{
|
|
371
514
|
id: `${form.key}-company-website`,
|
|
@@ -379,12 +522,12 @@ function BacanoForm(props) {
|
|
|
379
522
|
]
|
|
380
523
|
}
|
|
381
524
|
),
|
|
382
|
-
/* @__PURE__ */ (0,
|
|
383
|
-
form.description ? /* @__PURE__ */ (0,
|
|
384
|
-
activeStep ? /* @__PURE__ */ (0,
|
|
385
|
-
/* @__PURE__ */ (0,
|
|
386
|
-
activeStep.description ? /* @__PURE__ */ (0,
|
|
387
|
-
/* @__PURE__ */ (0,
|
|
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)(
|
|
388
531
|
FormFields,
|
|
389
532
|
{
|
|
390
533
|
fields: activeStep.fields,
|
|
@@ -394,7 +537,7 @@ function BacanoForm(props) {
|
|
|
394
537
|
onChange: setFieldValue
|
|
395
538
|
}
|
|
396
539
|
),
|
|
397
|
-
/* @__PURE__ */ (0,
|
|
540
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
398
541
|
FormSections,
|
|
399
542
|
{
|
|
400
543
|
sections: activeStep.sections,
|
|
@@ -404,8 +547,8 @@ function BacanoForm(props) {
|
|
|
404
547
|
onChange: setFieldValue
|
|
405
548
|
}
|
|
406
549
|
)
|
|
407
|
-
] }) : /* @__PURE__ */ (0,
|
|
408
|
-
/* @__PURE__ */ (0,
|
|
550
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
551
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
409
552
|
FormFields,
|
|
410
553
|
{
|
|
411
554
|
fields,
|
|
@@ -415,7 +558,7 @@ function BacanoForm(props) {
|
|
|
415
558
|
onChange: setFieldValue
|
|
416
559
|
}
|
|
417
560
|
),
|
|
418
|
-
/* @__PURE__ */ (0,
|
|
561
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
419
562
|
FormSections,
|
|
420
563
|
{
|
|
421
564
|
sections,
|
|
@@ -426,8 +569,8 @@ function BacanoForm(props) {
|
|
|
426
569
|
}
|
|
427
570
|
)
|
|
428
571
|
] }),
|
|
429
|
-
/* @__PURE__ */ (0,
|
|
430
|
-
isMultiStep && step > 0 ? /* @__PURE__ */ (0,
|
|
572
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: props.classNames?.actions, children: [
|
|
573
|
+
isMultiStep && step > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
431
574
|
"button",
|
|
432
575
|
{
|
|
433
576
|
type: "button",
|
|
@@ -435,7 +578,7 @@ function BacanoForm(props) {
|
|
|
435
578
|
children: "Anterior"
|
|
436
579
|
}
|
|
437
580
|
) : null,
|
|
438
|
-
/* @__PURE__ */ (0,
|
|
581
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
439
582
|
"button",
|
|
440
583
|
{
|
|
441
584
|
type: "submit",
|
|
@@ -445,7 +588,7 @@ function BacanoForm(props) {
|
|
|
445
588
|
}
|
|
446
589
|
)
|
|
447
590
|
] }),
|
|
448
|
-
submissionError ? /* @__PURE__ */ (0,
|
|
591
|
+
submissionError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
449
592
|
"div",
|
|
450
593
|
{
|
|
451
594
|
role: "alert",
|
|
@@ -458,3 +601,6 @@ function BacanoForm(props) {
|
|
|
458
601
|
}
|
|
459
602
|
);
|
|
460
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,17 +34,24 @@ 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;
|
|
39
|
+
/** Controlled values. Use together with onValuesChange; option values, not labels. */
|
|
40
|
+
values?: BacanoFormValues;
|
|
41
|
+
/** Receives the complete next values after a visitor edits a field. */
|
|
42
|
+
onValuesChange?(values: BacanoFormValues): void;
|
|
36
43
|
/** Definition embedded by a server/build; it is revalidated in runtime. */
|
|
37
44
|
initialForm?: WebsitePublishedForm | null;
|
|
38
45
|
/**
|
|
39
46
|
* Optional custom submission. When omitted, the official Bacano response
|
|
40
47
|
* endpoint is used. Existing storefronts may keep a custom handler.
|
|
41
48
|
*/
|
|
42
|
-
onSubmit?(values: BacanoFormValues, form: WebsitePublishedForm): void | Promise<void>;
|
|
49
|
+
onSubmit?(values: BacanoFormValues, form: WebsitePublishedForm): void | WebsiteFormResponseResult | Promise<void | WebsiteFormResponseResult>;
|
|
43
50
|
onSubmitted?(responseId: string | null): void;
|
|
44
51
|
classNames?: BacanoFormClassNames;
|
|
45
52
|
loadingFallback?: ReactNode;
|
|
46
53
|
unavailableFallback?: ReactNode;
|
|
47
54
|
errorFallback?: (error: Error) => ReactNode;
|
|
48
55
|
}
|
|
56
|
+
/** A different form cannot inherit pending submission state from the old key. */
|
|
49
57
|
export declare function BacanoForm(props: BacanoFormProps): import("react").JSX.Element;
|