@occluze/tether 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-NVHPA7AX.js +1 -0
- package/dist/esm/chunks/{analytics-22NMIVBV.js → analytics-SLATWPLC.js} +1 -1
- package/dist/esm/chunks/{chunk-YZMBXS6Y.js → chunk-64TUD3QR.js} +1 -1
- package/dist/esm/chunks/{chunk-YXCXIPQV.js → chunk-AJLPU2JC.js} +1 -1
- package/dist/esm/chunks/{chunk-ZVUPVSLF.js → chunk-FIOZPPC5.js} +1 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +1 -0
- package/dist/esm/chunks/chunk-L2TRRKYV.js +1 -0
- package/dist/esm/chunks/{chunk-4UR45XTC.js → chunk-RREHGH4A.js} +1 -1
- package/dist/esm/chunks/chunk-VCPI7MJ2.js +1 -0
- package/dist/esm/chunks/{chunk-6R752OYZ.js → chunk-ZIUIWEON.js} +1 -1
- package/dist/esm/chunks/diagnose-4JHU6RTC.js +11 -0
- package/dist/esm/chunks/{extras-QDCSW6VU.js → extras-R4X34VQL.js} +1 -1
- package/dist/esm/chunks/{markets-ui-HJCVLCKJ.js → markets-ui-F3R5T5EL.js} +1 -1
- package/dist/esm/chunks/{search-LN2H6SXS.js → search-NILMGYXG.js} +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +1 -1
- package/dist/tether.account.js +28 -2
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.diagnose.js +139 -40
- package/dist/tether.diagnose.min.js +11 -1
- package/dist/tether.js +31 -9
- package/dist/tether.min.js +2 -2
- package/dist/tether.min.js.map +3 -3
- package/package.json +1 -1
- package/src/account.js +25 -4
- package/src/astro.js +29 -4
- package/src/cart.js +15 -2
- package/src/client.js +12 -3
- package/src/collection.js +5 -1
- package/src/config.js +1 -1
- package/src/diagnose.js +79 -20
- package/src/dom.js +2 -1
- package/src/index.js +1 -1
- package/src/server.js +7 -6
- package/types/astro.d.ts +2 -0
- package/types/server.d.ts +16 -8
- package/dist/esm/chunks/account-JBZJN7X7.js +0 -1
- package/dist/esm/chunks/chunk-BGIKQY6H.js +0 -1
- package/dist/esm/chunks/chunk-CISGFYOF.js +0 -1
- package/dist/esm/chunks/chunk-KARKXC6G.js +0 -1
- package/dist/esm/chunks/diagnose-6ZSK5DGE.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@occluze/tether",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Free, open-source alternative to Smootify and Storesynk (Shopyflow): Shopify products, cart and subscriptions in Webflow with HTML attributes. Optional Shopify to Webflow CMS sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/esm/index.js",
|
package/src/account.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { cfg, log, reportError } from './config.js';
|
|
23
23
|
import { TetherError } from './client.js';
|
|
24
|
-
import { sel, get, qsa, setText, repeat, show, formatMoney, emit } from './dom.js';
|
|
24
|
+
import { sel, get, qsa, setText, setImage, repeat, show, formatMoney, emit } from './dom.js';
|
|
25
25
|
import { Cart } from './cart.js';
|
|
26
26
|
|
|
27
27
|
const KEY = () => `${cfg.prefix}:account:${cfg.domain}`;
|
|
@@ -41,7 +41,8 @@ let customerCache = null;
|
|
|
41
41
|
|
|
42
42
|
// Server mode (account.server): the Worker holds the tokens, this browser only a session cookie it
|
|
43
43
|
// can't read. Recommended for live sites.
|
|
44
|
-
|
|
44
|
+
// A full URL or a path on this site ('/_tether/account').
|
|
45
|
+
const server = () => (cfg.account?.server ? new URL(String(cfg.account.server), window.location.href).href.replace(/\/$/, '') : '');
|
|
45
46
|
const HINT = () => `${cfg.prefix}:account-hint:${cfg.domain}`; // "may be logged in": loads this part, no secret
|
|
46
47
|
let serverState = null; // { loggedIn, customer }
|
|
47
48
|
async function serverFetch(path, { method = 'GET', body } = {}) {
|
|
@@ -271,14 +272,34 @@ export async function render(root = document) {
|
|
|
271
272
|
}
|
|
272
273
|
const lists = qsa(root, sel('orders'));
|
|
273
274
|
if (lists.length) {
|
|
274
|
-
|
|
275
|
+
// tether-show="orders-loading" / "orders-empty" (no orders yet).
|
|
276
|
+
const ordersShow = (loading, empty) => qsa(root, sel('show')).forEach((el) => {
|
|
277
|
+
const k = get(el, 'show');
|
|
278
|
+
if (k === 'orders-loading') show(el, loading);
|
|
279
|
+
if (k === 'orders-empty') show(el, empty);
|
|
280
|
+
});
|
|
281
|
+
ordersShow(true, false);
|
|
282
|
+
let failed = false;
|
|
283
|
+
const list = await orders({ first: parseInt(get(lists[0], 'limit') || '10', 10) }).catch((err) => { reportError(err, 'orders'); failed = true; return []; });
|
|
284
|
+
ordersShow(false, !failed && list.length === 0);
|
|
275
285
|
lists.forEach((c) => repeat(c, list, (node, o) => {
|
|
286
|
+
const items = o.lineItems?.nodes || [];
|
|
276
287
|
qsa(node, sel('order-bind')).forEach((el) => {
|
|
277
288
|
const k = get(el, 'order-bind');
|
|
278
289
|
if (k === 'url') { if (https(o.statusPageUrl)) el.setAttribute('href', o.statusPageUrl); return; }
|
|
279
|
-
|
|
290
|
+
if (k === 'image') { const im = items.find((l) => l.image)?.image; if (im) setImage(el, im.url, im.altText || items[0]?.title || ''); return; }
|
|
291
|
+
const v = { name: o.name, date: date(o.processedAt), total: formatMoney(o.totalPrice), status: o.financialStatus?.toLowerCase(), fulfillment: o.fulfillmentStatus?.toLowerCase(), items: items.map((l) => `${l.quantity}× ${l.title}`).join(', ') }[k];
|
|
280
292
|
if (v != null) setText(el, v);
|
|
281
293
|
});
|
|
294
|
+
// Per product of the order: <div tether-order-lines><div><img tether-order-line="image"> <span tether-order-line="title"></span></div></div>
|
|
295
|
+
qsa(node, sel('order-lines')).forEach((box) => repeat(box, items, (row, l) => {
|
|
296
|
+
qsa(row, sel('order-line')).concat(row.matches(sel('order-line')) ? [row] : []).forEach((el) => {
|
|
297
|
+
const k = get(el, 'order-line');
|
|
298
|
+
if (k === 'image') { if (l.image) setImage(el, l.image.url, l.image.altText || l.title); return; }
|
|
299
|
+
const v = { title: l.title, 'variant-title': l.variantTitle || '', quantity: String(l.quantity) }[k];
|
|
300
|
+
if (v != null) setText(el, v);
|
|
301
|
+
});
|
|
302
|
+
}));
|
|
282
303
|
if (get(node, 'order-bind') === 'url' && https(o.statusPageUrl)) node.setAttribute('href', o.statusPageUrl);
|
|
283
304
|
}));
|
|
284
305
|
}
|
package/src/astro.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Options are Tether's config (domain and token override .env). Functions (onError, formatMoney,
|
|
14
14
|
* hooks) can't be passed here: set them in window.TETHER_CONFIG in your layout, it is merged in.
|
|
15
|
-
* View Transitions (<ClientRouter />) work without extra code.
|
|
15
|
+
* View Transitions (<ClientRouter />) work without extra code. In dev, changes to
|
|
16
|
+
* src/shop.config.js restart the dev server (other files: tether({ ..., watch: ['my/config.js'] })).
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
const ENV_KEYS = {
|
|
@@ -20,9 +21,16 @@ const ENV_KEYS = {
|
|
|
20
21
|
token: ['PUBLIC_SHOPIFY_STOREFRONT_TOKEN', 'PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN', 'SHOPIFY_STOREFRONT_TOKEN'],
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
/** The mode Astro runs in: --mode, else development for dev and production for build. */
|
|
25
|
+
function modeOf(command, argv = process.argv) {
|
|
26
|
+
const i = argv.findIndex((a) => a === '--mode' || a.startsWith('--mode='));
|
|
27
|
+
if (i >= 0) return argv[i].includes('=') ? argv[i].split('=')[1] : argv[i + 1];
|
|
28
|
+
return command === 'dev' ? 'development' : 'production';
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
/** .env files the way Vite reads them (later files win), plus process.env on top. */
|
|
24
32
|
async function readEnv(command, root) {
|
|
25
|
-
const mode = command
|
|
33
|
+
const mode = modeOf(command);
|
|
26
34
|
const out = {};
|
|
27
35
|
try {
|
|
28
36
|
const { readFile } = await import('node:fs/promises');
|
|
@@ -44,12 +52,29 @@ async function readEnv(command, root) {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export default function tether(options = {}) {
|
|
47
|
-
const { inject = true, ...config } = options;
|
|
55
|
+
const { inject = true, watch = ['src/shop.config.js', 'src/shop.config.mjs', 'src/shop.config.ts'], ...config } = options;
|
|
48
56
|
return {
|
|
49
57
|
name: '@occluze/tether',
|
|
50
58
|
hooks: {
|
|
51
|
-
'astro:config:setup': async ({ injectScript, command, config: astroConfig, logger }) => {
|
|
59
|
+
'astro:config:setup': async ({ injectScript, command, config: astroConfig, logger, addWatchFile, updateConfig }) => {
|
|
52
60
|
const root = astroConfig?.root ? decodeURIComponent(new URL(astroConfig.root).pathname) : process.cwd();
|
|
61
|
+
// The options usually come from a file the Astro config imports (src/shop.config.js):
|
|
62
|
+
// the dev server restarts when it changes, so the browser gets the new config too.
|
|
63
|
+
if (command === 'dev' && addWatchFile) {
|
|
64
|
+
const { existsSync } = await import('node:fs');
|
|
65
|
+
const { resolve } = await import('node:path');
|
|
66
|
+
[].concat(watch || []).map((f) => resolve(root, f)).filter((f) => existsSync(f)).forEach((f) => addWatchFile(f));
|
|
67
|
+
}
|
|
68
|
+
// Astro 5+: declare the store variables, so Astro checks them (astro:env).
|
|
69
|
+
if (updateConfig) {
|
|
70
|
+
try {
|
|
71
|
+
const { envField } = await import('astro/config');
|
|
72
|
+
if (envField) {
|
|
73
|
+
const field = () => envField.string({ context: 'client', access: 'public', optional: true });
|
|
74
|
+
updateConfig({ env: { schema: { PUBLIC_SHOPIFY_DOMAIN: field(), PUBLIC_SHOPIFY_STOREFRONT_TOKEN: field() } } });
|
|
75
|
+
}
|
|
76
|
+
} catch { /* Astro 4: no astro:env */ }
|
|
77
|
+
}
|
|
53
78
|
const env = await readEnv(command, root);
|
|
54
79
|
for (const [key, names] of Object.entries(ENV_KEYS)) {
|
|
55
80
|
if (!config[key]) config[key] = names.map((n) => env[n]).find(Boolean);
|
package/src/cart.js
CHANGED
|
@@ -217,8 +217,21 @@ export const Cart = {
|
|
|
217
217
|
this.pendingQty.set(lineId, job);
|
|
218
218
|
this.render();
|
|
219
219
|
emit('cart-updated', { cart: this.state, reason: 'pending' });
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
const latest = () => this.pendingQty.get(lineId) === job;
|
|
221
|
+
return this._queue(async () => {
|
|
222
|
+
// Quick clicks: only the latest target goes to Shopify; the ones it replaced are skipped.
|
|
223
|
+
if (!latest()) return this.state;
|
|
224
|
+
const now = this.lines.find((l) => l.id === lineId);
|
|
225
|
+
if (now && now.quantity === target) return this.state; // e.g. + then -: nothing to change
|
|
226
|
+
const cart = target > 0
|
|
227
|
+
? await this._mutate('cartLinesUpdate', '$lines: [CartLineUpdateInput!]!', 'lines: $lines', { lines: [{ id: lineId, quantity: target }] })
|
|
228
|
+
: await this._mutate('cartLinesRemove', '$lineIds: [ID!]!', 'lineIds: $lineIds', { lineIds: [lineId] });
|
|
229
|
+
// A newer click is on its way: keep this answer, but show totals and count only once that
|
|
230
|
+
// one is in (no stepping through every reply).
|
|
231
|
+
if (!latest()) { this.state = cart; if (cart?.id) this._setId(cart.id); return cart; }
|
|
232
|
+
return this._commit(cart, target > 0 ? 'update' : 'remove');
|
|
233
|
+
}).finally(() => {
|
|
234
|
+
if (latest()) { this.pendingQty.delete(lineId); this.render(); emit('cart-updated', { cart: this.state, reason: 'settled' }); }
|
|
222
235
|
});
|
|
223
236
|
},
|
|
224
237
|
|
package/src/client.js
CHANGED
|
@@ -39,6 +39,14 @@ export function withContext(query, ctx = cfg) {
|
|
|
39
39
|
|
|
40
40
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
41
41
|
|
|
42
|
+
/** What a status means, for errors Shopify sends without a message (401/403 have none). */
|
|
43
|
+
function statusText(status, conf) {
|
|
44
|
+
if (status === 401 || status === 403) {
|
|
45
|
+
return `Shopify rejected the Storefront token for ${conf.domain} (${status}): use the public Storefront API token (Headless channel)${/^shpat_/.test(conf.token) ? ', not an Admin API token (shpat_…)' : ''}`;
|
|
46
|
+
}
|
|
47
|
+
return status === 404 ? `Store or API version not found (404): check domain ${conf.domain} and apiVersion ${conf.apiVersion}` : `HTTP ${status} from ${conf.domain}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
/**
|
|
43
51
|
* One Storefront API request with retries, for any config object (the browser's shared config,
|
|
44
52
|
* or the server module's own). gql() below is the browser version.
|
|
@@ -83,7 +91,7 @@ export async function request(conf, query, variables = {}, { context = true } =
|
|
|
83
91
|
|
|
84
92
|
let json;
|
|
85
93
|
try { json = await res.json(); } catch {
|
|
86
|
-
throw new TetherError(`Invalid response (${res.status})
|
|
94
|
+
throw new TetherError(res.ok ? `Invalid response (${res.status})` : statusText(res.status, conf), { status: res.status });
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
if (json.errors?.length) {
|
|
@@ -99,11 +107,12 @@ export async function request(conf, query, variables = {}, { context = true } =
|
|
|
99
107
|
}
|
|
100
108
|
const throttled = json.errors.some((e) => e.extensions?.code === 'THROTTLED');
|
|
101
109
|
if (throttled && attempt++ < conf.retries) { await sleep(500 * 2 ** attempt); continue; }
|
|
102
|
-
|
|
110
|
+
const message = json.errors.map((e) => e.message).filter(Boolean).join(' | ');
|
|
111
|
+
throw new TetherError(res.ok ? message || 'Storefront API error' : (res.status === 401 || res.status === 403 || !message ? statusText(res.status, conf) : message), {
|
|
103
112
|
status: res.status, errors: json.errors,
|
|
104
113
|
});
|
|
105
114
|
}
|
|
106
|
-
if (!res.ok) throw new TetherError(
|
|
115
|
+
if (!res.ok) throw new TetherError(statusText(res.status, conf), { status: res.status });
|
|
107
116
|
|
|
108
117
|
return json.data;
|
|
109
118
|
}
|
package/src/collection.js
CHANGED
|
@@ -38,7 +38,11 @@ export class ListScope {
|
|
|
38
38
|
reverse: get(r, 'reverse') === 'true',
|
|
39
39
|
};
|
|
40
40
|
if (has(r, 'collection')) opts.collection = get(r, 'collection');
|
|
41
|
-
else
|
|
41
|
+
else {
|
|
42
|
+
// url:q = the search in the page's ?q= (a results page on a static site).
|
|
43
|
+
const raw = get(r, 'products') || '';
|
|
44
|
+
opts.query = raw.startsWith('url:') ? new URL(window.location.href).searchParams.get(raw.slice(4)) || '' : raw;
|
|
45
|
+
}
|
|
42
46
|
return opts;
|
|
43
47
|
}
|
|
44
48
|
|
package/src/config.js
CHANGED
|
@@ -78,7 +78,7 @@ export function log(...a) { if (cfg.debug) console.log(`[${cfg.prefix}]`, ...a);
|
|
|
78
78
|
export function warn(...a) { console.warn(`[${cfg.prefix}]`, ...a); }
|
|
79
79
|
|
|
80
80
|
export function reportError(err, context) {
|
|
81
|
-
warn(context, err && err.message ? err.message : err);
|
|
81
|
+
warn(context, err && (err.message || err.code) ? err.message || err.code : err);
|
|
82
82
|
if (typeof cfg.onError === 'function') {
|
|
83
83
|
try { cfg.onError(err, context); } catch { /* never let a hook break us */ }
|
|
84
84
|
}
|
package/src/diagnose.js
CHANGED
|
@@ -16,6 +16,51 @@ import { detectMarket } from './markets.js';
|
|
|
16
16
|
|
|
17
17
|
export { apiVersionStatus };
|
|
18
18
|
|
|
19
|
+
// Everything Tether reads, to tell valid markup from typos.
|
|
20
|
+
const SHOW = new Set([
|
|
21
|
+
'in-stock', 'out-of-stock', 'backorder', 'low-stock', 'plenty-in-stock', 'on-sale', 'not-on-sale', 'has-variants', 'single-variant',
|
|
22
|
+
'subscription-available', 'subscription-unavailable', 'subscription-only', 'one-time-available',
|
|
23
|
+
'subscription-selected', 'one-time-selected', 'has-savings', 'loading', 'ready', 'error', 'not-found',
|
|
24
|
+
'cart-empty', 'cart-filled', 'cart-has-subscription', 'cart-no-subscription', 'cart-has-discount', 'cart-error', 'cart-warning',
|
|
25
|
+
'list-empty', 'list-filled', 'search-empty', 'search-results', 'logged-in', 'logged-out', 'orders-empty', 'orders-loading',
|
|
26
|
+
]);
|
|
27
|
+
const BIND = new Set([
|
|
28
|
+
'title', 'description', 'description-text', 'vendor', 'type', 'handle', 'published-at', 'price', 'total-price',
|
|
29
|
+
'compare-at-price', 'unit-price', 'min-price', 'max-price', 'discount-percentage', 'discount-amount', 'variant-title',
|
|
30
|
+
'variant-id', 'sku', 'barcode', 'stock', 'quantity', 'featured-image', 'variant-image', 'images', 'tags', 'link', 'url',
|
|
31
|
+
'one-time-price', 'subscription-price', 'subscription-savings', 'subscription-savings-amount', 'checkout-charge',
|
|
32
|
+
'plan-name', 'plan-description', 'plan-group', 'option-value', 'swatch', 'image',
|
|
33
|
+
]);
|
|
34
|
+
const ATTRS = new Set(`account account-bind account-link account-login account-logout add-label add-to-cart adding-text
|
|
35
|
+
always-enabled bind cart-checkout-charge cart-clear cart-close cart-count cart-discounts cart-drawer cart-label cart-lines
|
|
36
|
+
cart-note cart-open cart-subtotal cart-total cart-warning checkout code collection collection-bind countries country
|
|
37
|
+
country-bind country-label country-select default-plan default-purchase discount discount-code discount-form discount-input
|
|
38
|
+
discount-remove display error group group-add hide-single language limit line line-attribute line-attributes line-id
|
|
39
|
+
line-minus line-plan line-plus line-remove line-show list load-more market-pending one-time-label option option-list
|
|
40
|
+
option-selected option-value order-bind order-line order-lines orders page-country page-language page-locale page-title plan plan-bind plan-list
|
|
41
|
+
plan-value product products purchase purchase-mode qty qty-minus qty-plus quantity recommendations reverse search
|
|
42
|
+
search-bind search-collections search-input search-limit search-min search-products search-queries search-results
|
|
43
|
+
search-state show soldout-text sort state subscribe-text variant variant-select variant-url max-width max-height crop
|
|
44
|
+
sig label hydrated busy`.split(/\s+/).filter(Boolean));
|
|
45
|
+
|
|
46
|
+
/** Levenshtein distance, for "did you mean". */
|
|
47
|
+
function distance(a, b) {
|
|
48
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
49
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
50
|
+
for (let i = 1; i <= a.length; i++) {
|
|
51
|
+
for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
52
|
+
}
|
|
53
|
+
return d[a.length][b.length];
|
|
54
|
+
}
|
|
55
|
+
export function didYouMean(value, options, prefix = '') {
|
|
56
|
+
let best = null; let score = Infinity;
|
|
57
|
+
for (const o of options) {
|
|
58
|
+
const n = distance(String(value), o);
|
|
59
|
+
if (n < score) { score = n; best = o; }
|
|
60
|
+
}
|
|
61
|
+
return best && score <= Math.max(2, Math.floor(String(value).length / 4)) ? ` Did you mean "${prefix}${best}"?` : '';
|
|
62
|
+
}
|
|
63
|
+
|
|
19
64
|
const mask = (t) => (t ? `${String(t).slice(0, 4)}…${String(t).slice(-4)} (${String(t).length} chars)` : '(missing)');
|
|
20
65
|
|
|
21
66
|
function describe(el) {
|
|
@@ -51,27 +96,38 @@ export function checkMarkup(root = document) {
|
|
|
51
96
|
if (!el.closest(sel('group'))) add('warn', `${attr('group-add')} outside a ${attr('group')}.`, el);
|
|
52
97
|
});
|
|
53
98
|
|
|
54
|
-
[
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
99
|
+
['option-list', 'plan-list', 'cart-lines', 'list'].forEach((name) => qsa(root, sel(name)).forEach((el) => {
|
|
100
|
+
if (!el.firstElementChild && !hasTemplate(el)) {
|
|
101
|
+
add('warn', `${attr(name)} without a template child: put one example element inside it.`, el);
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
60
104
|
|
|
61
105
|
qsa(root, listSel).forEach((el) => {
|
|
62
106
|
if (!qsa(el, sel('list')).length) add('error', `List without a ${attr('list')} child: products have nowhere to render.`, el);
|
|
63
107
|
});
|
|
64
108
|
|
|
65
|
-
const known = new Set([
|
|
66
|
-
'in-stock', 'out-of-stock', 'backorder', 'low-stock', 'plenty-in-stock', 'on-sale', 'not-on-sale', 'has-variants', 'single-variant',
|
|
67
|
-
'subscription-available', 'subscription-unavailable', 'subscription-only', 'one-time-available',
|
|
68
|
-
'subscription-selected', 'one-time-selected', 'has-savings', 'loading', 'ready', 'error', 'not-found',
|
|
69
|
-
'cart-empty', 'cart-filled', 'cart-has-subscription', 'cart-no-subscription', 'cart-has-discount', 'cart-error', 'cart-warning',
|
|
70
|
-
'list-empty', 'list-filled',
|
|
71
|
-
]);
|
|
72
109
|
qsa(root, sel('show')).forEach((el) => {
|
|
73
110
|
const k = get(el, 'show');
|
|
74
|
-
if (!
|
|
111
|
+
if (!SHOW.has(k)) add('warn', `Unknown ${attr('show')}="${k}".${didYouMean(k, SHOW)}`, el);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
qsa(root, sel('bind')).forEach((el) => {
|
|
115
|
+
const k = get(el, 'bind') || '';
|
|
116
|
+
if (!k || BIND.has(k) || /^image:\d+$/.test(k) || k.startsWith('metafield:')) return;
|
|
117
|
+
add('warn', `Unknown ${attr('bind')}="${k}": it stays empty.${didYouMean(k, BIND)}`, el);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Typos in attribute names (tether-bnd, tether-add-to-card) do nothing, silently.
|
|
121
|
+
const prefix = `${cfg.prefix}-`;
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
qsa(root, '*').forEach((el) => {
|
|
124
|
+
for (const a of el.attributes) {
|
|
125
|
+
if (!a.name.startsWith(prefix)) continue;
|
|
126
|
+
const name = a.name.slice(prefix.length);
|
|
127
|
+
if (ATTRS.has(name) || seen.has(name)) continue;
|
|
128
|
+
seen.add(name);
|
|
129
|
+
add('warn', `Unknown attribute ${a.name}: Tether ignores it.${didYouMean(name, ATTRS, prefix)}`, el);
|
|
130
|
+
}
|
|
75
131
|
});
|
|
76
132
|
|
|
77
133
|
qsa(root, sel('bind')).forEach((el) => {
|
|
@@ -126,10 +182,7 @@ export async function diagnose({ root = document, silent = false, version, now =
|
|
|
126
182
|
report.reachable = true;
|
|
127
183
|
report.shop = { name: data.shop?.name, url: data.shop?.primaryDomain?.url, currency: data.shop?.paymentSettings?.currencyCode };
|
|
128
184
|
} catch (err) {
|
|
129
|
-
|
|
130
|
-
? 'Token rejected: use the public Storefront token from the Headless channel, not an Admin token.'
|
|
131
|
-
: err.status === 404 ? 'Store or API version not found: check domain (x.myshopify.com) and apiVersion.' : '';
|
|
132
|
-
report.problems.push({ level: 'error', message: `Storefront API not reachable: ${err.message}${hint ? `. ${hint}` : ''}` });
|
|
185
|
+
report.problems.push({ level: 'error', message: `Storefront API not reachable: ${err.message || err.code || 'unknown error'}` });
|
|
133
186
|
}
|
|
134
187
|
report.latencyMs = Date.now() - t0;
|
|
135
188
|
|
|
@@ -146,13 +199,19 @@ export async function diagnose({ root = document, silent = false, version, now =
|
|
|
146
199
|
qsa(root, sel('product')).filter((el) => resolveProductKey(el).key === keys[i] && el.querySelector(sel('add-to-cart'))).forEach((el) => {
|
|
147
200
|
const shown = new Set(qsa(el, `${sel('option-list')},select${sel('option')}`).map((x) => get(x, 'option-list') || get(x, 'option')));
|
|
148
201
|
if (!shown.size && qsa(el, `select${sel('variant-select')}`).length) return;
|
|
202
|
+
const names = (p.options || []).map((o) => o.name);
|
|
203
|
+
if (!cfg.language) {
|
|
204
|
+
[...shown].filter((n) => n && !names.includes(n)).forEach((n) => {
|
|
205
|
+
report.problems.push({ level: 'warn', message: `Product "${p.handle}" has no option "${n}" (its options: ${names.join(', ') || 'none'}).${didYouMean(n, names)}` });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
149
208
|
(p.options || []).filter((o) => o.optionValues.length > 1 && !shown.has(o.name)).forEach((o) => {
|
|
150
209
|
report.problems.push({ level: 'warn', message: `Product "${p.handle}": option "${o.name}" (${o.optionValues.length} values) has no tether-option-list, so visitors can't choose it.` });
|
|
151
210
|
});
|
|
152
211
|
});
|
|
153
212
|
} else {
|
|
154
213
|
report.products.push({ key: keys[i], error: r.reason?.message });
|
|
155
|
-
report.problems.push({ level: 'error', message: `Product "${keys[i]}": ${r.reason?.message}. Is it published to the Headless channel?` });
|
|
214
|
+
report.problems.push({ level: 'error', message: `Product "${keys[i]}": ${r.reason?.message || r.reason?.code || 'not loaded'}. Is it published to the Headless channel?` });
|
|
156
215
|
}
|
|
157
216
|
});
|
|
158
217
|
}
|
|
@@ -173,7 +232,7 @@ function print(r) {
|
|
|
173
232
|
c.group?.(`[${cfg.prefix}] diagnose: ${errors ? `${errors} error(s)` : 'no errors'}, ${r.problems.length - errors} warning(s)`);
|
|
174
233
|
c.log('version', r.version, '| API', r.apiVersion.version, `(${r.apiVersion.note})`);
|
|
175
234
|
c.log('config', r.config);
|
|
176
|
-
if (r.market) c.log('market
|
|
235
|
+
if (r.market) c.log('market', `in use: ${[r.market.country, r.market.language].filter(Boolean).join(' / ') || 'the store default'}`, r.market.detected ? `(detection picks ${JSON.stringify(r.market.detected)})` : '');
|
|
177
236
|
c.log(r.reachable ? `Storefront API reachable: ${r.shop?.name} (${r.latencyMs} ms)` : 'Storefront API NOT reachable');
|
|
178
237
|
if (r.products.length) (c.table || c.log)(r.products);
|
|
179
238
|
if (r.sellingPlans.length) (c.table || c.log)(r.sellingPlans);
|
package/src/dom.js
CHANGED
|
@@ -76,7 +76,8 @@ export function imageUrl(url, { width, height, crop } = {}) {
|
|
|
76
76
|
|
|
77
77
|
export function cdnUrl(url, el) {
|
|
78
78
|
if (!url || !el) return url;
|
|
79
|
-
|
|
79
|
+
// max-width (Webflow's custom attribute), or valid HTML: tether-max-width / data-max-width.
|
|
80
|
+
const g = (k) => el.getAttribute(k) ?? el.getAttribute(`${cfg.prefix}-${k}`) ?? el.getAttribute(`data-${k}`);
|
|
80
81
|
return imageUrl(url, { width: g('max-width'), height: g('max-height'), crop: g('max-width') || g('max-height') ? g('crop') : null });
|
|
81
82
|
}
|
|
82
83
|
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { attr, sel, get, qsa, emit, formatMoney, busy, parseJSONAttr, toAttribut
|
|
|
21
21
|
import { loadPart, coreModules } from './lazy.js';
|
|
22
22
|
import { mountedLists, liveLists, pruneLive, within, mountProduct, retryFailed, hydrateFromPage, settleMarket, trackMarket } from './mounting.js';
|
|
23
23
|
|
|
24
|
-
export const VERSION = '0.6.
|
|
24
|
+
export const VERSION = '0.6.3';
|
|
25
25
|
|
|
26
26
|
let initPromise = null;
|
|
27
27
|
let observer = null;
|
package/src/server.js
CHANGED
|
@@ -156,12 +156,13 @@ ${fragment()}`, byId ? { id: key, intent } : { handle: key, intent });
|
|
|
156
156
|
*/
|
|
157
157
|
/**
|
|
158
158
|
* { card: true } for lists (home, collections, recommendations): about 40% smaller, without
|
|
159
|
-
* descriptions,
|
|
160
|
-
* plans per variant stay. The full
|
|
159
|
+
* descriptions, variant images and images after the first two (a hover image, tether-bind="image:2";
|
|
160
|
+
* more with { card: true, images: 3 }). Price, stock and plans per variant stay. The full
|
|
161
|
+
* product comes with the background refresh.
|
|
161
162
|
*/
|
|
162
|
-
function hydrate(products, { card = false } = {}) {
|
|
163
|
+
function hydrate(products, { card = false, images = 2 } = {}) {
|
|
163
164
|
let list = (Array.isArray(products) ? products : [products]).filter(Boolean);
|
|
164
|
-
if (card) list = list.map(slim);
|
|
165
|
+
if (card) list = list.map((p) => slim(p, images));
|
|
165
166
|
const payload = { v: 1, at: new Date().toISOString(), country: conf.country || null, language: conf.language || null, products: list };
|
|
166
167
|
return `<script type="application/json" data-tether-hydrate>${safeJson(payload)}</script>`;
|
|
167
168
|
}
|
|
@@ -220,11 +221,11 @@ ${fragment()}`, byId ? { id: key, intent } : { handle: key, intent });
|
|
|
220
221
|
}
|
|
221
222
|
const jsonLd = (p, opts) => (p ? `<script type="application/ld+json">${safeJson(jsonLdData(p, opts))}</script>` : '');
|
|
222
223
|
|
|
223
|
-
function slim(p) {
|
|
224
|
+
function slim(p, keep = 2) {
|
|
224
225
|
return {
|
|
225
226
|
...p,
|
|
226
227
|
description: '', descriptionHtml: '',
|
|
227
|
-
images: { nodes: (p.images?.nodes || []).slice(0, 1) },
|
|
228
|
+
images: { nodes: (p.images?.nodes || []).slice(0, Math.max(1, keep)) },
|
|
228
229
|
variants: { nodes: (p.variants?.nodes || []).map((v) => ({ ...v, image: null })) },
|
|
229
230
|
};
|
|
230
231
|
}
|
package/types/astro.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ type Serializable<T> = { [K in keyof T as T[K] extends ((...args: any[]) => any)
|
|
|
5
5
|
export interface TetherAstroOptions extends Partial<Serializable<TetherConfig>> {
|
|
6
6
|
/** Add the Tether script to every page (default true). False: import and init it yourself. */
|
|
7
7
|
inject?: boolean;
|
|
8
|
+
/** Files that restart the dev server when they change (default: src/shop.config.js/.mjs/.ts). */
|
|
9
|
+
watch?: string[];
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
package/types/server.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export interface StorefrontOptions extends Partial<Pick<TetherConfig,
|
|
|
15
15
|
fetch?: typeof fetch;
|
|
16
16
|
PUBLIC_SHOPIFY_DOMAIN?: string;
|
|
17
17
|
PUBLIC_SHOPIFY_STOREFRONT_TOKEN?: string;
|
|
18
|
-
|
|
18
|
+
/** Astro's import.meta.env carries these too; ignored. */
|
|
19
|
+
MODE?: string; BASE_URL?: string; PROD?: boolean; DEV?: boolean; SSR?: boolean; SITE?: string; ASSETS_PREFIX?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ShopifyCollection {
|
|
@@ -26,7 +27,8 @@ export interface ShopifyCollection {
|
|
|
26
27
|
|
|
27
28
|
export interface ProductQuery {
|
|
28
29
|
collection?: string;
|
|
29
|
-
query
|
|
30
|
+
/** A Shopify search query; null or '' = every product (e.g. searchParams.get('q')). */
|
|
31
|
+
query?: string | null;
|
|
30
32
|
first?: number;
|
|
31
33
|
after?: string | null;
|
|
32
34
|
sortKey?: string;
|
|
@@ -38,7 +40,7 @@ export interface Storefront {
|
|
|
38
40
|
readonly config: Required<Pick<StorefrontOptions, 'domain' | 'token'>> & StorefrontOptions;
|
|
39
41
|
query<T = any>(query: string, variables?: Record<string, unknown>, opts?: { context?: boolean }): Promise<T>;
|
|
40
42
|
/** By handle, numeric id, gid or product URL. null when it doesn't exist. */
|
|
41
|
-
getProduct(handleOrId: string | number): Promise<ShopifyProduct | null>;
|
|
43
|
+
getProduct(handleOrId: string | number | null | undefined): Promise<ShopifyProduct | null>;
|
|
42
44
|
getProducts(opts?: ProductQuery): Promise<{ collection: ShopifyCollection | null; products: ShopifyProduct[]; pageInfo: { hasNextPage: boolean; endCursor: string | null } }>;
|
|
43
45
|
/** Every product (of a collection or a search query), page by page. For getStaticPaths(). */
|
|
44
46
|
allProducts(opts?: Omit<ProductQuery, 'first' | 'after'>): Promise<ShopifyProduct[]>;
|
|
@@ -51,8 +53,12 @@ export interface Storefront {
|
|
|
51
53
|
* products without its own request, then refreshes prices and stock once in the background.
|
|
52
54
|
* Astro: <Fragment set:html={shop.hydrate(product)} />
|
|
53
55
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
hydrate(products: ShopifyProduct | ShopifyProduct[], opts?: {
|
|
57
|
+
/** For lists: without descriptions and variant images, about 40% smaller. */
|
|
58
|
+
card?: boolean;
|
|
59
|
+
/** Images a card keeps (default 2: the main one and a hover image). */
|
|
60
|
+
images?: number;
|
|
61
|
+
}): string;
|
|
56
62
|
formatMoney(money: Money | null | undefined, locale?: string): string;
|
|
57
63
|
/** 30 -> '30%' (nl, en) / '30 %' (de, fr), the way Tether shows savings. '' for 0. */
|
|
58
64
|
formatPercent(n: number, locale?: string): string;
|
|
@@ -63,7 +69,8 @@ export interface Storefront {
|
|
|
63
69
|
* What Tether shows first for this product (variant, price, purchase mode, tether-show conditions),
|
|
64
70
|
* so the server renders the same and nothing flashes. Pass Astro.url.searchParams.get('variant') in SSR.
|
|
65
71
|
*/
|
|
66
|
-
initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState
|
|
72
|
+
initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState;
|
|
73
|
+
initialState(product: ShopifyProduct | null | undefined, opts?: InitialOptions): InitialState | null;
|
|
67
74
|
/** schema.org Product / ProductGroup data for search engines, as a <script type="application/ld+json"> tag. */
|
|
68
75
|
jsonLd(product: ShopifyProduct | null, opts?: { url?: string; brand?: string }): string;
|
|
69
76
|
jsonLdData(product: ShopifyProduct | null, opts?: { url?: string; brand?: string }): Record<string, unknown> | null;
|
|
@@ -84,12 +91,13 @@ export interface InitialState {
|
|
|
84
91
|
compareAtPrice: Money | null;
|
|
85
92
|
available: boolean;
|
|
86
93
|
/** tether-show conditions: hidden={!state.show['out-of-stock']}. */
|
|
87
|
-
show: Record<'in-stock' | 'out-of-stock' | 'backorder' | 'on-sale' | 'not-on-sale' | 'has-variants' | 'single-variant' | 'low-stock' | 'subscription-available' | 'subscription-unavailable' | 'subscription-only' | 'one-time-available' | 'subscription-selected' | 'one-time-selected', boolean>;
|
|
94
|
+
show: Record<'in-stock' | 'out-of-stock' | 'backorder' | 'on-sale' | 'not-on-sale' | 'has-variants' | 'single-variant' | 'low-stock' | 'subscription-available' | 'subscription-unavailable' | 'subscription-only' | 'one-time-available' | 'subscription-selected' | 'one-time-selected' | 'plenty-in-stock' | 'has-savings', boolean>;
|
|
88
95
|
/** true for option values without an available variant: class is-unavailable. */
|
|
89
96
|
isUnavailable(optionName: string, value: string): boolean;
|
|
90
97
|
}
|
|
91
98
|
export interface InitialOptions { variant?: string | null; sellingPlan?: string | null; defaultPurchase?: 'one-time' | 'subscription' }
|
|
92
|
-
export declare function initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState
|
|
99
|
+
export declare function initialState(product: ShopifyProduct, opts?: InitialOptions): InitialState;
|
|
100
|
+
export declare function initialState(product: ShopifyProduct | null | undefined, opts?: InitialOptions): InitialState | null;
|
|
93
101
|
|
|
94
102
|
export interface ImageSize { width?: number; height?: number; crop?: 'center' | 'top' | 'bottom' | 'left' | 'right' }
|
|
95
103
|
export declare function imageUrl(url: string | null | undefined, size?: ImageSize): string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as u}from"./chunk-BGIKQY6H.js";import"./chunk-ZVUPVSLF.js";import"./chunk-YXCXIPQV.js";import{B as x,D as O,G as F,b as a,e as G,g,h as f,o as p,p as y,s as _,u as S,w as z}from"./chunk-KARKXC6G.js";import"./chunk-VC46IEJQ.js";var N=()=>`${a.prefix}:account:${a.domain}`,L=()=>`${a.prefix}:account-login:${a.domain}`,P=()=>a.account?.remember?window.localStorage:window.sessionStorage,H=t=>btoa(String.fromCharCode(...new Uint8Array(t))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),v=()=>H(crypto.getRandomValues(new Uint8Array(32))),Z=async t=>H(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t))),A=t=>(a.account?.navigate||(e=>window.location.assign(e)))(t),B=(t,e)=>{try{return JSON.parse(t.getItem(e)||"null")}catch{return null}},b=(t,e,n)=>{try{n?t.setItem(e,JSON.stringify(n)):t.removeItem(e)}catch{}},U=null,s=null,D=null,I=null,d=()=>a.account?.server?String(a.account.server).replace(/\/$/,""):"",R=()=>`${a.prefix}:account-hint:${a.domain}`,T=null;async function C(t,{method:e="GET",body:n}={}){let r=await fetch(`${d()}${t}`,{method:e,credentials:"include",headers:e==="POST"?{"Content-Type":"application/json","X-Tether-Account":"1"}:{},body:n?JSON.stringify(n):void 0}),c=await r.json().catch(()=>({}));if(!r.ok&&r.status!==401)throw new f(`Customer accounts: ${c.error||r.status}`,{status:r.status});return c}async function J(){T||(T=await C("/me"));try{T.loggedIn?window.localStorage.setItem(R(),"1"):window.localStorage.removeItem(R())}catch{}return T}async function $(){if(U)return U;let t=a.account?.shopDomain||a.domain,e=c=>fetch(`https://${t}/.well-known/${c}`).then(o=>{if(!o.ok)throw new f(`Customer accounts: ${c} answered ${o.status}`,{status:o.status});return o.json()}),[n,r]=await Promise.all([e("openid-configuration"),e("customer-account-api")]);return U={...n,graphql:r.graphql_api},U}var tt=()=>a.account?.callbackUrl||`${window.location.origin}${window.location.pathname}`;function V(){return d()?!!T?.loggedIn:!!(s?.refresh||s?.access&&s.expiresAt>Date.now())}async function et({returnTo:t=window.location.href,locale:e}={}){if(d()){let h=new URL(`${d()}/login`);h.searchParams.set("returnTo",t);let m=e||a.account.locale||(a.locale||"").split("-")[0];m&&h.searchParams.set("locale",m),a.country&&h.searchParams.set("country",String(a.country).toUpperCase()),A(h.toString());return}if(!a.account?.clientId)throw new f("Customer accounts: set account.clientId",{code:"NOT_CONFIGURED"});let n=await $(),r=v(),c=v(),o=v(),l=tt();b(window.sessionStorage,L(),{state:r,nonce:c,verifier:o,redirect:l,returnTo:t,at:Date.now()});let i=new URL(n.authorization_endpoint),w={client_id:a.account.clientId,scope:"openid email customer-account-api:full",response_type:"code",redirect_uri:l,state:r,nonce:c,code_challenge:await Z(o),code_challenge_method:"S256"},k=e||a.account.locale||(a.locale||"").split("-")[0];k&&(w.locale=k),a.country&&(w.region_country=String(a.country).toUpperCase()),Object.entries(w).forEach(([h,m])=>i.searchParams.set(h,m)),A(i.toString())}async function W(t){let e=await $(),n=await fetch(a.account.tokenUrl||e.token_endpoint,{method:"POST",body:new URLSearchParams(t)}),r=await n.json().catch(()=>({}));if(!n.ok||!r.access_token)throw new f(`Customer accounts: token ${n.status} ${r.error||""}`.trim(),{status:n.status,code:n.status===400||n.status===401?"SESSION_ENDED":"TOKEN_FAILED"});return r}function nt(t){try{return JSON.parse(atob(t.split(".")[1].replace(/-/g,"+").replace(/_/g,"/")))}catch{return null}}function X(t,e=s){s={access:t.access_token,expiresAt:Date.now()+Math.max(0,(Number(t.expires_in)||3600)-120)*1e3,refresh:t.refresh_token||e?.refresh,idToken:t.id_token||e?.idToken},b(P(),N(),s)}async function ot(){let t=new URLSearchParams(window.location.search),e=B(window.sessionStorage,L());if(!e||!t.has("code")&&!t.has("error")||t.get("state")!==e.state)return!1;b(window.sessionStorage,L(),null);let n=new URL(window.location.href);if(["code","state","iss","error","error_description"].forEach(w=>n.searchParams.delete(w)),window.history.replaceState(window.history.state,"",n),t.has("error"))return S("account-error",{error:t.get("error")}),!1;if(Date.now()-e.at>10*6e4)throw new f("Customer accounts: the login took too long, please try again");let r=await W({grant_type:"authorization_code",client_id:a.account.clientId,redirect_uri:e.redirect,code:t.get("code"),code_verifier:e.verifier}),c=await $(),o=nt(r.id_token||""),l=[].concat(o?.aud||[]);if(!o||o.nonce!==e.nonce||o.iss!==c.issuer||!l.includes(a.account.clientId)||o.exp*1e3<Date.now())throw new f("Customer accounts: the login answer did not check out (nonce/issuer/audience)");X(r,null),await Y(),S("account-login",{});let i=e.returnTo&&new URL(e.returnTo,window.location.href);return i&&i.origin===window.location.origin&&i.href!==n.href&&A(i.href),!0}async function E(){return d()||(s||(s=B(P(),N())),!s)?null:s.access&&s.expiresAt>Date.now()?s.access:s.refresh?(D||(D=(async()=>{try{X(await W({grant_type:"refresh_token",client_id:a.account.clientId,refresh_token:s.refresh})),await Y()}catch(t){if(t.code==="SESSION_ENDED")q();else throw t}finally{D=null}})()),await D,s?.access||null):(q(),null)}function q(){s=null,I=null,b(P(),N(),null),u.customerAccessToken=null}async function Y(){u.customerAccessToken=s?.access||null,u.id()&&s?.access&&await u.setBuyerIdentity({customerAccessToken:s.access},{create:!1}).catch(t=>g(t,"account cart"))}async function Q(t,e={}){if(d())throw new f("Customer accounts in server mode only answer customer() and orders()",{code:"NOT_ALLOWED"});let n=await E();if(!n)throw new f("Not logged in",{code:"NOT_LOGGED_IN"});let r=await $(),c=i=>fetch(r.graphql,{method:"POST",headers:{"Content-Type":"application/json",Authorization:i},body:JSON.stringify({query:t,variables:e})}),o=await c(n);o.status===401&&s?.refresh&&(s.expiresAt=0,o=await c(await E()));let l=await o.json().catch(()=>({}));if(!o.ok||l.errors?.length)throw new f(l.errors?.map(i=>i.message).join(" | ")||`Customer accounts: ${o.status}`,{status:o.status});return l.data}async function rt(){if(d()){let e=await J();return e.loggedIn?{...e.customer,emailAddress:{emailAddress:e.customer.email},defaultAddress:{formatted:e.customer.address}}:null}return I||(I=(await Q("query TetherCustomer{customer{id firstName lastName displayName emailAddress{emailAddress}defaultAddress{formatted}}}")).customer,I)}async function at({first:t=10}={}){return d()?(await C(`/orders?first=${t}`)).orders||[]:(await Q("query TetherOrders($first:Int!){customer{orders(first:$first,sortKey:PROCESSED_AT,reverse:true){nodes{id name processedAt financialStatus fulfillmentStatus statusPageUrl totalPrice{amount currencyCode}lineItems(first:20){nodes{title variantTitle quantity image{url altText}}}}}}}",{first:t})).customer?.orders?.nodes||[]}async function ct({returnTo:t=a.account?.logoutUrl||`${window.location.origin}/`}={}){if(d()){let c=await C("/logout",{method:"POST",body:{returnTo:t,cartId:u.id()}});T={loggedIn:!1};try{window.localStorage.removeItem(R())}catch{}S("account-logout",{}),c.logoutUrl?A(c.logoutUrl):j(document);return}let e=s?.idToken||B(P(),N())?.idToken;if(u.id()&&await u.setBuyerIdentity({customerAccessToken:null},{create:!1}).catch(()=>{}),q(),S("account-logout",{}),!e){j(document);return}let n=await $(),r=new URL(n.end_session_endpoint);r.searchParams.set("id_token_hint",e),r.searchParams.set("post_logout_redirect_uri",t),A(r.toString())}var K=t=>{try{return new URL(t).protocol==="https:"}catch{return!1}},st=t=>{try{return new Date(t).toLocaleDateString(a.locale||void 0)}catch{return t}};async function j(t=document){let e=d()?!!(await J().catch(()=>({loggedIn:!1}))).loggedIn:V()||!!await E().catch(()=>null);if(_(t,p("show")).forEach(c=>{let o=y(c,"show");o==="logged-in"&&O(c,e),o==="logged-out"&&O(c,!e)}),!e)return;let n=_(t,p("account-bind"));if(n.length){let c=await rt().catch(o=>(g(o,"account"),null));n.forEach(o=>{let l={"first-name":c?.firstName,"last-name":c?.lastName,name:c?.displayName,email:c?.emailAddress?.emailAddress,address:c?.defaultAddress?.formatted?.join(", ")}[y(o,"account-bind")];l!=null&&x(o,l)})}let r=_(t,p("orders"));if(r.length){let c=await at({first:parseInt(y(r[0],"limit")||"10",10)}).catch(o=>(g(o,"orders"),[]));r.forEach(o=>F(o,c,(l,i)=>{_(l,p("order-bind")).forEach(w=>{let k=y(w,"order-bind");if(k==="url"){K(i.statusPageUrl)&&w.setAttribute("href",i.statusPageUrl);return}let h={name:i.name,date:st(i.processedAt),total:z(i.totalPrice),status:i.financialStatus?.toLowerCase(),fulfillment:i.fulfillmentStatus?.toLowerCase(),items:i.lineItems.nodes.map(m=>`${m.quantity}\xD7 ${m.title}`).join(", ")}[k];h!=null&&x(w,h)}),y(l,"order-bind")==="url"&&K(i.statusPageUrl)&&l.setAttribute("href",i.statusPageUrl)}))}}var M=!1;async function ft(t=document){if(!M){if(M=!0,document.addEventListener("click",e=>{let n=e.target;n.closest?.(p("account-login"))?(e.preventDefault(),et().catch(r=>g(r,"login"))):n.closest?.(p("account-logout"))&&(e.preventDefault(),ct().catch(r=>g(r,"logout")))}),d()){let e=new URL(window.location.href);e.searchParams.get("account")&&(e.searchParams.delete("account"),window.history.replaceState(window.history.state,"",e),S("account-login",{})),u.beforeCheckout=async()=>{if(!(await J()).loggedIn||!u.id())return null;let n=await C("/cart",{method:"POST",body:{cartId:u.id()}}).catch(()=>null);return n?.checkoutUrl?`${n.checkoutUrl}${n.checkoutUrl.includes("?")?"&":"?"}logged_in=true`:null}}else{try{await ot()}catch(e){g(e,"login")}!u.customerAccessToken&&await E().catch(()=>null)&&(u.customerAccessToken=s.access)}G("account",V()?"logged in":"logged out")}await j(t)}export{rt as customer,E as getToken,ot as handleCallback,V as isLoggedIn,et as login,ct as logout,ft as mountAccount,at as orders,Q as query,j as render};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{c as U}from"./chunk-ZVUPVSLF.js";import{b as S,d as $,e as B}from"./chunk-YXCXIPQV.js";import{A as M,B as j,C as m,D as L,G as K,H as N,I as R,M as q,b as o,e as O,g as I,h as _,l as A,m as Q,n as E,o as u,p as g,r as F,s as l,u as h,w as b}from"./chunk-KARKXC6G.js";function G(t,e=1,n={}){let a=typeof t=="object"&&t!==null?t:{variantId:t,quantity:e,...n},r=$.variant(a.merchandiseId||a.variantId||a.id);if(!r)throw new _("add(): variantId is required");let s={merchandiseId:r,quantity:Math.max(1,parseInt(a.quantity??1,10)||1)},c=a.sellingPlanId||a.sellingPlan;c&&(s.sellingPlanId=$.sellingPlan(c));let i=q(a.attributes);return i.length&&(s.attributes=i),s}function V(t,e,n){return`mutation Tether_${t}(${e}){${t}(${n}){cart{...TetherCart}userErrors{field message}warnings{code message target}}}${S()}`}var H='a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])',k=null,W=t=>l(t,H).filter(e=>e.offsetParent!==null||e===document.activeElement);function x(t){l(document,u("cart-open")).forEach(e=>e.setAttribute("aria-expanded",String(t))),l(document,u("cart-drawer")).forEach(e=>{e.hasAttribute("role")||e.setAttribute("role","dialog"),!e.hasAttribute("aria-label")&&!e.hasAttribute("aria-labelledby")&&e.setAttribute("aria-label",g(e,"cart-label")||"Cart"),e.setAttribute("aria-modal",String(t)),e.classList.contains(o.cartDrawerOpenClass)||(e.setAttribute("aria-hidden","true"),o.cartFocus!==!1&&(e.inert=!0))})}var T=t=>/does not exist|not found|invalid id/i.test(t?.message||""),D={state:null,_chain:Promise.resolve(),id(){try{return localStorage.getItem(o.storageKey)}catch{return null}},_setId(t){try{t?localStorage.setItem(o.storageKey,t):localStorage.removeItem(o.storageKey)}catch{}},get lines(){return this.state?.lines?.nodes||[]},get count(){return this.state?.totalQuantity||0},lastError:null,customerAccessToken:null,pendingQty:new Map,warnings:[],loaded:!1,_take(t,e){let n=Q(t,e);return this.warnings=(n.warnings||[]).filter((a,r,s)=>s.findIndex(c=>c.message===a.message)===r).map(a=>({...a,message:typeof o.warningText=="function"&&o.warningText(a)||a.message})),this.warnings.length&&h("cart-warning",{warnings:this.warnings}),n.cart},_queue(t){let e=this._chain.then(t,t).then(n=>(this.lastError&&(this.lastError=null,this.render()),n),n=>{throw this.lastError=n,this.render(),h("cart-error",{error:n}),n});return this._chain=e.catch(()=>{}),e},_commit(t,e){this.state=t,t?.id&&this._setId(t.id);try{localStorage.setItem(`${o.storageKey}:seen`,`${Date.now()}|${t?.totalQuantity||0}`)}catch{}return this.render(),h("cart-updated",{cart:t,reason:e}),t},async init(){let t=this.id();if(!t)return this.state=null,this.loaded=!0,this.render(),h("cart-updated",{cart:null,reason:"load"}),null;if(!this.loaded)try{let e=(localStorage.getItem(`${o.storageKey}:seen`)||"").split("|")[1];e&&l(document,u("cart-count")).forEach(n=>{/^\s*0?\s*$/.test(n.textContent)&&(n.textContent=e)})}catch{}try{let e=await A(`query TetherCart($id:ID!){cart(id:$id){...TetherCart}}${S()}`,{id:t});e.cart?this.state=e.cart:(this._setId(null),this.state=null)}catch(e){I(e,"cart load failed"),this.lastError=e,T(e)&&this._setId(null),this.state=null}return this.loaded=!0,this.render(),h("cart-updated",{cart:this.state,reason:"load"}),this.state},async _create(t={}){let e=o.country||this.customerAccessToken?{...o.country?{countryCode:String(o.country).toUpperCase()}:{},...this.customerAccessToken?{customerAccessToken:this.customerAccessToken}:{}}:void 0,n=await A(V("cartCreate","$input: CartInput!","input: $input"),{input:{buyerIdentity:e,...t}});return this._take(n.cartCreate,"cartCreate")},async _mutate(t,e,n,a,r){let s=this.id();if(!s)return r?this._create(r):null;try{let c=await A(V(t,`$cartId: ID!, ${e}`,`cartId: $cartId, ${n}`),{...a,cartId:s});return this._take(c[t],t)}catch(c){if(r&&T(c))return O("cart expired, creating a new one"),this._setId(null),this._create(r);throw c}},add(t,e=1,n={}){let a=(Array.isArray(t)?t:[t]).map(r=>G(r,e,n));return this._queue(async()=>{let r=await this._mutate("cartLinesAdd","$lines: [CartLineInput!]!","lines: $lines",{lines:a},{lines:a});return this._commit(r,"add"),h("added",{cart:r,lines:a}),o.openCartOnAdd&&this.open(),r})},updateLine(t,e){let n=typeof e=="number"?{quantity:e}:{...e};if(n.quantity!==void 0&&n.quantity<=0)return this.removeLine(t);let a={id:t};return n.quantity!==void 0&&(a.quantity=n.quantity),"sellingPlanId"in n&&(a.sellingPlanId=n.sellingPlanId?$.sellingPlan(n.sellingPlanId):null),n.attributes&&(a.attributes=q(n.attributes)),(n.merchandiseId||n.variantId)&&(a.merchandiseId=$.variant(n.merchandiseId||n.variantId)),this._queue(async()=>this._commit(await this._mutate("cartLinesUpdate","$lines: [CartLineUpdateInput!]!","lines: $lines",{lines:[a]}),"update"))},changeQuantity(t,e){let n=this.lines.find(s=>s.id===t);if(!n)return Promise.resolve(this.state);let a=Math.max(0,(this.pendingQty.get(t)?.qty??n.quantity)+e),r={qty:a,done:!1};return this.pendingQty.set(t,r),this.render(),h("cart-updated",{cart:this.state,reason:"pending"}),this.updateLine(t,a).finally(()=>{this.pendingQty.get(t)===r&&(this.pendingQty.delete(t),this.render(),h("cart-updated",{cart:this.state,reason:"settled"}))})},quantityOf(t){return this.pendingQty.get(t.id)?.qty??t.quantity},removeLine(t){let e=Array.isArray(t)?t:[t];return this._queue(async()=>this._commit(await this._mutate("cartLinesRemove","$lineIds: [ID!]!","lineIds: $lineIds",{lineIds:e}),"remove"))},clear(){let t=this.lines.map(e=>e.id);return t.length?this.removeLine(t):Promise.resolve(this.state)},setNote(t){return this._queue(async()=>this._commit(await this._mutate("cartNoteUpdate","$note: String!","note: $note",{note:t??""},{note:t??""}),"note"))},setAttributes(t){let e=new Map((this.state?.attributes||[]).map(a=>[a.key,a.value]));q(t).forEach(a=>e.set(a.key,a.value));let n=[...e].filter(([,a])=>a!=="").map(([a,r])=>({key:a,value:r}));return this._queue(async()=>this._commit(await this._mutate("cartAttributesUpdate","$attributes: [AttributeInput!]!","attributes: $attributes",{attributes:n},{attributes:n}),"attributes"))},setDiscountCodes(t){let e=(Array.isArray(t)?t:[t]).map(n=>String(n).trim()).filter(Boolean);return this._queue(async()=>this._commit(await this._mutate("cartDiscountCodesUpdate","$discountCodes: [String!]!","discountCodes: $discountCodes",{discountCodes:e},{discountCodes:e}),"discount"))},async applyDiscount(t){let e=(this.state?.discountCodes||[]).map(r=>r.code),n=await this.setDiscountCodes([...new Set([...e,t])]);if((n?.discountCodes||[]).find(r=>r.code.toLowerCase()===String(t).toLowerCase())?.applicable)return n;throw await this.removeDiscount(t).catch(()=>{}),new _(`Discount code not valid: ${t}`,{code:"INVALID_DISCOUNT"})},removeDiscount(t){let e=String(t).toLowerCase();return this.setDiscountCodes((this.state?.discountCodes||[]).map(n=>n.code).filter(n=>n.toLowerCase()!==e))},setBuyerIdentity(t,{create:e=!0}={}){return this._queue(async()=>{let n=await this._mutate("cartBuyerIdentityUpdate","$buyerIdentity: CartBuyerIdentityInput!","buyerIdentity: $buyerIdentity",{buyerIdentity:t},e?{buyerIdentity:t}:null).catch(a=>{if(e||!T(a))throw a;return this._setId(null),null});return this._commit(n,"buyer-identity")})},async checkout(t=o.checkoutTarget){let e=this.id();if(!e||this.state&&!this.count)return null;let n=t==="_blank"?window.open("","_blank"):null;if(n)try{n.opener=null}catch{}try{let a=await A("query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }",{id:e}),r=this.beforeCheckout&&await this.beforeCheckout().catch(()=>null)||a.cart?.checkoutUrl;if(r&&this.customerAccessToken&&(r+=`${r.includes("?")?"&":"?"}logged_in=true`),!r)throw new _("No checkoutUrl (cart empty or expired)");return h("checkout",{url:r,cart:this.state}),n&&!n.closed?n.location.href=r:window.location.href=r,r}catch(a){return n&&n.close(),I(a,"checkout failed"),null}},drawers(){return l(document,u("cart-drawer"))},_open:!1,isOpen(){let t=this.drawers();return t.length?t.some(e=>e.classList.contains(o.cartDrawerOpenClass)):this._open},open(){if(this.isOpen())return;this._open=!0;let t=this.drawers().filter(e=>g(e,"cart-drawer")!=="manual");t.forEach(e=>{e.inert=!1,e.classList.add(o.cartDrawerOpenClass),e.setAttribute("aria-hidden","false")}),x(!0),o.cartFocus!==!1&&t[0]&&(k=document.activeElement,setTimeout(()=>{let e=W(t[0])[0]||t[0];e===t[0]&&!t[0].hasAttribute("tabindex")&&(t[0].tabIndex=-1),e.focus?.({preventScroll:!0})},60)),typeof o.onCartOpen=="function"&&o.onCartOpen(t),h("cart-toggled",{open:!0})},close(){if(!this.isOpen())return;this._open=!1;let t=this.drawers(),e=t.some(n=>n.contains(document.activeElement));t.forEach(n=>{n.classList.remove(o.cartDrawerOpenClass),n.setAttribute("aria-hidden","true"),o.cartFocus!==!1&&(n.inert=!0)}),x(!1),e&&k?.isConnected&&k.focus({preventScroll:!0}),k=null,typeof o.onCartClose=="function"&&o.onCartClose(t),h("cart-toggled",{open:!1})},toggle(){this.isOpen()?this.close():this.open()},subscribe(t){let e=()=>t(this.state,{open:this.isOpen(),count:this.count,loaded:this.loaded,warnings:this.warnings}),n=()=>e();return["cart-updated","cart-toggled"].forEach(a=>document.addEventListener(`${o.prefix}:${a}`,n)),e(),()=>["cart-updated","cart-toggled"].forEach(a=>document.removeEventListener(`${o.prefix}:${a}`,n))},render(){let t=this.state,e=t?t.totalQuantity:0,n=s=>t?b(t.cost?.[s]):"";x(this.isOpen()),l(document,u("cart-count")).forEach(s=>m(s,e)),l(document,u("cart-subtotal")).forEach(s=>m(s,n("subtotalAmount"))),l(document,u("cart-total")).forEach(s=>m(s,n("totalAmount"))),l(document,u("cart-checkout-charge")).forEach(s=>m(s,n("checkoutChargeAmount")));let a=this.lines.some(s=>s.sellingPlanAllocation),r={"cart-empty":e===0,"cart-filled":e>0,"cart-has-subscription":a,"cart-no-subscription":!a,"cart-has-discount":(t?.discountCodes||[]).some(s=>s.applicable),"cart-error":!!this.lastError,"cart-warning":this.warnings.length>0};l(document,u("cart-warning")).forEach(s=>m(s,[...new Set(this.warnings.map(c=>c.message))].join(" "))),l(document,u("show")).forEach(s=>{let c=g(s,"show");c in r&&!s.closest(u("product"))&&L(s,r[c])}),l(document,u("checkout")).forEach(s=>{s.classList.toggle("is-disabled",!e),s.setAttribute("aria-disabled",String(!e))}),l(document,`textarea${u("cart-note")},input${u("cart-note")}`).forEach(s=>{document.activeElement!==s&&(s.value=t?.note||"")}),l(document,u("cart-discounts")).forEach(s=>{K(s,t?.discountCodes||[],(c,i)=>{c.setAttribute(E("discount-code"),i.code);let d=c.querySelector(u("discount","code"))||(c.firstElementChild?null:c);d&&j(d,i.code),c.classList.toggle("is-inapplicable",!i.applicable)})}),l(document,u("cart-lines")).forEach(s=>{N(s,this.lines,c=>c.id,(c,i)=>J(c,i),{onEnter:o.onLineEnter,onLeave:o.onLineLeave})}),t&&R("cart")}};function z(t,e){let n=t.merchandise||{},a=t.sellingPlanAllocation?.sellingPlan;switch(e){case"title":return n.product?.title;case"product-title":return n.product?.title;case"variant-title":return n.title==="Default Title"?"":n.title;case"options":return(n.selectedOptions||[]).filter(r=>r.value!=="Default Title").map(r=>`${r.name}: ${r.value}`).join(", ");case"vendor":return n.product?.vendor;case"sku":return n.sku;case"price":return b(t.cost?.amountPerQuantity||n.price);case"compare-at-price":return b(t.cost?.compareAtAmountPerQuantity||n.compareAtPrice);case"line-total":return b(t.cost?.totalAmount);case"line-subtotal":return b(t.cost?.subtotalAmount);case"quantity":return D.quantityOf(t);case"plan":return a?.name||"";case"plan-description":return a?.description||"";case"components":return(t.lineComponents||[]).map(r=>`${r.quantity} \xD7 ${r.merchandise?.product?.title||""}`).join(", ");default:if(e.startsWith("attribute:")){let r=e.slice(10);return(t.attributes||[]).find(s=>s.key===r)?.value||""}return}}function J(t,e){let n=e.merchandise||{},a=!!e.sellingPlanAllocation;t.setAttribute(E("line-id"),e.id),t.classList.toggle("is-subscription",a),l(t,u("line")).forEach(i=>{let d=g(i,"line");if(d==="image")return M(i,n.image?.url||n.product?.featuredImage?.url,n.product?.title);if(d==="url"){let y=F(n.product?.handle,n.product?.id),v=new URLSearchParams;n.id&&v.set("variant",String(n.id).split("/").pop());let w=e.sellingPlanAllocation?.sellingPlan?.id;w&&v.set("selling_plan",w.split("/").pop());let C=v.toString();return i.setAttribute("href",C?`${y}${y.includes("?")?"&":"?"}${C}`:y)}let p=z(e,d);p!==void 0&&m(i,p)});let r=D.warnings.find(i=>i.target===e.id);t.classList.toggle("is-warning",!!r);let s=D.pendingQty.has(e.id);t.classList.toggle("is-busy",s),s?t.setAttribute("aria-busy","true"):t.removeAttribute("aria-busy"),l(t,u("line","warning")).forEach(i=>m(i,r?.message||""));let c={subscription:a,"one-time":!a,"on-sale":parseFloat(e.cost?.compareAtAmountPerQuantity?.amount)>parseFloat(e.cost?.amountPerQuantity?.amount),"has-components":(e.lineComponents||[]).length>0,warning:!!r};l(t,u("line-show")).forEach(i=>{let d=g(i,"line-show");d in c&&L(i,c[d])}),l(t,`select${u("line-plan")}`).forEach(i=>{let d=n.product?.handle;d&&U(d).then(p=>{let y=B(p),w=[...!p.requiresSellingPlan&&g(i,"line-plan")!=="subscription-only"?[{value:"",label:i.getAttribute(E("one-time-label"))||"One-time purchase"}]:[],...y.map(f=>({value:f.id,label:f.name}))],C=w.map(f=>f.value).join("|");i.dataset.tetherSig!==C&&(i.innerHTML="",w.forEach(f=>{let P=document.createElement("option");P.value=f.value,P.textContent=f.label,i.appendChild(P)}),i.dataset.tetherSig=C),i.value=e.sellingPlanAllocation?.sellingPlan?.id||"",L(i,y.length>0)}).catch(p=>I(p,"line plan options"))})}export{W as a,D as b};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as E,b as K,c as z}from"./chunk-6R752OYZ.js";import{a as g,c as k,f as p}from"./chunk-4UR45XTC.js";import{c as L,e as A,f as F,g as O,h as q,i as D}from"./chunk-ZVUPVSLF.js";import{A as R,B as C,D as x,G as U,I as W,K as J,b as h,e as T,f as b,g as m,h as I,n as l,o as a,p as c,q as S,s as N,t as M,u as f}from"./chunk-KARKXC6G.js";var v=()=>`${a("collection")},${a("products")},${a("recommendations")}`,B=class{constructor(e){this.root=e,this.products=[],this.pageInfo=null,this.collection=null,p(e,"loading"),e.addEventListener("click",n=>{let o=n.target.closest?.(a("load-more"));o&&o.closest(v())===e&&(n.preventDefault(),J(o,()=>this.load(!0)))})}$(e){return M(this.root,e,v())}options(){let e=this.root,n={first:parseInt(c(e,"limit")||"",10)||void 0,sortKey:c(e,"sort")||void 0,reverse:c(e,"reverse")==="true"};return S(e,"collection")?n.collection=c(e,"collection"):n.query=c(e,"products")||"",n}async load(e=!1){let n=this.loadToken=(this.loadToken||0)+1,o=()=>n!==this.loadToken;try{if(S(this.root,"recommendations")){let{key:r}=g(this.root,window.location,"recommendations"),s=this.root.closest(a("product"));if(!r&&s&&({key:r}=g(s)),!r)throw new I("No product for recommendations",{code:"NOT_FOUND"});let u=await D(r,c(this.root,"intent")||"RELATED");if(o())return;let i=parseInt(c(this.root,"limit")||"",10);this.products=i?u.slice(0,i):u}else{let r=await q({...this.options(),after:e?this.pageInfo?.endCursor:null});if(o())return;this.collection=r.collection,this.pageInfo=r.pageInfo,this.products=e?[...this.products,...r.products]:r.products}this.render(),p(this.root,"ready"),f("list-ready",{products:this.products,collection:this.collection},this.root)}catch(r){if(o())return;e||p(this.root,k(r)?"not-found":"error",r),k(r)&&S(this.root,"recommendations")||m(r,"list load failed"),f("error",{error:r,context:"list"},this.root)}}render(){let e=this.collection;this.$(a("collection-bind")).forEach(o=>{switch(c(o,"collection-bind")){case"title":return C(o,e?.title||"");case"description":o.innerHTML=e?.descriptionHtml||"";return;case"image":return R(o,e?.image?.url,e?.image?.altText||e?.title);case"count":return C(o,this.products.length);default:return}}),this.$(a("list")).forEach(o=>{U(o,this.products,(r,s)=>{r.setAttribute(l("product"),s.handle),r.removeAttribute(l("state"))}),f("list-rendered",{container:o},this.root)});let n=this.products.length===0;this.$(a("show")).forEach(o=>{let r=c(o,"show");r==="list-empty"&&x(o,n),r==="list-filled"&&x(o,!n)}),this.$(a("load-more")).forEach(o=>x(o,!!this.pageInfo?.hasNextPage)),W("list")}};var P=new WeakSet,ft=new WeakSet,w=new Set,H=new Set,j=new WeakMap;function _(){w.forEach(t=>{t.isConnected||w.delete(t)}),H.forEach(t=>{t.root.isConnected||H.delete(t)})}function pt(t,e){return[...t.matches?.(e)?[t]:[],...N(t,e)]}function G(t){if(P.has(t))return!1;let{key:e,fromUrl:n,raw:o}=g(t);if(!e)return n?(P.add(t),p(t,"not-found"),f("product-not-found",{key:null,source:o},t)):t.closest(v())||b(`[${l("product")}] without a value. Bind it to the Shopify handle field, or use "url:handle".`,t),!1;P.add(t),w.add(t);let r={};j.set(t,r);let s=()=>j.get(t)===r;E.get(t)?t.setAttribute(l("state"),"updating"):p(t,"loading");let u=L(e);return d?.add(u),u.then(i=>{if(!s())return;let y=E.get(t);y?y.update(i):new z(t,i),V(t,i),h.revalidate!==!1&&F(e)&&X(e)}).catch(i=>{if(!s())return;let y=k(i);p(t,y?"not-found":"error",i),y?(f("product-not-found",{key:e,source:o},t),n?T(`product not found: ${e}`):m(i,`product ${e}`)):(m(i,`product ${e}`),f("error",{error:i,context:"product",key:e},t))}),!0}function V(t,e){let n=c(t,"page-title");!n||typeof document>"u"||(document.title=n.replace(/\{title\}/g,e.title).replace(/\{vendor\}/g,e.vendor||"").replace(/\{type\}/g,e.productType||""))}function ht(){_(),w.forEach(t=>{t.getAttribute(l("state"))!=="error"||t.getAttribute(l("error"))==="not-found"||(P.delete(t),G(t))})}var $=new Set;function X(t){$.size||setTimeout(Y,0),$.add(t)}function Y(){let t=[...$];$=new Set,O(t).forEach((e,n)=>e.then(o=>{w.forEach(r=>{let s=E.get(r);if(!s||!r.isConnected)return;let u=g(r).key;u!==t[n]&&u!==o.handle&&u!==o.id||JSON.stringify(s.product)!==JSON.stringify(o)&&s.update(o)})}).catch(o=>m(o,`refresh ${t[n]}`)))}var Q=new WeakSet,d=null,mt=t=>(d?.add(t),t);function Z(){if(!d)return;let t=[...d];Promise.allSettled(t).then(()=>{!d||t.some(e=>!d.has(e))||(t.length===d.size?(d=null,document.documentElement.classList.remove(`${h.prefix}-market-pending`)):Z())})}function gt(t=document){N(t.ownerDocument||t,'script[type="application/json"][data-tether-hydrate]').forEach(e=>{if(!Q.has(e)){Q.add(e);try{let n=JSON.parse(e.textContent);K(n.products);let o=(r,s)=>String(r||"").toUpperCase()===String(s||"").toUpperCase();o(n.country,h.country)&&o(n.language,h.language)?A(n.products):(T("hydration skipped: other market"),d||(d=new Set,document.documentElement.classList.add(`${h.prefix}-market-pending`)))}catch(n){b("bad data-tether-hydrate",n)}}})}export{v as a,B as b,P as c,ft as d,w as e,H as f,_ as g,pt as h,G as i,ht as j,mt as k,Z as l,gt as m};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var C={domain:"",token:"",apiVersion:"2026-07",country:void 0,language:void 0,locale:void 0,markets:null,prefix:"tether",productPath:"/product",collectionPath:"/collection",metafields:[],imagesFirst:25,variantsFirst:100,sellingPlanGroupsFirst:10,sellingPlansFirst:25,collectionFirst:24,retries:2,revalidate:!0,storageKey:null,cartDrawerOpenClass:"is-open",openCartOnAdd:!0,cartFocus:!0,checkoutTarget:"_self",defaultPurchase:"one-time",variantUrl:!1,maxAge:60,lowStock:5,autoMount:!0,observe:!1,astro:!0,partsUrl:null,formatMoney:null,onLayoutChange:null,onCartOpen:null,onCartClose:null,onLineEnter:null,onLineLeave:null,onValueChange:null,onError:null,debug:!1},n={...C};function G(t={}){return Object.keys(n).forEach(e=>delete n[e]),Object.assign(n,C,t),n.domain&&(n.domain=String(n.domain).replace(/^https?:\/\//,"").replace(/\/.*$/,"")),n.storageKey||(n.storageKey=`${n.prefix}:cart:${n.domain}`),n.apiVersion==="unstable"&&x('apiVersion "unstable" changes without notice. Pin a dated version (e.g. 2026-07).'),n}var H=()=>!!(n.domain&&n.token);function S(...t){n.debug&&console.log(`[${n.prefix}]`,...t)}function x(...t){console.warn(`[${n.prefix}]`,...t)}function J(t,e){if(x(e,t&&t.message?t.message:t),typeof n.onError=="function")try{n.onError(t,e)}catch{}}var d=class extends Error{constructor(e,{status:r,errors:o,userErrors:a,code:u}={}){super(e),this.name="TetherError",this.code=u||(r?`HTTP_${r}`:"ERROR"),this.status=r,this.errors=o,this.userErrors=a}};function I(t=n){return`https://${t.domain}/api/${t.apiVersion}/graphql.json`}function L(t,e=n){let r=[];if(e.country&&r.push(`country: ${String(e.country).toUpperCase()}`),e.language&&r.push(`language: ${String(e.language).toUpperCase().replace("-","_")}`),!r.length||/@inContext/.test(t))return t;let o=`@inContext(${r.join(", ")})`;return/^\s*\{/.test(t)?`query ${o} ${t.trimStart()}`:t.replace(/^(\s*(?:query|mutation)(?:\s+\w+)?\s*(?:\([^)]*\))?)/,`$1 ${o}`)}var T=t=>new Promise(e=>setTimeout(e,t));async function v(t,e,r={},{context:o=!0}={}){if(!t.domain||!t.token)throw new d("Tether is not configured: domain and token are required",{code:"NOT_CONFIGURED"});let a=o?L(e,t):e,u=t.maxAge??60;u>0&&!/^\s*mutation\b/.test(a)&&(a+=` #${Math.floor(Date.now()/(u*1e3))}`);let g=JSON.stringify({query:a,variables:r}),l=0;for(;;){let i;try{i=await(t.fetch||fetch)(I(t),{method:"POST",headers:{"Content-Type":"application/json","X-Shopify-Storefront-Access-Token":t.token},body:g})}catch(h){if(l++<t.retries){await T(300*2**l);continue}throw new d(`Network error: ${h.message}`,{code:"NETWORK"})}if(i.status===430)throw new d("Shopify security rejection (430)",{status:430});if((i.status===429||i.status>=500)&&l++<t.retries){S("retry",i.status,l),await T(300*2**l);continue}let c;try{c=await i.json()}catch{throw new d(`Invalid response (${i.status})`,{status:i.status})}if(c.errors?.length){if(c.errors.some(p=>/quantityAvailable/.test(p.message||""))&&/\bquantityAvailable\b/.test(g))return S("no inventory permission, retrying without quantityAvailable"),v(t,e.replace(/\bquantityAvailable\b/g,""),r,{context:o});if(c.errors.some(p=>/'warnings'/.test(p.message||""))&&/warnings \{ code message target \}/.test(e))return v(t,e.replace(/ warnings \{ code message target \}/g,""),r,{context:o});if(c.errors.some(p=>p.extensions?.code==="THROTTLED")&&l++<t.retries){await T(500*2**l);continue}throw new d(c.errors.map(p=>p.message).join(" | "),{status:i.status,errors:c.errors})}if(!i.ok)throw new d(`HTTP ${i.status}`,{status:i.status});return c.data}}var X=(t,e,r)=>v(n,t,e,r);function z(t,e){if(!t)throw new d(`${e}: empty response`);let r=t.userErrors||[];if(r.length)throw new d(r.map(o=>o.message).join(" | "),{userErrors:r});return t}var b=t=>`${n.prefix}-${t}`,Q=(t,e)=>e===void 0?`[${b(t)}]`:`[${b(t)}="${e}"]`,U=(t,e)=>t.getAttribute(b(e)),Z=(t,e)=>t.hasAttribute(b(e)),F=()=>`data-${n.prefix}-key`;function tt(t,e){let r=n.productPath||"/product";if(!/\{(handle|id)\}/.test(r))return`${r.replace(/\/$/,"")}/${t}`;let o=e?String(e).split("/").pop():"";return r.replace(/\{handle\}/g,encodeURIComponent(t||"")).replace(/\{id\}/g,encodeURIComponent(o))}var P=(t,e)=>t?[...t.querySelectorAll(e)]:[],et=(t,e,r)=>P(t,e).filter(o=>o.closest(r)===t);function D(t,e,r=document){r.dispatchEvent(new CustomEvent(`${n.prefix}:${t}`,{detail:e,bubbles:!0}))}function rt(t,e=n.locale){if(!t)return"";try{return new Intl.NumberFormat(e||void 0,{style:"percent",maximumFractionDigits:0}).format(t/100)}catch{return`${t}%`}}function nt(t){if(!t)return"";if(typeof n.formatMoney=="function")return n.formatMoney(t,n.locale);let e=parseFloat(t.amount);if(Number.isNaN(e))return"";try{return new Intl.NumberFormat(n.locale||void 0,{style:"currency",currency:t.currencyCode}).format(e)}catch{return`${t.currencyCode} ${e.toFixed(2)}`}}function ot(t){if(!t)return"";try{return new Date(t).toLocaleDateString(n.locale||void 0)}catch{return""}}function M(t,{width:e,height:r,crop:o}={}){if(!t)return t;try{let a=new URL(t);return e&&a.searchParams.set("width",String(e)),r&&a.searchParams.set("height",String(r)),o&&a.searchParams.set("crop",o),a.toString()}catch{return t}}function R(t,e){if(!t||!e)return t;let r=o=>e.getAttribute(o);return M(t,{width:r("max-width"),height:r("max-height"),crop:r("max-width")||r("max-height")?r("crop"):null})}var j=(t,e)=>!!(t&&e)&&t.split("?")[0]===e.split("?")[0];function st(t,e,r){if(!e)return;let o=R(e,t);if(t.tagName==="IMG"&&j(t.getAttribute("src"),o)){r!=null&&!t.getAttribute("alt")&&t.setAttribute("alt",r);return}t.tagName==="IMG"?(t.removeAttribute("srcset"),t.removeAttribute("sizes"),t.setAttribute("src",o),r!=null&&t.setAttribute("alt",r),t.getAttribute("loading")||t.setAttribute("loading","lazy")):t.style.backgroundImage=`url("${o}")`}function V(t,e){let r=e==null?"":String(e);t.tagName==="INPUT"||t.tagName==="TEXTAREA"?t.value=r:t.textContent=r}var N=t=>t.tagName==="INPUT"||t.tagName==="TEXTAREA"?t.value:t.textContent;function at(t,e){let r=N(t);V(t,e);let o=N(t);if(r!==o&&t.dataset.tetherSeen&&typeof n.onValueChange=="function")try{n.onValueChange(t,r)}catch{}t.dataset.tetherSeen="1"}function it(t,e){let r="tetherPrevDisplay";if(e){if(t.dataset.tetherShown!==void 0)return;t.dataset.tetherShown="",t.removeAttribute("hidden"),t.dataset[r]!==void 0?(t.style.display=t.dataset[r],delete t.dataset[r]):t.style.display==="none"&&(t.style.display=""),typeof getComputedStyle=="function"&&getComputedStyle(t).display==="none"&&(t.style.display=t.getAttribute(`${n.prefix}-display`)||"block")}else delete t.dataset.tetherShown,t.dataset[r]===void 0&&t.style.display!=="none"&&(t.dataset[r]=t.style.display||""),t.style.display="none"}function ut(t,e){t.classList.toggle("is-selected",e),t.setAttribute("aria-pressed",e?"true":"false"),t.tagName==="INPUT"&&(t.type==="radio"||t.type==="checkbox")&&(t.checked=e);let r=t.querySelector?.('input[type="radio"],input[type="checkbox"]');r&&(r.checked=e)}var A=new WeakMap,ct=t=>!!A.get(t);function O(t){if(!A.has(t)){let e=t.firstElementChild?.cloneNode(!0)??null;A.set(t,e),t.innerHTML=""}return A.get(t)}function lt(t,e,r){let o=O(t);if(!o){e.length&&x("repeater without a template child",t);return}t.innerHTML="",e.forEach((a,u)=>{let g=o.cloneNode(!0);r(g,a,u),t.appendChild(g)})}function pt(t,e,r,o,{onEnter:a,onLeave:u}={}){let g=O(t);if(!g){x("repeater without a template child",t);return}let l=F(),i=`data-${n.prefix}-leaving`,$=()=>[...t.children].filter(s=>s.hasAttribute(l)&&!s.hasAttribute(i)),c=new Map($().map(s=>[s.getAttribute(l),s])),h=[],w=new Set;e.forEach((s,f)=>{let m=String(r(s,f)),y=c.get(m);y?c.delete(m):(y=g.cloneNode(!0),y.setAttribute(l,m),w.add(y)),o(y,s,f,w.has(y)),h.push(y)});let p=h.filter(s=>!w.has(s)),E=$().filter(s=>p.includes(s));if(!(E.length===p.length&&E.every((s,f)=>s===p[f]))){c.forEach(s=>s.remove()),h.forEach(s=>t.appendChild(s));return}h.forEach((s,f)=>{if(!w.has(s))return;let m=h[f-1];if(m&&m.parentNode===t?m.after(s):t.prepend(s),a)try{a(s)}catch{}}),c.forEach(s=>{s.setAttribute(i,""),s.removeAttribute(l),s.style.pointerEvents="none",s.inert=!0;let f=()=>s.remove();if(u)try{u(s,f)}catch{f()}else f()})}var k=null;function K(t){clearTimeout(k),k=setTimeout(()=>{typeof n.onLayoutChange=="function"?n.onLayoutChange(t):window.ScrollTrigger?.refresh&&window.ScrollTrigger.refresh(),D("layout-changed",{reason:t})},120)}function ft(t,e){let r=P(t,"img").filter(u=>!u.complete);if(!r.length)return;let o=r.length,a=()=>{--o===0&&K(`${e}:images`)};r.forEach(u=>{u.addEventListener("load",a,{once:!0}),u.addEventListener("error",a,{once:!0})})}function dt(t,e){return t.getAttribute("aria-busy")==="true"?Promise.resolve():(t.setAttribute("aria-busy","true"),Promise.resolve().then(e).finally(()=>t.removeAttribute("aria-busy")))}function gt(t,e){let r=t&&U(t,e);if(!r)return null;try{return JSON.parse(r)}catch{return x(`invalid JSON in ${b(e)}`,t),null}}function ht(t){return t?Array.isArray(t)?t.map(e=>({key:String(e.key),value:String(e.value??"")})):Object.entries(t).map(([e,r])=>({key:e,value:String(r??"")})):[]}export{C as a,n as b,G as c,H as d,S as e,x as f,J as g,d as h,I as i,L as j,v as k,X as l,z as m,b as n,Q as o,U as p,Z as q,tt as r,P as s,et as t,D as u,rt as v,nt as w,ot as x,M as y,j as z,st as A,V as B,at as C,it as D,ut as E,ct as F,lt as G,pt as H,K as I,ft as J,dt as K,gt as L,ht as M};
|