@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
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The cart on this page: what Shopify last said about it (`CartStatus`), the quantities quick clicks asked for and
3
+ * the one queue every change goes through. A change of what the page shows goes through
4
+ * here, so the document (`renderCart`) and the listeners (`tether:cart-updated`) always see the store's cart.
5
+ */
6
+ import { cfg } from '../config.ts';
7
+ import { emit } from '../dom.ts';
8
+ import { isOpen } from './drawer.ts';
9
+ import { renderCart, quantityOf, showSavedCount } from './render.ts';
10
+ import { getStored, removeStored, setStored } from '../storage.ts';
11
+ import type { CartWarning } from '../config.ts';
12
+ import type { Image } from '../fragments.ts';
13
+ import type { Money } from '../shopify-values.ts';
14
+
15
+ export interface Attribute {
16
+ key: string;
17
+ value: string;
18
+ }
19
+
20
+ /** A line's `merchandise`: Shopify's `Merchandise` union has only `ProductVariant`. */
21
+ export interface CartVariant {
22
+ id: string;
23
+ title: string;
24
+ sku: string | null;
25
+ availableForSale: boolean;
26
+ requiresShipping: boolean;
27
+ /** Absent when the store's token can't read inventory. */
28
+ quantityAvailable?: number | null;
29
+ image: Image | null;
30
+ price: Money;
31
+ compareAtPrice: Money | null;
32
+ selectedOptions: { name: string; value: string }[];
33
+ product: { id: string; title: string; handle: string; vendor: string; featuredImage: Image | null };
34
+ }
35
+
36
+ interface LinePlanAllocation {
37
+ sellingPlan: {
38
+ id: string;
39
+ name: string;
40
+ description: string | null;
41
+ recurringDeliveries: boolean;
42
+ options: { name: string; value: string }[];
43
+ };
44
+ checkoutChargeAmount: Money;
45
+ priceAdjustments: { price: Money; compareAtPrice: Money; perDeliveryPrice: Money }[];
46
+ }
47
+
48
+ export interface ShopifyCartLine {
49
+ id: string;
50
+ quantity: number;
51
+ attributes: Attribute[];
52
+ cost: {
53
+ totalAmount: Money;
54
+ subtotalAmount: Money;
55
+ amountPerQuantity: Money;
56
+ compareAtAmountPerQuantity: Money | null;
57
+ };
58
+ discountAllocations: { discountedAmount: Money }[];
59
+ sellingPlanAllocation: LinePlanAllocation | null;
60
+ merchandise: CartVariant;
61
+ /** Only on a bundle (`ComponentizableCartLine`). */
62
+ lineComponents?: {
63
+ id: string;
64
+ quantity: number;
65
+ merchandise: { id: string; title: string; product: { title: string; handle: string } };
66
+ }[];
67
+ }
68
+
69
+ /** The fields of `fragment TetherCart`. */
70
+ export interface ShopifyCart {
71
+ id: string;
72
+ checkoutUrl: string;
73
+ totalQuantity: number;
74
+ note: string | null;
75
+ createdAt: string;
76
+ updatedAt: string;
77
+ attributes: Attribute[];
78
+ buyerIdentity: { countryCode: string | null; email: string | null };
79
+ discountCodes: { code: string; applicable: boolean }[];
80
+ cost: { subtotalAmount: Money; totalAmount: Money; checkoutChargeAmount: Money };
81
+ lines: { nodes: ShopifyCartLine[] };
82
+ }
83
+
84
+ /**
85
+ * `unknown` until the saved cart has loaded (the page's own markup shows); `empty` without a cart (none saved, or
86
+ * Shopify no longer has it); `error` after a failed load or change, with the cart as Shopify last sent it.
87
+ */
88
+ export type CartStatus =
89
+ | { kind: 'unknown' }
90
+ | { kind: 'empty' }
91
+ | { kind: 'ready'; cart: ShopifyCart }
92
+ | { kind: 'error'; error: unknown; cart: ShopifyCart | null };
93
+
94
+ /** What one quick +/- click asked for; a newer click replaces it. */
95
+ export interface PendingQuantity {
96
+ quantity: number;
97
+ }
98
+
99
+ /** The second argument of a `subscribe` listener. */
100
+ interface CartMeta {
101
+ open: boolean;
102
+ count: number;
103
+ loaded: boolean;
104
+ /** A copy of Shopify's notes on the last change. */
105
+ warnings: CartWarning[];
106
+ }
107
+
108
+ let status: CartStatus = { kind: 'unknown' };
109
+
110
+ /** Shopify's notes on the last change. */
111
+ let warnings: readonly CartWarning[] = [];
112
+
113
+ /** Line id → what quick +/- clicks asked for, until Shopify has answered. */
114
+ export const pendingQuantities = new Map<string, PendingQuantity>();
115
+
116
+ export const cartStatus = (): CartStatus => status;
117
+
118
+ /** The cart as Shopify last sent it. */
119
+ export function currentCart(): ShopifyCart | null {
120
+ switch (status.kind) {
121
+ case 'ready':
122
+ case 'error':
123
+ return status.cart;
124
+ case 'unknown':
125
+ case 'empty':
126
+ return null;
127
+ }
128
+ }
129
+
130
+ export const currentWarnings = (): readonly CartWarning[] => warnings;
131
+
132
+ const statusOf = (cart: ShopifyCart | null): CartStatus => (cart ? { kind: 'ready', cart } : { kind: 'empty' });
133
+
134
+ const seenKey = () => `${cfg.storageKey}:seen`;
135
+
136
+ /** The id of the visitor's cart, kept across pages. */
137
+ export const savedCartId = (): string | null => getStored(cfg.storageKey) || null;
138
+
139
+ /** Forgets the saved cart, which Shopify no longer has. */
140
+ export function forgetSavedCart(): void {
141
+ removeStored(cfg.storageKey);
142
+ }
143
+
144
+ /** Keeps a cart Shopify sent without showing it, for an answer a newer change will replace. */
145
+ export function keepQuietly(cart: ShopifyCart | null): void {
146
+ status = statusOf(cart);
147
+ if (cart) setStored(cfg.storageKey, cart.id);
148
+ }
149
+
150
+ /** Renders the document with the store's cart. */
151
+ export function render(): void {
152
+ renderCart({
153
+ cart: currentCart(),
154
+ warnings,
155
+ pending: pendingQuantities,
156
+ failed: status.kind === 'error',
157
+ });
158
+ }
159
+
160
+ /** @param reason The `reason` of `tether:cart-updated`. */
161
+ export function announce(reason: string): void {
162
+ render();
163
+ emit('cart-updated', { cart: currentCart(), reason });
164
+ }
165
+
166
+ /**
167
+ * Keeps a cart Shopify sent after a change, shows it, and tells this page and the visitor's other tabs.
168
+ *
169
+ * @returns `cart`.
170
+ */
171
+ export function commit(cart: ShopifyCart | null, reason: string): ShopifyCart | null {
172
+ keepQuietly(cart);
173
+ // Other tabs reload the cart on this storage event; the next page shows the count before its cart has loaded.
174
+ setStored(seenKey(), `${Date.now()}|${cart?.totalQuantity ?? 0}`);
175
+ announce(reason);
176
+ return cart;
177
+ }
178
+
179
+ /** The outcome of loading the saved cart: the cart, `null` when there is none, or the failure. */
180
+ export function settleLoad(
181
+ outcome: { kind: 'loaded'; cart: ShopifyCart | null } | { kind: 'failed'; error: unknown },
182
+ ): void {
183
+ status = outcome.kind === 'loaded' ? statusOf(outcome.cart) : { kind: 'error', error: outcome.error, cart: null };
184
+ announce('load');
185
+ }
186
+
187
+ /**
188
+ * The cart's count on the last page, from `<storageKey>:seen` (`<time>|<count>`).
189
+ *
190
+ * @returns `null` unless the count is a whole number: other scripts and other Tether versions write storage too.
191
+ */
192
+ function lastKnownCount(): number | null {
193
+ const text = getStored(seenKey())?.split('|')[1] ?? '';
194
+ const count = Number(text);
195
+ return /^\d+$/.test(text) && Number.isSafeInteger(count) ? count : null;
196
+ }
197
+
198
+ /** Until the saved cart has loaded, the counts show the count it had on the last page instead of the page's 0. */
199
+ export function showLastKnownCount(): void {
200
+ if (status.kind !== 'unknown') return;
201
+ const count = lastKnownCount();
202
+ if (count !== null) showSavedCount(count);
203
+ }
204
+
205
+ /** Sets Shopify's notes on the last change: each message once, in the site's words (`warningText`). */
206
+ export function setWarnings(notes: readonly CartWarning[]): void {
207
+ warnings = notes
208
+ .filter((note, index) => notes.findIndex((other) => other.message === note.message) === index)
209
+ .map((note) => ({ ...note, message: cfg.warningText?.(note) || note.message }));
210
+ if (warnings.length) emit('cart-warning', { warnings });
211
+ }
212
+
213
+ /** The queue only waits for a task: its result, or failure, reaches the caller through the promise `run` returns. */
214
+ const waitOnly = () => undefined;
215
+
216
+ /** @returns A runner that starts each task once the one before has settled, whether it succeeded or failed. */
217
+ export function serial(): <T>(task: () => Promise<T>) => Promise<T> {
218
+ let previous: Promise<unknown> = Promise.resolve();
219
+ return <T>(task: () => Promise<T>) => {
220
+ const run = previous.then(task);
221
+ previous = run.then(waitOnly, waitOnly);
222
+ return run;
223
+ };
224
+ }
225
+
226
+ const runInOrder = serial();
227
+
228
+ /**
229
+ * Runs a cart change after the ones before it, so quick clicks never race. A failure sets the `error` status (shown by
230
+ * `tether-show="cart-error"` until the next change succeeds), emits `tether:cart-error` and rejects.
231
+ */
232
+ export function enqueue<T>(task: () => Promise<T>): Promise<T> {
233
+ return runInOrder(task).then(
234
+ (value) => {
235
+ if (status.kind === 'error') {
236
+ status = statusOf(status.cart);
237
+ render();
238
+ }
239
+ return value;
240
+ },
241
+ (error: unknown) => {
242
+ status = { kind: 'error', error, cart: currentCart() };
243
+ render();
244
+ emit('cart-error', { error });
245
+ throw error;
246
+ },
247
+ );
248
+ }
249
+
250
+ /** @returns The quantity to show for `line`. */
251
+ export const pendingQuantityOf = (line: ShopifyCartLine): number => quantityOf(line, pendingQuantities);
252
+
253
+ const meta = (): CartMeta => ({
254
+ open: isOpen(),
255
+ count: currentCart()?.totalQuantity ?? 0,
256
+ loaded: status.kind !== 'unknown',
257
+ warnings: [...warnings],
258
+ });
259
+
260
+ /**
261
+ * Calls `listener` now and after every change of the cart or the drawer.
262
+ *
263
+ * @returns Stops listening.
264
+ */
265
+ export function subscribe(listener: (cart: ShopifyCart | null, meta: CartMeta) => void): () => void {
266
+ const events = ['cart-updated', 'cart-toggled'].map((name) => `${cfg.prefix}:${name}`);
267
+ const call = () => {
268
+ listener(currentCart(), meta());
269
+ };
270
+ events.forEach((name) => {
271
+ document.addEventListener(name, call);
272
+ });
273
+ call();
274
+ return () => {
275
+ events.forEach((name) => {
276
+ document.removeEventListener(name, call);
277
+ });
278
+ };
279
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The cart, as one object: `Tether.cart`. Every change goes through one queue (fast clicks never race) and resolves
3
- * with the cart Shopify sent back. The state lives in `cart/store.js`, the Cart API calls in `cart/mutations.js`, the
4
- * drawer in `cart/drawer.js`, the document rendering in `cart/render.js` and the checkout in `cart/checkout.js`.
3
+ * with the cart Shopify sent back. The state lives in `cart/store.ts`, the Cart API calls in `cart/mutations.ts`, the
4
+ * drawer in `cart/drawer.ts`, the document rendering in `cart/render.ts` and the checkout in `cart/checkout.ts`.
5
5
  */
6
- import { cfg, reportError } from './config.js';
7
- import { TetherError } from './client.js';
8
- import { emit, toAttributes } from './dom.js';
6
+ import { cfg, reportError } from './config.ts';
7
+ import { TetherError } from './client.ts';
8
+ import { emit, toAttributes } from './dom.ts';
9
9
  import {
10
10
  fetchCart,
11
11
  isCartGone,
@@ -15,16 +15,15 @@ import {
15
15
  toLineInput,
16
16
  toLineUpdate,
17
17
  updateLines,
18
- } from './cart/mutations.js';
19
- import { checkout } from './cart/checkout.js';
20
- import { drawers, isOpen, openCart, closeCart, toggleCart, trapFocus } from './cart/drawer.js';
18
+ } from './cart/mutations.ts';
19
+ import { checkout, setCheckoutResolver } from './cart/checkout.ts';
20
+ import { drawers, isOpen, openCart, closeCart, toggleCart, trapFocus } from './cart/drawer.ts';
21
21
  import {
22
22
  announce,
23
23
  cartStatus,
24
24
  commit,
25
25
  currentCart,
26
26
  currentWarnings,
27
- customer,
28
27
  enqueue,
29
28
  forgetSavedCart,
30
29
  keepQuietly,
@@ -32,30 +31,26 @@ import {
32
31
  pendingQuantityOf,
33
32
  render,
34
33
  savedCartId,
35
- setFailure,
36
34
  settleLoad,
37
35
  showLastKnownCount,
38
36
  subscribe,
39
- } from './cart/store.js';
40
- /** @import { CartWarning } from './config.js' */
41
- /** @import { Attributes, LineSpec, CartLineInput, LinePatch } from './cart/mutations.js' */
42
- /** @import { ShopifyCart, ShopifyCartLine, CustomerLink, CartStatus } from './cart/store.js' */
37
+ } from './cart/store.ts';
38
+ import type { CartWarning } from './config.ts';
39
+ import type { Attributes, LineSpec, CartLineInput, LinePatch } from './cart/mutations.ts';
40
+ import type { ShopifyCart, ShopifyCartLine, CartStatus } from './cart/store.ts';
43
41
 
44
42
  export { trapFocus };
45
43
 
46
- /** @returns {readonly ShopifyCartLine[]} */
47
- const currentLines = () => currentCart()?.lines.nodes ?? [];
44
+ type DiscountCodes = string[] | string | ((current: string[]) => string[]);
45
+
46
+ const currentLines = (): readonly ShopifyCartLine[] => currentCart()?.lines.nodes ?? [];
48
47
 
49
48
  /**
50
49
  * +1 / -1 for a line. Quick clicks add up: each builds on the quantity the click before asked for, which shows at once
51
50
  * (`tether-line="quantity"`, `quantityOf`) until Shopify has answered. Only the latest target is sent; the ones it
52
51
  * replaced are skipped.
53
- *
54
- * @param {string} lineId
55
- * @param {number} delta
56
- * @returns {Promise<ShopifyCart | null>}
57
52
  */
58
- function changeQuantity(lineId, delta) {
53
+ function changeQuantity(lineId: string, delta: number): Promise<ShopifyCart | null> {
59
54
  const line = currentLines().find((candidate) => candidate.id === lineId);
60
55
  if (!line) return Promise.resolve(currentCart());
61
56
  const target = Math.max(0, pendingQuantityOf(line) + delta);
@@ -87,20 +82,16 @@ function changeQuantity(lineId, delta) {
87
82
  /**
88
83
  * The codes the cart has once a change is its turn in the queue.
89
84
  *
90
- * @param {string[] | string | ((current: string[]) => string[])} codes A function gets the codes as they are by then.
91
- * @returns {string[]}
85
+ * @param codes A function gets the codes as they are by then.
92
86
  */
93
- function discountCodesFrom(codes) {
87
+ function discountCodesFrom(codes: DiscountCodes): string[] {
94
88
  const current = (currentCart()?.discountCodes ?? []).map((discount) => discount.code);
95
89
  const list = typeof codes === 'function' ? codes(current) : codes;
96
90
  return (Array.isArray(list) ? list : [list]).map((code) => code.trim()).filter(Boolean);
97
91
  }
98
92
 
99
- /**
100
- * @param {Attributes} attributes
101
- * @returns {{ key: string, value: string }[]} The cart's attributes with `attributes` merged in; `''` removes a key.
102
- */
103
- function mergedAttributes(attributes) {
93
+ /** @returns The cart's attributes with `attributes` merged in; `''` removes a key. */
94
+ function mergedAttributes(attributes: Attributes): { key: string; value: string }[] {
104
95
  const merged = new Map((currentCart()?.attributes ?? []).map((attribute) => [attribute.key, attribute.value]));
105
96
  toAttributes(attributes).forEach((attribute) => merged.set(attribute.key, attribute.value));
106
97
  return [...merged].filter(([, value]) => value !== '').map(([key, value]) => ({ key, value }));
@@ -109,10 +100,8 @@ function mergedAttributes(attributes) {
109
100
  /**
110
101
  * Loads the saved cart. A cart Shopify no longer has is forgotten; one it can't be asked about (the network, the
111
102
  * token) is kept, because it is probably fine.
112
- *
113
- * @returns {Promise<ShopifyCart | null>}
114
103
  */
115
- async function init() {
104
+ async function init(): Promise<ShopifyCart | null> {
116
105
  const id = savedCartId();
117
106
  if (!id) {
118
107
  settleLoad({ kind: 'loaded', cart: null });
@@ -133,73 +122,59 @@ async function init() {
133
122
 
134
123
  export const Cart = {
135
124
  init,
136
- /** @returns {string | null} The saved cart's id. */
125
+ /** The saved cart's id. */
137
126
  id: savedCartId,
138
127
 
139
- /** @returns {CartStatus} */
140
- get status() {
128
+ get status(): CartStatus {
141
129
  return cartStatus();
142
130
  },
143
- /** @returns {ShopifyCart | null} */
144
- get state() {
131
+ get state(): ShopifyCart | null {
145
132
  return currentCart();
146
133
  },
147
- /** @returns {readonly ShopifyCartLine[]} */
148
- get lines() {
149
- return currentLines();
134
+ /** The cart's lines, a copy: changing it changes nothing in the cart. */
135
+ get lines(): ShopifyCartLine[] {
136
+ return [...currentLines()];
150
137
  },
151
- /** @returns {number} */
152
- get count() {
138
+ get count(): number {
153
139
  return currentCart()?.totalQuantity ?? 0;
154
140
  },
155
- /** @returns {boolean} True once the saved cart, or that there is none, is known. */
156
- get loaded() {
141
+ /** True once the saved cart, or that there is none, is known. */
142
+ get loaded(): boolean {
157
143
  return cartStatus().kind !== 'unknown';
158
144
  },
159
- /** @returns {unknown} The failure of the last load or change, until a change succeeds. */
160
- get lastError() {
145
+ /**
146
+ * The failure of the last load or change, until a change succeeds. Requests fail with an `Error`; anything else
147
+ * thrown is wrapped, so the public type holds.
148
+ */
149
+ get lastError(): Error | null {
161
150
  const current = cartStatus();
162
- return current.kind === 'error' ? current.error : null;
163
- },
164
- set lastError(error) {
165
- setFailure(error);
151
+ if (current.kind !== 'error') return null;
152
+ return current.error instanceof Error ? current.error : new Error(String(current.error));
166
153
  },
167
- /** @returns {readonly CartWarning[]} Shopify's notes on the last change. */
168
- get warnings() {
169
- return currentWarnings();
154
+ /** Shopify's notes on the last change, a copy. */
155
+ get warnings(): CartWarning[] {
156
+ return [...currentWarnings()];
170
157
  },
171
- /** @returns {ReadonlyMap<string, { quantity: number }>} Line id → the quantity quick clicks asked for. */
172
- get pendingQty() {
158
+ /** Line id → the quantity quick clicks asked for. */
159
+ get pendingQty(): ReadonlyMap<string, { quantity: number }> {
173
160
  return pendingQuantities;
174
161
  },
175
162
 
176
- /** @returns {string | null} The logged-in customer's Customer Account API token (set by the account part). */
177
- get customerAccessToken() {
178
- return customer.accessToken;
179
- },
180
- set customerAccessToken(token) {
181
- customer.accessToken = token || null;
182
- },
183
- /** @returns {(() => Promise<string | null>) | null} The account's own checkout URL (set by the account part). */
184
- get beforeCheckout() {
185
- return customer.beforeCheckout;
186
- },
187
- set beforeCheckout(fetchUrl) {
188
- customer.beforeCheckout = typeof fetchUrl === 'function' ? fetchUrl : null;
189
- },
190
163
  /**
191
- * Links the cart to a logged-in customer, or unlinks it with `null`s. Carts created from now on belong to the
192
- * customer; `beforeCheckout` gives the checkout URL (`null` from it uses the cart's).
193
- *
194
- * @param {Partial<CustomerLink>} link Fields left out keep their value.
164
+ * Sets where a logged-in customer checks out, or clears it with `null`: the account part's resolver answers the
165
+ * customer's checkout URL for the cart (`null` uses the cart's own).
195
166
  */
196
- setCustomer(link) {
197
- if (link.accessToken !== undefined) customer.accessToken = link.accessToken;
198
- if (link.beforeCheckout !== undefined) customer.beforeCheckout = link.beforeCheckout;
167
+ setCheckoutResolver,
168
+
169
+ /**
170
+ * Forgets the saved cart on this device, as if there were none: the page shows an empty cart and
171
+ * `tether:cart-updated` fires with reason `load`. The next add starts a new cart.
172
+ */
173
+ forgetSaved(): void {
174
+ forgetSavedCart();
175
+ settleLoad({ kind: 'loaded', cart: null });
199
176
  },
200
177
 
201
- /** Keeps a cart and shows it, as a change does; tests seed the cart with it. */
202
- _commit: commit,
203
178
  render,
204
179
  subscribe,
205
180
 
@@ -207,14 +182,16 @@ export const Cart = {
207
182
  * `add(variantId, 2)`, `add({ variantId, quantity, sellingPlanId, attributes })`, or `add([line, line])` for
208
183
  * bundles in one request. Creates the cart when there is none.
209
184
  *
210
- * @param {string | number | LineSpec | (string | number | LineSpec)[]} input
211
- * @param {number} [quantity]
212
- * @param {Omit<LineSpec, 'variantId' | 'quantity'>} [options]
213
- * @returns {Promise<ShopifyCart | null>}
185
+ * @throws {TetherError} (rejected) When the add fails, or Shopify answers it without a cart.
214
186
  */
215
- add(input, quantity = 1, options = {}) {
216
- /** @type {CartLineInput[]} */
217
- const lines = (Array.isArray(input) ? input : [input]).map((line) => toLineInput(line, quantity, options));
187
+ add(
188
+ input: string | number | LineSpec | (string | number | LineSpec)[],
189
+ quantity = 1,
190
+ options: Omit<LineSpec, 'variantId' | 'quantity'> = {},
191
+ ): Promise<ShopifyCart> {
192
+ const lines: CartLineInput[] = (Array.isArray(input) ? input : [input]).map((line) =>
193
+ toLineInput(line, quantity, options),
194
+ );
218
195
  return enqueue(async () => {
219
196
  const cart = await mutateCart({
220
197
  name: 'cartLinesAdd',
@@ -223,6 +200,8 @@ export const Cart = {
223
200
  variables: { lines },
224
201
  createInput: { lines },
225
202
  });
203
+ // A mutation answers without a cart only beside user errors, which reject above; an add always has one.
204
+ if (!cart) throw new TetherError('Shopify answered the add without a cart');
226
205
  commit(cart, 'add');
227
206
  emit('added', { cart, lines });
228
207
  if (cfg.openCartOnAdd) openCart();
@@ -233,11 +212,9 @@ export const Cart = {
233
212
  /**
234
213
  * `updateLine(lineId, 3)` or `updateLine(lineId, { quantity, sellingPlanId, attributes })`.
235
214
  *
236
- * @param {string} lineId
237
- * @param {number | LinePatch} patch A quantity of 0 or less removes the line.
238
- * @returns {Promise<ShopifyCart | null>}
215
+ * @param patch A quantity of 0 or less removes the line.
239
216
  */
240
- updateLine(lineId, patch) {
217
+ updateLine(lineId: string, patch: number | LinePatch): Promise<ShopifyCart | null> {
241
218
  const quantity = typeof patch === 'number' ? patch : patch.quantity;
242
219
  if (quantity !== undefined && quantity <= 0) return Cart.removeLine(lineId);
243
220
  const line = toLineUpdate(lineId, patch);
@@ -246,33 +223,21 @@ export const Cart = {
246
223
 
247
224
  changeQuantity,
248
225
 
249
- /**
250
- * @param {ShopifyCartLine} line
251
- * @returns {number} The quantity to show: the one asked for while an update is on its way.
252
- */
226
+ /** The quantity to show: the one asked for while an update is on its way. */
253
227
  quantityOf: pendingQuantityOf,
254
228
 
255
- /**
256
- * @param {string | string[]} lineIds
257
- * @returns {Promise<ShopifyCart | null>}
258
- */
259
- removeLine(lineIds) {
229
+ removeLine(lineIds: string | string[]): Promise<ShopifyCart | null> {
260
230
  const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
261
231
  return enqueue(async () => commit(await removeLines(ids), 'remove'));
262
232
  },
263
233
 
264
- /** @returns {Promise<ShopifyCart | null>} */
265
- clear() {
234
+ clear(): Promise<ShopifyCart | null> {
266
235
  const ids = currentLines().map((line) => line.id);
267
236
  if (!ids.length) return Promise.resolve(currentCart());
268
237
  return Cart.removeLine(ids);
269
238
  },
270
239
 
271
- /**
272
- * @param {string | null | undefined} note
273
- * @returns {Promise<ShopifyCart | null>}
274
- */
275
- setNote(note) {
240
+ setNote(note: string | null | undefined): Promise<ShopifyCart | null> {
276
241
  const variables = { note: note ?? '' };
277
242
  return enqueue(async () =>
278
243
  commit(
@@ -291,10 +256,9 @@ export const Cart = {
291
256
  /**
292
257
  * Cart attributes, merged with the cart's (as it is when the change runs, so quick calls don't drop each other).
293
258
  *
294
- * @param {Attributes} attributes The value `''` removes a key.
295
- * @returns {Promise<ShopifyCart | null>}
259
+ * @param attributes The value `''` removes a key.
296
260
  */
297
- setAttributes(attributes) {
261
+ setAttributes(attributes: Attributes): Promise<ShopifyCart | null> {
298
262
  return enqueue(async () => {
299
263
  const variables = { attributes: mergedAttributes(attributes) };
300
264
  const cart = await mutateCart({
@@ -308,13 +272,8 @@ export const Cart = {
308
272
  });
309
273
  },
310
274
 
311
- /**
312
- * Replaces the discount codes; `[]` removes them all.
313
- *
314
- * @param {string[] | string | ((current: string[]) => string[])} codes
315
- * @returns {Promise<ShopifyCart | null>}
316
- */
317
- setDiscountCodes(codes) {
275
+ /** Replaces the discount codes; `[]` removes them all. */
276
+ setDiscountCodes(codes: DiscountCodes): Promise<ShopifyCart | null> {
318
277
  return enqueue(async () => {
319
278
  const variables = { discountCodes: discountCodesFrom(codes) };
320
279
  const cart = await mutateCart({
@@ -331,11 +290,9 @@ export const Cart = {
331
290
  /**
332
291
  * Adds a code. A code Shopify doesn't accept is removed again, so it doesn't warn on every later change.
333
292
  *
334
- * @param {string} code
335
- * @returns {Promise<ShopifyCart | null>}
336
293
  * @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
337
294
  */
338
- async applyDiscount(code) {
295
+ async applyDiscount(code: string): Promise<ShopifyCart | null> {
339
296
  const cart = await Cart.setDiscountCodes((current) => [...new Set([...current, code])]);
340
297
  const wanted = code.toLowerCase();
341
298
  const applied = (cart?.discountCodes ?? []).find((discount) => discount.code.toLowerCase() === wanted);
@@ -348,11 +305,7 @@ export const Cart = {
348
305
  throw new TetherError(`Discount code not valid: ${code}`, { code: 'INVALID_DISCOUNT' });
349
306
  },
350
307
 
351
- /**
352
- * @param {string} code
353
- * @returns {Promise<ShopifyCart | null>}
354
- */
355
- removeDiscount(code) {
308
+ removeDiscount(code: string): Promise<ShopifyCart | null> {
356
309
  const unwanted = code.toLowerCase();
357
310
  return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
358
311
  },
@@ -360,11 +313,12 @@ export const Cart = {
360
313
  /**
361
314
  * Sets who is buying; a country changes the cart's currency and prices to that market's.
362
315
  *
363
- * @param {{ countryCode?: string, email?: string, phone?: string, customerAccessToken?: string | null }} buyerIdentity
364
- * @param {{ create?: boolean }} [options] `create: false` forgets an expired cart instead of creating an empty one.
365
- * @returns {Promise<ShopifyCart | null>}
316
+ * @param options `create: false` forgets an expired cart instead of creating an empty one.
366
317
  */
367
- setBuyerIdentity(buyerIdentity, { create = true } = {}) {
318
+ setBuyerIdentity(
319
+ buyerIdentity: { countryCode?: string; email?: string; phone?: string },
320
+ { create = true }: { create?: boolean } = {},
321
+ ): Promise<ShopifyCart | null> {
368
322
  return enqueue(async () => {
369
323
  const cart = await mutateCart({
370
324
  name: 'cartBuyerIdentityUpdate',
@@ -372,7 +326,7 @@ export const Cart = {
372
326
  inner: 'buyerIdentity: $buyerIdentity',
373
327
  variables: { buyerIdentity },
374
328
  createInput: create ? { buyerIdentity } : null,
375
- }).catch((/** @type {unknown} */ error) => {
329
+ }).catch((error: unknown) => {
376
330
  // mutateCart has forgotten the cart Shopify no longer has; without `create` that leaves no cart.
377
331
  if (create || !isCartGone(error)) throw error;
378
332
  return null;