@bacano/sdk 1.2.1 → 1.2.3

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/flows.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { AccessTokenProvider, WebsiteFormDefinition, WebsiteFormResponseValue } from "./types";
2
+ export interface FlowCaseSummary {
3
+ id: string;
4
+ title: string;
5
+ reference: string;
6
+ status: "open" | "completed" | "rejected";
7
+ flowName: string;
8
+ }
9
+ export interface ParticipantFlowStep {
10
+ id: string;
11
+ kind: "content" | "form" | "task" | "review";
12
+ title: string;
13
+ required: boolean;
14
+ completed: boolean;
15
+ available: boolean;
16
+ canComplete: boolean;
17
+ internal: boolean;
18
+ canReadResponse: boolean;
19
+ completion?: "reference" | "continue" | "confirmation";
20
+ confirmationText?: string;
21
+ instructions?: string;
22
+ }
23
+ export interface ParticipantJourney extends FlowCaseSummary {
24
+ revision: number;
25
+ flowRevision: number;
26
+ stageId: string;
27
+ dueDate: string | null;
28
+ stages: Array<{
29
+ id: string;
30
+ name: string;
31
+ current: boolean;
32
+ /** Presentation only; availability and completion remain server-authoritative. */
33
+ presentation?: "step_by_step" | "single_view";
34
+ steps: ParticipantFlowStep[];
35
+ }>;
36
+ messages: Array<{
37
+ createdAt: string;
38
+ message: string;
39
+ }>;
40
+ }
41
+ export interface FlowPublishedForm {
42
+ name: string;
43
+ version: number;
44
+ definitionHash: string;
45
+ definition: WebsiteFormDefinition;
46
+ /** Own previous answers, scoped and filtered by the backend for a correction. */
47
+ initialValues?: Record<string, WebsiteFormResponseValue>;
48
+ correction?: {
49
+ attempt: number;
50
+ formChanged: boolean;
51
+ };
52
+ }
53
+ export interface FlowContentResource {
54
+ id: string;
55
+ name: string;
56
+ content: Record<string, unknown>;
57
+ }
58
+ export interface FlowResponse {
59
+ snapshot: Array<{
60
+ key: string;
61
+ label: string;
62
+ displayValue: unknown;
63
+ }>;
64
+ attempt: number;
65
+ submittedAt: string;
66
+ }
67
+ export interface FlowParticipantCommand {
68
+ stepId: string;
69
+ revision: number;
70
+ flowRevision: number;
71
+ idempotencyKey: string;
72
+ operation: "complete_content" | "complete_task" | "submit_form";
73
+ confirmed?: boolean;
74
+ submission?: {
75
+ formVersion: number;
76
+ definitionHash: string;
77
+ values: Record<string, WebsiteFormResponseValue>;
78
+ };
79
+ }
80
+ export interface FlowsClientConfig {
81
+ apiUrl: string;
82
+ getAccessToken?: AccessTokenProvider;
83
+ websiteSlug?: string;
84
+ }
85
+ export interface FlowDemoSession {
86
+ token: string;
87
+ name: string;
88
+ expiresAt: string;
89
+ }
90
+ /** Gateway only. Tokens are supplied per request; this client stores no credentials. */
91
+ export declare function createFlowsClient(config: FlowsClientConfig): {
92
+ /** Only the explicitly enabled local demo gateway accepts these credentials. */
93
+ localDemoLogin: (email: string, password: string) => Promise<FlowDemoSession>;
94
+ logout: () => Promise<{
95
+ success: boolean;
96
+ }>;
97
+ listCases: () => Promise<FlowCaseSummary[]>;
98
+ getJourney: (caseId: string) => Promise<ParticipantJourney>;
99
+ getContent: (caseId: string, stepId: string) => Promise<FlowContentResource[]>;
100
+ getForm: (caseId: string, stepId: string) => Promise<FlowPublishedForm>;
101
+ getResponse: (caseId: string, stepId: string) => Promise<FlowResponse>;
102
+ getFile: (caseId: string, stepId: string, fileId: string) => Promise<Blob>;
103
+ act: (caseId: string, command: FlowParticipantCommand) => Promise<{
104
+ caseId: string;
105
+ revision: number;
106
+ responseId: string | null;
107
+ }>;
108
+ };
109
+ export type FlowsClient = ReturnType<typeof createFlowsClient>;
@@ -0,0 +1,6 @@
1
+ import type { WebsiteFormDefinition, WebsiteFormField } from "./types";
2
+ export declare function formRootFields(definition: WebsiteFormDefinition): WebsiteFormField[];
3
+ /** Unselected branches do not participate in validation or submissions. */
4
+ export declare function visibleFormFields(fields: WebsiteFormField[], values: Record<string, unknown>): WebsiteFormField[];
5
+ export declare function allFormFields(fields: WebsiteFormField[]): WebsiteFormField[];
6
+ export declare function activeFormValues<T>(definition: WebsiteFormDefinition, values: Record<string, T>): Record<string, T>;
package/index.cjs CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  RateLimitError: () => RateLimitError,
31
31
  WEBSITE_MARKETING_CONTACT_PLACEMENTS: () => WEBSITE_MARKETING_CONTACT_PLACEMENTS,
32
32
  WebsiteError: () => WebsiteError,
33
+ activeFormValues: () => activeFormValues,
33
34
  buildStorageContentMediaUrls: () => buildStorageContentMediaUrls,
34
35
  buildStorageFileUrl: () => buildStorageFileUrl,
35
36
  buildStorageMediaUrls: () => buildStorageMediaUrls,
@@ -37,7 +38,9 @@ __export(index_exports, {
37
38
  buildStorageTransformedFileUrl: () => buildStorageTransformedFileUrl,
38
39
  captureWebsiteMarketingAttribution: () => captureWebsiteMarketingAttribution,
39
40
  createBacanoClient: () => createBacanoClient,
41
+ createFlowsClient: () => createFlowsClient,
40
42
  createWebsiteMarketingRuntime: () => createWebsiteMarketingRuntime,
43
+ formRootFields: () => formRootFields,
41
44
  formatMediaSrcSet: () => formatMediaSrcSet,
42
45
  getAllowedMarketingParameters: () => getAllowedMarketingParameters,
43
46
  getContentEntryLocatorAttributes: () => getContentEntryLocatorAttributes,
@@ -51,7 +54,8 @@ __export(index_exports, {
51
54
  getWebsiteMarketingAttribution: () => getWebsiteMarketingAttribution,
52
55
  getWebsitePublicConfig: () => getWebsitePublicConfig,
53
56
  resolveProductPricing: () => resolveProductPricing,
54
- sanitizeMarketingUrl: () => sanitizeMarketingUrl
57
+ sanitizeMarketingUrl: () => sanitizeMarketingUrl,
58
+ visibleFormFields: () => visibleFormFields
55
59
  });
56
60
  module.exports = __toCommonJS(index_exports);
57
61
 
@@ -110,6 +114,52 @@ var GraphQLRequestError = class extends WebsiteError {
110
114
  }
111
115
  };
112
116
 
117
+ // libs/sdk/src/flows.ts
118
+ function createFlowsClient(config) {
119
+ const base = config.apiUrl.replace(/\/+$/, "") + "/api/v1/website/flows";
120
+ async function request(path, body, binary = false) {
121
+ const token = await config.getAccessToken?.();
122
+ const response = await fetch(base + path, {
123
+ method: body === void 0 ? "GET" : "POST",
124
+ cache: "no-store",
125
+ headers: {
126
+ ...token ? { Authorization: `Bearer ${token}` } : {},
127
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {},
128
+ ...config.websiteSlug ? { "x-bacano-website-slug": config.websiteSlug } : {}
129
+ },
130
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
131
+ });
132
+ if (!response.ok) {
133
+ const result = await response.json().catch(() => ({ error: "FLOW_REQUEST_FAILED" }));
134
+ throw new WebsiteError(
135
+ result.error ?? "FLOW_REQUEST_FAILED",
136
+ result.error ?? "FLOW_REQUEST_FAILED"
137
+ );
138
+ }
139
+ return binary ? await response.blob() : await response.json();
140
+ }
141
+ const stepPath = (caseId, stepId) => `/cases/${encodeURIComponent(caseId)}/steps/${encodeURIComponent(stepId)}`;
142
+ return {
143
+ /** Only the explicitly enabled local demo gateway accepts these credentials. */
144
+ localDemoLogin: (email, password) => request("/local-session", { email, password }),
145
+ logout: () => request("/logout", {}),
146
+ listCases: () => request("/cases"),
147
+ getJourney: (caseId) => request(`/cases/${encodeURIComponent(caseId)}`),
148
+ getContent: (caseId, stepId) => request(stepPath(caseId, stepId) + "/content"),
149
+ getForm: (caseId, stepId) => request(stepPath(caseId, stepId) + "/form"),
150
+ getResponse: (caseId, stepId) => request(stepPath(caseId, stepId) + "/response"),
151
+ getFile: (caseId, stepId, fileId) => request(
152
+ stepPath(caseId, stepId) + `/files/${encodeURIComponent(fileId)}`,
153
+ void 0,
154
+ true
155
+ ),
156
+ act: (caseId, command) => request(
157
+ `/cases/${encodeURIComponent(caseId)}/actions`,
158
+ command
159
+ )
160
+ };
161
+ }
162
+
113
163
  // libs/sdk/src/media.ts
114
164
  var DEFAULT_RESPONSIVE_WIDTHS = [240, 384, 640, 960, 1400];
115
165
  var CONTENT_HERO_WIDTHS = [640, 960, 1400, 1920, 2560];
@@ -394,7 +444,8 @@ async function getWebsitePublicConfig(options) {
394
444
  pixelId: marketing.destinations.meta.pixel_id,
395
445
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
396
446
  } : null,
397
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
447
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
448
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
398
449
  }
399
450
  },
400
451
  legal: {
@@ -2239,7 +2290,7 @@ var RECONCILE_PAYMENT_SESSION = (
2239
2290
  $orderId: uuid!
2240
2291
  $publicOrderToken: String!
2241
2292
  $paymentSessionId: uuid!
2242
- $gatewayTransactionId: String!
2293
+ $gatewayTransactionId: String
2243
2294
  ) {
2244
2295
  websiteReconcilePaymentSession(
2245
2296
  order_id: $orderId
@@ -6066,6 +6117,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6066
6117
  }
6067
6118
  }
6068
6119
 
6120
+ // libs/sdk/src/marketing/gtm.ts
6121
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
6122
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
6123
+ function prepareGtm(containerId) {
6124
+ const target = marketingWindow();
6125
+ if (!target || !CONTAINER_ID.test(containerId)) return;
6126
+ target.dataLayer ??= [];
6127
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
6128
+ if (containers.has(containerId)) return;
6129
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
6130
+ containers.add(containerId);
6131
+ preparedQueues.set(target.dataLayer, containers);
6132
+ }
6133
+ function loadGtmScript(containerId, onError) {
6134
+ const target = marketingWindow();
6135
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
6136
+ return;
6137
+ const id = `bacano-gtm-${containerId}`;
6138
+ if (document.getElementById(id)) return;
6139
+ prepareGtm(containerId);
6140
+ const script = document.createElement("script");
6141
+ script.id = id;
6142
+ script.async = true;
6143
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
6144
+ script.referrerPolicy = "strict-origin-when-cross-origin";
6145
+ script.onerror = () => onError();
6146
+ document.head.appendChild(script);
6147
+ }
6148
+ function sendGtm(event) {
6149
+ const target = marketingWindow();
6150
+ if (!target?.dataLayer) return;
6151
+ target.dataLayer.push({ bacano: null });
6152
+ target.dataLayer.push({
6153
+ event: `bacano_${event.eventName}`,
6154
+ bacano: {
6155
+ schema_version: event.schemaVersion,
6156
+ event_name: event.eventName,
6157
+ event_id: event.eventId,
6158
+ ...interactionParameters(event),
6159
+ page_location: event.pageLocation,
6160
+ currency: event.currency,
6161
+ value: event.value,
6162
+ items: mapGa4Items(event.items),
6163
+ item_list_id: event.itemListId,
6164
+ item_list_name: event.itemListName,
6165
+ shipping_tier: event.shippingTier,
6166
+ payment_type: event.paymentType,
6167
+ method: event.contactMethod,
6168
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
6169
+ (value) => value === event.contactPlacement
6170
+ ) ? event.contactPlacement : void 0
6171
+ }
6172
+ });
6173
+ }
6174
+
6069
6175
  // libs/sdk/src/marketing/runtime.ts
6070
6176
  function createWebsiteMarketingRuntime(options) {
6071
6177
  let destroyed = false;
@@ -6086,7 +6192,7 @@ function createWebsiteMarketingRuntime(options) {
6086
6192
  target.location.hostname
6087
6193
  )
6088
6194
  ) && Boolean(
6089
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6195
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
6090
6196
  );
6091
6197
  if (!enabled)
6092
6198
  diagnostic({
@@ -6127,6 +6233,10 @@ function createWebsiteMarketingRuntime(options) {
6127
6233
  const { projectId } = options.config.destinations.clarity;
6128
6234
  guard("Clarity", () => prepareClarity(projectId));
6129
6235
  }
6236
+ if (options.config.destinations.gtm) {
6237
+ const { containerId } = options.config.destinations.gtm;
6238
+ guard("GTM", () => prepareGtm(containerId));
6239
+ }
6130
6240
  cancelScheduledLoad = scheduleScriptLoad(() => {
6131
6241
  if (destroyed) return;
6132
6242
  if (options.config.destinations.ga4) {
@@ -6142,6 +6252,10 @@ function createWebsiteMarketingRuntime(options) {
6142
6252
  () => loadClarityScript(projectId, scriptError("Clarity"))
6143
6253
  );
6144
6254
  }
6255
+ if (options.config.destinations.gtm) {
6256
+ const { containerId } = options.config.destinations.gtm;
6257
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
6258
+ }
6145
6259
  }, options.scriptLoadStrategy);
6146
6260
  captureWebsiteMarketingAttribution(
6147
6261
  options.websiteId,
@@ -6261,6 +6375,9 @@ function createWebsiteMarketingRuntime(options) {
6261
6375
  if (options.config.destinations.clarity) {
6262
6376
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
6263
6377
  }
6378
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
6379
+ dispatch("GTM", () => sendGtm(event));
6380
+ }
6264
6381
  },
6265
6382
  captureAttribution() {
6266
6383
  if (!enabled || destroyed) return null;
@@ -6997,6 +7114,7 @@ function createBacanoClient(config) {
6997
7114
  await initialization;
6998
7115
  },
6999
7116
  getContext,
7117
+ flows: createFlowsClient(config),
7000
7118
  auth: {
7001
7119
  async getAccessToken() {
7002
7120
  return config.getAccessToken ? await config.getAccessToken() : null;
@@ -7096,3 +7214,34 @@ function getContentEntryLocatorAttributes(entry) {
7096
7214
  "data-bacano-entry-name": entry.name.trim() || key
7097
7215
  };
7098
7216
  }
7217
+
7218
+ // libs/sdk/src/form-conditions.ts
7219
+ function formRootFields(definition) {
7220
+ return [
7221
+ ...definition.fields,
7222
+ ...definition.sections.flatMap((section) => section.fields),
7223
+ ...definition.steps.flatMap((step) => [
7224
+ ...step.fields,
7225
+ ...step.sections.flatMap((section) => section.fields)
7226
+ ])
7227
+ ];
7228
+ }
7229
+ function visibleFormFields(fields, values) {
7230
+ return fields.flatMap((field) => [
7231
+ field,
7232
+ ...visibleFormFields(
7233
+ field.branches?.find((branch) => branch.value === values[field.key])?.fields ?? [],
7234
+ values
7235
+ )
7236
+ ]);
7237
+ }
7238
+ function activeFormValues(definition, values) {
7239
+ const keys = new Set(
7240
+ visibleFormFields(formRootFields(definition), values).map(
7241
+ (field) => field.key
7242
+ )
7243
+ );
7244
+ return Object.fromEntries(
7245
+ Object.entries(values).filter(([key]) => keys.has(key))
7246
+ );
7247
+ }
package/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export { createBacanoClient } from "./client";
3
3
  export type { ContentEntryLocatorAttributes, ContentEntryLocatorSource, } from "./content-entry-locator";
4
4
  export { CONTENT_ENTRY_LOCATOR_SELECTOR, CONTENT_ENTRY_LOCATOR_VERSION, getContentEntryLocatorAttributes, } from "./content-entry-locator";
5
5
  export { AuthError, CheckoutError, GraphQLRequestError, NotInitializedError, RateLimitError, WebsiteError, } from "./errors";
6
+ export type { FlowCaseSummary, FlowContentResource, FlowDemoSession, FlowParticipantCommand, FlowPublishedForm, FlowResponse, FlowsClient, FlowsClientConfig, ParticipantFlowStep, ParticipantJourney, } from "./flows";
7
+ export { createFlowsClient } from "./flows";
8
+ export { activeFormValues, formRootFields, visibleFormFields, } from "./form-conditions";
6
9
  export type { WebsiteAdvertisingConsent, WebsiteMarketingAttribution, WebsiteMarketingBrowserEvent, WebsiteMarketingBrowserEventName, WebsiteMarketingContactPlacement, WebsiteMarketingDiagnostic, WebsiteMarketingItem, WebsiteMarketingRuntime, WebsiteMarketingRuntimeOptions, WebsiteMarketingScriptLoadStrategy, WebsiteMarketingTouchpoint, } from "./marketing";
7
10
  export { captureWebsiteMarketingAttribution, createWebsiteMarketingRuntime, getAllowedMarketingParameters, getWebsiteMarketingAttribution, MARKETING_SCHEMA_VERSION, sanitizeMarketingUrl, WEBSITE_MARKETING_CONTACT_PLACEMENTS, } from "./marketing";
8
11
  export type { MediaResponsiveSource, MediaResponsiveSourceOptions, MediaTransformFormat, MediaTransformOptions, MediaUrlVariant, } from "./media";
package/index.js CHANGED
@@ -53,6 +53,52 @@ var GraphQLRequestError = class extends WebsiteError {
53
53
  }
54
54
  };
55
55
 
56
+ // libs/sdk/src/flows.ts
57
+ function createFlowsClient(config) {
58
+ const base = config.apiUrl.replace(/\/+$/, "") + "/api/v1/website/flows";
59
+ async function request(path, body, binary = false) {
60
+ const token = await config.getAccessToken?.();
61
+ const response = await fetch(base + path, {
62
+ method: body === void 0 ? "GET" : "POST",
63
+ cache: "no-store",
64
+ headers: {
65
+ ...token ? { Authorization: `Bearer ${token}` } : {},
66
+ ...body !== void 0 ? { "Content-Type": "application/json" } : {},
67
+ ...config.websiteSlug ? { "x-bacano-website-slug": config.websiteSlug } : {}
68
+ },
69
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
70
+ });
71
+ if (!response.ok) {
72
+ const result = await response.json().catch(() => ({ error: "FLOW_REQUEST_FAILED" }));
73
+ throw new WebsiteError(
74
+ result.error ?? "FLOW_REQUEST_FAILED",
75
+ result.error ?? "FLOW_REQUEST_FAILED"
76
+ );
77
+ }
78
+ return binary ? await response.blob() : await response.json();
79
+ }
80
+ const stepPath = (caseId, stepId) => `/cases/${encodeURIComponent(caseId)}/steps/${encodeURIComponent(stepId)}`;
81
+ return {
82
+ /** Only the explicitly enabled local demo gateway accepts these credentials. */
83
+ localDemoLogin: (email, password) => request("/local-session", { email, password }),
84
+ logout: () => request("/logout", {}),
85
+ listCases: () => request("/cases"),
86
+ getJourney: (caseId) => request(`/cases/${encodeURIComponent(caseId)}`),
87
+ getContent: (caseId, stepId) => request(stepPath(caseId, stepId) + "/content"),
88
+ getForm: (caseId, stepId) => request(stepPath(caseId, stepId) + "/form"),
89
+ getResponse: (caseId, stepId) => request(stepPath(caseId, stepId) + "/response"),
90
+ getFile: (caseId, stepId, fileId) => request(
91
+ stepPath(caseId, stepId) + `/files/${encodeURIComponent(fileId)}`,
92
+ void 0,
93
+ true
94
+ ),
95
+ act: (caseId, command) => request(
96
+ `/cases/${encodeURIComponent(caseId)}/actions`,
97
+ command
98
+ )
99
+ };
100
+ }
101
+
56
102
  // libs/sdk/src/media.ts
57
103
  var DEFAULT_RESPONSIVE_WIDTHS = [240, 384, 640, 960, 1400];
58
104
  var CONTENT_HERO_WIDTHS = [640, 960, 1400, 1920, 2560];
@@ -337,7 +383,8 @@ async function getWebsitePublicConfig(options) {
337
383
  pixelId: marketing.destinations.meta.pixel_id,
338
384
  diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
339
385
  } : null,
340
- clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
386
+ clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
387
+ ...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
341
388
  }
342
389
  },
343
390
  legal: {
@@ -2182,7 +2229,7 @@ var RECONCILE_PAYMENT_SESSION = (
2182
2229
  $orderId: uuid!
2183
2230
  $publicOrderToken: String!
2184
2231
  $paymentSessionId: uuid!
2185
- $gatewayTransactionId: String!
2232
+ $gatewayTransactionId: String
2186
2233
  ) {
2187
2234
  websiteReconcilePaymentSession(
2188
2235
  order_id: $orderId
@@ -6009,6 +6056,61 @@ function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
6009
6056
  }
6010
6057
  }
6011
6058
 
6059
+ // libs/sdk/src/marketing/gtm.ts
6060
+ var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
6061
+ var preparedQueues = /* @__PURE__ */ new WeakMap();
6062
+ function prepareGtm(containerId) {
6063
+ const target = marketingWindow();
6064
+ if (!target || !CONTAINER_ID.test(containerId)) return;
6065
+ target.dataLayer ??= [];
6066
+ const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
6067
+ if (containers.has(containerId)) return;
6068
+ target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
6069
+ containers.add(containerId);
6070
+ preparedQueues.set(target.dataLayer, containers);
6071
+ }
6072
+ function loadGtmScript(containerId, onError) {
6073
+ const target = marketingWindow();
6074
+ if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
6075
+ return;
6076
+ const id = `bacano-gtm-${containerId}`;
6077
+ if (document.getElementById(id)) return;
6078
+ prepareGtm(containerId);
6079
+ const script = document.createElement("script");
6080
+ script.id = id;
6081
+ script.async = true;
6082
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
6083
+ script.referrerPolicy = "strict-origin-when-cross-origin";
6084
+ script.onerror = () => onError();
6085
+ document.head.appendChild(script);
6086
+ }
6087
+ function sendGtm(event) {
6088
+ const target = marketingWindow();
6089
+ if (!target?.dataLayer) return;
6090
+ target.dataLayer.push({ bacano: null });
6091
+ target.dataLayer.push({
6092
+ event: `bacano_${event.eventName}`,
6093
+ bacano: {
6094
+ schema_version: event.schemaVersion,
6095
+ event_name: event.eventName,
6096
+ event_id: event.eventId,
6097
+ ...interactionParameters(event),
6098
+ page_location: event.pageLocation,
6099
+ currency: event.currency,
6100
+ value: event.value,
6101
+ items: mapGa4Items(event.items),
6102
+ item_list_id: event.itemListId,
6103
+ item_list_name: event.itemListName,
6104
+ shipping_tier: event.shippingTier,
6105
+ payment_type: event.paymentType,
6106
+ method: event.contactMethod,
6107
+ contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
6108
+ (value) => value === event.contactPlacement
6109
+ ) ? event.contactPlacement : void 0
6110
+ }
6111
+ });
6112
+ }
6113
+
6012
6114
  // libs/sdk/src/marketing/runtime.ts
6013
6115
  function createWebsiteMarketingRuntime(options) {
6014
6116
  let destroyed = false;
@@ -6029,7 +6131,7 @@ function createWebsiteMarketingRuntime(options) {
6029
6131
  target.location.hostname
6030
6132
  )
6031
6133
  ) && Boolean(
6032
- options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
6134
+ options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
6033
6135
  );
6034
6136
  if (!enabled)
6035
6137
  diagnostic({
@@ -6070,6 +6172,10 @@ function createWebsiteMarketingRuntime(options) {
6070
6172
  const { projectId } = options.config.destinations.clarity;
6071
6173
  guard("Clarity", () => prepareClarity(projectId));
6072
6174
  }
6175
+ if (options.config.destinations.gtm) {
6176
+ const { containerId } = options.config.destinations.gtm;
6177
+ guard("GTM", () => prepareGtm(containerId));
6178
+ }
6073
6179
  cancelScheduledLoad = scheduleScriptLoad(() => {
6074
6180
  if (destroyed) return;
6075
6181
  if (options.config.destinations.ga4) {
@@ -6085,6 +6191,10 @@ function createWebsiteMarketingRuntime(options) {
6085
6191
  () => loadClarityScript(projectId, scriptError("Clarity"))
6086
6192
  );
6087
6193
  }
6194
+ if (options.config.destinations.gtm) {
6195
+ const { containerId } = options.config.destinations.gtm;
6196
+ guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
6197
+ }
6088
6198
  }, options.scriptLoadStrategy);
6089
6199
  captureWebsiteMarketingAttribution(
6090
6200
  options.websiteId,
@@ -6204,6 +6314,9 @@ function createWebsiteMarketingRuntime(options) {
6204
6314
  if (options.config.destinations.clarity) {
6205
6315
  dispatch("Clarity", () => sendClarityEvent(event.eventName));
6206
6316
  }
6317
+ if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
6318
+ dispatch("GTM", () => sendGtm(event));
6319
+ }
6207
6320
  },
6208
6321
  captureAttribution() {
6209
6322
  if (!enabled || destroyed) return null;
@@ -6940,6 +7053,7 @@ function createBacanoClient(config) {
6940
7053
  await initialization;
6941
7054
  },
6942
7055
  getContext,
7056
+ flows: createFlowsClient(config),
6943
7057
  auth: {
6944
7058
  async getAccessToken() {
6945
7059
  return config.getAccessToken ? await config.getAccessToken() : null;
@@ -7039,6 +7153,37 @@ function getContentEntryLocatorAttributes(entry) {
7039
7153
  "data-bacano-entry-name": entry.name.trim() || key
7040
7154
  };
7041
7155
  }
7156
+
7157
+ // libs/sdk/src/form-conditions.ts
7158
+ function formRootFields(definition) {
7159
+ return [
7160
+ ...definition.fields,
7161
+ ...definition.sections.flatMap((section) => section.fields),
7162
+ ...definition.steps.flatMap((step) => [
7163
+ ...step.fields,
7164
+ ...step.sections.flatMap((section) => section.fields)
7165
+ ])
7166
+ ];
7167
+ }
7168
+ function visibleFormFields(fields, values) {
7169
+ return fields.flatMap((field) => [
7170
+ field,
7171
+ ...visibleFormFields(
7172
+ field.branches?.find((branch) => branch.value === values[field.key])?.fields ?? [],
7173
+ values
7174
+ )
7175
+ ]);
7176
+ }
7177
+ function activeFormValues(definition, values) {
7178
+ const keys = new Set(
7179
+ visibleFormFields(formRootFields(definition), values).map(
7180
+ (field) => field.key
7181
+ )
7182
+ );
7183
+ return Object.fromEntries(
7184
+ Object.entries(values).filter(([key]) => keys.has(key))
7185
+ );
7186
+ }
7042
7187
  export {
7043
7188
  AuthError,
7044
7189
  CONTENT_ENTRY_LOCATOR_SELECTOR,
@@ -7050,6 +7195,7 @@ export {
7050
7195
  RateLimitError,
7051
7196
  WEBSITE_MARKETING_CONTACT_PLACEMENTS,
7052
7197
  WebsiteError,
7198
+ activeFormValues,
7053
7199
  buildStorageContentMediaUrls,
7054
7200
  buildStorageFileUrl,
7055
7201
  buildStorageMediaUrls,
@@ -7057,7 +7203,9 @@ export {
7057
7203
  buildStorageTransformedFileUrl,
7058
7204
  captureWebsiteMarketingAttribution,
7059
7205
  createBacanoClient,
7206
+ createFlowsClient,
7060
7207
  createWebsiteMarketingRuntime,
7208
+ formRootFields,
7061
7209
  formatMediaSrcSet,
7062
7210
  getAllowedMarketingParameters,
7063
7211
  getContentEntryLocatorAttributes,
@@ -7071,5 +7219,6 @@ export {
7071
7219
  getWebsiteMarketingAttribution,
7072
7220
  getWebsitePublicConfig,
7073
7221
  resolveProductPricing,
7074
- sanitizeMarketingUrl
7222
+ sanitizeMarketingUrl,
7223
+ visibleFormFields
7075
7224
  };
@@ -0,0 +1,6 @@
1
+ import type { WebsiteMarketingBrowserEvent } from "./types";
2
+ /** Preserve the shared Google queue, including an existing native gtag.js. */
3
+ export declare function prepareGtm(containerId: string): void;
4
+ export declare function loadGtmScript(containerId: string, onError: () => void): void;
5
+ /** Explicit allowlist: never spread caller input, customer data or attribution. */
6
+ export declare function sendGtm(event: WebsiteMarketingBrowserEvent): void;
@@ -36,7 +36,7 @@ export interface CheckoutModule {
36
36
  orderId: string;
37
37
  publicOrderToken: string;
38
38
  paymentSessionId: string;
39
- gatewayTransactionId: string;
39
+ gatewayTransactionId?: string;
40
40
  }): Promise<WebsitePaymentSessionResult>;
41
41
  }
42
42
  export declare function createCheckoutModule(graphqlUrl: string, getHeaders: RequestHeadersProvider, getContext: () => BacanoClientContext, cartSessionStorage: CartSessionStorage): CheckoutModule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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,9 @@
1
+ import type { FlowPublishedForm } from "../flows";
2
+ import type { BacanoFormClassNames, BacanoFormValues } from "./forms";
3
+ /** Reuses the Forms controls but never calls a website form-entry endpoint. */
4
+ export declare function BacanoFlowForm({ form, onSubmit, onRefresh, classNames, }: {
5
+ form: FlowPublishedForm;
6
+ onSubmit(values: BacanoFormValues, form: FlowPublishedForm): Promise<void>;
7
+ onRefresh(): Promise<void>;
8
+ classNames?: BacanoFormClassNames;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import type { WebsiteFormField } from "../types";
2
+ /** Discrete radios preserve keyboard support, required validation and an empty initial answer. */
3
+ export declare function FormScaleControl({ field, id, value, onChange, className, }: {
4
+ field: WebsiteFormField;
5
+ id: string;
6
+ value: string | string[] | boolean;
7
+ onChange(value: string): void;
8
+ className?: string;
9
+ }): import("react").JSX.Element | null;