@occluze/tether 0.4.0 → 0.6.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 (68) hide show
  1. package/README.md +12 -8
  2. package/dist/esm/astro.js +2 -0
  3. package/dist/esm/chunks/account-WAWNASZU.js +1 -0
  4. package/dist/esm/chunks/analytics-22NMIVBV.js +1 -0
  5. package/dist/esm/chunks/chunk-4UR45XTC.js +1 -0
  6. package/dist/esm/chunks/chunk-6R752OYZ.js +1 -0
  7. package/dist/esm/chunks/chunk-BGIKQY6H.js +1 -0
  8. package/dist/esm/chunks/chunk-CISGFYOF.js +1 -0
  9. package/dist/esm/chunks/chunk-KARKXC6G.js +1 -0
  10. package/dist/esm/chunks/chunk-VC46IEJQ.js +1 -0
  11. package/dist/esm/chunks/chunk-YXCXIPQV.js +4 -0
  12. package/dist/esm/chunks/{chunk-KYQ5OGBX.js → chunk-YZMBXS6Y.js} +1 -1
  13. package/dist/esm/chunks/chunk-ZVUPVSLF.js +1 -0
  14. package/dist/esm/chunks/diagnose-6ZSK5DGE.js +1 -0
  15. package/dist/esm/chunks/extras-QDCSW6VU.js +1 -0
  16. package/dist/esm/chunks/markets-ui-HJCVLCKJ.js +1 -0
  17. package/dist/esm/chunks/search-LN2H6SXS.js +1 -0
  18. package/dist/esm/index.js +1 -1
  19. package/dist/esm/server.js +1 -0
  20. package/dist/tether.account.js +374 -0
  21. package/dist/tether.account.min.js +1 -0
  22. package/dist/tether.analytics.js +259 -0
  23. package/dist/tether.analytics.min.js +1 -0
  24. package/dist/tether.diagnose.js +14 -4
  25. package/dist/tether.diagnose.min.js +1 -1
  26. package/dist/tether.extras.js +87 -0
  27. package/dist/tether.extras.min.js +1 -0
  28. package/dist/tether.js +486 -125
  29. package/dist/tether.markets.js +17 -11
  30. package/dist/tether.markets.min.js +1 -1
  31. package/dist/tether.min.js +3 -3
  32. package/dist/tether.min.js.map +3 -3
  33. package/dist/tether.search.js +176 -0
  34. package/dist/tether.search.min.js +1 -0
  35. package/package.json +31 -5
  36. package/src/account.js +313 -0
  37. package/src/analytics.js +232 -0
  38. package/src/astro.js +76 -0
  39. package/src/cart.js +119 -16
  40. package/src/client.js +30 -15
  41. package/src/collection.js +15 -10
  42. package/src/config.js +5 -0
  43. package/src/diagnose.js +12 -3
  44. package/src/dom.js +39 -10
  45. package/src/extras.js +63 -0
  46. package/src/fragments.js +13 -12
  47. package/src/index.js +114 -40
  48. package/src/initial.js +75 -0
  49. package/src/lazy.js +9 -1
  50. package/src/loader.js +53 -7
  51. package/src/markets-ui.js +15 -6
  52. package/src/markets.js +1 -1
  53. package/src/mounting.js +68 -3
  54. package/src/parts/account.js +3 -0
  55. package/src/parts/analytics.js +3 -0
  56. package/src/parts/extras.js +3 -0
  57. package/src/parts/search.js +3 -0
  58. package/src/product.js +51 -13
  59. package/src/search.js +153 -0
  60. package/src/server.js +251 -0
  61. package/src/status.js +3 -8
  62. package/types/astro.d.ts +15 -0
  63. package/types/index.d.ts +72 -4
  64. package/types/server.d.ts +102 -0
  65. package/dist/esm/chunks/chunk-GHIUMD5H.js +0 -1
  66. package/dist/esm/chunks/chunk-LI3YDMGF.js +0 -4
  67. package/dist/esm/chunks/diagnose-ZXXXJIQE.js +0 -1
  68. package/dist/esm/chunks/markets-ui-AG6Y5VUE.js +0 -1
package/dist/tether.js CHANGED
@@ -46,6 +46,8 @@
46
46
  sellingPlansFirst: 25,
47
47
  collectionFirst: 24,
48
48
  retries: 2,
49
+ revalidate: true,
50
+ // products from the server (tether/server hydrate()): refresh once in the background
49
51
  // Cart
50
52
  storageKey: null,
51
53
  // defaults to `tether:cart:<domain>`
@@ -58,11 +60,19 @@
58
60
  // Subscriptions
59
61
  defaultPurchase: "one-time",
60
62
  // 'one-time' | 'subscription'
63
+ variantUrl: false,
64
+ // true: ?variant= follows the visitor's choice on the main product (shareable links)
65
+ maxAge: 60,
66
+ // seconds a Shopify answer may be cached (0 = Shopify's own caching, can be hours)
67
+ lowStock: 5,
68
+ // tether-show="low-stock" at this many or fewer (needs inventory access)
61
69
  // Mounting
62
70
  autoMount: true,
63
71
  // mount document on DOMContentLoaded
64
72
  observe: false,
65
73
  // auto-mount nodes added later (CMS load-more, filters)
74
+ astro: true,
75
+ // mount on Astro View Transitions (astro:page-load); false if you do it yourself
66
76
  partsUrl: null,
67
77
  // folder of tether.markets/diagnose(.min).js for the <script> build (default: next to it)
68
78
  // Formatting
@@ -117,6 +127,7 @@
117
127
  TetherError: () => TetherError,
118
128
  endpoint: () => endpoint,
119
129
  gql: () => gql,
130
+ request: () => request,
120
131
  unwrap: () => unwrap,
121
132
  withContext: () => withContext
122
133
  });
@@ -130,39 +141,41 @@
130
141
  this.userErrors = userErrors;
131
142
  }
132
143
  };
133
- function endpoint() {
134
- return `https://${cfg.domain}/api/${cfg.apiVersion}/graphql.json`;
144
+ function endpoint(conf = cfg) {
145
+ return `https://${conf.domain}/api/${conf.apiVersion}/graphql.json`;
135
146
  }
136
147
  function withContext(query, ctx = cfg) {
137
148
  const args = [];
138
149
  if (ctx.country) args.push(`country: ${String(ctx.country).toUpperCase()}`);
139
150
  if (ctx.language) args.push(`language: ${String(ctx.language).toUpperCase().replace("-", "_")}`);
140
151
  if (!args.length || /@inContext/.test(query)) return query;
141
- return query.replace(
142
- /^(\s*(?:query|mutation)\s+\w+\s*(?:\([^)]*\))?)/,
143
- `$1 @inContext(${args.join(", ")})`
144
- );
152
+ const tag = `@inContext(${args.join(", ")})`;
153
+ if (/^\s*\{/.test(query)) return `query ${tag} ${query.trimStart()}`;
154
+ return query.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/, `$1 ${tag}`);
145
155
  }
146
156
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
147
- async function gql(query, variables = {}, { context = true } = {}) {
148
- if (!cfg.domain || !cfg.token) {
157
+ async function request(conf, query, variables = {}, { context = true } = {}) {
158
+ if (!conf.domain || !conf.token) {
149
159
  throw new TetherError("Tether is not configured: domain and token are required", { code: "NOT_CONFIGURED" });
150
160
  }
151
- const body = JSON.stringify({ query: context ? withContext(query) : query, variables });
161
+ let text = context ? withContext(query, conf) : query;
162
+ const age = conf.maxAge ?? 60;
163
+ if (age > 0 && !/^\s*mutation\b/.test(text)) text += ` #${Math.floor(Date.now() / (age * 1e3))}`;
164
+ const body = JSON.stringify({ query: text, variables });
152
165
  let attempt = 0;
153
166
  for (; ; ) {
154
167
  let res;
155
168
  try {
156
- res = await fetch(endpoint(), {
169
+ res = await (conf.fetch || fetch)(endpoint(conf), {
157
170
  method: "POST",
158
171
  headers: {
159
172
  "Content-Type": "application/json",
160
- "X-Shopify-Storefront-Access-Token": cfg.token
173
+ "X-Shopify-Storefront-Access-Token": conf.token
161
174
  },
162
175
  body
163
176
  });
164
177
  } catch (err) {
165
- if (attempt++ < cfg.retries) {
178
+ if (attempt++ < conf.retries) {
166
179
  await sleep(300 * 2 ** attempt);
167
180
  continue;
168
181
  }
@@ -170,7 +183,7 @@
170
183
  }
171
184
  if (res.status === 430) throw new TetherError("Shopify security rejection (430)", { status: 430 });
172
185
  const retryable = res.status === 429 || res.status >= 500;
173
- if (retryable && attempt++ < cfg.retries) {
186
+ if (retryable && attempt++ < conf.retries) {
174
187
  log("retry", res.status, attempt);
175
188
  await sleep(300 * 2 ** attempt);
176
189
  continue;
@@ -185,10 +198,13 @@
185
198
  const inventoryDenied = json.errors.some((e) => /quantityAvailable/.test(e.message || ""));
186
199
  if (inventoryDenied && /\bquantityAvailable\b/.test(body)) {
187
200
  log("no inventory permission, retrying without quantityAvailable");
188
- return gql(query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
201
+ return request(conf, query.replace(/\bquantityAvailable\b/g, ""), variables, { context });
202
+ }
203
+ if (json.errors.some((e) => /'warnings'/.test(e.message || "")) && /warnings \{ code message target \}/.test(query)) {
204
+ return request(conf, query.replace(/ warnings \{ code message target \}/g, ""), variables, { context });
189
205
  }
190
206
  const throttled = json.errors.some((e) => e.extensions?.code === "THROTTLED");
191
- if (throttled && attempt++ < cfg.retries) {
207
+ if (throttled && attempt++ < conf.retries) {
192
208
  await sleep(500 * 2 ** attempt);
193
209
  continue;
194
210
  }
@@ -201,6 +217,7 @@
201
217
  return json.data;
202
218
  }
203
219
  }
220
+ var gql = (query, variables, opts) => request(cfg, query, variables, opts);
204
221
  function unwrap(payload, name) {
205
222
  if (!payload) throw new TetherError(`${name}: empty response`);
206
223
  const errs = payload.userErrors || [];
@@ -230,8 +247,8 @@
230
247
  });
231
248
  var MONEY = "amount currencyCode";
232
249
  var IMAGE = "url altText width height";
233
- function metafieldIdentifiers() {
234
- return (cfg.metafields || []).map((m) => {
250
+ function metafieldIdentifiers(o = cfg) {
251
+ return (o.metafields || []).map((m) => {
235
252
  if (typeof m === "string") {
236
253
  const i = m.indexOf(".");
237
254
  return { namespace: m.slice(0, i), key: m.slice(i + 1) };
@@ -239,19 +256,19 @@
239
256
  return m;
240
257
  }).filter((m) => m.namespace && m.key);
241
258
  }
242
- function metafieldsField() {
243
- const ids = metafieldIdentifiers();
259
+ function metafieldsField(o = cfg) {
260
+ const ids = metafieldIdentifiers(o);
244
261
  if (!ids.length) return "";
245
262
  const list = ids.map((m) => `{namespace: ${JSON.stringify(m.namespace)}, key: ${JSON.stringify(m.key)}}`).join(", ");
246
263
  return `metafields(identifiers:[${list}]){namespace key type value reference{... on MediaImage{image{${IMAGE}}}}}`;
247
264
  }
248
265
  var SELLING_PLAN_FIELDS = ` id name description recurringDeliveries options{name value}priceAdjustments{orderCount adjustmentValue{__typename ... on SellingPlanPercentagePriceAdjustment{adjustmentPercentage}... on SellingPlanFixedAmountPriceAdjustment{adjustmentAmount{${MONEY}}}... on SellingPlanFixedPriceAdjustment{price{${MONEY}}}}}`;
249
266
  var ALLOCATION_FIELDS = ` sellingPlan{id}checkoutChargeAmount{${MONEY}}remainingBalanceChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}unitPrice{${MONEY}}}`;
250
- function productFragment() {
251
- return ` fragment TetherProduct on Product{id handle title description descriptionHtml productType vendor tags availableForSale publishedAt onlineStoreUrl requiresSellingPlan seo{title description}featuredImage{${IMAGE}}images(first:${cfg.imagesFirst}){nodes{${IMAGE}}}options{name optionValues{name swatch{color image{previewImage{${IMAGE}}}}}}priceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}compareAtPriceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}${metafieldsField()} sellingPlanGroups(first:${cfg.sellingPlanGroupsFirst}){nodes{name appName options{name values}sellingPlans(first:${cfg.sellingPlansFirst}){nodes{${SELLING_PLAN_FIELDS}}}}}variants(first:${cfg.variantsFirst}){nodes{id title availableForSale currentlyNotInStock quantityAvailable sku barcode selectedOptions{name value}image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}unitPrice{${MONEY}}sellingPlanAllocations(first:${cfg.sellingPlansFirst}){nodes{${ALLOCATION_FIELDS}}}}}}`;
267
+ function productFragment(o = cfg) {
268
+ return ` fragment TetherProduct on Product{id handle title description descriptionHtml productType vendor tags availableForSale publishedAt onlineStoreUrl requiresSellingPlan seo{title description}featuredImage{${IMAGE}}images(first:${o.imagesFirst}){nodes{${IMAGE}}}options{name optionValues{name swatch{color image{previewImage{${IMAGE}}}}}}priceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}compareAtPriceRange{minVariantPrice{${MONEY}}maxVariantPrice{${MONEY}}}${metafieldsField(o)} sellingPlanGroups(first:${o.sellingPlanGroupsFirst}){nodes{name appName options{name values}sellingPlans(first:${o.sellingPlansFirst}){nodes{${SELLING_PLAN_FIELDS}}}}}variants(first:${o.variantsFirst}){nodes{id title availableForSale currentlyNotInStock quantityAvailable sku barcode selectedOptions{name value}image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}unitPrice{${MONEY}}sellingPlanAllocations(first:${o.sellingPlansFirst}){nodes{${ALLOCATION_FIELDS}}}}}}`;
252
269
  }
253
270
  function cartFragment() {
254
- return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
271
+ return ` fragment TetherCart on Cart{id checkoutUrl totalQuantity note createdAt updatedAt attributes{key value}buyerIdentity{countryCode email}discountCodes{code applicable}cost{subtotalAmount{${MONEY}}totalAmount{${MONEY}}checkoutChargeAmount{${MONEY}}}lines(first:100){nodes{id quantity attributes{key value}cost{totalAmount{${MONEY}}subtotalAmount{${MONEY}}amountPerQuantity{${MONEY}}compareAtAmountPerQuantity{${MONEY}}}discountAllocations{discountedAmount{${MONEY}}}sellingPlanAllocation{sellingPlan{id name description recurringDeliveries options{name value}}checkoutChargeAmount{${MONEY}}priceAdjustments{price{${MONEY}}compareAtPrice{${MONEY}}perDeliveryPrice{${MONEY}}}}merchandise{... on ProductVariant{id title sku availableForSale requiresShipping quantityAvailable image{${IMAGE}}price{${MONEY}}compareAtPrice{${MONEY}}selectedOptions{name value}product{id title handle vendor featuredImage{${IMAGE}}}}}... on ComponentizableCartLine{lineComponents{id quantity merchandise{... on ProductVariant{id title product{title handle}}}}}}}}`;
255
272
  }
256
273
  function collectionFields() {
257
274
  return `
@@ -267,8 +284,11 @@
267
284
  getProduct: () => getProduct,
268
285
  getProducts: () => getProducts,
269
286
  getRecommendations: () => getRecommendations,
287
+ hydrateProducts: () => hydrateProducts,
270
288
  normalizeProductKey: () => normalizeProductKey,
271
- primeProduct: () => primeProduct
289
+ primeProduct: () => primeProduct,
290
+ refreshProducts: () => refreshProducts,
291
+ takeStale: () => takeStale
272
292
  });
273
293
  var productCache = /* @__PURE__ */ new Map();
274
294
  var pending = [];
@@ -343,10 +363,18 @@
343
363
  });
344
364
  keys.forEach((k) => settle(k, (r) => {
345
365
  if (got.has(k)) {
346
- if (gen === generation) primeProduct(got.get(k));
347
- r.resolve(got.get(k));
366
+ const p = got.get(k);
367
+ if (gen === generation) {
368
+ primeProduct(p);
369
+ freshAt.set(p.handle, Date.now());
370
+ freshAt.set(p.id, Date.now());
371
+ }
372
+ r.resolve(p);
348
373
  } else {
349
- if (gen === generation) productCache.delete(k);
374
+ const miss = productCache.get(k);
375
+ if (gen === generation) setTimeout(() => {
376
+ if (productCache.get(k) === miss) productCache.delete(k);
377
+ }, 1e4);
350
378
  r.reject(new TetherError(`Product not found: ${k}`, { code: "NOT_FOUND" }));
351
379
  }
352
380
  }));
@@ -360,6 +388,35 @@
360
388
  function clearProductCache() {
361
389
  productCache.clear();
362
390
  generation++;
391
+ stale.clear();
392
+ freshAt.clear();
393
+ }
394
+ var stale = /* @__PURE__ */ new Set();
395
+ var freshAt = /* @__PURE__ */ new Map();
396
+ var FRESH_MS = 6e4;
397
+ function hydrateProducts(products) {
398
+ (products || []).forEach((p) => {
399
+ if (!p?.id || !p.handle) return;
400
+ if (Date.now() - (freshAt.get(p.handle) || 0) < FRESH_MS && productCache.has(p.handle)) return;
401
+ primeProduct(p);
402
+ stale.add(p.handle);
403
+ stale.add(p.id);
404
+ });
405
+ }
406
+ function takeStale(key) {
407
+ const k = normalizeProductKey(key);
408
+ if (!stale.has(k)) return false;
409
+ const p = productCache.get(k);
410
+ stale.delete(k);
411
+ p?.then?.((x) => {
412
+ stale.delete(x.handle);
413
+ stale.delete(x.id);
414
+ });
415
+ return true;
416
+ }
417
+ function refreshProducts(keys) {
418
+ keys.forEach((k) => productCache.delete(normalizeProductKey(k)));
419
+ return keys.map((k) => getProduct(k));
363
420
  }
364
421
  async function getProducts({
365
422
  collection,
@@ -389,8 +446,10 @@
389
446
  data.products.nodes.forEach(primeProduct);
390
447
  return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
391
448
  }
392
- async function getRecommendations(productId, intent = "RELATED") {
393
- const data = await gql(`query TetherRecs($id:ID!,$intent:ProductRecommendationIntent){productRecommendations(productId:$id,intent:$intent){...TetherProduct}}${productFragment()}`, { id: productId, intent });
449
+ async function getRecommendations(product, intent = "RELATED") {
450
+ const key = normalizeProductKey(typeof product === "object" ? product?.id : product);
451
+ const byId = key?.startsWith("gid://");
452
+ const data = await gql(`query TetherRecs(${byId ? "$id: ID!" : "$handle: String!"},$intent:ProductRecommendationIntent){productRecommendations(${byId ? "productId: $id" : "productHandle: $handle"},intent:$intent){...TetherProduct}}${productFragment()}`, byId ? { id: key, intent } : { handle: key, intent });
394
453
  const list = data.productRecommendations || [];
395
454
  list.forEach(primeProduct);
396
455
  return list;
@@ -489,9 +548,11 @@
489
548
  emit: () => emit,
490
549
  formatDate: () => formatDate,
491
550
  formatMoney: () => formatMoney,
551
+ formatPercent: () => formatPercent,
492
552
  get: () => get,
493
553
  has: () => has,
494
554
  hasTemplate: () => hasTemplate,
555
+ imageUrl: () => imageUrl,
495
556
  notifyLayoutChanged: () => notifyLayoutChanged,
496
557
  parseJSONAttr: () => parseJSONAttr,
497
558
  productUrl: () => productUrl,
@@ -499,6 +560,7 @@
499
560
  readText: () => readText,
500
561
  repeat: () => repeat,
501
562
  repeatKeyed: () => repeatKeyed,
563
+ sameImage: () => sameImage,
502
564
  scoped: () => scoped,
503
565
  sel: () => sel,
504
566
  setImage: () => setImage,
@@ -525,6 +587,14 @@
525
587
  function emit(name, detail, target = document) {
526
588
  target.dispatchEvent(new CustomEvent(`${cfg.prefix}:${name}`, { detail, bubbles: true }));
527
589
  }
590
+ function formatPercent(n, locale = cfg.locale) {
591
+ if (!n) return "";
592
+ try {
593
+ return new Intl.NumberFormat(locale || void 0, { style: "percent", maximumFractionDigits: 0 }).format(n / 100);
594
+ } catch {
595
+ return `${n}%`;
596
+ }
597
+ }
528
598
  function formatMoney(money) {
529
599
  if (!money) return "";
530
600
  if (typeof cfg.formatMoney === "function") return cfg.formatMoney(money, cfg.locale);
@@ -547,25 +617,31 @@
547
617
  return "";
548
618
  }
549
619
  }
550
- function cdnUrl(url, el) {
551
- if (!url || !el) return url;
552
- const w = el.getAttribute("max-width");
553
- const h = el.getAttribute("max-height");
554
- const crop = el.getAttribute("crop");
555
- if (!w && !h) return url;
620
+ function imageUrl(url, { width, height, crop } = {}) {
621
+ if (!url) return url;
556
622
  try {
557
623
  const u = new URL(url);
558
- if (w) u.searchParams.set("width", w);
559
- if (h) u.searchParams.set("height", h);
624
+ if (width) u.searchParams.set("width", String(width));
625
+ if (height) u.searchParams.set("height", String(height));
560
626
  if (crop) u.searchParams.set("crop", crop);
561
627
  return u.toString();
562
628
  } catch {
563
629
  return url;
564
630
  }
565
631
  }
632
+ function cdnUrl(url, el) {
633
+ if (!url || !el) return url;
634
+ const g = (k) => el.getAttribute(k);
635
+ return imageUrl(url, { width: g("max-width"), height: g("max-height"), crop: g("max-width") || g("max-height") ? g("crop") : null });
636
+ }
637
+ var sameImage = (a, b) => Boolean(a && b) && a.split("?")[0] === b.split("?")[0];
566
638
  function setImage(el, url, alt) {
567
639
  if (!url) return;
568
640
  const src = cdnUrl(url, el);
641
+ if (el.tagName === "IMG" && sameImage(el.getAttribute("src"), src)) {
642
+ if (alt != null && !el.getAttribute("alt")) el.setAttribute("alt", alt);
643
+ return;
644
+ }
569
645
  if (el.tagName === "IMG") {
570
646
  el.removeAttribute("srcset");
571
647
  el.removeAttribute("sizes");
@@ -597,14 +673,21 @@
597
673
  function show(el, visible) {
598
674
  const k = "tetherPrevDisplay";
599
675
  if (visible) {
676
+ if (el.dataset.tetherShown !== void 0) return;
677
+ el.dataset.tetherShown = "";
678
+ el.removeAttribute("hidden");
600
679
  if (el.dataset[k] !== void 0) {
601
680
  el.style.display = el.dataset[k];
602
681
  delete el.dataset[k];
603
682
  } else if (el.style.display === "none") {
604
683
  el.style.display = "";
605
684
  }
685
+ if (typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
686
+ el.style.display = el.getAttribute(`${cfg.prefix}-display`) || "block";
687
+ }
606
688
  } else {
607
- if (el.dataset[k] === void 0) el.dataset[k] = el.style.display || "";
689
+ delete el.dataset.tetherShown;
690
+ if (el.dataset[k] === void 0 && el.style.display !== "none") el.dataset[k] = el.style.display || "";
608
691
  el.style.display = "none";
609
692
  }
610
693
  }
@@ -628,7 +711,7 @@
628
711
  function repeat(container, items, render) {
629
712
  const tpl = template(container);
630
713
  if (!tpl) {
631
- warn("repeater without a template child", container);
714
+ if (items.length) warn("repeater without a template child", container);
632
715
  return;
633
716
  }
634
717
  container.innerHTML = "";
@@ -752,7 +835,7 @@
752
835
  return line;
753
836
  }
754
837
  function mutation(name, args, inner) {
755
- return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}}}${cartFragment()}`;
838
+ return `mutation Tether_${name}(${args}){${name}(${inner}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${cartFragment()}`;
756
839
  }
757
840
  var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
758
841
  var returnFocus = null;
@@ -763,7 +846,10 @@
763
846
  if (!d.hasAttribute("role")) d.setAttribute("role", "dialog");
764
847
  if (!d.hasAttribute("aria-label") && !d.hasAttribute("aria-labelledby")) d.setAttribute("aria-label", get(d, "cart-label") || "Cart");
765
848
  d.setAttribute("aria-modal", String(open));
766
- if (!d.classList.contains(cfg.cartDrawerOpenClass)) d.setAttribute("aria-hidden", "true");
849
+ if (!d.classList.contains(cfg.cartDrawerOpenClass)) {
850
+ d.setAttribute("aria-hidden", "true");
851
+ if (cfg.cartFocus !== false) d.inert = true;
852
+ }
767
853
  });
768
854
  }
769
855
  var isMissingCart = (err) => /does not exist|not found|invalid id/i.test(err?.message || "");
@@ -791,6 +877,20 @@
791
877
  return this.state?.totalQuantity || 0;
792
878
  },
793
879
  lastError: null,
880
+ /** Set by the account part while a customer is logged in (Customer Account API token). */
881
+ customerAccessToken: null,
882
+ /** line id -> { qty, done }: what quick +/- clicks asked for, shown until Shopify has answered. */
883
+ pendingQty: /* @__PURE__ */ new Map(),
884
+ /** Shopify's notes on the last change, e.g. less stock than asked: [{ code, message }]. */
885
+ warnings: [],
886
+ /** true once the saved cart (or the absence of one) is known. */
887
+ loaded: false,
888
+ _take(payload, name) {
889
+ const p = unwrap(payload, name);
890
+ this.warnings = (p.warnings || []).filter((w, i, all) => all.findIndex((x) => x.message === w.message) === i).map((w) => ({ ...w, message: typeof cfg.warningText === "function" && cfg.warningText(w) || w.message }));
891
+ if (this.warnings.length) emit("cart-warning", { warnings: this.warnings });
892
+ return p.cart;
893
+ },
794
894
  _queue(fn) {
795
895
  const run = this._chain.then(fn, fn).then((value) => {
796
896
  if (this.lastError) {
@@ -811,6 +911,10 @@
811
911
  _commit(cart, reason) {
812
912
  this.state = cart;
813
913
  if (cart?.id) this._setId(cart.id);
914
+ try {
915
+ localStorage.setItem(`${cfg.storageKey}:seen`, `${Date.now()}|${cart?.totalQuantity || 0}`);
916
+ } catch {
917
+ }
814
918
  this.render();
815
919
  emit("cart-updated", { cart, reason });
816
920
  return cart;
@@ -819,9 +923,20 @@
819
923
  const id = this.id();
820
924
  if (!id) {
821
925
  this.state = null;
926
+ this.loaded = true;
822
927
  this.render();
928
+ emit("cart-updated", { cart: null, reason: "load" });
823
929
  return null;
824
930
  }
931
+ if (!this.loaded) {
932
+ try {
933
+ const n = (localStorage.getItem(`${cfg.storageKey}:seen`) || "").split("|")[1];
934
+ if (n) qsa(document, sel("cart-count")).forEach((el) => {
935
+ if (/^\s*0?\s*$/.test(el.textContent)) el.textContent = n;
936
+ });
937
+ } catch {
938
+ }
939
+ }
825
940
  try {
826
941
  const data = await gql(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${cartFragment()}`, { id });
827
942
  if (!data.cart) {
@@ -834,16 +949,21 @@
834
949
  if (isMissingCart(err)) this._setId(null);
835
950
  this.state = null;
836
951
  }
952
+ this.loaded = true;
837
953
  this.render();
954
+ emit("cart-updated", { cart: this.state, reason: "load" });
838
955
  return this.state;
839
956
  },
840
957
  async _create(input = {}) {
841
- const buyerIdentity = cfg.country ? { countryCode: String(cfg.country).toUpperCase() } : void 0;
958
+ const buyerIdentity = cfg.country || this.customerAccessToken ? {
959
+ ...cfg.country ? { countryCode: String(cfg.country).toUpperCase() } : {},
960
+ ...this.customerAccessToken ? { customerAccessToken: this.customerAccessToken } : {}
961
+ } : void 0;
842
962
  const data = await gql(
843
963
  mutation("cartCreate", "$input: CartInput!", "input: $input"),
844
964
  { input: { buyerIdentity, ...input } }
845
965
  );
846
- return unwrap(data.cartCreate, "cartCreate").cart;
966
+ return this._take(data.cartCreate, "cartCreate");
847
967
  },
848
968
  /** Runs a cart mutation; creates a fresh cart if ours expired. */
849
969
  async _mutate(name, args, inner, variables, createInput) {
@@ -851,7 +971,7 @@
851
971
  if (!cartId) return createInput ? this._create(createInput) : null;
852
972
  try {
853
973
  const data = await gql(mutation(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`), { ...variables, cartId });
854
- return unwrap(data[name], name).cart;
974
+ return this._take(data[name], name);
855
975
  } catch (err) {
856
976
  if (createInput && isMissingCart(err)) {
857
977
  log("cart expired, creating a new one");
@@ -894,6 +1014,30 @@
894
1014
  "update"
895
1015
  ));
896
1016
  },
1017
+ /**
1018
+ * +1 / -1 for a line. Quick clicks add up: each builds on the quantity the last click asked for,
1019
+ * which shows at once (tether-line="quantity", Cart.lines) until Shopify has answered.
1020
+ */
1021
+ changeQuantity(lineId, delta) {
1022
+ const current = this.lines.find((l) => l.id === lineId);
1023
+ if (!current) return Promise.resolve(this.state);
1024
+ const target = Math.max(0, (this.pendingQty.get(lineId)?.qty ?? current.quantity) + delta);
1025
+ const job = { qty: target, done: false };
1026
+ this.pendingQty.set(lineId, job);
1027
+ this.render();
1028
+ emit("cart-updated", { cart: this.state, reason: "pending" });
1029
+ return this.updateLine(lineId, target).finally(() => {
1030
+ if (this.pendingQty.get(lineId) === job) {
1031
+ this.pendingQty.delete(lineId);
1032
+ this.render();
1033
+ emit("cart-updated", { cart: this.state, reason: "settled" });
1034
+ }
1035
+ });
1036
+ },
1037
+ /** Quantity to show for a line: the one asked for while an update is on its way. */
1038
+ quantityOf(line) {
1039
+ return this.pendingQty.get(line.id)?.qty ?? line.quantity;
1040
+ },
897
1041
  removeLine(lineIds) {
898
1042
  const ids = Array.isArray(lineIds) ? lineIds : [lineIds];
899
1043
  return this._queue(async () => this._commit(
@@ -930,12 +1074,22 @@
930
1074
  "discount"
931
1075
  ));
932
1076
  },
933
- applyDiscount(code) {
1077
+ /**
1078
+ * Adds a code. A code Shopify doesn't accept isn't kept in the cart (it would warn on every change):
1079
+ * it's removed again and the promise rejects with code INVALID_DISCOUNT.
1080
+ */
1081
+ async applyDiscount(code) {
934
1082
  const current = (this.state?.discountCodes || []).map((d) => d.code);
935
- return this.setDiscountCodes([.../* @__PURE__ */ new Set([...current, code])]);
1083
+ const cart = await this.setDiscountCodes([.../* @__PURE__ */ new Set([...current, code])]);
1084
+ const hit = (cart?.discountCodes || []).find((d) => d.code.toLowerCase() === String(code).toLowerCase());
1085
+ if (hit?.applicable) return cart;
1086
+ await this.removeDiscount(code).catch(() => {
1087
+ });
1088
+ throw new TetherError(`Discount code not valid: ${code}`, { code: "INVALID_DISCOUNT" });
936
1089
  },
937
1090
  removeDiscount(code) {
938
- return this.setDiscountCodes((this.state?.discountCodes || []).map((d) => d.code).filter((c) => c !== code));
1091
+ const k = String(code).toLowerCase();
1092
+ return this.setDiscountCodes((this.state?.discountCodes || []).map((d) => d.code).filter((c) => c.toLowerCase() !== k));
939
1093
  },
940
1094
  /** { countryCode, email, phone } — changes currency/prices for that market. */
941
1095
  setBuyerIdentity(buyerIdentity, { create = true } = {}) {
@@ -955,7 +1109,7 @@
955
1109
  */
956
1110
  async checkout(target = cfg.checkoutTarget) {
957
1111
  const id = this.id();
958
- if (!id) return null;
1112
+ if (!id || this.state && !this.count) return null;
959
1113
  const tab = target === "_blank" ? window.open("", "_blank") : null;
960
1114
  if (tab) {
961
1115
  try {
@@ -965,7 +1119,8 @@
965
1119
  }
966
1120
  try {
967
1121
  const data = await gql("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }", { id });
968
- const url = data.cart?.checkoutUrl;
1122
+ let url = this.beforeCheckout && await this.beforeCheckout().catch(() => null) || data.cart?.checkoutUrl;
1123
+ if (url && this.customerAccessToken) url += `${url.includes("?") ? "&" : "?"}logged_in=true`;
969
1124
  if (!url) throw new TetherError("No checkoutUrl (cart empty or expired)");
970
1125
  emit("checkout", { url, cart: this.state });
971
1126
  if (tab && !tab.closed) tab.location.href = url;
@@ -981,13 +1136,18 @@
981
1136
  drawers() {
982
1137
  return qsa(document, sel("cart-drawer"));
983
1138
  },
1139
+ /** Without a tether-cart-drawer (a React/Svelte/Vue cart), open/close still work: as state for subscribe. */
1140
+ _open: false,
984
1141
  isOpen() {
985
- return this.drawers().some((d) => d.classList.contains(cfg.cartDrawerOpenClass));
1142
+ const ds = this.drawers();
1143
+ return ds.length ? ds.some((d) => d.classList.contains(cfg.cartDrawerOpenClass)) : this._open;
986
1144
  },
987
1145
  open() {
988
1146
  if (this.isOpen()) return;
1147
+ this._open = true;
989
1148
  const ds = this.drawers().filter((d) => get(d, "cart-drawer") !== "manual");
990
1149
  ds.forEach((d) => {
1150
+ d.inert = false;
991
1151
  d.classList.add(cfg.cartDrawerOpenClass);
992
1152
  d.setAttribute("aria-hidden", "false");
993
1153
  });
@@ -1005,11 +1165,13 @@
1005
1165
  },
1006
1166
  close() {
1007
1167
  if (!this.isOpen()) return;
1168
+ this._open = false;
1008
1169
  const ds = this.drawers();
1009
1170
  const hadFocus = ds.some((d) => d.contains(document.activeElement));
1010
1171
  ds.forEach((d) => {
1011
1172
  d.classList.remove(cfg.cartDrawerOpenClass);
1012
1173
  d.setAttribute("aria-hidden", "true");
1174
+ if (cfg.cartFocus !== false) d.inert = true;
1013
1175
  });
1014
1176
  ariaState(false);
1015
1177
  if (hadFocus && returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
@@ -1021,6 +1183,17 @@
1021
1183
  if (this.isOpen()) this.close();
1022
1184
  else this.open();
1023
1185
  },
1186
+ /**
1187
+ * For framework islands (React, Svelte, Vue): fn(cart, { open }) now and on every change.
1188
+ * Returns a function that stops listening.
1189
+ */
1190
+ subscribe(fn) {
1191
+ const call = () => fn(this.state, { open: this.isOpen(), count: this.count, loaded: this.loaded, warnings: this.warnings });
1192
+ const h = () => call();
1193
+ ["cart-updated", "cart-toggled"].forEach((e) => document.addEventListener(`${cfg.prefix}:${e}`, h));
1194
+ call();
1195
+ return () => ["cart-updated", "cart-toggled"].forEach((e) => document.removeEventListener(`${cfg.prefix}:${e}`, h));
1196
+ },
1024
1197
  // -------------------------------------------------------------- render
1025
1198
  render() {
1026
1199
  const cart = this.state;
@@ -1038,12 +1211,18 @@
1038
1211
  "cart-has-subscription": hasSub,
1039
1212
  "cart-no-subscription": !hasSub,
1040
1213
  "cart-has-discount": (cart?.discountCodes || []).some((d) => d.applicable),
1041
- "cart-error": Boolean(this.lastError)
1214
+ "cart-error": Boolean(this.lastError),
1215
+ "cart-warning": this.warnings.length > 0
1042
1216
  };
1217
+ qsa(document, sel("cart-warning")).forEach((el) => setTextTracked(el, [...new Set(this.warnings.map((w) => w.message))].join(" ")));
1043
1218
  qsa(document, sel("show")).forEach((el) => {
1044
1219
  const k = get(el, "show");
1045
1220
  if (k in conds && !el.closest(sel("product"))) show(el, conds[k]);
1046
1221
  });
1222
+ qsa(document, sel("checkout")).forEach((el) => {
1223
+ el.classList.toggle("is-disabled", !count);
1224
+ el.setAttribute("aria-disabled", String(!count));
1225
+ });
1047
1226
  qsa(document, `textarea${sel("cart-note")},input${sel("cart-note")}`).forEach((el) => {
1048
1227
  if (document.activeElement !== el) el.value = cart?.note || "";
1049
1228
  });
@@ -1089,7 +1268,7 @@
1089
1268
  case "line-subtotal":
1090
1269
  return formatMoney(line.cost?.subtotalAmount);
1091
1270
  case "quantity":
1092
- return line.quantity;
1271
+ return Cart.quantityOf(line);
1093
1272
  case "plan":
1094
1273
  return plan?.name || "";
1095
1274
  case "plan-description":
@@ -1112,16 +1291,32 @@
1112
1291
  qsa(node, sel("line")).forEach((el) => {
1113
1292
  const key = get(el, "line");
1114
1293
  if (key === "image") return setImage(el, m.image?.url || m.product?.featuredImage?.url, m.product?.title);
1115
- if (key === "url") return el.setAttribute("href", productUrl(m.product?.handle, m.product?.id));
1294
+ if (key === "url") {
1295
+ const u = productUrl(m.product?.handle, m.product?.id);
1296
+ const q = new URLSearchParams();
1297
+ if (m.id) q.set("variant", String(m.id).split("/").pop());
1298
+ const plan = line.sellingPlanAllocation?.sellingPlan?.id;
1299
+ if (plan) q.set("selling_plan", plan.split("/").pop());
1300
+ const qs = q.toString();
1301
+ return el.setAttribute("href", qs ? `${u}${u.includes("?") ? "&" : "?"}${qs}` : u);
1302
+ }
1116
1303
  const v = lineValue(line, key);
1117
1304
  if (v !== void 0) setTextTracked(el, v);
1118
1305
  return void 0;
1119
1306
  });
1307
+ const warning = Cart.warnings.find((w) => w.target === line.id);
1308
+ node.classList.toggle("is-warning", Boolean(warning));
1309
+ const pending2 = Cart.pendingQty.has(line.id);
1310
+ node.classList.toggle("is-busy", pending2);
1311
+ if (pending2) node.setAttribute("aria-busy", "true");
1312
+ else node.removeAttribute("aria-busy");
1313
+ qsa(node, sel("line", "warning")).forEach((el) => setTextTracked(el, warning?.message || ""));
1120
1314
  const lineConds = {
1121
1315
  subscription: isSub,
1122
1316
  "one-time": !isSub,
1123
1317
  "on-sale": parseFloat(line.cost?.compareAtAmountPerQuantity?.amount) > parseFloat(line.cost?.amountPerQuantity?.amount),
1124
- "has-components": (line.lineComponents || []).length > 0
1318
+ "has-components": (line.lineComponents || []).length > 0,
1319
+ warning: Boolean(warning)
1125
1320
  };
1126
1321
  qsa(node, sel("line-show")).forEach((el) => {
1127
1322
  const k = get(el, "line-show");
@@ -1158,6 +1353,7 @@
1158
1353
  var product_exports = {};
1159
1354
  __export(product_exports, {
1160
1355
  ProductScope: () => ProductScope,
1356
+ rememberOptionNames: () => rememberOptionNames,
1161
1357
  scopes: () => scopes
1162
1358
  });
1163
1359
 
@@ -1171,8 +1367,8 @@
1171
1367
  resolveProductKey: () => resolveProductKey,
1172
1368
  setScopeStatus: () => setScopeStatus
1173
1369
  });
1174
- function resolveProductKey(el, location = window.location) {
1175
- const raw = (get(el, "product") || "").trim();
1370
+ function resolveProductKey(el, location = window.location, name = "product") {
1371
+ const raw = (get(el, name) || "").trim();
1176
1372
  if (!raw.toLowerCase().startsWith("url:")) return { key: raw || null, fromUrl: false, raw };
1177
1373
  const what = raw.slice(4).trim();
1178
1374
  let key = null;
@@ -1204,11 +1400,7 @@
1204
1400
  scoped(root, sel("show"), boundary()).forEach((el) => {
1205
1401
  const k = get(el, "show");
1206
1402
  if (!STATUS_KEYS.includes(k)) return;
1207
- const visible = k === status;
1208
- show(el, visible);
1209
- if (visible && typeof getComputedStyle === "function" && getComputedStyle(el).display === "none") {
1210
- el.style.display = el.getAttribute(attr("display")) || "block";
1211
- }
1403
+ show(el, k === status);
1212
1404
  });
1213
1405
  if (status === "error" || notFound) {
1214
1406
  scoped(root, sel("add-to-cart"), boundary()).forEach((btn) => {
@@ -1229,6 +1421,12 @@
1229
1421
  const mf = (product.metafields || []).find((m) => m && m.namespace === ns && m.key === key);
1230
1422
  return mf || null;
1231
1423
  }
1424
+ var knownOptionNames = /* @__PURE__ */ new Map();
1425
+ function rememberOptionNames(products) {
1426
+ (products || []).forEach((p) => {
1427
+ if (p?.handle && !knownOptionNames.has(p.handle)) knownOptionNames.set(p.handle, (p.options || []).map((o) => o.name));
1428
+ });
1429
+ }
1232
1430
  var ProductScope = class {
1233
1431
  constructor(root, product) {
1234
1432
  this.root = root;
@@ -1278,6 +1476,13 @@
1278
1476
  return this.root.matches(s) ? [this.root, ...inner] : inner;
1279
1477
  }
1280
1478
  // ------------------------------------------------------------ state
1479
+ /** The product's option name for a name used in the markup (same name, or same position). */
1480
+ optName(authored) {
1481
+ const opts = this.product?.options || [];
1482
+ if (!authored || opts.some((o) => o.name === authored)) return authored;
1483
+ const i = (knownOptionNames.get(this.product.handle) || []).indexOf(authored);
1484
+ return i >= 0 && opts[i] ? opts[i].name : authored;
1485
+ }
1281
1486
  initSelection() {
1282
1487
  const vs = this.product.variants.nodes;
1283
1488
  const first = vs.find((v) => v.availableForSale) || vs[0];
@@ -1345,7 +1550,17 @@
1345
1550
  // ------------------------------------------------------------ actions
1346
1551
  select(name, value) {
1347
1552
  if (this.selected[name] === value) return;
1348
- this.selected[name] = value;
1553
+ const vs = this.product.variants.nodes.filter((v) => v.selectedOptions.some((o) => o.name === name && o.value === value));
1554
+ if (!vs.length) return;
1555
+ const next = { ...this.selected, [name]: value };
1556
+ if (!vs.some((v) => v.selectedOptions.every((o) => next[o.name] === o.value))) {
1557
+ const score = (v) => v.selectedOptions.filter((o) => o.name !== name && this.selected[o.name] === o.value).length * 2 + (v.availableForSale ? 1 : 0);
1558
+ const best = vs.reduce((a, b) => score(b) > score(a) ? b : a);
1559
+ best.selectedOptions.forEach((o) => {
1560
+ next[o.name] = o.value;
1561
+ });
1562
+ }
1563
+ this.selected = next;
1349
1564
  this.renderVariant();
1350
1565
  }
1351
1566
  selectVariant(id) {
@@ -1394,7 +1609,7 @@
1394
1609
  const opt = t.closest(sel("option-value"));
1395
1610
  if (own(opt) && opt.closest(sel("option-list"))) {
1396
1611
  e.preventDefault();
1397
- this.select(get(opt.closest(sel("option-list")), "option-list"), get(opt, "option-value"));
1612
+ this.select(this.optName(get(opt.closest(sel("option-list")), "option-list")), get(opt, "option-value"));
1398
1613
  return;
1399
1614
  }
1400
1615
  const plan = t.closest(sel("plan-value"));
@@ -1430,7 +1645,7 @@
1430
1645
  this.root.addEventListener("change", (e) => {
1431
1646
  const t = e.target;
1432
1647
  if (!own(t)) return;
1433
- if (t.matches(`select${sel("option")}`)) this.select(get(t, "option"), t.value);
1648
+ if (t.matches(`select${sel("option")}`)) this.select(this.optName(get(t, "option")), t.value);
1434
1649
  else if (t.matches(`select${sel("variant-select")}`)) this.selectVariant(t.value);
1435
1650
  else if (t.matches(`select${sel("plan")}`)) {
1436
1651
  if (t.value) this.selectPlan(t.value);
@@ -1460,6 +1675,11 @@
1460
1675
  }
1461
1676
  // ------------------------------------------------------------ render
1462
1677
  renderProduct() {
1678
+ const p0 = this.product;
1679
+ if (!knownOptionNames.has(p0.handle)) {
1680
+ const authored = this.$(`${sel("option-list")},select${sel("option")}`).map((e) => get(e, "option-list") || get(e, "option"));
1681
+ if (authored.some((n) => p0.options.some((o) => o.name === n))) rememberOptionNames([p0]);
1682
+ }
1463
1683
  const p = this.product;
1464
1684
  this.$(sel("bind")).forEach((el) => {
1465
1685
  const key = get(el, "bind") || "";
@@ -1489,6 +1709,10 @@
1489
1709
  if (key === "url" && !el.firstElementChild && !el.textContent.trim()) setText(el, p.title);
1490
1710
  return void 0;
1491
1711
  case "images":
1712
+ {
1713
+ const imgs = [...el.children].map((n) => n.tagName === "IMG" ? n : n.querySelector("img"));
1714
+ if (!hasTemplate(el) && imgs.length === p.images.nodes.length && imgs.every((im, i) => sameImage(im?.getAttribute("src"), p.images.nodes[i].url))) return void 0;
1715
+ }
1492
1716
  return repeat(el, p.images.nodes, (node, img) => {
1493
1717
  const target = node.matches(`img,${sel("bind", "image")}`) ? node : node.querySelector("img") || node;
1494
1718
  setImage(target, img.url, img.altText || p.title);
@@ -1513,7 +1737,7 @@
1513
1737
  }
1514
1738
  });
1515
1739
  this.$(sel("option-list")).forEach((container) => {
1516
- const name = get(container, "option-list");
1740
+ const name = this.optName(get(container, "option-list"));
1517
1741
  const opt = p.options.find((o) => o.name === name);
1518
1742
  if (!opt || opt.optionValues.length <= 1 && has(container, "hide-single")) return show(container, false);
1519
1743
  return repeat(container, opt.optionValues, (node, v) => {
@@ -1527,7 +1751,7 @@
1527
1751
  });
1528
1752
  });
1529
1753
  this.$(`select${sel("option")}`).forEach((s) => {
1530
- const opt = p.options.find((o) => o.name === get(s, "option"));
1754
+ const opt = p.options.find((o) => o.name === this.optName(get(s, "option")));
1531
1755
  if (!opt) return show(s, false);
1532
1756
  s.innerHTML = "";
1533
1757
  opt.optionValues.forEach((v) => s.appendChild(new Option(v.name, v.name)));
@@ -1583,7 +1807,7 @@
1583
1807
  const plan = this.plan;
1584
1808
  const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
1585
1809
  const fm = formatMoney;
1586
- const pct = (n) => n ? `${n}%` : "";
1810
+ const pct = (n) => formatPercent(n);
1587
1811
  const text = {
1588
1812
  price: () => fm(price),
1589
1813
  "total-price": () => fm(total),
@@ -1591,7 +1815,11 @@
1591
1815
  "subscription-price": () => fm(subPrice),
1592
1816
  "subscription-savings": () => pct(savings.percentage),
1593
1817
  "subscription-savings-amount": () => fm(savings.amount),
1594
- "checkout-charge": () => fm(sub ? planCheckoutCharge(v, this.planId) : price),
1818
+ // What the visitor pays today for the chosen quantity (prepaid plans charge more up front).
1819
+ "checkout-charge": () => {
1820
+ const c = sub ? planCheckoutCharge(v, this.planId) : price;
1821
+ return c && fm({ amount: (parseFloat(c.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: c.currencyCode });
1822
+ },
1595
1823
  "plan-name": () => plan?.name,
1596
1824
  "plan-description": () => plan?.description,
1597
1825
  "plan-group": () => plan?.group?.name,
@@ -1611,9 +1839,9 @@
1611
1839
  if (k === "variant-image") setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
1612
1840
  else if (text[k]) setText(el, text[k]());
1613
1841
  });
1614
- this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[get(el, "option-selected")] || ""));
1842
+ this.$(sel("option-selected")).forEach((el) => setText(el, this.selected[this.optName(get(el, "option-selected"))] || ""));
1615
1843
  this.$(sel("option-list")).forEach((container) => {
1616
- const name = get(container, "option-list");
1844
+ const name = this.optName(get(container, "option-list"));
1617
1845
  qsa(container, sel("option-value")).forEach((node) => {
1618
1846
  const value = get(node, "option-value");
1619
1847
  setSelected(node, this.selected[name] === value);
@@ -1623,7 +1851,7 @@
1623
1851
  });
1624
1852
  });
1625
1853
  this.$(`select${sel("option")}`).forEach((s) => {
1626
- s.value = this.selected[get(s, "option")] || "";
1854
+ s.value = this.selected[this.optName(get(s, "option"))] || "";
1627
1855
  });
1628
1856
  this.$(`select${sel("variant-select")}`).forEach((s) => {
1629
1857
  s.value = v.id;
@@ -1650,6 +1878,8 @@
1650
1878
  const k = get(el, "plan-bind");
1651
1879
  const val = {
1652
1880
  name: () => planObj?.name,
1881
+ option: () => (planObj?.options || []).map((o) => o.value).join(", "),
1882
+ // e.g. "1 month", without the store's price text
1653
1883
  description: () => planObj?.description,
1654
1884
  price: () => fm(planPrice(v, id)),
1655
1885
  savings: () => pct(s.percentage),
@@ -1662,6 +1892,7 @@
1662
1892
  this.$(`select${sel("plan")}`).forEach((s) => {
1663
1893
  s.value = sub ? this.planId : "";
1664
1894
  });
1895
+ const low = v.quantityAvailable != null && v.quantityAvailable > 0 && v.quantityAvailable <= (cfg.lowStock ?? 5);
1665
1896
  const conds = {
1666
1897
  "in-stock": v.availableForSale,
1667
1898
  "out-of-stock": !v.availableForSale,
@@ -1670,7 +1901,8 @@
1670
1901
  "not-on-sale": !onSale,
1671
1902
  "has-variants": p.variants.nodes.length > 1,
1672
1903
  "single-variant": p.variants.nodes.length === 1,
1673
- "low-stock": v.quantityAvailable != null && v.quantityAvailable > 0 && v.quantityAvailable <= 5,
1904
+ "low-stock": low,
1905
+ "plenty-in-stock": v.availableForSale && !low,
1674
1906
  "subscription-available": this.subscriptionAvailable,
1675
1907
  "subscription-unavailable": !this.subscriptionAvailable,
1676
1908
  "subscription-only": Boolean(p.requiresSellingPlan),
@@ -1756,18 +1988,20 @@
1756
1988
  }
1757
1989
  async load(more = false) {
1758
1990
  const token = this.loadToken = (this.loadToken || 0) + 1;
1759
- const stale = () => token !== this.loadToken;
1991
+ const stale2 = () => token !== this.loadToken;
1760
1992
  try {
1761
1993
  if (has(this.root, "recommendations")) {
1762
- const key = get(this.root, "recommendations");
1763
- const base = await getProduct(key);
1764
- const recs = await getRecommendations(base.id, get(this.root, "intent") || "RELATED");
1765
- if (stale()) return;
1994
+ let { key } = resolveProductKey(this.root, window.location, "recommendations");
1995
+ const scope = this.root.closest(sel("product"));
1996
+ if (!key && scope) ({ key } = resolveProductKey(scope));
1997
+ if (!key) throw new TetherError("No product for recommendations", { code: "NOT_FOUND" });
1998
+ const recs = await getRecommendations(key, get(this.root, "intent") || "RELATED");
1999
+ if (stale2()) return;
1766
2000
  const limit = parseInt(get(this.root, "limit") || "", 10);
1767
2001
  this.products = limit ? recs.slice(0, limit) : recs;
1768
2002
  } else {
1769
2003
  const res = await getProducts({ ...this.options(), after: more ? this.pageInfo?.endCursor : null });
1770
- if (stale()) return;
2004
+ if (stale2()) return;
1771
2005
  this.collection = res.collection;
1772
2006
  this.pageInfo = res.pageInfo;
1773
2007
  this.products = more ? [...this.products, ...res.products] : res.products;
@@ -1776,9 +2010,9 @@
1776
2010
  setScopeStatus(this.root, "ready");
1777
2011
  emit("list-ready", { products: this.products, collection: this.collection }, this.root);
1778
2012
  } catch (err) {
1779
- if (stale()) return;
2013
+ if (stale2()) return;
1780
2014
  if (!more) setScopeStatus(this.root, isNotFound(err) ? "not-found" : "error", err);
1781
- reportError(err, "list load failed");
2015
+ if (!(isNotFound(err) && has(this.root, "recommendations"))) reportError(err, "list load failed");
1782
2016
  emit("error", { error: err, context: "list" }, this.root);
1783
2017
  }
1784
2018
  }
@@ -1809,12 +2043,10 @@
1809
2043
  const empty = this.products.length === 0;
1810
2044
  this.$(sel("show")).forEach((el) => {
1811
2045
  const k = get(el, "show");
1812
- if (k === "list-empty") el.style.display = empty ? "" : "none";
1813
- if (k === "list-filled") el.style.display = empty ? "none" : "";
1814
- });
1815
- this.$(sel("load-more")).forEach((el) => {
1816
- el.style.display = this.pageInfo?.hasNextPage ? "" : "none";
2046
+ if (k === "list-empty") show(el, empty);
2047
+ if (k === "list-filled") show(el, !empty);
1817
2048
  });
2049
+ this.$(sel("load-more")).forEach((el) => show(el, Boolean(this.pageInfo?.hasNextPage)));
1818
2050
  notifyLayoutChanged("list");
1819
2051
  }
1820
2052
  };
@@ -1822,6 +2054,7 @@
1822
2054
  // src/mounting.js
1823
2055
  var mounting_exports = {};
1824
2056
  __export(mounting_exports, {
2057
+ hydrateFromPage: () => hydrateFromPage,
1825
2058
  liveLists: () => liveLists,
1826
2059
  liveProducts: () => liveProducts,
1827
2060
  mountProduct: () => mountProduct,
@@ -1829,6 +2062,8 @@
1829
2062
  mountedProducts: () => mountedProducts,
1830
2063
  pruneLive: () => pruneLive,
1831
2064
  retryFailed: () => retryFailed,
2065
+ settleMarket: () => settleMarket,
2066
+ trackMarket: () => trackMarket,
1832
2067
  within: () => within
1833
2068
  });
1834
2069
  var mountedProducts = /* @__PURE__ */ new WeakSet();
@@ -1867,12 +2102,15 @@
1867
2102
  const current = () => requestToken.get(el) === token;
1868
2103
  if (!scopes.get(el)) setScopeStatus(el, "loading");
1869
2104
  else el.setAttribute(attr("state"), "updating");
1870
- getProduct(key).then((p) => {
2105
+ const loading = getProduct(key);
2106
+ marketPending?.add(loading);
2107
+ loading.then((p) => {
1871
2108
  if (!current()) return;
1872
2109
  const existing = scopes.get(el);
1873
2110
  if (existing) existing.update(p);
1874
2111
  else new ProductScope(el, p);
1875
2112
  applyPageTitle(el, p);
2113
+ if (cfg.revalidate !== false && takeStale(key)) queueRefresh(key);
1876
2114
  }).catch((err) => {
1877
2115
  if (!current()) return;
1878
2116
  const notFound = isNotFound(err);
@@ -1901,6 +2139,62 @@
1901
2139
  mountProduct(el);
1902
2140
  });
1903
2141
  }
2142
+ var refreshQueue = /* @__PURE__ */ new Set();
2143
+ function queueRefresh(key) {
2144
+ if (!refreshQueue.size) setTimeout(flushRefresh, 0);
2145
+ refreshQueue.add(key);
2146
+ }
2147
+ function flushRefresh() {
2148
+ const keys = [...refreshQueue];
2149
+ refreshQueue = /* @__PURE__ */ new Set();
2150
+ refreshProducts(keys).forEach((promise, i) => promise.then((fresh) => {
2151
+ liveProducts.forEach((el) => {
2152
+ const scope = scopes.get(el);
2153
+ if (!scope || !el.isConnected) return;
2154
+ const k = resolveProductKey(el).key;
2155
+ if (k !== keys[i] && k !== fresh.handle && k !== fresh.id) return;
2156
+ if (JSON.stringify(scope.product) !== JSON.stringify(fresh)) scope.update(fresh);
2157
+ });
2158
+ }).catch((err) => reportError(err, `refresh ${keys[i]}`)));
2159
+ }
2160
+ var readScripts = /* @__PURE__ */ new WeakSet();
2161
+ var marketPending = null;
2162
+ var trackMarket = (p) => {
2163
+ marketPending?.add(p);
2164
+ return p;
2165
+ };
2166
+ function settleMarket() {
2167
+ if (!marketPending) return;
2168
+ const waits = [...marketPending];
2169
+ Promise.allSettled(waits).then(() => {
2170
+ if (!marketPending || waits.some((w) => !marketPending.has(w))) return;
2171
+ if (waits.length === marketPending.size) {
2172
+ marketPending = null;
2173
+ document.documentElement.classList.remove(`${cfg.prefix}-market-pending`);
2174
+ } else settleMarket();
2175
+ });
2176
+ }
2177
+ function hydrateFromPage(root = document) {
2178
+ qsa(root.ownerDocument || root, 'script[type="application/json"][data-tether-hydrate]').forEach((s) => {
2179
+ if (readScripts.has(s)) return;
2180
+ readScripts.add(s);
2181
+ try {
2182
+ const data = JSON.parse(s.textContent);
2183
+ rememberOptionNames(data.products);
2184
+ const same = (a, b) => String(a || "").toUpperCase() === String(b || "").toUpperCase();
2185
+ if (same(data.country, cfg.country) && same(data.language, cfg.language)) hydrateProducts(data.products);
2186
+ else {
2187
+ log("hydration skipped: other market");
2188
+ if (!marketPending) {
2189
+ marketPending = /* @__PURE__ */ new Set();
2190
+ document.documentElement.classList.add(`${cfg.prefix}-market-pending`);
2191
+ }
2192
+ }
2193
+ } catch (err) {
2194
+ warn("bad data-tether-hydrate", err);
2195
+ }
2196
+ });
2197
+ }
1904
2198
 
1905
2199
  // src/lazy-iife.js
1906
2200
  var { cfg: cfg2 } = config_exports;
@@ -1939,13 +2233,14 @@
1939
2233
  }
1940
2234
 
1941
2235
  // src/index.js
1942
- var VERSION = "0.4.0";
2236
+ var VERSION = "0.6.0";
1943
2237
  var initPromise = null;
1944
2238
  var observer = null;
1945
2239
  function mount(root = document) {
1946
2240
  if (!isConfigured()) return 0;
1947
2241
  let count = 0;
1948
2242
  pruneLive();
2243
+ hydrateFromPage(root);
1949
2244
  within(root, listSelector()).forEach((el) => {
1950
2245
  if (mountedLists.has(el)) return;
1951
2246
  mountedLists.add(el);
@@ -1953,7 +2248,7 @@
1953
2248
  const list = new ListScope(el);
1954
2249
  liveLists.add(list);
1955
2250
  el.addEventListener(`${cfg.prefix}:list-rendered`, (e) => mount(e.detail.container));
1956
- list.load();
2251
+ trackMarket(list.load());
1957
2252
  });
1958
2253
  within(root, sel("product")).forEach((el) => {
1959
2254
  if (mountProduct(el)) count++;
@@ -1961,9 +2256,16 @@
1961
2256
  if (within(root, `${sel("country-select")},${sel("country-bind")}`).length) {
1962
2257
  loadPart("markets").then((m) => m.renderCountrySelects(root)).catch((err) => reportError(err, "markets"));
1963
2258
  }
2259
+ if ((cfg.account?.clientId || cfg.account?.server) && (within(root, `[${attr("account-login")}],[${attr("account-bind")}],[${attr("orders")}],[${attr("show")}^="logged-"]`).length || /[?&]code=/.test(window.location.search) || hasAccountSession())) {
2260
+ loadPart("account").then((m) => m.mountAccount(root)).catch((err) => reportError(err, "account"));
2261
+ }
2262
+ if (within(root, sel("search")).length) {
2263
+ loadPart("search").then((m) => m.mountSearch(root)).catch((err) => reportError(err, "search"));
2264
+ }
1964
2265
  qsa(root, sel("account-link")).forEach((a) => {
1965
2266
  a.setAttribute("href", `https://${cfg.domain}/account`);
1966
2267
  });
2268
+ settleMarket();
1967
2269
  Cart.render();
1968
2270
  log(`mounted ${count} scope(s)`, root);
1969
2271
  return count;
@@ -2012,9 +2314,12 @@
2012
2314
  e.preventDefault();
2013
2315
  fn().catch((err) => reportError(err, "cart line"));
2014
2316
  };
2015
- if (t.closest(sel("line-remove"))) return run(() => Cart.removeLine(id));
2016
- if (current && t.closest(sel("line-plus"))) return run(() => Cart.updateLine(id, current.quantity + 1));
2017
- if (current && t.closest(sel("line-minus"))) return run(() => Cart.updateLine(id, current.quantity - 1));
2317
+ if (t.closest(sel("line-remove"))) {
2318
+ line.classList.add("is-busy");
2319
+ return run(() => Cart.removeLine(id));
2320
+ }
2321
+ if (current && t.closest(sel("line-plus"))) return run(() => Cart.changeQuantity(id, 1));
2322
+ if (current && t.closest(sel("line-minus"))) return run(() => Cart.changeQuantity(id, -1));
2018
2323
  }
2019
2324
  const removeCode = t.closest(sel("discount-remove"));
2020
2325
  if (removeCode) {
@@ -2034,10 +2339,15 @@
2034
2339
  if (!t?.closest) return;
2035
2340
  const lineEl = t.closest(dataKeySel());
2036
2341
  const id = lineEl?.getAttribute(`data-${cfg.prefix}-key`);
2342
+ const busyLine = (p, what) => {
2343
+ lineEl.classList.add("is-busy");
2344
+ lineEl.setAttribute("aria-busy", "true");
2345
+ p.catch((err) => reportError(err, what));
2346
+ };
2037
2347
  if (id && t.matches(`input${sel("line", "quantity")}`)) {
2038
- Cart.updateLine(id, parseInt(t.value, 10) || 0).catch((err) => reportError(err, "cart line"));
2348
+ busyLine(Cart.updateLine(id, parseInt(t.value, 10) || 0), "cart line");
2039
2349
  } else if (id && t.matches(`select${sel("line-plan")}`)) {
2040
- Cart.updateLine(id, { sellingPlanId: t.value || null }).catch((err) => reportError(err, "cart line plan"));
2350
+ busyLine(Cart.updateLine(id, { sellingPlanId: t.value || null }), "cart line plan");
2041
2351
  } else if (t.matches(`select${sel("country-select")}`)) {
2042
2352
  if (t.value) setCountry(t.value).catch((err) => reportError(err, "country"));
2043
2353
  } else if (t.matches(sel("cart-note"))) {
@@ -2052,18 +2362,59 @@
2052
2362
  const code = input?.value.trim();
2053
2363
  if (!code) return;
2054
2364
  busy(form, async () => {
2365
+ const mark = (bad) => {
2366
+ form.classList.toggle("is-invalid", bad);
2367
+ if (input) {
2368
+ if (bad) input.setAttribute("aria-invalid", "true");
2369
+ else input.removeAttribute("aria-invalid");
2370
+ }
2371
+ };
2055
2372
  try {
2056
- const cart = await Cart.applyDiscount(code);
2057
- const ok = (cart?.discountCodes || []).find((d) => d.code.toLowerCase() === code.toLowerCase())?.applicable;
2058
- form.classList.toggle("is-invalid", !ok);
2059
- emit(ok ? "discount-applied" : "discount-invalid", { code }, form);
2060
- if (ok && input) input.value = "";
2373
+ await Cart.applyDiscount(code);
2374
+ mark(false);
2375
+ emit("discount-applied", { code }, form);
2376
+ if (input) input.value = "";
2061
2377
  } catch (err) {
2062
- form.classList.add("is-invalid");
2063
- reportError(err, "discount");
2378
+ mark(true);
2379
+ if (err?.code === "INVALID_DISCOUNT") emit("discount-invalid", { code }, form);
2380
+ else reportError(err, "discount");
2064
2381
  }
2065
2382
  });
2383
+ }, true);
2384
+ document.addEventListener("input", (e) => {
2385
+ const f = e.target.closest?.(sel("discount-form"));
2386
+ if (f) {
2387
+ f.classList.remove("is-invalid");
2388
+ e.target.removeAttribute?.("aria-invalid");
2389
+ }
2066
2390
  });
2391
+ window.addEventListener("storage", (e) => {
2392
+ if (cfg.domain && e.key?.startsWith(`${cfg.prefix}:`)) loadPart("extras").then((m) => m.onStorage(e)).catch(() => {
2393
+ });
2394
+ });
2395
+ document.addEventListener(`${cfg.prefix}:variant-changed`, (e) => {
2396
+ const root = e.target;
2397
+ if (cfg.variantUrl || root.hasAttribute?.(attr("variant-url"))) loadPart("extras").then((m) => m.writeVariantUrl(root, e.detail)).catch(() => {
2398
+ });
2399
+ });
2400
+ if (cfg.astro !== false) {
2401
+ document.addEventListener("astro:page-load", () => (initPromise || Promise.resolve()).then(() => {
2402
+ let target = pageMarket();
2403
+ if (!target && onPageMarket) {
2404
+ try {
2405
+ target = JSON.parse(localStorage.getItem(`${cfg.prefix}:market:${cfg.domain}`) || "null");
2406
+ } catch {
2407
+ target = null;
2408
+ }
2409
+ target = target?.country ? target : baseMarket;
2410
+ }
2411
+ onPageMarket = Boolean(pageMarket());
2412
+ const differs = target?.country && (String(target.country).toUpperCase() !== String(cfg.country || "").toUpperCase() || String(target.language || "").toUpperCase() !== String(cfg.language || "").toUpperCase());
2413
+ if (differs) setCountry(target.country, { language: target.language ?? null, locale: target.locale, remember: false }).then(() => mount(document), () => mount(document));
2414
+ else mount(document);
2415
+ }));
2416
+ document.addEventListener("astro:before-swap", () => unmount());
2417
+ }
2067
2418
  window.addEventListener("online", () => {
2068
2419
  retryFailed();
2069
2420
  liveLists.forEach((l) => {
@@ -2091,30 +2442,8 @@
2091
2442
  function dataKeySel() {
2092
2443
  return `[data-${cfg.prefix}-key]`;
2093
2444
  }
2094
- async function addGroup(btn) {
2095
- const group = btn.closest(sel("group"));
2096
- if (!group) return null;
2097
- const name = get(group, "group");
2098
- const extra = toAttributes(parseJSONAttr(group, "line-attributes"));
2099
- const groupAttrs = name ? [{ key: "_group", value: name }, ...extra] : extra;
2100
- const lines = qsa(group, sel("product")).map((el) => scopes.get(el)).filter((s) => s && s.variant?.availableForSale && s.quantity > 0).map((s) => {
2101
- const l = s.getLine();
2102
- l.attributes = [...l.attributes || [], ...groupAttrs];
2103
- return l;
2104
- });
2105
- if (!lines.length) {
2106
- warn("group-add: nothing selected");
2107
- return null;
2108
- }
2109
- try {
2110
- const cart = await Cart.add(lines);
2111
- emit("group-added", { group: name, lines }, group);
2112
- return cart;
2113
- } catch (err) {
2114
- reportError(err, "group add");
2115
- return null;
2116
- }
2117
- }
2445
+ var addGroup = (btn) => loadPart("extras").then((m) => m.addGroup(btn));
2446
+ var analyticsQueue = null;
2118
2447
  function init(overrides = {}) {
2119
2448
  if (initPromise && !Object.keys(overrides).length) return initPromise;
2120
2449
  configure({ ...window.TETHER_CONFIG || {}, ...overrides });
@@ -2125,6 +2454,12 @@
2125
2454
  }
2126
2455
  bindControls();
2127
2456
  if (cfg.observe) startObserver();
2457
+ if (cfg.analytics && !analyticsQueue) {
2458
+ analyticsQueue = [];
2459
+ ["product-ready", "list-ready", "cart-updated", "cart-toggled", "checkout", "search"].forEach((n) => document.addEventListener(`${cfg.prefix}:${n}`, (e) => analyticsQueue.push(e)));
2460
+ document.addEventListener("astro:page-load", () => analyticsQueue.push(new CustomEvent(`${cfg.prefix}:page`)));
2461
+ loadPart("analytics").then((m) => m.startAnalytics(analyticsQueue)).catch((err) => reportError(err, "analytics"));
2462
+ }
2128
2463
  initPromise = applyMarket(overrides).then(() => Cart.init()).then(async (cart) => {
2129
2464
  const want = cfg.country ? String(cfg.country).toUpperCase() : null;
2130
2465
  if (cart && want && cart.buyerIdentity?.countryCode && cart.buyerIdentity.countryCode !== want) {
@@ -2139,8 +2474,24 @@
2139
2474
  });
2140
2475
  return initPromise;
2141
2476
  }
2477
+ function pageMarket(doc = document) {
2478
+ const get2 = (n) => doc.querySelector(`meta[name="${cfg.prefix}-page-${n}"]`)?.getAttribute("content") || void 0;
2479
+ const country = get2("country");
2480
+ return country ? { country: country.toUpperCase(), language: get2("language"), locale: get2("locale") } : null;
2481
+ }
2482
+ var baseMarket = null;
2483
+ var onPageMarket = false;
2142
2484
  async function applyMarket(overrides) {
2143
2485
  const m = cfg.markets || {};
2486
+ baseMarket = { country: cfg.country, language: cfg.language, locale: cfg.locale };
2487
+ const page = pageMarket();
2488
+ onPageMarket = Boolean(page);
2489
+ if (page) {
2490
+ cfg.country = page.country;
2491
+ cfg.language = page.language ? page.language.toUpperCase() : void 0;
2492
+ if (page.locale) cfg.locale = page.locale;
2493
+ return;
2494
+ }
2144
2495
  if (m.detect === false) return;
2145
2496
  let market = null;
2146
2497
  if (m.detect) {
@@ -2160,9 +2511,17 @@
2160
2511
  if (!market) return;
2161
2512
  if (market.country) cfg.country = market.country;
2162
2513
  if (market.language) cfg.language = market.language;
2163
- if (market.locale && !overrides.locale && !window.TETHER_CONFIG?.locale) cfg.locale = market.locale;
2514
+ if (market.locale && !overrides.locale) cfg.locale = market.locale;
2164
2515
  log("market", market);
2165
2516
  }
2517
+ function hasAccountSession() {
2518
+ try {
2519
+ return Boolean(sessionStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account:${cfg.domain}`] || localStorage[`${cfg.prefix}:account-hint:${cfg.domain}`]);
2520
+ } catch {
2521
+ return false;
2522
+ }
2523
+ }
2524
+ var accountApi = new Proxy({}, { get: (_, n) => (...a) => loadPart("account").then((m) => m[n](...a)) });
2166
2525
  function setCountry(code, opts) {
2167
2526
  return loadPart("markets").then((m) => m.setCountry(code, opts));
2168
2527
  }
@@ -2203,10 +2562,12 @@
2203
2562
  getRecommendations,
2204
2563
  getScope,
2205
2564
  addGroup,
2565
+ account: accountApi,
2206
2566
  primeProduct,
2207
2567
  normalizeProductKey,
2208
2568
  clearProductCache,
2209
2569
  formatMoney,
2570
+ imageUrl,
2210
2571
  plans: selling_plans_exports,
2211
2572
  TetherError,
2212
2573
  resolveProductKey,
@@ -2225,7 +2586,7 @@
2225
2586
  var index_default = api;
2226
2587
  if (typeof window !== "undefined" && typeof document !== "undefined") {
2227
2588
  window.Tether = api;
2228
- const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false;
2589
+ const auto = window.TETHER_CONFIG && window.TETHER_CONFIG.autoMount !== false && !window.TETHER_INTEGRATION;
2229
2590
  if (auto) {
2230
2591
  const start = () => init().then(() => mount(document));
2231
2592
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start, { once: true });