@occluze/tether 0.6.7 → 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 (250) 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 -1
  24. package/dist/presets/barba.esm.js +55 -39
  25. package/dist/tether.account-pages.js +781 -495
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +462 -381
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +299 -194
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +354 -262
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +82 -45
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +3653 -2116
  36. package/dist/tether.markets.js +238 -150
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +3 -3
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +207 -136
  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 +42 -22
  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.ts +75 -0
  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.ts +253 -0
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/subscriptions-view.ts +115 -0
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/account-pages.ts +81 -0
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/analytics-shopify.ts +246 -0
  135. package/src/analytics.ts +315 -0
  136. package/src/api-version.ts +79 -0
  137. package/src/astro.ts +272 -0
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/drawer.ts +125 -0
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/render.ts +266 -0
  143. package/src/cart/store.ts +279 -0
  144. package/src/cart.ts +345 -0
  145. package/src/client.ts +391 -0
  146. package/src/collection.ts +175 -0
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/cart-controls.ts +123 -0
  150. package/src/controls/discount.ts +75 -0
  151. package/src/controls/extras.ts +54 -0
  152. package/src/controls/navigation.ts +77 -0
  153. package/src/diagnose-markup.ts +298 -0
  154. package/src/diagnose.ts +294 -0
  155. package/src/dom.ts +268 -0
  156. package/src/extras.ts +136 -0
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/images.ts +91 -0
  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.ts +363 -0
  165. package/src/market-codes.ts +6 -0
  166. package/src/markets-ui.ts +227 -0
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/mounting.ts +264 -0
  170. package/src/page-market.ts +110 -0
  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.ts +115 -0
  181. package/src/product-render.ts +218 -0
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/product.ts +308 -0
  185. package/src/repeaters.ts +161 -0
  186. package/src/search.ts +305 -0
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/shopify-values.ts +91 -0
  190. package/src/status.ts +106 -0
  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.ts +144 -0
  195. package/src/variant-render.ts +139 -0
  196. package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
  197. package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
  198. package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
  199. package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
  200. package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
  201. package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
  202. package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
  203. package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
  204. package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
  205. package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
  206. package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
  207. package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
  208. package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
  209. package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
  210. package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
  211. package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
  212. package/dist/esm/chunks/search-NILMGYXG.js +0 -1
  213. package/src/account-pages.js +0 -318
  214. package/src/account.js +0 -385
  215. package/src/analytics.js +0 -232
  216. package/src/api-version.js +0 -35
  217. package/src/astro.js +0 -101
  218. package/src/cart.js +0 -559
  219. package/src/client.js +0 -131
  220. package/src/collection.js +0 -114
  221. package/src/config.js +0 -85
  222. package/src/diagnose.js +0 -246
  223. package/src/dom.js +0 -288
  224. package/src/extras.js +0 -63
  225. package/src/fragments.js +0 -144
  226. package/src/index.js +0 -395
  227. package/src/initial.js +0 -75
  228. package/src/lazy-iife.js +0 -46
  229. package/src/lazy.js +0 -18
  230. package/src/loader.js +0 -198
  231. package/src/markets-ui.js +0 -90
  232. package/src/markets.js +0 -133
  233. package/src/mounting.js +0 -154
  234. package/src/parts/account-pages.js +0 -3
  235. package/src/parts/account.js +0 -3
  236. package/src/parts/analytics.js +0 -3
  237. package/src/parts/diagnose.js +0 -3
  238. package/src/parts/extras.js +0 -3
  239. package/src/parts/markets.js +0 -3
  240. package/src/parts/search.js +0 -3
  241. package/src/presets/barba.js +0 -100
  242. package/src/product.js +0 -570
  243. package/src/search.js +0 -153
  244. package/src/selling-plans.js +0 -94
  245. package/src/server.js +0 -252
  246. package/src/status.js +0 -66
  247. package/types/astro.d.ts +0 -17
  248. package/types/barba.d.ts +0 -16
  249. package/types/index.d.ts +0 -426
  250. package/types/server.d.ts +0 -110
package/src/cart.ts ADDED
@@ -0,0 +1,345 @@
1
+ /**
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.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
+ */
6
+ import { cfg, reportError } from './config.ts';
7
+ import { TetherError } from './client.ts';
8
+ import { emit, toAttributes } from './dom.ts';
9
+ import {
10
+ fetchCart,
11
+ isCartGone,
12
+ mutateCart,
13
+ removeLines,
14
+ setLineQuantity,
15
+ toLineInput,
16
+ toLineUpdate,
17
+ updateLines,
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
+ import {
22
+ announce,
23
+ cartStatus,
24
+ commit,
25
+ currentCart,
26
+ currentWarnings,
27
+ enqueue,
28
+ forgetSavedCart,
29
+ keepQuietly,
30
+ pendingQuantities,
31
+ pendingQuantityOf,
32
+ render,
33
+ savedCartId,
34
+ settleLoad,
35
+ showLastKnownCount,
36
+ subscribe,
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';
41
+
42
+ export { trapFocus };
43
+
44
+ type DiscountCodes = string[] | string | ((current: string[]) => string[]);
45
+
46
+ const currentLines = (): readonly ShopifyCartLine[] => currentCart()?.lines.nodes ?? [];
47
+
48
+ /**
49
+ * +1 / -1 for a line. Quick clicks add up: each builds on the quantity the click before asked for, which shows at once
50
+ * (`tether-line="quantity"`, `quantityOf`) until Shopify has answered. Only the latest target is sent; the ones it
51
+ * replaced are skipped.
52
+ */
53
+ function changeQuantity(lineId: string, delta: number): Promise<ShopifyCart | null> {
54
+ const line = currentLines().find((candidate) => candidate.id === lineId);
55
+ if (!line) return Promise.resolve(currentCart());
56
+ const target = Math.max(0, pendingQuantityOf(line) + delta);
57
+ const job = { quantity: target };
58
+ pendingQuantities.set(lineId, job);
59
+ announce('pending');
60
+ const isLatest = () => pendingQuantities.get(lineId) === job;
61
+ const sent = enqueue(async () => {
62
+ if (!isLatest()) return currentCart();
63
+ const now = currentLines().find((candidate) => candidate.id === lineId);
64
+ // A + then a -: Shopify already has the quantity.
65
+ if (now?.quantity === target) return currentCart();
66
+ const cart = await setLineQuantity(lineId, target);
67
+ // A newer click is on its way: keep this answer, but show totals and count once that one is in, instead of
68
+ // stepping through every reply.
69
+ if (!isLatest()) {
70
+ keepQuietly(cart);
71
+ return cart;
72
+ }
73
+ return commit(cart, target > 0 ? 'update' : 'remove');
74
+ });
75
+ return sent.finally(() => {
76
+ if (!isLatest()) return;
77
+ pendingQuantities.delete(lineId);
78
+ announce('settled');
79
+ });
80
+ }
81
+
82
+ /**
83
+ * The codes the cart has once a change is its turn in the queue.
84
+ *
85
+ * @param codes A function gets the codes as they are by then.
86
+ */
87
+ function discountCodesFrom(codes: DiscountCodes): string[] {
88
+ const current = (currentCart()?.discountCodes ?? []).map((discount) => discount.code);
89
+ const list = typeof codes === 'function' ? codes(current) : codes;
90
+ return (Array.isArray(list) ? list : [list]).map((code) => code.trim()).filter(Boolean);
91
+ }
92
+
93
+ /** @returns The cart's attributes with `attributes` merged in; `''` removes a key. */
94
+ function mergedAttributes(attributes: Attributes): { key: string; value: string }[] {
95
+ const merged = new Map((currentCart()?.attributes ?? []).map((attribute) => [attribute.key, attribute.value]));
96
+ toAttributes(attributes).forEach((attribute) => merged.set(attribute.key, attribute.value));
97
+ return [...merged].filter(([, value]) => value !== '').map(([key, value]) => ({ key, value }));
98
+ }
99
+
100
+ /**
101
+ * Loads the saved cart. A cart Shopify no longer has is forgotten; one it can't be asked about (the network, the
102
+ * token) is kept, because it is probably fine.
103
+ */
104
+ async function init(): Promise<ShopifyCart | null> {
105
+ const id = savedCartId();
106
+ if (!id) {
107
+ settleLoad({ kind: 'loaded', cart: null });
108
+ return null;
109
+ }
110
+ showLastKnownCount();
111
+ try {
112
+ const cart = await fetchCart(id);
113
+ if (!cart) forgetSavedCart();
114
+ settleLoad({ kind: 'loaded', cart });
115
+ } catch (error) {
116
+ reportError(error, 'cart load failed');
117
+ if (isCartGone(error)) forgetSavedCart();
118
+ settleLoad({ kind: 'failed', error });
119
+ }
120
+ return currentCart();
121
+ }
122
+
123
+ export const Cart = {
124
+ init,
125
+ /** The saved cart's id. */
126
+ id: savedCartId,
127
+
128
+ get status(): CartStatus {
129
+ return cartStatus();
130
+ },
131
+ get state(): ShopifyCart | null {
132
+ return currentCart();
133
+ },
134
+ /** The cart's lines, a copy: changing it changes nothing in the cart. */
135
+ get lines(): ShopifyCartLine[] {
136
+ return [...currentLines()];
137
+ },
138
+ get count(): number {
139
+ return currentCart()?.totalQuantity ?? 0;
140
+ },
141
+ /** True once the saved cart, or that there is none, is known. */
142
+ get loaded(): boolean {
143
+ return cartStatus().kind !== 'unknown';
144
+ },
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 {
150
+ const current = cartStatus();
151
+ if (current.kind !== 'error') return null;
152
+ return current.error instanceof Error ? current.error : new Error(String(current.error));
153
+ },
154
+ /** Shopify's notes on the last change, a copy. */
155
+ get warnings(): CartWarning[] {
156
+ return [...currentWarnings()];
157
+ },
158
+ /** Line id → the quantity quick clicks asked for. */
159
+ get pendingQty(): ReadonlyMap<string, { quantity: number }> {
160
+ return pendingQuantities;
161
+ },
162
+
163
+ /**
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).
166
+ */
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 });
176
+ },
177
+
178
+ render,
179
+ subscribe,
180
+
181
+ /**
182
+ * `add(variantId, 2)`, `add({ variantId, quantity, sellingPlanId, attributes })`, or `add([line, line])` for
183
+ * bundles in one request. Creates the cart when there is none.
184
+ *
185
+ * @throws {TetherError} (rejected) When the add fails, or Shopify answers it without a cart.
186
+ */
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
+ );
195
+ return enqueue(async () => {
196
+ const cart = await mutateCart({
197
+ name: 'cartLinesAdd',
198
+ args: '$lines: [CartLineInput!]!',
199
+ inner: 'lines: $lines',
200
+ variables: { lines },
201
+ createInput: { lines },
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');
205
+ commit(cart, 'add');
206
+ emit('added', { cart, lines });
207
+ if (cfg.openCartOnAdd) openCart();
208
+ return cart;
209
+ });
210
+ },
211
+
212
+ /**
213
+ * `updateLine(lineId, 3)` or `updateLine(lineId, { quantity, sellingPlanId, attributes })`.
214
+ *
215
+ * @param patch A quantity of 0 or less removes the line.
216
+ */
217
+ updateLine(lineId: string, patch: number | LinePatch): Promise<ShopifyCart | null> {
218
+ const quantity = typeof patch === 'number' ? patch : patch.quantity;
219
+ if (quantity !== undefined && quantity <= 0) return Cart.removeLine(lineId);
220
+ const line = toLineUpdate(lineId, patch);
221
+ return enqueue(async () => commit(await updateLines([line]), 'update'));
222
+ },
223
+
224
+ changeQuantity,
225
+
226
+ /** The quantity to show: the one asked for while an update is on its way. */
227
+ quantityOf: pendingQuantityOf,
228
+
229
+ removeLine(lineIds: string | string[]): Promise<ShopifyCart | null> {
230
+ const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
231
+ return enqueue(async () => commit(await removeLines(ids), 'remove'));
232
+ },
233
+
234
+ clear(): Promise<ShopifyCart | null> {
235
+ const ids = currentLines().map((line) => line.id);
236
+ if (!ids.length) return Promise.resolve(currentCart());
237
+ return Cart.removeLine(ids);
238
+ },
239
+
240
+ setNote(note: string | null | undefined): Promise<ShopifyCart | null> {
241
+ const variables = { note: note ?? '' };
242
+ return enqueue(async () =>
243
+ commit(
244
+ await mutateCart({
245
+ name: 'cartNoteUpdate',
246
+ args: '$note: String!',
247
+ inner: 'note: $note',
248
+ variables,
249
+ createInput: variables,
250
+ }),
251
+ 'note',
252
+ ),
253
+ );
254
+ },
255
+
256
+ /**
257
+ * Cart attributes, merged with the cart's (as it is when the change runs, so quick calls don't drop each other).
258
+ *
259
+ * @param attributes The value `''` removes a key.
260
+ */
261
+ setAttributes(attributes: Attributes): Promise<ShopifyCart | null> {
262
+ return enqueue(async () => {
263
+ const variables = { attributes: mergedAttributes(attributes) };
264
+ const cart = await mutateCart({
265
+ name: 'cartAttributesUpdate',
266
+ args: '$attributes: [AttributeInput!]!',
267
+ inner: 'attributes: $attributes',
268
+ variables,
269
+ createInput: variables,
270
+ });
271
+ return commit(cart, 'attributes');
272
+ });
273
+ },
274
+
275
+ /** Replaces the discount codes; `[]` removes them all. */
276
+ setDiscountCodes(codes: DiscountCodes): Promise<ShopifyCart | null> {
277
+ return enqueue(async () => {
278
+ const variables = { discountCodes: discountCodesFrom(codes) };
279
+ const cart = await mutateCart({
280
+ name: 'cartDiscountCodesUpdate',
281
+ args: '$discountCodes: [String!]!',
282
+ inner: 'discountCodes: $discountCodes',
283
+ variables,
284
+ createInput: variables,
285
+ });
286
+ return commit(cart, 'discount');
287
+ });
288
+ },
289
+
290
+ /**
291
+ * Adds a code. A code Shopify doesn't accept is removed again, so it doesn't warn on every later change.
292
+ *
293
+ * @throws {TetherError} `INVALID_DISCOUNT` (rejected) when Shopify doesn't accept the code.
294
+ */
295
+ async applyDiscount(code: string): Promise<ShopifyCart | null> {
296
+ const cart = await Cart.setDiscountCodes((current) => [...new Set([...current, code])]);
297
+ const wanted = code.toLowerCase();
298
+ const applied = (cart?.discountCodes ?? []).find((discount) => discount.code.toLowerCase() === wanted);
299
+ if (applied?.applicable) return cart;
300
+ try {
301
+ await Cart.removeDiscount(code);
302
+ } catch (error) {
303
+ reportError(error, 'removing a discount code Shopify refused');
304
+ }
305
+ throw new TetherError(`Discount code not valid: ${code}`, { code: 'INVALID_DISCOUNT' });
306
+ },
307
+
308
+ removeDiscount(code: string): Promise<ShopifyCart | null> {
309
+ const unwanted = code.toLowerCase();
310
+ return Cart.setDiscountCodes((current) => current.filter((existing) => existing.toLowerCase() !== unwanted));
311
+ },
312
+
313
+ /**
314
+ * Sets who is buying; a country changes the cart's currency and prices to that market's.
315
+ *
316
+ * @param options `create: false` forgets an expired cart instead of creating an empty one.
317
+ */
318
+ setBuyerIdentity(
319
+ buyerIdentity: { countryCode?: string; email?: string; phone?: string },
320
+ { create = true }: { create?: boolean } = {},
321
+ ): Promise<ShopifyCart | null> {
322
+ return enqueue(async () => {
323
+ const cart = await mutateCart({
324
+ name: 'cartBuyerIdentityUpdate',
325
+ args: '$buyerIdentity: CartBuyerIdentityInput!',
326
+ inner: 'buyerIdentity: $buyerIdentity',
327
+ variables: { buyerIdentity },
328
+ createInput: create ? { buyerIdentity } : null,
329
+ }).catch((error: unknown) => {
330
+ // mutateCart has forgotten the cart Shopify no longer has; without `create` that leaves no cart.
331
+ if (create || !isCartGone(error)) throw error;
332
+ return null;
333
+ });
334
+ return commit(cart, 'buyer-identity');
335
+ });
336
+ },
337
+
338
+ checkout,
339
+
340
+ drawers,
341
+ isOpen,
342
+ open: openCart,
343
+ close: closeCart,
344
+ toggle: toggleCart,
345
+ };