@bacano/sdk 1.1.20 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/STOREFRONT_FORMS.md +7 -0
- package/STOREFRONT_MARKETING.md +161 -6
- package/index.cjs +322 -86
- package/index.js +322 -86
- 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/gtm.d.ts +6 -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 +230 -93
- package/react/forms.d.ts +6 -2
- package/react/forms.js +222 -85
- package/react/marketing-provider.d.ts +2 -1
- package/react.cjs +411 -136
- package/react.d.ts +1 -0
- package/react.js +394 -119
- 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/types.d.ts +4 -0
package/react.js
CHANGED
|
@@ -4093,8 +4093,8 @@ function createCatalogModule(graphqlUrl, getHeaders, getContext, storageUrl) {
|
|
|
4093
4093
|
};
|
|
4094
4094
|
}
|
|
4095
4095
|
|
|
4096
|
-
// libs/sdk/src/react/
|
|
4097
|
-
import { useCallback, useContext,
|
|
4096
|
+
// libs/sdk/src/react/cta-tracking.ts
|
|
4097
|
+
import { useCallback, useContext, useRef } from "react";
|
|
4098
4098
|
|
|
4099
4099
|
// libs/sdk/src/react/context-registry.ts
|
|
4100
4100
|
import { createContext } from "react";
|
|
@@ -4122,13 +4122,6 @@ function getOrCreateReactContext(key, defaultValue) {
|
|
|
4122
4122
|
return context;
|
|
4123
4123
|
}
|
|
4124
4124
|
|
|
4125
|
-
// libs/sdk/src/react/context.ts
|
|
4126
|
-
var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
|
|
4127
|
-
client: null,
|
|
4128
|
-
loading: true,
|
|
4129
|
-
error: null
|
|
4130
|
-
});
|
|
4131
|
-
|
|
4132
4125
|
// libs/sdk/src/react/marketing-context.ts
|
|
4133
4126
|
var NOOP_MARKETING_RUNTIME = {
|
|
4134
4127
|
enabled: false,
|
|
@@ -4142,15 +4135,44 @@ var BacanoMarketingContext = getOrCreateReactContext(
|
|
|
4142
4135
|
NOOP_MARKETING_RUNTIME
|
|
4143
4136
|
);
|
|
4144
4137
|
|
|
4138
|
+
// libs/sdk/src/react/cta-tracking.ts
|
|
4139
|
+
function useBacanoCta(context) {
|
|
4140
|
+
const runtime = useContext(BacanoMarketingContext);
|
|
4141
|
+
const lastClick = useRef({ identity: "", at: -Infinity });
|
|
4142
|
+
const { ctaId, formKey, placement } = context;
|
|
4143
|
+
return useCallback(() => {
|
|
4144
|
+
if (!runtime.enabled) return;
|
|
4145
|
+
const identity = `${ctaId}:${formKey ?? ""}:${placement ?? ""}`;
|
|
4146
|
+
const at = Date.now();
|
|
4147
|
+
if (identity === lastClick.current.identity && at - lastClick.current.at < 400)
|
|
4148
|
+
return;
|
|
4149
|
+
lastClick.current = { identity, at };
|
|
4150
|
+
try {
|
|
4151
|
+
runtime.track({ eventName: "cta_click", ctaId, formKey, placement });
|
|
4152
|
+
} catch {
|
|
4153
|
+
}
|
|
4154
|
+
}, [runtime, ctaId, formKey, placement]);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// libs/sdk/src/react/hooks.ts
|
|
4158
|
+
import { useCallback as useCallback2, useContext as useContext2, useEffect, useRef as useRef2, useState } from "react";
|
|
4159
|
+
|
|
4160
|
+
// libs/sdk/src/react/context.ts
|
|
4161
|
+
var BacanoReactContext = getOrCreateReactContext("bacano-client-v1", {
|
|
4162
|
+
client: null,
|
|
4163
|
+
loading: true,
|
|
4164
|
+
error: null
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4145
4167
|
// libs/sdk/src/react/hooks.ts
|
|
4146
4168
|
function useClient() {
|
|
4147
|
-
const { client, loading, error } =
|
|
4169
|
+
const { client, loading, error } = useContext2(BacanoReactContext);
|
|
4148
4170
|
if (error) throw error;
|
|
4149
4171
|
if (loading || !client) throw new NotInitializedError();
|
|
4150
4172
|
return client;
|
|
4151
4173
|
}
|
|
4152
4174
|
function useSafeClient() {
|
|
4153
|
-
const { client } =
|
|
4175
|
+
const { client } = useContext2(BacanoReactContext);
|
|
4154
4176
|
return client;
|
|
4155
4177
|
}
|
|
4156
4178
|
function useAsync(fetcher, deps = []) {
|
|
@@ -4160,8 +4182,8 @@ function useAsync(fetcher, deps = []) {
|
|
|
4160
4182
|
loading: true,
|
|
4161
4183
|
error: null
|
|
4162
4184
|
});
|
|
4163
|
-
const mountedRef =
|
|
4164
|
-
const fetchData =
|
|
4185
|
+
const mountedRef = useRef2(true);
|
|
4186
|
+
const fetchData = useCallback2(async () => {
|
|
4165
4187
|
if (!client) return;
|
|
4166
4188
|
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
4167
4189
|
try {
|
|
@@ -4192,10 +4214,10 @@ function useBacano() {
|
|
|
4192
4214
|
return useClient();
|
|
4193
4215
|
}
|
|
4194
4216
|
function useBacanoState() {
|
|
4195
|
-
return
|
|
4217
|
+
return useContext2(BacanoReactContext);
|
|
4196
4218
|
}
|
|
4197
4219
|
function useMarketing() {
|
|
4198
|
-
return
|
|
4220
|
+
return useContext2(BacanoMarketingContext);
|
|
4199
4221
|
}
|
|
4200
4222
|
function useProducts(opts) {
|
|
4201
4223
|
return useAsync(
|
|
@@ -4289,7 +4311,7 @@ function useCart() {
|
|
|
4289
4311
|
const [cart, setCart] = useState(null);
|
|
4290
4312
|
const [loading, setLoading] = useState(false);
|
|
4291
4313
|
const [error, setError] = useState(null);
|
|
4292
|
-
const refreshCart =
|
|
4314
|
+
const refreshCart = useCallback2(async () => {
|
|
4293
4315
|
if (!client) return;
|
|
4294
4316
|
setLoading(true);
|
|
4295
4317
|
try {
|
|
@@ -4305,7 +4327,7 @@ function useCart() {
|
|
|
4305
4327
|
useEffect(() => {
|
|
4306
4328
|
refreshCart();
|
|
4307
4329
|
}, [refreshCart]);
|
|
4308
|
-
const addItem =
|
|
4330
|
+
const addItem = useCallback2(
|
|
4309
4331
|
async (productVariantId, quantity) => {
|
|
4310
4332
|
if (!client) throw new NotInitializedError();
|
|
4311
4333
|
const item = await client.cart.addItem(productVariantId, quantity);
|
|
@@ -4314,7 +4336,7 @@ function useCart() {
|
|
|
4314
4336
|
},
|
|
4315
4337
|
[client, refreshCart]
|
|
4316
4338
|
);
|
|
4317
|
-
const updateItem =
|
|
4339
|
+
const updateItem = useCallback2(
|
|
4318
4340
|
async (cartItemId, quantity) => {
|
|
4319
4341
|
if (!client) throw new NotInitializedError();
|
|
4320
4342
|
const result = await client.cart.updateItem(cartItemId, quantity);
|
|
@@ -4323,7 +4345,7 @@ function useCart() {
|
|
|
4323
4345
|
},
|
|
4324
4346
|
[client, refreshCart]
|
|
4325
4347
|
);
|
|
4326
|
-
const removeItem =
|
|
4348
|
+
const removeItem = useCallback2(
|
|
4327
4349
|
async (cartItemId) => {
|
|
4328
4350
|
if (!client) throw new NotInitializedError();
|
|
4329
4351
|
await client.cart.removeItem(cartItemId);
|
|
@@ -4331,7 +4353,7 @@ function useCart() {
|
|
|
4331
4353
|
},
|
|
4332
4354
|
[client, refreshCart]
|
|
4333
4355
|
);
|
|
4334
|
-
const clear =
|
|
4356
|
+
const clear = useCallback2(async () => {
|
|
4335
4357
|
if (!client) throw new NotInitializedError();
|
|
4336
4358
|
setLoading(true);
|
|
4337
4359
|
try {
|
|
@@ -4346,7 +4368,7 @@ function useCart() {
|
|
|
4346
4368
|
setLoading(false);
|
|
4347
4369
|
}
|
|
4348
4370
|
}, [client]);
|
|
4349
|
-
const getOrCreate =
|
|
4371
|
+
const getOrCreate = useCallback2(
|
|
4350
4372
|
async (contact, fulfillmentFamily) => {
|
|
4351
4373
|
if (!client) throw new NotInitializedError();
|
|
4352
4374
|
const c = await client.cart.getOrCreate(contact, fulfillmentFamily);
|
|
@@ -4355,7 +4377,7 @@ function useCart() {
|
|
|
4355
4377
|
},
|
|
4356
4378
|
[client]
|
|
4357
4379
|
);
|
|
4358
|
-
const startCheckout =
|
|
4380
|
+
const startCheckout = useCallback2(
|
|
4359
4381
|
async (input) => {
|
|
4360
4382
|
if (!client) throw new NotInitializedError();
|
|
4361
4383
|
const c = await client.cart.startCheckout(input);
|
|
@@ -4381,7 +4403,7 @@ function useCheckout() {
|
|
|
4381
4403
|
const client = useSafeClient();
|
|
4382
4404
|
const [loading, setLoading] = useState(false);
|
|
4383
4405
|
const [error, setError] = useState(null);
|
|
4384
|
-
const checkout =
|
|
4406
|
+
const checkout = useCallback2(
|
|
4385
4407
|
async (input) => {
|
|
4386
4408
|
if (!client) throw new NotInitializedError();
|
|
4387
4409
|
setLoading(true);
|
|
@@ -4421,7 +4443,7 @@ function useProfile() {
|
|
|
4421
4443
|
error,
|
|
4422
4444
|
refetch
|
|
4423
4445
|
} = useAsync((client2) => client2.profile.get());
|
|
4424
|
-
const updateProfile =
|
|
4446
|
+
const updateProfile = useCallback2(
|
|
4425
4447
|
async (data) => {
|
|
4426
4448
|
if (!client) throw new NotInitializedError();
|
|
4427
4449
|
const updated = await client.profile.update(data);
|
|
@@ -4435,7 +4457,7 @@ function useProfile() {
|
|
|
4435
4457
|
|
|
4436
4458
|
// libs/sdk/src/react/marketing-provider.tsx
|
|
4437
4459
|
import {
|
|
4438
|
-
useContext as
|
|
4460
|
+
useContext as useContext4,
|
|
4439
4461
|
useEffect as useEffect2,
|
|
4440
4462
|
useState as useState2
|
|
4441
4463
|
} from "react";
|
|
@@ -4478,6 +4500,9 @@ var ALLOWED_MARKETING_PARAMETERS = /* @__PURE__ */ new Set([
|
|
|
4478
4500
|
function sanitizeMarketingUrl(input, baseUrl) {
|
|
4479
4501
|
try {
|
|
4480
4502
|
const url = new URL(input, baseUrl);
|
|
4503
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
|
4504
|
+
url.username = "";
|
|
4505
|
+
url.password = "";
|
|
4481
4506
|
const safeParameters = new URLSearchParams();
|
|
4482
4507
|
for (const [key, value] of url.searchParams.entries()) {
|
|
4483
4508
|
if (ALLOWED_MARKETING_PARAMETERS.has(key.toLowerCase()) && !SENSITIVE_PARAMETER_PATTERNS.some((pattern) => pattern.test(key))) {
|
|
@@ -4697,50 +4722,65 @@ function serializeWebsiteMarketingAttribution(attribution) {
|
|
|
4697
4722
|
};
|
|
4698
4723
|
}
|
|
4699
4724
|
|
|
4700
|
-
// libs/sdk/src/marketing/
|
|
4701
|
-
var
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4725
|
+
// libs/sdk/src/marketing/interactions.ts
|
|
4726
|
+
var WEBSITE_INTERACTION_EVENTS = [
|
|
4727
|
+
"cta_click",
|
|
4728
|
+
"form_open",
|
|
4729
|
+
"form_start",
|
|
4730
|
+
"form_step_complete",
|
|
4731
|
+
"form_submit_attempt",
|
|
4732
|
+
"form_submit_error",
|
|
4733
|
+
"form_submit_success"
|
|
4734
|
+
];
|
|
4735
|
+
function isInteractionEvent(name) {
|
|
4736
|
+
return WEBSITE_INTERACTION_EVENTS.some((candidate) => candidate === name);
|
|
4737
|
+
}
|
|
4738
|
+
function isInteractionIdentifier(value) {
|
|
4739
|
+
return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
|
|
4740
|
+
}
|
|
4741
|
+
function isBrowserMarketingEvent(name) {
|
|
4742
|
+
return isInteractionEvent(name) || [
|
|
4743
|
+
"page_view",
|
|
4744
|
+
"search",
|
|
4745
|
+
"view_item_list",
|
|
4746
|
+
"select_item",
|
|
4747
|
+
"view_item",
|
|
4748
|
+
"add_to_cart",
|
|
4749
|
+
"remove_from_cart",
|
|
4750
|
+
"view_cart",
|
|
4751
|
+
"begin_checkout",
|
|
4752
|
+
"add_shipping_info",
|
|
4753
|
+
"add_payment_info",
|
|
4754
|
+
"contact"
|
|
4755
|
+
].includes(name);
|
|
4756
|
+
}
|
|
4757
|
+
function interactionParameters(event) {
|
|
4758
|
+
if (!isInteractionEvent(event.eventName)) return {};
|
|
4759
|
+
if (event.eventName === "cta_click" ? !isInteractionIdentifier(event.ctaId) : !isInteractionIdentifier(event.formKey))
|
|
4760
|
+
return null;
|
|
4761
|
+
if (event.eventName === "form_step_complete" && !isInteractionIdentifier(event.stepKey))
|
|
4762
|
+
return null;
|
|
4763
|
+
if (event.eventName === "form_submit_error" && !["validation", "network", "rejected", "unavailable"].includes(
|
|
4764
|
+
event.errorCategory ?? ""
|
|
4765
|
+
))
|
|
4766
|
+
return null;
|
|
4767
|
+
const params = {};
|
|
4768
|
+
for (const [key, value] of Object.entries({
|
|
4769
|
+
form_key: event.formKey,
|
|
4770
|
+
cta_id: event.ctaId,
|
|
4771
|
+
placement: event.placement,
|
|
4772
|
+
step_key: event.stepKey
|
|
4773
|
+
})) {
|
|
4774
|
+
if (value === void 0) continue;
|
|
4775
|
+
if (!isInteractionIdentifier(value)) return null;
|
|
4776
|
+
params[key] = value;
|
|
4714
4777
|
}
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
if (typeof document === "undefined") return;
|
|
4719
|
-
const id = `bacano-clarity-${projectId}`;
|
|
4720
|
-
if (document.getElementById(id)) return;
|
|
4721
|
-
const script = document.createElement("script");
|
|
4722
|
-
script.id = id;
|
|
4723
|
-
script.async = true;
|
|
4724
|
-
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
|
|
4725
|
-
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
4726
|
-
document.head.appendChild(script);
|
|
4727
|
-
}
|
|
4728
|
-
function sendClarityEvent(eventName) {
|
|
4729
|
-
clarityWindow()?.clarity?.("event", eventName);
|
|
4778
|
+
if (event.eventName === "form_submit_error" && event.errorCategory)
|
|
4779
|
+
params.error_category = event.errorCategory;
|
|
4780
|
+
return params;
|
|
4730
4781
|
}
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
|
|
4734
|
-
if (canonicalDomain === void 0) return true;
|
|
4735
|
-
if (!canonicalDomain?.trim()) return false;
|
|
4736
|
-
try {
|
|
4737
|
-
const configuredHost = new URL(
|
|
4738
|
-
canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
|
|
4739
|
-
).hostname.toLowerCase();
|
|
4740
|
-
return currentHostname.toLowerCase() === configuredHost;
|
|
4741
|
-
} catch {
|
|
4742
|
-
return false;
|
|
4743
|
-
}
|
|
4782
|
+
function newMarketingEventId() {
|
|
4783
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
4744
4784
|
}
|
|
4745
4785
|
|
|
4746
4786
|
// libs/sdk/src/marketing/ga4.ts
|
|
@@ -4767,19 +4807,20 @@ var WEBSITE_MARKETING_CONTACT_PLACEMENTS = [
|
|
|
4767
4807
|
"contact_page"
|
|
4768
4808
|
];
|
|
4769
4809
|
|
|
4770
|
-
// libs/sdk/src/marketing/
|
|
4810
|
+
// libs/sdk/src/marketing/browser-adapters.ts
|
|
4771
4811
|
var initializedGa4Destinations = /* @__PURE__ */ new Set();
|
|
4772
4812
|
var initializedMetaDestinations = /* @__PURE__ */ new Set();
|
|
4773
4813
|
function marketingWindow() {
|
|
4774
4814
|
return typeof window === "undefined" ? null : window;
|
|
4775
4815
|
}
|
|
4776
|
-
function loadScriptOnce(id, source) {
|
|
4816
|
+
function loadScriptOnce(id, source, onError) {
|
|
4777
4817
|
if (typeof document === "undefined" || document.getElementById(id)) return;
|
|
4778
4818
|
const script = document.createElement("script");
|
|
4779
4819
|
script.id = id;
|
|
4780
4820
|
script.async = true;
|
|
4781
4821
|
script.src = source;
|
|
4782
4822
|
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
4823
|
+
script.onerror = () => onError?.();
|
|
4783
4824
|
document.head.appendChild(script);
|
|
4784
4825
|
}
|
|
4785
4826
|
function prepareGa4(measurementId) {
|
|
@@ -4794,10 +4835,11 @@ function prepareGa4(measurementId) {
|
|
|
4794
4835
|
target.gtag("config", measurementId, { send_page_view: false });
|
|
4795
4836
|
initializedGa4Destinations.add(measurementId);
|
|
4796
4837
|
}
|
|
4797
|
-
function loadGa4Script(measurementId) {
|
|
4838
|
+
function loadGa4Script(measurementId, onError) {
|
|
4798
4839
|
loadScriptOnce(
|
|
4799
4840
|
`bacano-ga4-${measurementId}`,
|
|
4800
|
-
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}
|
|
4841
|
+
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(measurementId)}`,
|
|
4842
|
+
onError
|
|
4801
4843
|
);
|
|
4802
4844
|
}
|
|
4803
4845
|
function captureGa4ClientId(measurementId, websiteId, diagnostic) {
|
|
@@ -4845,10 +4887,11 @@ function prepareMeta(pixelId) {
|
|
|
4845
4887
|
target.fbq("init", pixelId);
|
|
4846
4888
|
initializedMetaDestinations.add(pixelId);
|
|
4847
4889
|
}
|
|
4848
|
-
function loadMetaScript() {
|
|
4890
|
+
function loadMetaScript(onError) {
|
|
4849
4891
|
loadScriptOnce(
|
|
4850
4892
|
"bacano-meta-pixel",
|
|
4851
|
-
"https://connect.facebook.net/en_US/fbevents.js"
|
|
4893
|
+
"https://connect.facebook.net/en_US/fbevents.js",
|
|
4894
|
+
onError
|
|
4852
4895
|
);
|
|
4853
4896
|
}
|
|
4854
4897
|
function scheduleScriptLoad(load, strategy) {
|
|
@@ -4912,10 +4955,12 @@ function contactPlacementFor(event) {
|
|
|
4912
4955
|
}
|
|
4913
4956
|
return placement;
|
|
4914
4957
|
}
|
|
4915
|
-
function sendGa4(event, diagnosticMode) {
|
|
4958
|
+
function sendGa4(event, diagnosticMode, measurementId) {
|
|
4916
4959
|
const target = marketingWindow();
|
|
4917
4960
|
const contactPlacement = contactPlacementFor(event);
|
|
4918
4961
|
target?.gtag?.("event", event.eventName, {
|
|
4962
|
+
send_to: measurementId,
|
|
4963
|
+
...interactionParameters(event),
|
|
4919
4964
|
currency: event.currency,
|
|
4920
4965
|
value: event.value,
|
|
4921
4966
|
search_term: event.searchTerm,
|
|
@@ -4932,15 +4977,17 @@ function sendGa4(event, diagnosticMode) {
|
|
|
4932
4977
|
debug_mode: diagnosticMode ? true : void 0
|
|
4933
4978
|
});
|
|
4934
4979
|
}
|
|
4935
|
-
function sendMeta(event) {
|
|
4980
|
+
function sendMeta(event, pixelId) {
|
|
4936
4981
|
const target = marketingWindow();
|
|
4937
4982
|
const eventName = metaEventName(event.eventName);
|
|
4938
4983
|
const isStandard = eventName !== event.eventName;
|
|
4939
4984
|
const contactPlacement = contactPlacementFor(event);
|
|
4940
4985
|
target?.fbq?.(
|
|
4941
|
-
isStandard ? "
|
|
4986
|
+
isStandard ? "trackSingle" : "trackSingleCustom",
|
|
4987
|
+
pixelId,
|
|
4942
4988
|
eventName,
|
|
4943
4989
|
{
|
|
4990
|
+
...interactionParameters(event),
|
|
4944
4991
|
currency: event.currency,
|
|
4945
4992
|
value: event.value,
|
|
4946
4993
|
search_string: event.searchTerm,
|
|
@@ -4957,40 +5004,192 @@ function sendMeta(event) {
|
|
|
4957
5004
|
{ eventID: event.eventId }
|
|
4958
5005
|
);
|
|
4959
5006
|
}
|
|
4960
|
-
|
|
4961
|
-
|
|
5007
|
+
|
|
5008
|
+
// libs/sdk/src/marketing/clarity.ts
|
|
5009
|
+
var initializedDestinations = /* @__PURE__ */ new Set();
|
|
5010
|
+
function clarityWindow() {
|
|
5011
|
+
return typeof window === "undefined" ? null : window;
|
|
5012
|
+
}
|
|
5013
|
+
function prepareClarity(projectId) {
|
|
5014
|
+
const target = clarityWindow();
|
|
5015
|
+
if (!target || initializedDestinations.has(projectId)) return;
|
|
5016
|
+
if (!target.clarity) {
|
|
5017
|
+
const clarity = function() {
|
|
5018
|
+
clarity.q ??= [];
|
|
5019
|
+
clarity.q.push(arguments);
|
|
5020
|
+
};
|
|
5021
|
+
target.clarity = clarity;
|
|
5022
|
+
}
|
|
5023
|
+
initializedDestinations.add(projectId);
|
|
4962
5024
|
}
|
|
5025
|
+
function loadClarityScript(projectId, onError) {
|
|
5026
|
+
if (typeof document === "undefined") return;
|
|
5027
|
+
const id = `bacano-clarity-${projectId}`;
|
|
5028
|
+
if (document.getElementById(id)) return;
|
|
5029
|
+
const script = document.createElement("script");
|
|
5030
|
+
script.id = id;
|
|
5031
|
+
script.async = true;
|
|
5032
|
+
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(projectId)}`;
|
|
5033
|
+
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
5034
|
+
script.onerror = () => onError?.();
|
|
5035
|
+
document.head.appendChild(script);
|
|
5036
|
+
}
|
|
5037
|
+
function sendClarityEvent(eventName) {
|
|
5038
|
+
clarityWindow()?.clarity?.("event", eventName);
|
|
5039
|
+
}
|
|
5040
|
+
|
|
5041
|
+
// libs/sdk/src/marketing/domain.ts
|
|
5042
|
+
function matchesCanonicalMarketingHost(canonicalDomain, currentHostname) {
|
|
5043
|
+
if (canonicalDomain === void 0) return true;
|
|
5044
|
+
if (!canonicalDomain?.trim()) return false;
|
|
5045
|
+
try {
|
|
5046
|
+
const configuredHost = new URL(
|
|
5047
|
+
canonicalDomain.includes("://") ? canonicalDomain : `https://${canonicalDomain}`
|
|
5048
|
+
).hostname.toLowerCase();
|
|
5049
|
+
return currentHostname.toLowerCase() === configuredHost;
|
|
5050
|
+
} catch {
|
|
5051
|
+
return false;
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
|
|
5055
|
+
// libs/sdk/src/marketing/gtm.ts
|
|
5056
|
+
var CONTAINER_ID = /^GTM-[A-Z0-9]{4,32}$/;
|
|
5057
|
+
var preparedQueues = /* @__PURE__ */ new WeakMap();
|
|
5058
|
+
function prepareGtm(containerId) {
|
|
5059
|
+
const target = marketingWindow();
|
|
5060
|
+
if (!target || !CONTAINER_ID.test(containerId)) return;
|
|
5061
|
+
target.dataLayer ??= [];
|
|
5062
|
+
const containers = preparedQueues.get(target.dataLayer) ?? /* @__PURE__ */ new Set();
|
|
5063
|
+
if (containers.has(containerId)) return;
|
|
5064
|
+
target.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
|
|
5065
|
+
containers.add(containerId);
|
|
5066
|
+
preparedQueues.set(target.dataLayer, containers);
|
|
5067
|
+
}
|
|
5068
|
+
function loadGtmScript(containerId, onError) {
|
|
5069
|
+
const target = marketingWindow();
|
|
5070
|
+
if (!target || typeof document === "undefined" || !CONTAINER_ID.test(containerId))
|
|
5071
|
+
return;
|
|
5072
|
+
const id = `bacano-gtm-${containerId}`;
|
|
5073
|
+
if (document.getElementById(id)) return;
|
|
5074
|
+
prepareGtm(containerId);
|
|
5075
|
+
const script = document.createElement("script");
|
|
5076
|
+
script.id = id;
|
|
5077
|
+
script.async = true;
|
|
5078
|
+
script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
|
|
5079
|
+
script.referrerPolicy = "strict-origin-when-cross-origin";
|
|
5080
|
+
script.onerror = () => onError();
|
|
5081
|
+
document.head.appendChild(script);
|
|
5082
|
+
}
|
|
5083
|
+
function sendGtm(event) {
|
|
5084
|
+
const target = marketingWindow();
|
|
5085
|
+
if (!target?.dataLayer) return;
|
|
5086
|
+
target.dataLayer.push({ bacano: null });
|
|
5087
|
+
target.dataLayer.push({
|
|
5088
|
+
event: `bacano_${event.eventName}`,
|
|
5089
|
+
bacano: {
|
|
5090
|
+
schema_version: event.schemaVersion,
|
|
5091
|
+
event_name: event.eventName,
|
|
5092
|
+
event_id: event.eventId,
|
|
5093
|
+
...interactionParameters(event),
|
|
5094
|
+
page_location: event.pageLocation,
|
|
5095
|
+
currency: event.currency,
|
|
5096
|
+
value: event.value,
|
|
5097
|
+
items: mapGa4Items(event.items),
|
|
5098
|
+
item_list_id: event.itemListId,
|
|
5099
|
+
item_list_name: event.itemListName,
|
|
5100
|
+
shipping_tier: event.shippingTier,
|
|
5101
|
+
payment_type: event.paymentType,
|
|
5102
|
+
method: event.contactMethod,
|
|
5103
|
+
contact_placement: event.eventName === "contact" && WEBSITE_MARKETING_CONTACT_PLACEMENTS.some(
|
|
5104
|
+
(value) => value === event.contactPlacement
|
|
5105
|
+
) ? event.contactPlacement : void 0
|
|
5106
|
+
}
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
// libs/sdk/src/marketing/runtime.ts
|
|
4963
5111
|
function createWebsiteMarketingRuntime(options) {
|
|
4964
5112
|
let destroyed = false;
|
|
5113
|
+
const dispatched = /* @__PURE__ */ new Set();
|
|
5114
|
+
const observer = options.diagnostic;
|
|
5115
|
+
const diagnostic = (value) => {
|
|
5116
|
+
try {
|
|
5117
|
+
observer?.(value);
|
|
5118
|
+
} catch {
|
|
5119
|
+
}
|
|
5120
|
+
};
|
|
5121
|
+
options = { ...options, diagnostic };
|
|
4965
5122
|
let cancelScheduledLoad = () => void 0;
|
|
4966
5123
|
const target = marketingWindow();
|
|
4967
|
-
const enabled = options.config.enabled && Boolean(
|
|
5124
|
+
const enabled = options.config.enabled && options.measurementAllowed !== false && Boolean(
|
|
4968
5125
|
target && matchesCanonicalMarketingHost(
|
|
4969
5126
|
options.canonicalDomain,
|
|
4970
5127
|
target.location.hostname
|
|
4971
5128
|
)
|
|
4972
5129
|
) && Boolean(
|
|
4973
|
-
options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity
|
|
5130
|
+
options.config.destinations.ga4 || options.config.destinations.meta || options.config.destinations.clarity || options.config.destinations.gtm
|
|
4974
5131
|
);
|
|
5132
|
+
if (!enabled)
|
|
5133
|
+
diagnostic({
|
|
5134
|
+
level: "debug",
|
|
5135
|
+
code: !options.config.enabled ? "MARKETING_DISABLED" : options.measurementAllowed === false ? "MEASUREMENT_NOT_ALLOWED" : !target ? "SERVER_RENDER" : !matchesCanonicalMarketingHost(
|
|
5136
|
+
options.canonicalDomain,
|
|
5137
|
+
target.location.hostname
|
|
5138
|
+
) ? "DOMAIN_NOT_ALLOWED" : "PROVIDER_NOT_CONFIGURED",
|
|
5139
|
+
message: "Measurement is inactive; no events are emitted"
|
|
5140
|
+
});
|
|
4975
5141
|
if (enabled) {
|
|
5142
|
+
const guard = (provider, action) => {
|
|
5143
|
+
try {
|
|
5144
|
+
action();
|
|
5145
|
+
} catch {
|
|
5146
|
+
diagnostic({
|
|
5147
|
+
level: "warning",
|
|
5148
|
+
code: "RUNTIME_INITIALIZATION_FAILED",
|
|
5149
|
+
message: `${provider}: initialization failed`
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
const scriptError = (provider) => () => diagnostic({
|
|
5154
|
+
level: "warning",
|
|
5155
|
+
code: "SCRIPT_LOAD_FAILED",
|
|
5156
|
+
message: `${provider}: script could not load (network, CSP or blocker); external reception is unverified`
|
|
5157
|
+
});
|
|
4976
5158
|
try {
|
|
4977
5159
|
if (options.config.destinations.ga4) {
|
|
4978
|
-
|
|
5160
|
+
const { measurementId } = options.config.destinations.ga4;
|
|
5161
|
+
guard("GA4", () => prepareGa4(measurementId));
|
|
4979
5162
|
}
|
|
4980
5163
|
if (options.config.destinations.meta) {
|
|
4981
|
-
|
|
5164
|
+
const { pixelId } = options.config.destinations.meta;
|
|
5165
|
+
guard("Meta", () => prepareMeta(pixelId));
|
|
4982
5166
|
}
|
|
4983
5167
|
if (options.config.destinations.clarity) {
|
|
4984
|
-
|
|
5168
|
+
const { projectId } = options.config.destinations.clarity;
|
|
5169
|
+
guard("Clarity", () => prepareClarity(projectId));
|
|
5170
|
+
}
|
|
5171
|
+
if (options.config.destinations.gtm) {
|
|
5172
|
+
const { containerId } = options.config.destinations.gtm;
|
|
5173
|
+
guard("GTM", () => prepareGtm(containerId));
|
|
4985
5174
|
}
|
|
4986
5175
|
cancelScheduledLoad = scheduleScriptLoad(() => {
|
|
4987
5176
|
if (destroyed) return;
|
|
4988
5177
|
if (options.config.destinations.ga4) {
|
|
4989
|
-
|
|
5178
|
+
const { measurementId } = options.config.destinations.ga4;
|
|
5179
|
+
guard("GA4", () => loadGa4Script(measurementId, scriptError("GA4")));
|
|
4990
5180
|
}
|
|
4991
|
-
if (options.config.destinations.meta)
|
|
5181
|
+
if (options.config.destinations.meta)
|
|
5182
|
+
guard("Meta", () => loadMetaScript(scriptError("Meta")));
|
|
4992
5183
|
if (options.config.destinations.clarity) {
|
|
4993
|
-
|
|
5184
|
+
const { projectId } = options.config.destinations.clarity;
|
|
5185
|
+
guard(
|
|
5186
|
+
"Clarity",
|
|
5187
|
+
() => loadClarityScript(projectId, scriptError("Clarity"))
|
|
5188
|
+
);
|
|
5189
|
+
}
|
|
5190
|
+
if (options.config.destinations.gtm) {
|
|
5191
|
+
const { containerId } = options.config.destinations.gtm;
|
|
5192
|
+
guard("GTM", () => loadGtmScript(containerId, scriptError("GTM")));
|
|
4994
5193
|
}
|
|
4995
5194
|
}, options.scriptLoadStrategy);
|
|
4996
5195
|
captureWebsiteMarketingAttribution(
|
|
@@ -5012,11 +5211,11 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
5012
5211
|
if (options.config.destinations.meta) {
|
|
5013
5212
|
refreshWebsiteMarketingMetaContext(options.websiteId);
|
|
5014
5213
|
}
|
|
5015
|
-
} catch
|
|
5214
|
+
} catch {
|
|
5016
5215
|
options.diagnostic?.({
|
|
5017
5216
|
level: "warning",
|
|
5018
5217
|
code: "RUNTIME_INITIALIZATION_FAILED",
|
|
5019
|
-
message:
|
|
5218
|
+
message: "Marketing initialization failed; external reception is unverified"
|
|
5020
5219
|
});
|
|
5021
5220
|
}
|
|
5022
5221
|
}
|
|
@@ -5024,41 +5223,99 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
5024
5223
|
enabled,
|
|
5025
5224
|
track(input) {
|
|
5026
5225
|
if (!enabled || destroyed) return;
|
|
5226
|
+
if (!isBrowserMarketingEvent(input.eventName)) {
|
|
5227
|
+
diagnostic({
|
|
5228
|
+
level: "warning",
|
|
5229
|
+
code: "INVALID_EVENT",
|
|
5230
|
+
message: "Unknown or server-owned event rejected"
|
|
5231
|
+
});
|
|
5232
|
+
return;
|
|
5233
|
+
}
|
|
5234
|
+
try {
|
|
5235
|
+
captureWebsiteMarketingAttribution(
|
|
5236
|
+
options.websiteId,
|
|
5237
|
+
options.config.attributionWindowDays
|
|
5238
|
+
);
|
|
5239
|
+
} catch {
|
|
5240
|
+
}
|
|
5027
5241
|
const event = {
|
|
5028
5242
|
...input,
|
|
5029
5243
|
schemaVersion: 1,
|
|
5030
|
-
eventId: input.eventId ??
|
|
5244
|
+
eventId: input.eventId ?? newMarketingEventId(),
|
|
5031
5245
|
occurredAt: input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5032
5246
|
currency: input.currency ?? options.config.currencyCode,
|
|
5033
5247
|
pageLocation: sanitizeMarketingUrl(
|
|
5034
5248
|
input.pageLocation ?? (typeof window === "undefined" ? "" : window.location.href)
|
|
5035
5249
|
)
|
|
5036
5250
|
};
|
|
5037
|
-
|
|
5038
|
-
if (
|
|
5039
|
-
|
|
5251
|
+
if (isInteractionEvent(event.eventName)) {
|
|
5252
|
+
if (!interactionParameters(event) || !/^[a-zA-Z0-9_-]{1,100}$/.test(event.eventId)) {
|
|
5253
|
+
diagnostic({
|
|
5254
|
+
level: "warning",
|
|
5255
|
+
code: "INVALID_INTERACTION",
|
|
5256
|
+
message: "Interaction rejected by the public contract",
|
|
5257
|
+
eventName: event.eventName
|
|
5258
|
+
});
|
|
5259
|
+
return;
|
|
5040
5260
|
}
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5261
|
+
for (const key of [
|
|
5262
|
+
"currency",
|
|
5263
|
+
"value",
|
|
5264
|
+
"searchTerm",
|
|
5265
|
+
"itemListId",
|
|
5266
|
+
"itemListName",
|
|
5267
|
+
"shippingTier",
|
|
5268
|
+
"paymentType",
|
|
5269
|
+
"contactMethod",
|
|
5270
|
+
"contactPlacement",
|
|
5271
|
+
"items",
|
|
5272
|
+
"pageTitle"
|
|
5273
|
+
])
|
|
5274
|
+
delete event[key];
|
|
5275
|
+
}
|
|
5276
|
+
const identity = `${event.eventName}:${event.eventId}`;
|
|
5277
|
+
if (dispatched.has(identity)) return;
|
|
5278
|
+
dispatched.add(identity);
|
|
5279
|
+
const oldest = dispatched.values().next().value;
|
|
5280
|
+
if (dispatched.size > 1e3 && oldest) dispatched.delete(oldest);
|
|
5281
|
+
const dispatch = (provider, send) => {
|
|
5282
|
+
try {
|
|
5283
|
+
send();
|
|
5284
|
+
diagnostic({
|
|
5285
|
+
level: "debug",
|
|
5286
|
+
code: "EVENT_DISPATCHED",
|
|
5287
|
+
message: `${provider}: passed to adapter; external reception is unverified`,
|
|
5288
|
+
eventName: event.eventName
|
|
5289
|
+
});
|
|
5290
|
+
} catch {
|
|
5291
|
+
diagnostic({
|
|
5292
|
+
level: "warning",
|
|
5293
|
+
code: "EVENT_DISPATCH_FAILED",
|
|
5294
|
+
message: `${provider}: adapter failed`,
|
|
5295
|
+
eventName: event.eventName
|
|
5296
|
+
});
|
|
5044
5297
|
}
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5298
|
+
};
|
|
5299
|
+
if (options.config.destinations.ga4) {
|
|
5300
|
+
const ga4 = options.config.destinations.ga4;
|
|
5301
|
+
dispatch(
|
|
5302
|
+
"GA4",
|
|
5303
|
+
() => sendGa4(event, ga4.diagnosticMode, ga4.measurementId)
|
|
5304
|
+
);
|
|
5305
|
+
}
|
|
5306
|
+
if (options.config.destinations.meta) {
|
|
5307
|
+
const meta = options.config.destinations.meta;
|
|
5308
|
+
dispatch("Meta", () => sendMeta(event, meta.pixelId));
|
|
5309
|
+
}
|
|
5310
|
+
if (options.config.destinations.clarity) {
|
|
5311
|
+
dispatch("Clarity", () => sendClarityEvent(event.eventName));
|
|
5312
|
+
}
|
|
5313
|
+
if (options.config.destinations.gtm && /^GTM-[A-Z0-9]{4,32}$/.test(options.config.destinations.gtm.containerId)) {
|
|
5314
|
+
dispatch("GTM", () => sendGtm(event));
|
|
5058
5315
|
}
|
|
5059
5316
|
},
|
|
5060
5317
|
captureAttribution() {
|
|
5061
|
-
if (!enabled) return null;
|
|
5318
|
+
if (!enabled || destroyed) return null;
|
|
5062
5319
|
captureWebsiteMarketingAttribution(
|
|
5063
5320
|
options.websiteId,
|
|
5064
5321
|
options.config.attributionWindowDays
|
|
@@ -5069,7 +5326,7 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
5069
5326
|
return getWebsiteMarketingAttribution(options.websiteId);
|
|
5070
5327
|
},
|
|
5071
5328
|
getAttribution() {
|
|
5072
|
-
if (!enabled) return null;
|
|
5329
|
+
if (!enabled || destroyed) return null;
|
|
5073
5330
|
if (options.config.destinations.meta) {
|
|
5074
5331
|
refreshWebsiteMarketingMetaContext(options.websiteId);
|
|
5075
5332
|
}
|
|
@@ -5083,7 +5340,7 @@ function createWebsiteMarketingRuntime(options) {
|
|
|
5083
5340
|
}
|
|
5084
5341
|
|
|
5085
5342
|
// libs/sdk/src/react/preview/context.ts
|
|
5086
|
-
import { useContext as
|
|
5343
|
+
import { useContext as useContext3 } from "react";
|
|
5087
5344
|
var BacanoPreviewModeContext = getOrCreateReactContext(
|
|
5088
5345
|
"bacano-preview-mode-v1",
|
|
5089
5346
|
false
|
|
@@ -5099,15 +5356,24 @@ function BacanoMarketingProvider({
|
|
|
5099
5356
|
publicConfig,
|
|
5100
5357
|
children,
|
|
5101
5358
|
scriptLoadStrategy,
|
|
5102
|
-
diagnostic
|
|
5359
|
+
diagnostic,
|
|
5360
|
+
measurementAllowed
|
|
5103
5361
|
}) {
|
|
5104
|
-
const isPreview =
|
|
5362
|
+
const isPreview = useContext4(BacanoPreviewModeContext);
|
|
5105
5363
|
const [runtime, setRuntime] = useState2(
|
|
5106
5364
|
NOOP_MARKETING_RUNTIME
|
|
5107
5365
|
);
|
|
5108
5366
|
useEffect2(() => {
|
|
5109
5367
|
if (isPreview || !publicConfig) {
|
|
5110
5368
|
setRuntime(NOOP_MARKETING_RUNTIME);
|
|
5369
|
+
try {
|
|
5370
|
+
diagnostic?.({
|
|
5371
|
+
level: "debug",
|
|
5372
|
+
code: isPreview ? "PREVIEW_DISABLED" : "PUBLIC_CONFIG_UNAVAILABLE",
|
|
5373
|
+
message: "Measurement is inactive"
|
|
5374
|
+
});
|
|
5375
|
+
} catch {
|
|
5376
|
+
}
|
|
5111
5377
|
return;
|
|
5112
5378
|
}
|
|
5113
5379
|
const next = createWebsiteMarketingRuntime({
|
|
@@ -5115,22 +5381,29 @@ function BacanoMarketingProvider({
|
|
|
5115
5381
|
config: publicConfig.marketing,
|
|
5116
5382
|
canonicalDomain: publicConfig.domain,
|
|
5117
5383
|
scriptLoadStrategy,
|
|
5118
|
-
diagnostic
|
|
5384
|
+
diagnostic,
|
|
5385
|
+
measurementAllowed
|
|
5119
5386
|
});
|
|
5120
5387
|
setRuntime(next);
|
|
5121
5388
|
return () => next.destroy();
|
|
5122
|
-
}, [
|
|
5389
|
+
}, [
|
|
5390
|
+
diagnostic,
|
|
5391
|
+
isPreview,
|
|
5392
|
+
publicConfig,
|
|
5393
|
+
scriptLoadStrategy,
|
|
5394
|
+
measurementAllowed
|
|
5395
|
+
]);
|
|
5123
5396
|
return /* @__PURE__ */ jsx(
|
|
5124
5397
|
BacanoMarketingContext.Provider,
|
|
5125
5398
|
{
|
|
5126
|
-
value: isPreview ? NOOP_MARKETING_RUNTIME : runtime,
|
|
5399
|
+
value: isPreview || measurementAllowed === false ? NOOP_MARKETING_RUNTIME : runtime,
|
|
5127
5400
|
children
|
|
5128
5401
|
}
|
|
5129
5402
|
);
|
|
5130
5403
|
}
|
|
5131
5404
|
|
|
5132
5405
|
// libs/sdk/src/react/provider.tsx
|
|
5133
|
-
import { useEffect as useEffect3, useRef as
|
|
5406
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
5134
5407
|
|
|
5135
5408
|
// libs/sdk/src/modules/auth.ts
|
|
5136
5409
|
function createAuthHeaders(accessToken) {
|
|
@@ -5314,7 +5587,8 @@ async function getWebsitePublicConfig(options) {
|
|
|
5314
5587
|
pixelId: marketing.destinations.meta.pixel_id,
|
|
5315
5588
|
diagnosticMode: marketing.destinations.meta.diagnostic_mode ?? false
|
|
5316
5589
|
} : null,
|
|
5317
|
-
clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null
|
|
5590
|
+
clarity: marketing.destinations.clarity ? { projectId: marketing.destinations.clarity.project_id } : null,
|
|
5591
|
+
...marketing.destinations.gtm ? { gtm: { containerId: marketing.destinations.gtm.container_id } } : {}
|
|
5318
5592
|
}
|
|
5319
5593
|
},
|
|
5320
5594
|
legal: {
|
|
@@ -7241,7 +7515,7 @@ function BacanoProvider({
|
|
|
7241
7515
|
config,
|
|
7242
7516
|
children
|
|
7243
7517
|
}) {
|
|
7244
|
-
const getAccessTokenRef =
|
|
7518
|
+
const getAccessTokenRef = useRef3(config.getAccessToken);
|
|
7245
7519
|
getAccessTokenRef.current = config.getAccessToken;
|
|
7246
7520
|
const [state, setState] = useState3({
|
|
7247
7521
|
client: null,
|
|
@@ -7460,6 +7734,7 @@ export {
|
|
|
7460
7734
|
useAttributeList,
|
|
7461
7735
|
useAvailability,
|
|
7462
7736
|
useBacano,
|
|
7737
|
+
useBacanoCta,
|
|
7463
7738
|
useBacanoState,
|
|
7464
7739
|
useBrands,
|
|
7465
7740
|
useCart,
|