@occluze/tether 0.6.8 → 0.7.0

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.
Files changed (259) hide show
  1. package/README.md +11 -11
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
  4. package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
  5. package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
  6. package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
  7. package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
  8. package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
  9. package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
  10. package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
  11. package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
  12. package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
  13. package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
  14. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  15. package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
  16. package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
  17. package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
  18. package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
  19. package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
  20. package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
  21. package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -2
  24. package/dist/presets/barba.esm.js +19 -23
  25. package/dist/tether.account-pages.js +320 -365
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +242 -491
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +66 -88
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +36 -33
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +28 -36
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +1270 -894
  36. package/dist/tether.markets.js +73 -90
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +44 -68
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  43. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  44. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  45. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  46. package/dist/types/server/src/shared/json.d.ts +25 -0
  47. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  48. package/dist/types/src/account/addresses-view.d.ts +5 -0
  49. package/dist/types/src/account/answers.d.ts +40 -0
  50. package/dist/types/src/account/markup.d.ts +19 -0
  51. package/dist/types/src/account/operations.d.ts +41 -0
  52. package/dist/types/src/account/order-lines.d.ts +13 -0
  53. package/dist/types/src/account/order-list.d.ts +12 -0
  54. package/dist/types/src/account/order-page.d.ts +6 -0
  55. package/dist/types/src/account/page-answers.d.ts +182 -0
  56. package/dist/types/src/account/page-events.d.ts +2 -0
  57. package/dist/types/src/account/server-client.d.ts +60 -0
  58. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  59. package/dist/types/src/account-api.d.ts +18 -0
  60. package/dist/types/src/account-marks.d.ts +15 -0
  61. package/dist/types/src/account-pages.d.ts +8 -0
  62. package/dist/types/src/account.d.ts +64 -0
  63. package/dist/types/src/amounts.d.ts +12 -0
  64. package/dist/types/src/analytics-shopify.d.ts +41 -0
  65. package/dist/types/src/analytics.d.ts +2 -0
  66. package/dist/types/src/api-version.d.ts +28 -0
  67. package/dist/types/src/astro.d.ts +42 -0
  68. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  69. package/dist/types/src/cart/checkout.d.ts +16 -0
  70. package/dist/types/src/cart/drawer.d.ts +18 -0
  71. package/dist/types/src/cart/mutations.d.ts +79 -0
  72. package/dist/types/src/cart/render.d.ts +17 -0
  73. package/dist/types/src/cart/store.d.ts +182 -0
  74. package/dist/types/src/cart.d.ts +106 -0
  75. package/dist/types/src/client.d.ts +100 -0
  76. package/dist/types/src/collection.d.ts +25 -0
  77. package/dist/types/src/config-sections.d.ts +40 -0
  78. package/dist/types/src/config.d.ts +185 -0
  79. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  80. package/dist/types/src/controls/discount.d.ts +2 -0
  81. package/dist/types/src/controls/extras.d.ts +5 -0
  82. package/dist/types/src/controls/navigation.d.ts +12 -0
  83. package/dist/types/src/diagnose-markup.d.ts +19 -0
  84. package/dist/types/src/diagnose.d.ts +60 -0
  85. package/dist/types/src/dom.d.ts +75 -0
  86. package/dist/types/src/extras.d.ts +21 -0
  87. package/dist/types/src/fragments.d.ts +158 -0
  88. package/dist/types/src/globals.d.ts +15 -0
  89. package/dist/types/src/images.d.ts +20 -0
  90. package/dist/types/src/index.d.ts +167 -0
  91. package/dist/types/src/initial.d.ts +56 -0
  92. package/dist/types/src/lazy.d.ts +28 -0
  93. package/dist/types/src/loader.d.ts +84 -0
  94. package/dist/types/src/market-codes.d.ts +6 -0
  95. package/dist/types/src/markets-ui.d.ts +32 -0
  96. package/dist/types/src/markets.d.ts +72 -0
  97. package/dist/types/src/markup.d.ts +131 -0
  98. package/dist/types/src/mounting.d.ts +46 -0
  99. package/dist/types/src/page-market.d.ts +27 -0
  100. package/dist/types/src/presets/barba.d.ts +56 -0
  101. package/dist/types/src/product-events.d.ts +2 -0
  102. package/dist/types/src/product-render.d.ts +17 -0
  103. package/dist/types/src/product-shape.d.ts +88 -0
  104. package/dist/types/src/product-state.d.ts +110 -0
  105. package/dist/types/src/product.d.ts +66 -0
  106. package/dist/types/src/repeaters.d.ts +16 -0
  107. package/dist/types/src/search.d.ts +33 -0
  108. package/dist/types/src/selling-plans.d.ts +64 -0
  109. package/dist/types/src/server.d.ts +88 -0
  110. package/dist/types/src/shopify-values.d.ts +46 -0
  111. package/dist/types/src/status.d.ts +33 -0
  112. package/dist/types/src/storage.d.ts +12 -0
  113. package/dist/types/src/store-html.d.ts +39 -0
  114. package/dist/types/src/storefront/catalog.d.ts +38 -0
  115. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  116. package/dist/types/src/variant-render.d.ts +14 -0
  117. package/package.json +24 -23
  118. package/src/account/addresses-view.ts +58 -0
  119. package/src/account/answers.ts +79 -0
  120. package/src/account/markup.ts +36 -0
  121. package/src/account/operations.ts +145 -0
  122. package/src/account/{order-lines.js → order-lines.ts} +20 -41
  123. package/src/account/order-list.ts +107 -0
  124. package/src/account/order-page.ts +137 -0
  125. package/src/account/page-answers.ts +317 -0
  126. package/src/account/{page-events.js → page-events.ts} +55 -90
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/{account-pages.js → account-pages.ts} +29 -37
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
  135. package/src/analytics.ts +315 -0
  136. package/src/{api-version.js → api-version.ts} +25 -31
  137. package/src/{astro.js → astro.ts} +110 -104
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/{drawer.js → drawer.ts} +18 -31
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/{render.js → render.ts} +68 -96
  143. package/src/cart/store.ts +279 -0
  144. package/src/{cart.js → cart.ts} +82 -128
  145. package/src/client.ts +391 -0
  146. package/src/{collection.js → collection.ts} +30 -38
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
  150. package/src/controls/{discount.js → discount.ts} +13 -34
  151. package/src/controls/{extras.js → extras.ts} +15 -24
  152. package/src/controls/{navigation.js → navigation.ts} +24 -30
  153. package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
  154. package/src/{diagnose.js → diagnose.ts} +95 -96
  155. package/src/dom.ts +268 -0
  156. package/src/{extras.js → extras.ts} +43 -52
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/{images.js → images.ts} +30 -43
  160. package/src/index.ts +415 -0
  161. package/src/initial.ts +101 -0
  162. package/src/lazy-iife.ts +197 -0
  163. package/src/lazy.ts +42 -0
  164. package/src/{loader.js → loader.ts} +149 -123
  165. package/src/market-codes.ts +6 -0
  166. package/src/{markets-ui.js → markets-ui.ts} +91 -68
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/{mounting.js → mounting.ts} +88 -101
  170. package/src/{page-market.js → page-market.ts} +32 -38
  171. package/src/part-integrity.ts +7 -0
  172. package/src/parts/account-pages.ts +7 -0
  173. package/src/parts/account.ts +7 -0
  174. package/src/parts/analytics.ts +7 -0
  175. package/src/parts/diagnose.ts +7 -0
  176. package/src/parts/extras.ts +7 -0
  177. package/src/parts/markets.ts +7 -0
  178. package/src/parts/search.ts +7 -0
  179. package/src/presets/barba.ts +194 -0
  180. package/src/{product-events.js → product-events.ts} +33 -48
  181. package/src/{product-render.js → product-render.ts} +38 -77
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/{product.js → product.ts} +90 -126
  185. package/src/{repeaters.js → repeaters.ts} +40 -52
  186. package/src/{search.js → search.ts} +85 -103
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/{shopify-values.js → shopify-values.ts} +24 -22
  190. package/src/{status.js → status.ts} +34 -54
  191. package/src/storage.ts +34 -0
  192. package/src/store-html.ts +359 -0
  193. package/src/storefront/catalog.ts +196 -0
  194. package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
  195. package/src/{variant-render.js → variant-render.ts} +24 -50
  196. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  197. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  198. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  199. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  200. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  201. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  202. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  203. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  204. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  205. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  206. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  207. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  208. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  209. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  210. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  211. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  212. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  213. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  214. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  215. package/src/account/addresses-view.js +0 -92
  216. package/src/account/browser-session.js +0 -359
  217. package/src/account/markup.js +0 -48
  218. package/src/account/operations.js +0 -180
  219. package/src/account/order-list.js +0 -91
  220. package/src/account/order-page.js +0 -192
  221. package/src/account/server-client.js +0 -172
  222. package/src/account/shared.js +0 -111
  223. package/src/account/shopify-auth.js +0 -150
  224. package/src/account-api.js +0 -118
  225. package/src/account-links.js +0 -35
  226. package/src/account.js +0 -342
  227. package/src/amounts.js +0 -36
  228. package/src/analytics.js +0 -352
  229. package/src/cart/checkout.js +0 -97
  230. package/src/cart/mutations.js +0 -234
  231. package/src/cart/store.js +0 -326
  232. package/src/client.js +0 -267
  233. package/src/config.js +0 -349
  234. package/src/dom.js +0 -287
  235. package/src/fragments.js +0 -237
  236. package/src/index.js +0 -353
  237. package/src/initial.js +0 -87
  238. package/src/lazy-iife.js +0 -116
  239. package/src/lazy.js +0 -39
  240. package/src/market-codes.js +0 -35
  241. package/src/markets.js +0 -334
  242. package/src/markup.js +0 -64
  243. package/src/parts/account-pages.js +0 -6
  244. package/src/parts/account.js +0 -6
  245. package/src/parts/analytics.js +0 -6
  246. package/src/parts/diagnose.js +0 -6
  247. package/src/parts/extras.js +0 -6
  248. package/src/parts/markets.js +0 -6
  249. package/src/parts/search.js +0 -6
  250. package/src/presets/barba.js +0 -190
  251. package/src/product-state.js +0 -318
  252. package/src/selling-plans.js +0 -154
  253. package/src/server.js +0 -215
  254. package/src/store-html.js +0 -0
  255. package/src/storefront/catalog.js +0 -199
  256. package/types/astro.d.ts +0 -28
  257. package/types/barba.d.ts +0 -16
  258. package/types/index.d.ts +0 -577
  259. 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
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * Sending the visitor to Shopify's checkout. The URL comes from Shopify at click time, because the cart's checkout URL
3
- * changes with its market and customer; a logged-in customer's account part may supply its own (`beforeCheckout`).
4
- */
5
- import { cfg, reportError } from '../config.js';
6
- import { TetherError } from '../client.js';
7
- import { isHttpsUrl } from '../shopify-values.js';
8
- import { emit } from '../dom.js';
9
- import { fetchCheckoutUrl } from './mutations.js';
10
- import { customer, currentCart, savedCartId } from './store.js';
11
- /** @import { CheckoutTarget } from '../config.js' */
12
-
13
- /** @typedef {{ kind: 'url', url: string | null } | { kind: 'failed', error: unknown }} CheckoutUrlAnswer */
14
-
15
- /**
16
- * @param {string} cartId
17
- * @returns {Promise<CheckoutUrlAnswer>} Shopify's answer as a value, so the request can run alongside the account's
18
- * and its failure is read only when its URL is the one used.
19
- */
20
- const askCheckoutUrl = (cartId) =>
21
- fetchCheckoutUrl(cartId).then(
22
- (url) => ({ kind: /** @type {const} */ ('url'), url }),
23
- (/** @type {unknown} */ error) => ({ kind: /** @type {const} */ ('failed'), error }),
24
- );
25
-
26
- /**
27
- * @returns {Promise<string | null>} The logged-in customer's checkout URL. When the account can't give one, the
28
- * failure is reported and the cart's own checkout URL is used, which still checks out (without the login).
29
- */
30
- async function accountCheckoutUrl() {
31
- if (!customer.beforeCheckout) return null;
32
- try {
33
- return await customer.beforeCheckout();
34
- } catch (error) {
35
- reportError(error, 'account checkout');
36
- return null;
37
- }
38
- }
39
-
40
- /**
41
- * @param {string} cartId
42
- * @returns {Promise<string>}
43
- * @throws {TetherError} `ERROR` without a checkout URL (the cart is empty or Shopify no longer has it).
44
- */
45
- async function checkoutUrl(cartId) {
46
- const cartAnswer = askCheckoutUrl(cartId);
47
- let url = await accountCheckoutUrl();
48
- if (!url) {
49
- const answer = await cartAnswer;
50
- if (answer.kind === 'failed') throw answer.error;
51
- url = answer.url;
52
- }
53
- if (!url) throw new TetherError('No checkoutUrl (cart empty or expired)');
54
- if (!customer.accessToken) return url;
55
- return `${url}${url.includes('?') ? '&' : '?'}logged_in=true`;
56
- }
57
-
58
- /**
59
- * A new tab's `opener` gives the checkout page a handle on this one; a browser that doesn't let a script clear it
60
- * leaves the tab as it opened.
61
- *
62
- * @param {Window} tab
63
- */
64
- function detachOpenerIfAllowed(tab) {
65
- try {
66
- tab.opener = null;
67
- } catch {
68
- // The browser keeps `opener` read-only: see the function's comment.
69
- }
70
- }
71
-
72
- /**
73
- * Goes to Shopify's checkout. With `_blank` the tab opens inside the click (Safari blocks `window.open` after an
74
- * await) and gets its URL once there is one.
75
- *
76
- * @param {CheckoutTarget} target
77
- * @returns {Promise<string | null>} The checkout URL, `null` with nothing to check out or after a reported failure.
78
- */
79
- export async function checkout(target = cfg.checkoutTarget) {
80
- const cartId = savedCartId();
81
- const cart = currentCart();
82
- if (!cartId || (cart && !cart.totalQuantity)) return null;
83
- const tab = target === '_blank' ? window.open('', '_blank') : null;
84
- if (tab) detachOpenerIfAllowed(tab);
85
- try {
86
- const url = await checkoutUrl(cartId);
87
- if (!isHttpsUrl(url)) throw new TetherError('The checkout URL is not https: not opened');
88
- emit('checkout', { url, cart: currentCart() });
89
- if (tab && !tab.closed) tab.location.href = url;
90
- else window.location.href = url;
91
- return url;
92
- } catch (error) {
93
- tab?.close();
94
- reportError(error, 'checkout failed');
95
- return null;
96
- }
97
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * The Storefront Cart API: loading the saved cart, creating one, and the mutations that change it. Shopify forgets a
3
- * cart some days after its last change and once it has been checked out; a change that needs a cart then creates a
4
- * new one with the same input, so the visitor's click still lands.
5
- */
6
- import { cfg, log } from '../config.js';
7
- import { gql, unwrap, TetherError } from '../client.js';
8
- import { cartFragment } from '../fragments.js';
9
- import { GID } from '../selling-plans.js';
10
- import { toAttributes } from '../dom.js';
11
- import { customer, savedCartId, forgetSavedCart, setWarnings } from './store.js';
12
- /** @import { CartWarning } from '../config.js' */
13
- /** @import { UserError } from '../client.js' */
14
- /** @import { AttributeValue } from '../dom.js' */
15
- /** @import { ShopifyCart, Attribute } from './store.js' */
16
-
17
- /** @typedef {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[]} Attributes */
18
- /**
19
- * @typedef {object} LineSpec A line to add, in any of the shapes `Cart.add` takes.
20
- * @property {string | number} [variantId]
21
- * @property {string | number} [merchandiseId]
22
- * @property {string | number} [id]
23
- * @property {number | string} [quantity]
24
- * @property {string | number | null} [sellingPlanId]
25
- * @property {string | number | null} [sellingPlan]
26
- * @property {Attributes | null} [attributes]
27
- */
28
- /**
29
- * @typedef {object} CartLineInput Shopify's `CartLineInput`.
30
- * @property {string} merchandiseId
31
- * @property {number} quantity
32
- * @property {string} [sellingPlanId]
33
- * @property {Attribute[]} [attributes]
34
- */
35
- /**
36
- * @typedef {object} CartPayload What every cart mutation answers.
37
- * @property {ShopifyCart | null} cart
38
- * @property {UserError[]} userErrors
39
- * @property {CartWarning[]} [warnings]
40
- */
41
-
42
- /**
43
- * Shopify answers a cart id it can't resolve, in a mutation, with a top-level GraphQL error whose only detail is
44
- * this message (no `extensions.code`). A user error on the `cartId` field is recognised by its shape instead.
45
- */
46
- const UNKNOWN_CART_MESSAGE = /does not exist|not found|invalid id/i;
47
-
48
- /**
49
- * @param {unknown} error
50
- * @returns {boolean} True when `error` says Shopify doesn't have the cart, false for any other failure (the network,
51
- * the token, a line it refused), after which the saved cart is still good.
52
- */
53
- export function isCartGone(error) {
54
- if (!(error instanceof TetherError)) return false;
55
- if (error.userErrors?.some((userError) => userError.field?.[0] === 'cartId')) return true;
56
- return (error.errors ?? []).some((graphQLError) => UNKNOWN_CART_MESSAGE.test(graphQLError.message));
57
- }
58
-
59
- /**
60
- * @param {string | number | LineSpec | null | undefined} input A variant id, or a line.
61
- * @param {number} [quantity] For a variant id.
62
- * @param {Omit<LineSpec, 'variantId' | 'quantity'>} [options] For a variant id.
63
- * @returns {CartLineInput}
64
- * @throws {TetherError} `ERROR` without a variant id.
65
- */
66
- export function toLineInput(input, quantity = 1, options = {}) {
67
- const spec = typeof input === 'object' && input !== null ? input : { ...options, variantId: input, quantity };
68
- const merchandiseId = GID.variant(spec.merchandiseId || spec.variantId || spec.id);
69
- if (!merchandiseId) throw new TetherError('add(): variantId is required');
70
- const asked = parseInt(String(spec.quantity ?? 1), 10);
71
- /** @type {CartLineInput} */
72
- const line = { merchandiseId, quantity: Math.max(1, asked || 1) };
73
- const planId = GID.sellingPlan(spec.sellingPlanId || spec.sellingPlan);
74
- if (planId) line.sellingPlanId = planId;
75
- const attributes = toAttributes(spec.attributes);
76
- if (attributes.length) line.attributes = attributes;
77
- return line;
78
- }
79
-
80
- /**
81
- * @param {string} name The mutation, which is also its payload's field.
82
- * @param {string} args The operation's variable definitions.
83
- * @param {string} inner The mutation field's arguments.
84
- */
85
- function mutationText(name, args, inner) {
86
- return `mutation Tether_${name}(${args}) {
87
- ${name}(${inner}) { cart { ...TetherCart } userErrors { field message } warnings { code message target } }
88
- }
89
- ${cartFragment()}`;
90
- }
91
-
92
- /**
93
- * @param {string} name
94
- * @param {string} args
95
- * @param {string} inner
96
- * @param {Record<string, unknown>} variables
97
- * @returns {Promise<ShopifyCart | null>} The cart in Shopify's answer, after keeping its warnings.
98
- * @throws {TetherError} For a failed request, or the mutation's user errors.
99
- */
100
- async function send(name, args, inner, variables) {
101
- /** @type {Record<string, CartPayload | null | undefined>} */
102
- const data = await gql(mutationText(name, args, inner), variables);
103
- const payload = unwrap(data[name], name);
104
- setWarnings(payload.warnings ?? []);
105
- return payload.cart;
106
- }
107
-
108
- /**
109
- * A new cart in the visitor's market, for the logged-in customer if there is one.
110
- *
111
- * @param {Record<string, unknown>} input More of Shopify's `CartInput`, e.g. the first lines.
112
- * @returns {Promise<ShopifyCart | null>}
113
- */
114
- function createCart(input) {
115
- const buyerIdentity = {
116
- ...(cfg.country ? { countryCode: cfg.country.toUpperCase() } : {}),
117
- ...(customer.accessToken ? { customerAccessToken: customer.accessToken } : {}),
118
- };
119
- const withIdentity = Object.keys(buyerIdentity).length ? { buyerIdentity, ...input } : input;
120
- return send('cartCreate', '$input: CartInput!', 'input: $input', { input: withIdentity });
121
- }
122
-
123
- /**
124
- * @typedef {object} CartMutation A mutation on the saved cart.
125
- * @property {string} name The mutation, e.g. `cartLinesAdd`.
126
- * @property {string} args Its variable definitions besides `$cartId`.
127
- * @property {string} inner Its arguments besides `cartId`.
128
- * @property {Record<string, unknown>} variables
129
- * @property {Record<string, unknown> | null} createInput The `CartInput` of a new cart that does what the mutation
130
- * asks, when there is no cart (or Shopify no longer has it); `null` leaves the visitor without a cart instead.
131
- */
132
-
133
- /**
134
- * Runs a mutation on the saved cart, or creates a cart from `createInput` when there is none to change.
135
- *
136
- * @param {CartMutation} mutation
137
- * @returns {Promise<ShopifyCart | null>} The changed cart; `null` without a cart and without a `createInput`.
138
- * @throws {TetherError} For a failed request or Shopify's user errors; also when the cart is gone and there is no
139
- * `createInput` (the saved cart is forgotten first).
140
- */
141
- export async function mutateCart({ name, args, inner, variables, createInput }) {
142
- const cartId = savedCartId();
143
- if (!cartId) return createInput ? createCart(createInput) : null;
144
- try {
145
- return await send(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`, { ...variables, cartId });
146
- } catch (error) {
147
- if (!isCartGone(error)) throw error;
148
- forgetSavedCart();
149
- if (!createInput) throw error;
150
- log('cart expired, creating a new one');
151
- return createCart(createInput);
152
- }
153
- }
154
-
155
- /**
156
- * @param {string} id
157
- * @returns {Promise<ShopifyCart | null>} The cart, `null` when Shopify doesn't have it (expired or checked out).
158
- */
159
- export async function fetchCart(id) {
160
- /** @type {{ cart: ShopifyCart | null }} */
161
- const data = await gql(`query TetherCart($id: ID!) { cart(id: $id) { ...TetherCart } }\n${cartFragment()}`, { id });
162
- return data.cart;
163
- }
164
-
165
- /**
166
- * @param {string} id
167
- * @returns {Promise<string | null>} The cart's current checkout URL, `null` when Shopify doesn't have the cart.
168
- */
169
- export async function fetchCheckoutUrl(id) {
170
- /** @type {{ cart: { checkoutUrl: string } | null }} */
171
- const data = await gql('query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }', { id });
172
- return data.cart?.checkoutUrl ?? null;
173
- }
174
-
175
- /**
176
- * @typedef {object} LinePatch A change to one line, for `updateLine`.
177
- * @property {number} [quantity] 0 or less removes the line.
178
- * @property {string | number | null} [sellingPlanId] `null` switches the line back to one-time purchase.
179
- * @property {Attributes} [attributes]
180
- * @property {string | number} [merchandiseId]
181
- * @property {string | number} [variantId]
182
- */
183
-
184
- /**
185
- * @param {string} lineId
186
- * @param {number | LinePatch} patch
187
- * @returns {Record<string, unknown>} Shopify's `CartLineUpdateInput`.
188
- */
189
- export function toLineUpdate(lineId, patch) {
190
- const change = typeof patch === 'number' ? { quantity: patch } : patch;
191
- /** @type {Record<string, unknown>} */
192
- const line = { id: lineId };
193
- if (change.quantity !== undefined) line['quantity'] = change.quantity;
194
- if ('sellingPlanId' in change)
195
- line['sellingPlanId'] = change.sellingPlanId ? GID.sellingPlan(change.sellingPlanId) : null;
196
- if (change.attributes) line['attributes'] = toAttributes(change.attributes);
197
- const variantId = change.merchandiseId || change.variantId;
198
- if (variantId) line['merchandiseId'] = GID.variant(variantId);
199
- return line;
200
- }
201
-
202
- /**
203
- * @param {Record<string, unknown>[]} lines
204
- * @returns {Promise<ShopifyCart | null>}
205
- */
206
- export const updateLines = (lines) =>
207
- mutateCart({
208
- name: 'cartLinesUpdate',
209
- args: '$lines: [CartLineUpdateInput!]!',
210
- inner: 'lines: $lines',
211
- variables: { lines },
212
- createInput: null,
213
- });
214
-
215
- /**
216
- * @param {string[]} lineIds
217
- * @returns {Promise<ShopifyCart | null>}
218
- */
219
- export const removeLines = (lineIds) =>
220
- mutateCart({
221
- name: 'cartLinesRemove',
222
- args: '$lineIds: [ID!]!',
223
- inner: 'lineIds: $lineIds',
224
- variables: { lineIds },
225
- createInput: null,
226
- });
227
-
228
- /**
229
- * @param {string} lineId
230
- * @param {number} target
231
- * @returns {Promise<ShopifyCart | null>} The cart once `lineId` has `target` (0 removes it).
232
- */
233
- export const setLineQuantity = (lineId, target) =>
234
- target > 0 ? updateLines([{ id: lineId, quantity: target }]) : removeLines([lineId]);