@occluze/tether 0.6.8 → 0.7.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/README.md +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +60 -69
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/cart-id.d.ts +2 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +190 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +90 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -24
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
package/src/analytics.js
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analytics, as an optional part (loaded when config.analytics is set).
|
|
3
|
-
*
|
|
4
|
-
* analytics: {
|
|
5
|
-
* dataLayer: true, // GA4 / Google Tag Manager ecommerce events (window.dataLayer)
|
|
6
|
-
* shopify: true, // Shopify's own analytics (experimental, see docs/analytics.md)
|
|
7
|
-
* onEvent(name, data) {}, // any other tool: Meta, TikTok, Plausible
|
|
8
|
-
* }
|
|
9
|
-
*
|
|
10
|
-
* Events: view_item (the page's product), view_item_list (lists), add_to_cart / remove_from_cart (from the cart's
|
|
11
|
-
* changes, so quick +/- and JS calls count too), view_cart (cart opened), begin_checkout, search (search as you type).
|
|
12
|
-
* Purchases happen on Shopify's checkout: use Shopify's Google & YouTube app or a checkout pixel for those.
|
|
13
|
-
* Shopify's analytics are set up in analytics-shopify.js.
|
|
14
|
-
*/
|
|
15
|
-
import { cfg, reportError } from './config.js';
|
|
16
|
-
import { sel } from './dom.js';
|
|
17
|
-
import { Cart } from './cart.js';
|
|
18
|
-
import { startShopifyAnalytics } from './analytics-shopify.js';
|
|
19
|
-
/** @import { Money } from './shopify-values.js' */
|
|
20
|
-
/** @import { Product, ProductVariant } from './fragments.js' */
|
|
21
|
-
/** @import { AnalyticsBus, EventPayload } from './analytics-shopify.js' */
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* cart.js has no types yet; these are the fields of its cart (`fragment TetherCart`) this module reads.
|
|
25
|
-
*
|
|
26
|
-
* @typedef {object} LineProduct
|
|
27
|
-
* @property {string} id
|
|
28
|
-
* @property {string} title
|
|
29
|
-
* @property {string} handle
|
|
30
|
-
* @property {string} vendor
|
|
31
|
-
* @typedef {{ id: string, title: string, sku: string | null, price: Money, product: LineProduct }} LineVariant
|
|
32
|
-
* @typedef {{ id: string, quantity: number, merchandise: LineVariant }} CartLine
|
|
33
|
-
* @typedef {object} AnalyticsCart
|
|
34
|
-
* @property {{ nodes: CartLine[] }} lines
|
|
35
|
-
* @property {{ totalAmount: Money }} cost
|
|
36
|
-
* @property {{ code: string, applicable: boolean }[]} discountCodes
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {object} ItemProduct What a GA4 item is built from: a product, or a cart line's product.
|
|
41
|
-
* @property {string} id
|
|
42
|
-
* @property {string} title
|
|
43
|
-
* @property {string} [vendor]
|
|
44
|
-
* @property {string} [productType]
|
|
45
|
-
* @property {{ minVariantPrice: Money }} [priceRange]
|
|
46
|
-
*/
|
|
47
|
-
/** @typedef {Pick<ProductVariant, 'id' | 'title' | 'sku' | 'price'>} ItemVariant */
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {object} AnalyticsItem A GA4 ecommerce item.
|
|
50
|
-
* @property {string} item_id
|
|
51
|
-
* @property {string} item_name
|
|
52
|
-
* @property {string | undefined} item_variant
|
|
53
|
-
* @property {string | undefined} item_brand
|
|
54
|
-
* @property {string | undefined} item_category
|
|
55
|
-
* @property {number} price
|
|
56
|
-
* @property {number} quantity
|
|
57
|
-
* @property {number} [index]
|
|
58
|
-
*/
|
|
59
|
-
/** @typedef {{ id: string, handle: string, title: string }} ListCollection */
|
|
60
|
-
/** @typedef {{ product: Product, scope: { root: Element, variant: ProductVariant | null } | null }} ProductReadyDetail */
|
|
61
|
-
/** @typedef {{ products: Product[] | undefined, collection: ListCollection | null }} ListReadyDetail */
|
|
62
|
-
/** @typedef {{ cart: AnalyticsCart | null, reason: string }} CartUpdatedDetail */
|
|
63
|
-
|
|
64
|
-
/** Shopify's title for the one variant of a product without options. */
|
|
65
|
-
const DEFAULT_VARIANT_TITLE = 'Default Title';
|
|
66
|
-
const CENTS_PER_UNIT = 100;
|
|
67
|
-
/** GA4 drops an event with more items than this. */
|
|
68
|
-
const MAX_LIST_ITEMS = 200;
|
|
69
|
-
|
|
70
|
-
/** @param {Money | null | undefined} money */
|
|
71
|
-
const amountOf = (money) => (money ? parseFloat(money.amount) : 0);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {string | null | undefined} gid
|
|
75
|
-
* @returns {string} The numeric part of a gid.
|
|
76
|
-
*/
|
|
77
|
-
const numericId = (gid) => (gid ?? '').split('/').pop() ?? '';
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @param {ItemProduct} product
|
|
81
|
-
* @param {ItemVariant | null | undefined} variant
|
|
82
|
-
* @param {number} [quantity]
|
|
83
|
-
* @param {number} [index] The position in a list.
|
|
84
|
-
* @returns {AnalyticsItem}
|
|
85
|
-
*/
|
|
86
|
-
function itemFromVariant(product, variant, quantity = 1, index) {
|
|
87
|
-
/** @type {AnalyticsItem} */
|
|
88
|
-
const item = {
|
|
89
|
-
item_id: variant?.sku || numericId(variant?.id) || numericId(product.id),
|
|
90
|
-
item_name: product.title,
|
|
91
|
-
item_variant: variant?.title === DEFAULT_VARIANT_TITLE ? undefined : variant?.title,
|
|
92
|
-
item_brand: product.vendor || undefined,
|
|
93
|
-
item_category: product.productType || undefined,
|
|
94
|
-
price: amountOf(variant?.price ?? product.priceRange?.minVariantPrice),
|
|
95
|
-
quantity,
|
|
96
|
-
};
|
|
97
|
-
if (index != null) item.index = index;
|
|
98
|
-
return item;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @param {CartLine} line
|
|
103
|
-
* @param {number} [quantity]
|
|
104
|
-
*/
|
|
105
|
-
const itemFromLine = (line, quantity = line.quantity) =>
|
|
106
|
-
itemFromVariant(line.merchandise.product, line.merchandise, quantity);
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {AnalyticsItem[]} items
|
|
110
|
-
* @returns {number} The items' total, rounded to cents.
|
|
111
|
-
*/
|
|
112
|
-
function totalValue(items) {
|
|
113
|
-
const total = items.reduce((sum, item) => sum + item.price * (item.quantity || 1), 0);
|
|
114
|
-
return Math.round(total * CENTS_PER_UNIT) / CENTS_PER_UNIT;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @param {Product} product
|
|
119
|
-
* @param {ItemVariant} variant
|
|
120
|
-
* @param {number} [quantity]
|
|
121
|
-
*/
|
|
122
|
-
const productPayload = (product, variant, quantity = 1) => ({
|
|
123
|
-
id: product.id,
|
|
124
|
-
title: product.title,
|
|
125
|
-
price: variant.price,
|
|
126
|
-
vendor: product.vendor || '',
|
|
127
|
-
variantId: variant.id,
|
|
128
|
-
variantTitle: variant.title,
|
|
129
|
-
quantity,
|
|
130
|
-
sku: variant.sku || undefined,
|
|
131
|
-
productType: product.productType || undefined,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/** @param {CartLine} line */
|
|
135
|
-
const linePayload = (line) => ({
|
|
136
|
-
id: line.id,
|
|
137
|
-
quantity: line.quantity,
|
|
138
|
-
merchandise: {
|
|
139
|
-
id: line.merchandise.id,
|
|
140
|
-
title: line.merchandise.title,
|
|
141
|
-
price: line.merchandise.price,
|
|
142
|
-
sku: line.merchandise.sku,
|
|
143
|
-
product: {
|
|
144
|
-
id: line.merchandise.product.id,
|
|
145
|
-
title: line.merchandise.product.title,
|
|
146
|
-
vendor: line.merchandise.product.vendor || '',
|
|
147
|
-
handle: line.merchandise.product.handle,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
/** @returns {unknown[]} The GA4 / GTM data layer, created when the page has none. */
|
|
153
|
-
function dataLayer() {
|
|
154
|
-
const { dataLayer: option } = cfg.analytics ?? {};
|
|
155
|
-
const name = typeof option === 'string' ? option : 'dataLayer';
|
|
156
|
-
/** @type {unknown} */
|
|
157
|
-
const existing = Reflect.get(window, name);
|
|
158
|
-
if (Array.isArray(existing)) return existing;
|
|
159
|
-
/** @type {unknown[]} */
|
|
160
|
-
const created = [];
|
|
161
|
-
Reflect.set(window, name, created);
|
|
162
|
-
return created;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {string} event
|
|
167
|
-
* @param {Record<string, unknown> | undefined} ecommerce
|
|
168
|
-
*/
|
|
169
|
-
function pushToDataLayer(event, ecommerce) {
|
|
170
|
-
const layer = dataLayer();
|
|
171
|
-
if (!ecommerce) {
|
|
172
|
-
layer.push({ event });
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Google asks for the previous ecommerce object to be cleared first.
|
|
176
|
-
layer.push({ ecommerce: null });
|
|
177
|
-
layer.push({ event, ecommerce });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** @type {AnalyticsBus | null} */
|
|
181
|
-
let bus = null;
|
|
182
|
-
/** @type {Map<string, CartLine> | null} Variant id to line, to turn cart changes into add and remove events. */
|
|
183
|
-
let lastLines = null;
|
|
184
|
-
/** @type {AnalyticsCart | null} */
|
|
185
|
-
let lastCart = null;
|
|
186
|
-
/** @type {WeakSet<Element>} Product roots whose view_item was sent on this page. */
|
|
187
|
-
let viewed = new WeakSet();
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* @param {string} name The GA4 event.
|
|
191
|
-
* @param {Record<string, unknown>} data
|
|
192
|
-
* @param {string} [shopifyEvent]
|
|
193
|
-
* @param {EventPayload} [shopifyPayload]
|
|
194
|
-
*/
|
|
195
|
-
function send(name, data, shopifyEvent, shopifyPayload) {
|
|
196
|
-
if (cfg.analytics?.dataLayer) pushToDataLayer(name, data);
|
|
197
|
-
if (bus && shopifyEvent) bus.publish(shopifyEvent, shopifyPayload);
|
|
198
|
-
try {
|
|
199
|
-
cfg.analytics?.onEvent?.(name, data);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
reportError(error, 'analytics onEvent');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** @returns {unknown} The scope of the page's own product: a product root outside every list. */
|
|
206
|
-
function mainProduct() {
|
|
207
|
-
const el = document.querySelector(sel('product'));
|
|
208
|
-
if (!el || el.closest(`${sel('collection')},${sel('products')},${sel('recommendations')}`)) return null;
|
|
209
|
-
/** @type {unknown} */
|
|
210
|
-
const tether = Reflect.get(window, 'Tether');
|
|
211
|
-
// The core sets window.Tether before it loads this part.
|
|
212
|
-
return /** @type {{ getScope: (el: Element) => unknown }} */ (tether).getScope(el);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** @param {ProductReadyDetail} detail */
|
|
216
|
-
function onProductReady({ product, scope }) {
|
|
217
|
-
if (!scope || viewed.has(scope.root) || mainProduct() !== scope) return;
|
|
218
|
-
viewed.add(scope.root);
|
|
219
|
-
const variant = scope.variant ?? product.variants.nodes[0];
|
|
220
|
-
const items = [itemFromVariant(product, variant)];
|
|
221
|
-
const products = variant ? [productPayload(product, variant)] : [];
|
|
222
|
-
send('view_item', { currency: variant?.price.currencyCode, value: totalValue(items), items }, 'product_viewed', {
|
|
223
|
-
products,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** @param {ListReadyDetail} detail */
|
|
228
|
-
function onListReady({ products = [], collection }) {
|
|
229
|
-
const items = products
|
|
230
|
-
.slice(0, MAX_LIST_ITEMS)
|
|
231
|
-
.map((product, index) => itemFromVariant(product, product.variants.nodes[0], 1, index));
|
|
232
|
-
const list = { item_list_id: collection?.handle || 'products', item_list_name: collection?.title || 'Products' };
|
|
233
|
-
if (!collection) {
|
|
234
|
-
send('view_item_list', { ...list, items });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
send('view_item_list', { ...list, items }, 'collection_viewed', {
|
|
238
|
-
collection: { id: collection.id, handle: collection.handle },
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* @param {AnalyticsCart | null} cart
|
|
244
|
-
* @param {Map<string, CartLine>} now
|
|
245
|
-
* @param {Map<string, CartLine>} before
|
|
246
|
-
*/
|
|
247
|
-
function sendCartChanges(cart, now, before) {
|
|
248
|
-
const currency = cart?.cost.totalAmount.currencyCode;
|
|
249
|
-
now.forEach((line, variantId) => {
|
|
250
|
-
const previous = before.get(variantId);
|
|
251
|
-
const added = line.quantity - (previous?.quantity ?? 0);
|
|
252
|
-
if (added <= 0) return;
|
|
253
|
-
const items = [itemFromLine(line, added)];
|
|
254
|
-
send('add_to_cart', { currency, value: totalValue(items), items }, 'product_added_to_cart', {
|
|
255
|
-
cart,
|
|
256
|
-
prevCart: lastCart,
|
|
257
|
-
currentLine: linePayload(line),
|
|
258
|
-
prevLine: previous ? linePayload(previous) : undefined,
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
before.forEach((line, variantId) => {
|
|
262
|
-
const removed = line.quantity - (now.get(variantId)?.quantity ?? 0);
|
|
263
|
-
if (removed <= 0) return;
|
|
264
|
-
const items = [itemFromLine(line, removed)];
|
|
265
|
-
send('remove_from_cart', { currency, value: totalValue(items), items });
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** @param {CartUpdatedDetail} detail */
|
|
270
|
-
function onCartUpdated({ cart, reason }) {
|
|
271
|
-
const now = new Map((cart?.lines.nodes ?? []).map((line) => [line.merchandise.id, line]));
|
|
272
|
-
// Loading the saved cart and showing a pending change aren't changes the visitor made.
|
|
273
|
-
if (lastLines && reason !== 'load' && reason !== 'pending') sendCartChanges(cart, now, lastLines);
|
|
274
|
-
lastLines = now;
|
|
275
|
-
lastCart = cart;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function onCartOpened() {
|
|
279
|
-
const items = Cart.lines.map((line) => itemFromLine(line));
|
|
280
|
-
send('view_cart', { currency: Cart.state?.cost.totalAmount.currencyCode, value: totalValue(items), items });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** @param {{ cart: AnalyticsCart }} detail */
|
|
284
|
-
function onCheckout({ cart }) {
|
|
285
|
-
const items = cart.lines.nodes.map((line) => itemFromLine(line));
|
|
286
|
-
const coupons = cart.discountCodes.filter((discount) => discount.applicable).map((discount) => discount.code);
|
|
287
|
-
send('begin_checkout', {
|
|
288
|
-
currency: cart.cost.totalAmount.currencyCode,
|
|
289
|
-
value: totalValue(items),
|
|
290
|
-
coupon: coupons.join(',') || undefined,
|
|
291
|
-
items,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Turns one Tether event into analytics events. The details are Tether's own (`emit`), typed per event here.
|
|
297
|
-
*
|
|
298
|
-
* @param {Event} event `<prefix>:<name>`.
|
|
299
|
-
*/
|
|
300
|
-
function handle(event) {
|
|
301
|
-
const name = event.type.slice(cfg.prefix.length + 1);
|
|
302
|
-
/** @type {unknown} */
|
|
303
|
-
const detail = Reflect.get(event, 'detail');
|
|
304
|
-
switch (name) {
|
|
305
|
-
case 'product-ready':
|
|
306
|
-
onProductReady(/** @type {ProductReadyDetail} */ (detail));
|
|
307
|
-
break;
|
|
308
|
-
case 'list-ready':
|
|
309
|
-
onListReady(/** @type {ListReadyDetail} */ (detail));
|
|
310
|
-
break;
|
|
311
|
-
case 'cart-updated':
|
|
312
|
-
onCartUpdated(/** @type {CartUpdatedDetail} */ (detail));
|
|
313
|
-
break;
|
|
314
|
-
case 'cart-toggled':
|
|
315
|
-
if (/** @type {{ open: boolean }} */ (detail).open) onCartOpened();
|
|
316
|
-
break;
|
|
317
|
-
case 'checkout':
|
|
318
|
-
onCheckout(/** @type {{ cart: AnalyticsCart }} */ (detail));
|
|
319
|
-
break;
|
|
320
|
-
case 'search': {
|
|
321
|
-
const { term } = /** @type {{ term: string }} */ (detail);
|
|
322
|
-
if (term) send('search', { search_term: term }, 'search_viewed', { searchTerm: term });
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
case 'page':
|
|
326
|
-
viewed = new WeakSet();
|
|
327
|
-
bus?.publish('page_viewed', {});
|
|
328
|
-
break;
|
|
329
|
-
default:
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Called by the core with the events it buffered while this part loaded; the queue then handles events as they come.
|
|
336
|
-
*
|
|
337
|
-
* @param {Event[]} queue
|
|
338
|
-
*/
|
|
339
|
-
export async function startAnalytics(queue) {
|
|
340
|
-
if (cfg.analytics?.shopify) {
|
|
341
|
-
bus = await startShopifyAnalytics(() => mainProduct() != null).catch((/** @type {unknown} */ error) => {
|
|
342
|
-
reportError(error, 'shopify analytics');
|
|
343
|
-
return null;
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
bus?.publish('page_viewed', {});
|
|
347
|
-
queue.splice(0).forEach(handle);
|
|
348
|
-
queue.push = (...events) => {
|
|
349
|
-
events.forEach(handle);
|
|
350
|
-
return queue.length;
|
|
351
|
-
};
|
|
352
|
-
}
|
package/src/api-version.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify API versions are quarterly (YYYY-01, -04, -07, -10) and supported for 12 months. Tether pins a dated version
|
|
3
|
-
* and never targets `unstable` (it changes without notice). Used by the diagnose part and the release scripts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const QUARTERLY_VERSION = /^(\d{4})-(01|04|07|10)$/;
|
|
7
|
-
const MONTHS_PER_QUARTER = 3;
|
|
8
|
-
const QUARTERS_PER_YEAR = 4;
|
|
9
|
-
const MONTHS_SUPPORTED = 12;
|
|
10
|
-
/** Months left at which the status starts asking for an upgrade. */
|
|
11
|
-
const UPGRADE_WARNING_MONTHS = 3;
|
|
12
|
-
const AVERAGE_MONTH_MS = 30.44 * 24 * 3600 * 1000;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} ApiVersionStatus
|
|
16
|
-
* @property {string} version
|
|
17
|
-
* @property {boolean} ok Released and still supported.
|
|
18
|
-
* @property {string} note What to do about it, for people.
|
|
19
|
-
* @property {number} [monthsLeft] Months of support left, rounded; negative once support ended.
|
|
20
|
-
* @property {string} [next] The version after this one.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {unknown} version
|
|
25
|
-
* @returns {{ year: number, month: number } | null}
|
|
26
|
-
*/
|
|
27
|
-
function parseVersion(version) {
|
|
28
|
-
const match = QUARTERLY_VERSION.exec(typeof version === 'string' ? version : '');
|
|
29
|
-
if (!match) return null;
|
|
30
|
-
return { year: Number(match[1]), month: Number(match[2]) };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {number} year
|
|
35
|
-
* @param {number} month 1 to 12.
|
|
36
|
-
*/
|
|
37
|
-
const formatVersion = (year, month) => `${year}-${String(month).padStart(2, '0')}`;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* `'2026-07'` → `'2026-10'`, `'2026-10'` → `'2027-01'`.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} version
|
|
43
|
-
* @param {number} [steps] Quarters ahead.
|
|
44
|
-
* @returns {string}
|
|
45
|
-
* @throws {Error} When `version` isn't a quarterly version.
|
|
46
|
-
*/
|
|
47
|
-
export function nextApiVersion(version, steps = 1) {
|
|
48
|
-
const parsed = parseVersion(version);
|
|
49
|
-
if (!parsed) throw new Error(`not a quarterly Shopify API version: ${version}`);
|
|
50
|
-
const quarter = parsed.year * QUARTERS_PER_YEAR + (parsed.month - 1) / MONTHS_PER_QUARTER + steps;
|
|
51
|
-
return formatVersion(Math.floor(quarter / QUARTERS_PER_YEAR), (quarter % QUARTERS_PER_YEAR) * MONTHS_PER_QUARTER + 1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* The support window of `version`, relative to `now`.
|
|
56
|
-
*
|
|
57
|
-
* @param {string} version
|
|
58
|
-
* @param {Date} [now]
|
|
59
|
-
* @returns {ApiVersionStatus}
|
|
60
|
-
*/
|
|
61
|
-
export function apiVersionStatus(version, now = new Date()) {
|
|
62
|
-
if (version === 'unstable') {
|
|
63
|
-
return { version, ok: false, note: '"unstable" changes without notice: pin a dated version such as 2026-07' };
|
|
64
|
-
}
|
|
65
|
-
const parsed = parseVersion(version);
|
|
66
|
-
if (!parsed) return { version, ok: false, note: 'not a quarterly version (YYYY-01, -04, -07 or -10)' };
|
|
67
|
-
const released = new Date(Date.UTC(parsed.year, parsed.month - 1, 1));
|
|
68
|
-
const supportedUntil = new Date(Date.UTC(parsed.year, parsed.month - 1 + MONTHS_SUPPORTED, 1));
|
|
69
|
-
const monthsLeft = (supportedUntil.getTime() - now.getTime()) / AVERAGE_MONTH_MS;
|
|
70
|
-
const isReleased = released <= now;
|
|
71
|
-
let note = `supported until about ${supportedUntil.toISOString().slice(0, 7)}`;
|
|
72
|
-
if (!isReleased) note = 'not released yet (release candidate at best)';
|
|
73
|
-
else if (monthsLeft <= 0) {
|
|
74
|
-
note = 'no longer supported: Shopify falls back to the oldest supported version. Upgrade (see README)';
|
|
75
|
-
} else if (monthsLeft < UPGRADE_WARNING_MONTHS) {
|
|
76
|
-
note = `${note}: plan the upgrade to ${nextApiVersion(version)} or later (see README)`;
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
version,
|
|
80
|
-
ok: isReleased && monthsLeft > 0,
|
|
81
|
-
monthsLeft: Math.round(monthsLeft),
|
|
82
|
-
next: nextApiVersion(version),
|
|
83
|
-
note,
|
|
84
|
-
};
|
|
85
|
-
}
|