@shoppexio/storefront 1.0.101 → 1.0.103
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/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/dist/index.cjs +196 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +196 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.103
|
|
4
|
+
|
|
5
|
+
- Web analytics (PR #3989): after a yes to analytics on the cookie banner, the SDK keeps an analytics session (30 idle minutes) and the session ids of the last 30 days for checkout, so an order keeps the visit it came from. A yes given without a reload starts the session on the page already counted (`session_start`, no second page view). Time on screen goes to the session the page was viewed in, also after that session went idle.
|
|
6
|
+
|
|
7
|
+
## 1.0.102
|
|
8
|
+
|
|
9
|
+
- The checkout URL carries the buyer's cookie banner answer in its hash (`#shoppex_consent=…`, PR #3938), so the full hosted checkout page sends the shop's Purchase tags under the same consent. The hash never reaches a server; without an answer on a shop with the banner on, the checkout sends no tags.
|
|
10
|
+
|
|
3
11
|
## 1.0.101
|
|
4
12
|
|
|
5
13
|
- Supplier sourcing is retired (PR #3825): `Product.supplier_backed` and `ProductVariant.supplier_backed` are gone, and `orderable` no longer turns an empty local stock into unlimited stock. Stock helpers read local units only; `orderable` stays the buy/sold-out gate.
|
package/README.md
CHANGED
|
@@ -107,8 +107,8 @@ if (firstOrder) {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
The publishable key
|
|
111
|
-
**
|
|
110
|
+
The publishable key comes from **Settings -> Developer -> Headless**; the
|
|
111
|
+
current browser origin must be an allowed site in **Store -> Embed Hub**. `rememberMe` is opt-in and keeps
|
|
112
112
|
the buyer signed in for up to 30 days.
|
|
113
113
|
|
|
114
114
|
The order detail includes delivery status, service instructions, codes, serials,
|
package/dist/index.cjs
CHANGED
|
@@ -399,27 +399,9 @@ function buildEndpoint(template, params) {
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// ../sdk/src/core/telemetry.ts
|
|
402
|
-
var CONNECTION_ID_STORAGE_PREFIX = "presence_connection_";
|
|
403
402
|
var CLIENT_ERROR_DEDUPE_WINDOW_MS = 15e3;
|
|
404
403
|
var CLIENT_ERROR_TEXT_MAX_LENGTH = 500;
|
|
405
404
|
var CLIENT_ERROR_URL_MAX_LENGTH = 2e3;
|
|
406
|
-
function createPresenceConnectionId() {
|
|
407
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
408
|
-
return crypto.randomUUID();
|
|
409
|
-
}
|
|
410
|
-
return `spx_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
411
|
-
}
|
|
412
|
-
function getStorefrontConnectionId(storeSlug) {
|
|
413
|
-
if (typeof window === "undefined") return null;
|
|
414
|
-
const storageKey2 = `${CONNECTION_ID_STORAGE_PREFIX}${storeSlug}`;
|
|
415
|
-
const existing = getItem(storageKey2);
|
|
416
|
-
if (existing && existing.trim()) {
|
|
417
|
-
return existing;
|
|
418
|
-
}
|
|
419
|
-
const nextId = createPresenceConnectionId();
|
|
420
|
-
setItem(storageKey2, nextId);
|
|
421
|
-
return nextId;
|
|
422
|
-
}
|
|
423
405
|
function getRecentClientErrorKey(payload) {
|
|
424
406
|
return [
|
|
425
407
|
payload.method,
|
|
@@ -449,7 +431,6 @@ async function reportStorefrontClientError(payload) {
|
|
|
449
431
|
const body = JSON.stringify({
|
|
450
432
|
event_type: "client_error",
|
|
451
433
|
referer: document.referrer || void 0,
|
|
452
|
-
connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
|
|
453
434
|
client_error: {
|
|
454
435
|
source: payload.source ?? "sdk",
|
|
455
436
|
phase: payload.phase ?? "request",
|
|
@@ -1634,6 +1615,138 @@ async function quoteCart(coupon, currency) {
|
|
|
1634
1615
|
return response;
|
|
1635
1616
|
}
|
|
1636
1617
|
|
|
1618
|
+
// ../contracts/src/storefront-consent-choice.ts
|
|
1619
|
+
var COOKIE_CONSENT_VERSION = 1;
|
|
1620
|
+
var COOKIE_CONSENT_COOKIE = "shoppex_consent";
|
|
1621
|
+
var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
|
|
1622
|
+
var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
|
|
1623
|
+
function parseCookieConsentValue(value) {
|
|
1624
|
+
const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
|
|
1625
|
+
return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// ../sdk/src/core/analytics-session.ts
|
|
1629
|
+
var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
|
|
1630
|
+
var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1631
|
+
var ANALYTICS_VISITS_MAX = 20;
|
|
1632
|
+
var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
|
|
1633
|
+
var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
|
|
1634
|
+
var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
|
|
1635
|
+
function readAnalyticsConsent() {
|
|
1636
|
+
if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
|
|
1637
|
+
const privacy = navigator;
|
|
1638
|
+
if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
|
|
1639
|
+
const prefix = `${COOKIE_CONSENT_COOKIE}=`;
|
|
1640
|
+
let raw;
|
|
1641
|
+
try {
|
|
1642
|
+
raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
|
|
1643
|
+
} catch {
|
|
1644
|
+
return "unknown";
|
|
1645
|
+
}
|
|
1646
|
+
const choice = parseCookieConsentValue(raw ?? null);
|
|
1647
|
+
if (!choice) return "unknown";
|
|
1648
|
+
return choice.analytics ? "granted" : "declined";
|
|
1649
|
+
}
|
|
1650
|
+
function readVisits(storeSlug, now) {
|
|
1651
|
+
const stored = getItem(analyticsVisitsKey(storeSlug));
|
|
1652
|
+
if (!Array.isArray(stored)) return [];
|
|
1653
|
+
return stored.filter((visit2) => typeof visit2?.id === "string" && typeof visit2.at === "number" && now - visit2.at < ANALYTICS_VISITS_WINDOW_MS);
|
|
1654
|
+
}
|
|
1655
|
+
function forgetAnalyticsData(storeSlug) {
|
|
1656
|
+
removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
|
|
1657
|
+
removeItem(analyticsVisitsKey(storeSlug));
|
|
1658
|
+
}
|
|
1659
|
+
function analyticsSessionId(storeSlug, options, now = Date.now()) {
|
|
1660
|
+
removeItem(`presence_connection_${storeSlug}`);
|
|
1661
|
+
if (readAnalyticsConsent() !== "granted") {
|
|
1662
|
+
forgetAnalyticsData(storeSlug);
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
|
|
1666
|
+
const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
|
|
1667
|
+
if (!options.touch) return live;
|
|
1668
|
+
if (live) {
|
|
1669
|
+
setItem(analyticsSessionKey(storeSlug), { id: live, at: now }, "sessionStorage");
|
|
1670
|
+
return live;
|
|
1671
|
+
}
|
|
1672
|
+
if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
|
|
1673
|
+
const id = crypto.randomUUID();
|
|
1674
|
+
setItem(analyticsSessionKey(storeSlug), { id, at: now }, "sessionStorage");
|
|
1675
|
+
setItem(
|
|
1676
|
+
analyticsVisitsKey(storeSlug),
|
|
1677
|
+
[{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
|
|
1678
|
+
);
|
|
1679
|
+
return id;
|
|
1680
|
+
}
|
|
1681
|
+
function checkoutAttribution(storeSlug, now = Date.now()) {
|
|
1682
|
+
const consent = readAnalyticsConsent();
|
|
1683
|
+
if (consent === "declined") {
|
|
1684
|
+
forgetAnalyticsData(storeSlug);
|
|
1685
|
+
return { declined: true };
|
|
1686
|
+
}
|
|
1687
|
+
if (consent === "unknown") return void 0;
|
|
1688
|
+
const sessionIds = readVisits(storeSlug, now).map((visit2) => visit2.id);
|
|
1689
|
+
return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
|
|
1690
|
+
}
|
|
1691
|
+
var engagedMs = 0;
|
|
1692
|
+
var visibleSince = null;
|
|
1693
|
+
var clockInstalled = false;
|
|
1694
|
+
var pageViewed = false;
|
|
1695
|
+
var pageSession = null;
|
|
1696
|
+
function markPageViewed(sessionId) {
|
|
1697
|
+
pageViewed = true;
|
|
1698
|
+
pageSession = sessionId;
|
|
1699
|
+
}
|
|
1700
|
+
function currentPageSession() {
|
|
1701
|
+
return pageSession;
|
|
1702
|
+
}
|
|
1703
|
+
function isPageViewed() {
|
|
1704
|
+
return pageViewed;
|
|
1705
|
+
}
|
|
1706
|
+
function settleClock(now) {
|
|
1707
|
+
if (visibleSince !== null) {
|
|
1708
|
+
engagedMs += now - visibleSince;
|
|
1709
|
+
visibleSince = document.visibilityState === "visible" ? now : null;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
var consentListenerInstalled = false;
|
|
1713
|
+
function onAnalyticsConsent(onYes) {
|
|
1714
|
+
if (consentListenerInstalled || typeof window === "undefined") return;
|
|
1715
|
+
consentListenerInstalled = true;
|
|
1716
|
+
window.addEventListener("shoppex:consent", () => {
|
|
1717
|
+
if (readAnalyticsConsent() === "granted") onYes();
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
function installEngagedClock(onHidden) {
|
|
1721
|
+
if (clockInstalled || typeof document === "undefined") return;
|
|
1722
|
+
clockInstalled = true;
|
|
1723
|
+
visibleSince = document.visibilityState === "visible" ? Date.now() : null;
|
|
1724
|
+
document.addEventListener("visibilitychange", () => {
|
|
1725
|
+
const now = Date.now();
|
|
1726
|
+
if (document.visibilityState === "visible") {
|
|
1727
|
+
visibleSince ?? (visibleSince = now);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
settleClock(now);
|
|
1731
|
+
visibleSince = null;
|
|
1732
|
+
onHidden();
|
|
1733
|
+
});
|
|
1734
|
+
window.addEventListener("pagehide", () => {
|
|
1735
|
+
settleClock(Date.now());
|
|
1736
|
+
visibleSince = null;
|
|
1737
|
+
onHidden();
|
|
1738
|
+
});
|
|
1739
|
+
window.addEventListener("pageshow", (event) => {
|
|
1740
|
+
if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
function takeEngagedMs(now = Date.now()) {
|
|
1744
|
+
settleClock(now);
|
|
1745
|
+
const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
|
|
1746
|
+
engagedMs = 0;
|
|
1747
|
+
return taken;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1637
1750
|
// ../sdk/src/core/attribution.ts
|
|
1638
1751
|
var STORAGE_PREFIX2 = "shoppex:utm:";
|
|
1639
1752
|
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -1692,6 +1805,13 @@ function resolveUtmParameters(storeSlug) {
|
|
|
1692
1805
|
return void 0;
|
|
1693
1806
|
}
|
|
1694
1807
|
const fromUrl = readFromUrl(window.location.search);
|
|
1808
|
+
if (readAnalyticsConsent() !== "granted") {
|
|
1809
|
+
try {
|
|
1810
|
+
window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
|
|
1811
|
+
} catch {
|
|
1812
|
+
}
|
|
1813
|
+
return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
|
|
1814
|
+
}
|
|
1695
1815
|
if (Object.keys(fromUrl).length > 0) {
|
|
1696
1816
|
writeStored(storeSlug, fromUrl);
|
|
1697
1817
|
return fromUrl;
|
|
@@ -1858,13 +1978,21 @@ async function trackPageView(cartValue, itemCount) {
|
|
|
1858
1978
|
const sendView = async () => {
|
|
1859
1979
|
const order2 = reservedOrder ? await reservedOrder : void 0;
|
|
1860
1980
|
const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
|
|
1981
|
+
const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
|
|
1982
|
+
const previousSession = currentPageSession();
|
|
1983
|
+
if (previousSession && previousSession !== sessionId) reportPageLeave();
|
|
1984
|
+
markPageViewed(sessionId);
|
|
1985
|
+
if (sessionId) installEngagedClock(reportPageLeave);
|
|
1986
|
+
onAnalyticsConsent(startSessionOnConsent);
|
|
1861
1987
|
const delivered = await postPing({
|
|
1862
1988
|
referer: document.referrer || void 0,
|
|
1863
1989
|
cart_value: cartValue,
|
|
1864
1990
|
item_count: itemCount,
|
|
1865
1991
|
cart_seq: order2?.seq,
|
|
1866
1992
|
cart_seq_scope: order2?.scope,
|
|
1867
|
-
|
|
1993
|
+
session_id: sessionId ?? void 0,
|
|
1994
|
+
// Time on screen of the page before this one (client-side navigation).
|
|
1995
|
+
engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
|
|
1868
1996
|
path: resolveStorefrontPagePath(),
|
|
1869
1997
|
utm: resolveUtmParameters(config2.storeSlug)
|
|
1870
1998
|
});
|
|
@@ -1892,11 +2020,44 @@ async function sendCartActivity(order2, cartValue, itemCount) {
|
|
|
1892
2020
|
item_count: itemCount,
|
|
1893
2021
|
cart_seq: order2.seq,
|
|
1894
2022
|
cart_seq_scope: order2.scope,
|
|
1895
|
-
|
|
2023
|
+
// A cart marks the analytics session as one that did not bounce.
|
|
2024
|
+
session_id: analyticsSessionId(config2.storeSlug, { touch: false }) ?? void 0,
|
|
1896
2025
|
path: resolveStorefrontPagePath()
|
|
1897
2026
|
});
|
|
1898
2027
|
settle2(delivered);
|
|
1899
2028
|
}
|
|
2029
|
+
function startSessionOnConsent() {
|
|
2030
|
+
const config2 = getConfig();
|
|
2031
|
+
if (!isPageViewed() || analyticsSessionId(config2.storeSlug, { touch: false })) return;
|
|
2032
|
+
const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
|
|
2033
|
+
if (!sessionId) return;
|
|
2034
|
+
markPageViewed(sessionId);
|
|
2035
|
+
installEngagedClock(reportPageLeave);
|
|
2036
|
+
void postPing({
|
|
2037
|
+
event_type: "session_start",
|
|
2038
|
+
referer: document.referrer || void 0,
|
|
2039
|
+
session_id: sessionId,
|
|
2040
|
+
path: resolveStorefrontPagePath(),
|
|
2041
|
+
utm: resolveUtmParameters(config2.storeSlug)
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
function reportPageLeave() {
|
|
2045
|
+
const config2 = getConfig();
|
|
2046
|
+
const sessionId = currentPageSession();
|
|
2047
|
+
const engagedMs2 = takeEngagedMs();
|
|
2048
|
+
if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
|
|
2049
|
+
try {
|
|
2050
|
+
const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config2.storeSlug });
|
|
2051
|
+
void fetch(`${config2.apiBaseUrl}${endpoint}`, {
|
|
2052
|
+
method: "POST",
|
|
2053
|
+
headers: { "Content-Type": "application/json" },
|
|
2054
|
+
body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
|
|
2055
|
+
keepalive: true
|
|
2056
|
+
}).catch(() => {
|
|
2057
|
+
});
|
|
2058
|
+
} catch {
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
1900
2061
|
|
|
1901
2062
|
// ../sdk/src/storefront-storage-sdk.ts
|
|
1902
2063
|
function init(storeSlugOrOptions, options) {
|
|
@@ -2953,10 +3114,16 @@ function buildCheckoutUrlFromInvoiceId(checkoutBaseUrl, invoiceId) {
|
|
|
2953
3114
|
return null;
|
|
2954
3115
|
}
|
|
2955
3116
|
}
|
|
3117
|
+
function readStorefrontConsentValue() {
|
|
3118
|
+
if (typeof document === "undefined" || typeof document.cookie !== "string") return null;
|
|
3119
|
+
const value = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${COOKIE_CONSENT_COOKIE}=`))?.slice(COOKIE_CONSENT_COOKIE.length + 1);
|
|
3120
|
+
return parseCookieConsentValue(value) ? value : null;
|
|
3121
|
+
}
|
|
2956
3122
|
function appendCheckoutUrlOptions(checkoutUrl, options) {
|
|
2957
3123
|
const normalizedEmail = normalizeEmail(options.email);
|
|
2958
3124
|
const normalizedLocale = typeof options.locale === "string" ? options.locale.trim() : "";
|
|
2959
|
-
|
|
3125
|
+
const consent = readStorefrontConsentValue();
|
|
3126
|
+
if (!normalizedEmail && !normalizedLocale && !consent) {
|
|
2960
3127
|
return checkoutUrl;
|
|
2961
3128
|
}
|
|
2962
3129
|
try {
|
|
@@ -2964,9 +3131,10 @@ function appendCheckoutUrlOptions(checkoutUrl, options) {
|
|
|
2964
3131
|
if (normalizedLocale) {
|
|
2965
3132
|
parsedCheckoutUrl.searchParams.set("locale", normalizedLocale);
|
|
2966
3133
|
}
|
|
2967
|
-
if (normalizedEmail) {
|
|
3134
|
+
if (normalizedEmail || consent) {
|
|
2968
3135
|
const hashParams = new URLSearchParams(parsedCheckoutUrl.hash.startsWith("#") ? parsedCheckoutUrl.hash.slice(1) : parsedCheckoutUrl.hash);
|
|
2969
|
-
hashParams.set(CHECKOUT_PREFILL_EMAIL_HASH_KEY, normalizedEmail);
|
|
3136
|
+
if (normalizedEmail) hashParams.set(CHECKOUT_PREFILL_EMAIL_HASH_KEY, normalizedEmail);
|
|
3137
|
+
if (consent) hashParams.set(COOKIE_CONSENT_CHECKOUT_HASH_KEY, consent);
|
|
2970
3138
|
parsedCheckoutUrl.hash = hashParams.toString();
|
|
2971
3139
|
}
|
|
2972
3140
|
return parsedCheckoutUrl.toString();
|
|
@@ -3080,7 +3248,10 @@ async function performCheckout(resolvedOptions) {
|
|
|
3080
3248
|
// automatically so the server can refuse an invoice priced above it.
|
|
3081
3249
|
// Undefined when nothing has been quoted this session — the endpoint
|
|
3082
3250
|
// treats that exactly as an older SDK.
|
|
3083
|
-
quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
|
|
3251
|
+
quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0,
|
|
3252
|
+
// Where the buyer came from: their analytics sessions after a yes on the
|
|
3253
|
+
// cookie banner, `declined` after a no. The server looks up the sources.
|
|
3254
|
+
attribution: checkoutAttribution(config2.storeSlug)
|
|
3084
3255
|
};
|
|
3085
3256
|
const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
|
|
3086
3257
|
const response = await post(
|
|
@@ -3929,7 +4100,6 @@ async function touchStorefrontPresence() {
|
|
|
3929
4100
|
{
|
|
3930
4101
|
event_type: "presence",
|
|
3931
4102
|
referer: document.referrer || void 0,
|
|
3932
|
-
connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
|
|
3933
4103
|
path: resolveStorefrontPagePath(),
|
|
3934
4104
|
utm: resolveUtmParameters(config2.storeSlug)
|
|
3935
4105
|
},
|