@occluze/tether 0.3.6 → 0.4.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.
- package/README.md +9 -6
- package/dist/esm/chunks/chunk-GHIUMD5H.js +1 -0
- package/dist/esm/chunks/chunk-KYQ5OGBX.js +1 -0
- package/dist/esm/chunks/chunk-LI3YDMGF.js +4 -0
- package/dist/esm/chunks/diagnose-ZXXXJIQE.js +1 -0
- package/dist/esm/chunks/markets-ui-AG6Y5VUE.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/presets/barba.esm.js +4 -5
- package/dist/tether.diagnose.js +319 -0
- package/dist/tether.diagnose.min.js +1 -0
- package/dist/tether.esm.js +2 -2608
- package/dist/tether.js +416 -780
- package/dist/tether.markets.js +207 -0
- package/dist/tether.markets.min.js +1 -0
- package/dist/tether.min.js +3 -132
- package/dist/tether.min.js.map +4 -4
- package/package.json +29 -11
- package/src/cart.js +32 -0
- package/src/config.js +2 -0
- package/src/index.js +64 -156
- package/src/lazy-iife.js +43 -0
- package/src/lazy.js +9 -0
- package/src/loader.js +1 -1
- package/src/markets-ui.js +81 -0
- package/src/mounting.js +89 -0
- package/src/parts/diagnose.js +3 -0
- package/src/parts/markets.js +3 -0
- package/src/product.js +53 -52
- package/src/status.js +1 -0
- package/types/barba.d.ts +16 -0
- package/types/index.d.ts +332 -0
package/src/product.js
CHANGED
|
@@ -269,14 +269,17 @@ export class ProductScope {
|
|
|
269
269
|
const p = this.product;
|
|
270
270
|
|
|
271
271
|
this.$(sel('bind')).forEach((el) => {
|
|
272
|
-
const key = get(el, 'bind');
|
|
272
|
+
const key = get(el, 'bind') || '';
|
|
273
|
+
const txt = {
|
|
274
|
+
title: p.title,
|
|
275
|
+
'description-text': p.description,
|
|
276
|
+
vendor: p.vendor,
|
|
277
|
+
type: p.productType,
|
|
278
|
+
handle: p.handle,
|
|
279
|
+
}[key];
|
|
280
|
+
if (txt !== undefined) return setText(el, txt);
|
|
273
281
|
switch (key) {
|
|
274
|
-
case 'title': return setText(el, p.title);
|
|
275
282
|
case 'description': el.innerHTML = p.descriptionHtml || ''; return undefined;
|
|
276
|
-
case 'description-text': return setText(el, p.description);
|
|
277
|
-
case 'vendor': return setText(el, p.vendor);
|
|
278
|
-
case 'type': return setText(el, p.productType);
|
|
279
|
-
case 'handle': return setText(el, p.handle);
|
|
280
283
|
case 'published-at': return setText(el, formatDate(p.publishedAt));
|
|
281
284
|
case 'min-price': return setText(el, formatMoney(p.priceRange.minVariantPrice));
|
|
282
285
|
case 'max-price': return setText(el, formatMoney(p.priceRange.maxVariantPrice));
|
|
@@ -297,17 +300,15 @@ export class ProductScope {
|
|
|
297
300
|
}
|
|
298
301
|
default:
|
|
299
302
|
// image:2 = the second product image (falls back to the featured image)
|
|
300
|
-
if (
|
|
301
|
-
const img = p.images.nodes[
|
|
303
|
+
if (/^image:\d+$/.test(key)) {
|
|
304
|
+
const img = p.images.nodes[key.slice(6) - 1] || p.featuredImage;
|
|
302
305
|
return setImage(el, img?.url, img?.altText || p.title);
|
|
303
306
|
}
|
|
304
|
-
if (key
|
|
305
|
-
const mf = metafieldValue(p, key.slice(
|
|
307
|
+
if (key.startsWith('metafield:')) {
|
|
308
|
+
const mf = metafieldValue(p, key.slice(10));
|
|
306
309
|
if (!mf) return setText(el, '');
|
|
307
310
|
if (mf.reference?.image) return setImage(el, mf.reference.image.url, mf.reference.image.altText);
|
|
308
|
-
if (mf.type
|
|
309
|
-
el.textContent = mf.value; el.style.whiteSpace = 'pre-line'; return undefined;
|
|
310
|
-
}
|
|
311
|
+
if (/^(rich_text|multi_line_text)_field$/.test(mf.type)) el.style.whiteSpace = 'pre-line';
|
|
311
312
|
return setText(el, mf.value);
|
|
312
313
|
}
|
|
313
314
|
return undefined;
|
|
@@ -393,35 +394,35 @@ export class ProductScope {
|
|
|
393
394
|
const plan = this.plan;
|
|
394
395
|
const total = { amount: (parseFloat(price.amount) * Math.max(1, this.quantity)).toFixed(2), currencyCode: price.currencyCode };
|
|
395
396
|
|
|
397
|
+
const fm = formatMoney;
|
|
398
|
+
const pct = (n) => (n ? `${n}%` : '');
|
|
399
|
+
// Text binds: name -> value (functions, so only the binds on the page are computed).
|
|
400
|
+
const text = {
|
|
401
|
+
price: () => fm(price),
|
|
402
|
+
'total-price': () => fm(total),
|
|
403
|
+
'one-time-price': () => fm(oneTimePrice),
|
|
404
|
+
'subscription-price': () => fm(subPrice),
|
|
405
|
+
'subscription-savings': () => pct(savings.percentage),
|
|
406
|
+
'subscription-savings-amount': () => fm(savings.amount),
|
|
407
|
+
'checkout-charge': () => fm(sub ? planCheckoutCharge(v, this.planId) : price),
|
|
408
|
+
'plan-name': () => plan?.name,
|
|
409
|
+
'plan-description': () => plan?.description,
|
|
410
|
+
'plan-group': () => plan?.group?.name,
|
|
411
|
+
'compare-at-price': () => (onSale ? fm(compareAt) : ''),
|
|
412
|
+
'unit-price': () => fm(v.unitPrice),
|
|
413
|
+
'discount-percentage': () => `${discountPct}%`,
|
|
414
|
+
'discount-amount': () => (onSale ? fm({ amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)), currencyCode: price.currencyCode }) : ''),
|
|
415
|
+
'variant-title': () => v.title,
|
|
416
|
+
'variant-id': () => v.id,
|
|
417
|
+
sku: () => v.sku,
|
|
418
|
+
barcode: () => v.barcode,
|
|
419
|
+
stock: () => v.quantityAvailable,
|
|
420
|
+
quantity: () => this.quantity,
|
|
421
|
+
};
|
|
396
422
|
this.$(sel('bind')).forEach((el) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
case 'one-time-price': return setText(el, formatMoney(oneTimePrice));
|
|
401
|
-
case 'subscription-price': return setText(el, formatMoney(subPrice));
|
|
402
|
-
case 'subscription-savings': return setText(el, savings.percentage ? `${savings.percentage}%` : '');
|
|
403
|
-
case 'subscription-savings-amount': return setText(el, formatMoney(savings.amount));
|
|
404
|
-
case 'checkout-charge': return setText(el, formatMoney(sub ? planCheckoutCharge(v, this.planId) : price));
|
|
405
|
-
case 'plan-name': return setText(el, plan?.name || '');
|
|
406
|
-
case 'plan-description': return setText(el, plan?.description || '');
|
|
407
|
-
case 'plan-group': return setText(el, plan?.group?.name || '');
|
|
408
|
-
case 'compare-at-price': return setText(el, onSale ? formatMoney(compareAt) : '');
|
|
409
|
-
case 'unit-price': return setText(el, formatMoney(v.unitPrice));
|
|
410
|
-
case 'discount-percentage': return setText(el, `${discountPct}%`);
|
|
411
|
-
case 'discount-amount':
|
|
412
|
-
return setText(el, onSale ? formatMoney({
|
|
413
|
-
amount: String(parseFloat(compareAt.amount) - parseFloat(price.amount)),
|
|
414
|
-
currencyCode: price.currencyCode,
|
|
415
|
-
}) : '');
|
|
416
|
-
case 'variant-title': return setText(el, v.title);
|
|
417
|
-
case 'variant-id': return setText(el, v.id);
|
|
418
|
-
case 'sku': return setText(el, v.sku);
|
|
419
|
-
case 'barcode': return setText(el, v.barcode);
|
|
420
|
-
case 'stock': return setText(el, v.quantityAvailable ?? '');
|
|
421
|
-
case 'quantity': return setText(el, this.quantity);
|
|
422
|
-
case 'variant-image': return setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
|
|
423
|
-
default: return undefined;
|
|
424
|
-
}
|
|
423
|
+
const k = get(el, 'bind');
|
|
424
|
+
if (k === 'variant-image') setImage(el, v.image?.url || p.featuredImage?.url, v.image?.altText || p.title);
|
|
425
|
+
else if (text[k]) setText(el, text[k]());
|
|
425
426
|
});
|
|
426
427
|
|
|
427
428
|
this.$(sel('option-selected')).forEach((el) => setText(el, this.selected[get(el, 'option-selected')] || ''));
|
|
@@ -458,17 +459,16 @@ export class ProductScope {
|
|
|
458
459
|
qsa(node, sel('plan-bind')).forEach((el) => {
|
|
459
460
|
const adj = planAdjustment(planObj);
|
|
460
461
|
const s = planSavings(v, id);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
462
|
+
const k = get(el, 'plan-bind');
|
|
463
|
+
const val = {
|
|
464
|
+
name: () => planObj?.name,
|
|
465
|
+
description: () => planObj?.description,
|
|
466
|
+
price: () => fm(planPrice(v, id)),
|
|
467
|
+
savings: () => pct(s.percentage),
|
|
468
|
+
'savings-amount': () => fm(s.amount),
|
|
469
|
+
adjustment: () => (!adj ? '' : adj.type === 'percentage' ? `${adj.value}%` : fm(adj.value)),
|
|
470
|
+
}[k];
|
|
471
|
+
if (val) setText(el, val());
|
|
472
472
|
});
|
|
473
473
|
});
|
|
474
474
|
|
|
@@ -510,6 +510,7 @@ export class ProductScope {
|
|
|
510
510
|
this.$(sel('add-to-cart')).forEach((btn) => {
|
|
511
511
|
const disabled = !v.availableForSale;
|
|
512
512
|
btn.classList.toggle('is-disabled', disabled);
|
|
513
|
+
btn.setAttribute('aria-disabled', String(disabled));
|
|
513
514
|
if ('disabled' in btn) btn.disabled = disabled;
|
|
514
515
|
const soldOut = get(btn, 'soldout-text');
|
|
515
516
|
const subText = get(btn, 'subscribe-text');
|
package/src/status.js
CHANGED
|
@@ -64,6 +64,7 @@ export function setScopeStatus(root, status, err) {
|
|
|
64
64
|
if (status === 'error' || notFound) {
|
|
65
65
|
scoped(root, sel('add-to-cart'), boundary()).forEach((btn) => {
|
|
66
66
|
btn.classList.add('is-disabled');
|
|
67
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
67
68
|
if ('disabled' in btn) btn.disabled = true;
|
|
68
69
|
});
|
|
69
70
|
}
|
package/types/barba.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TetherConfig } from './index.js';
|
|
2
|
+
|
|
3
|
+
export interface BarbaPresetOptions {
|
|
4
|
+
gsap?: () => unknown;
|
|
5
|
+
lenis?: (() => { resize?(): void; stop?(): void; start?(): void } | null | undefined) | null;
|
|
6
|
+
scrollTrigger?: () => unknown;
|
|
7
|
+
lockScroll?: ((drawer: HTMLElement) => void) | null;
|
|
8
|
+
unlockScroll?: (() => void) | null;
|
|
9
|
+
onResize?: (() => void) | null;
|
|
10
|
+
onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
|
|
11
|
+
animateLines?: boolean;
|
|
12
|
+
lineDuration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Hooks for sites with page transitions, smooth scroll and GSAP: spread into Tether.init(). */
|
|
16
|
+
export declare function barbaPreset(options?: BarbaPresetOptions): Partial<TetherConfig>;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Type definitions for @occluze/tether
|
|
2
|
+
// Shopify Storefront API objects are typed loosely (`ShopifyProduct` etc.): Tether passes them
|
|
3
|
+
// through as Shopify returns them. See https://shopify.dev/docs/api/storefront for the full shape.
|
|
4
|
+
|
|
5
|
+
export interface Money { amount: string; currencyCode: string }
|
|
6
|
+
export interface ShopifyImage { url: string; altText?: string | null; width?: number; height?: number }
|
|
7
|
+
|
|
8
|
+
export interface ShopifySellingPlan {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string | null;
|
|
12
|
+
recurringDeliveries?: boolean;
|
|
13
|
+
options: { name: string; value: string }[];
|
|
14
|
+
priceAdjustments: unknown[];
|
|
15
|
+
/** Added by Tether: the plan's group. */
|
|
16
|
+
group?: { name: string; [k: string]: unknown };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ShopifyVariant {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
availableForSale: boolean;
|
|
23
|
+
currentlyNotInStock?: boolean;
|
|
24
|
+
quantityAvailable?: number | null;
|
|
25
|
+
sku?: string | null;
|
|
26
|
+
barcode?: string | null;
|
|
27
|
+
selectedOptions: { name: string; value: string }[];
|
|
28
|
+
image?: ShopifyImage | null;
|
|
29
|
+
price: Money;
|
|
30
|
+
compareAtPrice?: Money | null;
|
|
31
|
+
unitPrice?: Money | null;
|
|
32
|
+
sellingPlanAllocations?: { nodes: unknown[] };
|
|
33
|
+
[k: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ShopifyProduct {
|
|
37
|
+
id: string;
|
|
38
|
+
handle: string;
|
|
39
|
+
title: string;
|
|
40
|
+
description: string;
|
|
41
|
+
descriptionHtml: string;
|
|
42
|
+
productType?: string;
|
|
43
|
+
vendor?: string;
|
|
44
|
+
tags: string[];
|
|
45
|
+
availableForSale: boolean;
|
|
46
|
+
requiresSellingPlan?: boolean;
|
|
47
|
+
featuredImage?: ShopifyImage | null;
|
|
48
|
+
images: { nodes: ShopifyImage[] };
|
|
49
|
+
priceRange: { minVariantPrice: Money; maxVariantPrice: Money };
|
|
50
|
+
options: { name: string; optionValues: { name: string; swatch?: unknown }[] }[];
|
|
51
|
+
variants: { nodes: ShopifyVariant[] };
|
|
52
|
+
sellingPlanGroups?: { nodes: unknown[] };
|
|
53
|
+
metafields?: ({ namespace: string; key: string; type: string; value: string; reference?: unknown } | null)[];
|
|
54
|
+
[k: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ShopifyCartLine {
|
|
58
|
+
id: string;
|
|
59
|
+
quantity: number;
|
|
60
|
+
merchandise: ShopifyVariant & { product: Pick<ShopifyProduct, 'id' | 'handle' | 'title'> & Record<string, unknown> };
|
|
61
|
+
sellingPlanAllocation?: { sellingPlan: { id: string; name: string } } | null;
|
|
62
|
+
attributes?: { key: string; value: string }[];
|
|
63
|
+
cost: Record<string, Money | null | undefined>;
|
|
64
|
+
[k: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ShopifyCart {
|
|
68
|
+
id: string;
|
|
69
|
+
checkoutUrl: string;
|
|
70
|
+
totalQuantity: number;
|
|
71
|
+
note?: string | null;
|
|
72
|
+
lines: { nodes: ShopifyCartLine[] };
|
|
73
|
+
cost: Record<string, Money | null | undefined>;
|
|
74
|
+
discountCodes: { code: string; applicable: boolean }[];
|
|
75
|
+
attributes: { key: string; value: string }[];
|
|
76
|
+
buyerIdentity?: { countryCode?: string | null; email?: string | null };
|
|
77
|
+
[k: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface MarketsConfig {
|
|
81
|
+
/** true: saved choice, server hints, Webflow Localization. Or a list of those sources. false: off. */
|
|
82
|
+
detect?: boolean | ('saved' | 'server' | 'webflow')[];
|
|
83
|
+
/** Webflow locale (subdirectory or html lang) -> market. */
|
|
84
|
+
locales?: Record<string, { country?: string; language?: string; locale?: string }>;
|
|
85
|
+
/** Cookie with the visitor's country, set by your server (default "tether_country"). */
|
|
86
|
+
cookie?: string;
|
|
87
|
+
/** Remember the visitor's country choice (default true). */
|
|
88
|
+
remember?: boolean;
|
|
89
|
+
/** With locales: ignore an html lang that isn't in the map. */
|
|
90
|
+
strict?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TetherConfig {
|
|
94
|
+
/** your-store.myshopify.com */
|
|
95
|
+
domain: string;
|
|
96
|
+
/** Public Storefront API access token (never an Admin token). */
|
|
97
|
+
token: string;
|
|
98
|
+
/** Pinned Shopify API version, e.g. "2026-07". Never "unstable". */
|
|
99
|
+
apiVersion?: string;
|
|
100
|
+
/** ISO country code for prices and currency, e.g. "NL". */
|
|
101
|
+
country?: string;
|
|
102
|
+
/** Language for translated content, e.g. "NL". */
|
|
103
|
+
language?: string;
|
|
104
|
+
/** Intl locale for money and dates, e.g. "nl-NL". */
|
|
105
|
+
locale?: string;
|
|
106
|
+
markets?: MarketsConfig | null;
|
|
107
|
+
/** Attribute prefix (default "tether"). */
|
|
108
|
+
prefix?: string;
|
|
109
|
+
/** Product URL template: "/product" or "/product?handle={handle}" ({handle}, {id}). */
|
|
110
|
+
productPath?: string;
|
|
111
|
+
collectionPath?: string;
|
|
112
|
+
/** Metafields to fetch: "namespace.key" or { namespace, key }. */
|
|
113
|
+
metafields?: (string | { namespace: string; key: string })[];
|
|
114
|
+
imagesFirst?: number;
|
|
115
|
+
variantsFirst?: number;
|
|
116
|
+
sellingPlanGroupsFirst?: number;
|
|
117
|
+
sellingPlansFirst?: number;
|
|
118
|
+
collectionFirst?: number;
|
|
119
|
+
retries?: number;
|
|
120
|
+
storageKey?: string | null;
|
|
121
|
+
cartDrawerOpenClass?: string;
|
|
122
|
+
openCartOnAdd?: boolean;
|
|
123
|
+
/** Move focus into the open cart, keep Tab inside it, restore it on close (default true). */
|
|
124
|
+
cartFocus?: boolean;
|
|
125
|
+
checkoutTarget?: '_self' | '_blank';
|
|
126
|
+
defaultPurchase?: 'one-time' | 'subscription';
|
|
127
|
+
/** Mount the document on DOMContentLoaded (default true). False with page transitions. */
|
|
128
|
+
autoMount?: boolean;
|
|
129
|
+
/** Mount elements added later (CMS load more, filters). */
|
|
130
|
+
observe?: boolean;
|
|
131
|
+
/** Folder of the optional parts (tether.markets.min.js, tether.diagnose.min.js) for the <script> build. */
|
|
132
|
+
partsUrl?: string | null;
|
|
133
|
+
formatMoney?: ((money: Money, locale?: string) => string) | null;
|
|
134
|
+
onLayoutChange?: ((reason: string) => void) | null;
|
|
135
|
+
onCartOpen?: ((drawers: HTMLElement[]) => void) | null;
|
|
136
|
+
onCartClose?: ((drawers: HTMLElement[]) => void) | null;
|
|
137
|
+
onLineEnter?: ((node: HTMLElement) => void) | null;
|
|
138
|
+
onLineLeave?: ((node: HTMLElement, done: () => void) => void) | null;
|
|
139
|
+
onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
|
|
140
|
+
/** Every failure that reaches the visitor. Send it to Sentry or similar. */
|
|
141
|
+
onError?: ((error: TetherError | Error, context: string) => void) | null;
|
|
142
|
+
debug?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export type TetherErrorCode = 'NOT_FOUND' | 'NETWORK' | 'NOT_CONFIGURED' | 'INVALID_COUNTRY' | 'ERROR' | `HTTP_${number}`;
|
|
146
|
+
|
|
147
|
+
export declare class TetherError extends Error {
|
|
148
|
+
name: 'TetherError';
|
|
149
|
+
code: TetherErrorCode | string;
|
|
150
|
+
status?: number;
|
|
151
|
+
errors?: { message: string; extensions?: Record<string, unknown> }[];
|
|
152
|
+
userErrors?: { field?: string[]; message: string }[];
|
|
153
|
+
constructor(message: string, opts?: { status?: number; errors?: unknown[]; userErrors?: unknown[]; code?: string });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type Attributes = Record<string, string | number | boolean> | { key: string; value: string }[];
|
|
157
|
+
|
|
158
|
+
export interface LineInput {
|
|
159
|
+
variantId?: string;
|
|
160
|
+
merchandiseId?: string;
|
|
161
|
+
quantity?: number;
|
|
162
|
+
sellingPlanId?: string | null;
|
|
163
|
+
attributes?: Attributes;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Cart {
|
|
167
|
+
readonly state: ShopifyCart | null;
|
|
168
|
+
readonly lines: ShopifyCartLine[];
|
|
169
|
+
readonly count: number;
|
|
170
|
+
lastError: Error | null;
|
|
171
|
+
id(): string | null;
|
|
172
|
+
init(): Promise<ShopifyCart | null>;
|
|
173
|
+
/** add(variantId, qty) · add({ variantId, quantity, sellingPlanId, attributes }) · add([line, line]) */
|
|
174
|
+
add(input: string | LineInput | LineInput[], quantity?: number, opts?: Omit<LineInput, 'variantId' | 'quantity'>): Promise<ShopifyCart>;
|
|
175
|
+
/** updateLine(id, 3) · updateLine(id, { sellingPlanId: null }) switches to one-time. */
|
|
176
|
+
updateLine(lineId: string, patch: number | (LineInput & { quantity?: number })): Promise<ShopifyCart>;
|
|
177
|
+
removeLine(lineIds: string | string[]): Promise<ShopifyCart>;
|
|
178
|
+
clear(): Promise<ShopifyCart | null>;
|
|
179
|
+
setNote(note: string): Promise<ShopifyCart>;
|
|
180
|
+
setAttributes(attributes: Attributes): Promise<ShopifyCart>;
|
|
181
|
+
setDiscountCodes(codes: string[]): Promise<ShopifyCart>;
|
|
182
|
+
applyDiscount(code: string): Promise<ShopifyCart>;
|
|
183
|
+
removeDiscount(code: string): Promise<ShopifyCart>;
|
|
184
|
+
setBuyerIdentity(identity: { countryCode?: string; email?: string; phone?: string }, opts?: { create?: boolean }): Promise<ShopifyCart | null>;
|
|
185
|
+
checkout(target?: '_self' | '_blank'): Promise<void>;
|
|
186
|
+
open(): void;
|
|
187
|
+
close(): void;
|
|
188
|
+
toggle(): void;
|
|
189
|
+
isOpen(): boolean;
|
|
190
|
+
drawers(): HTMLElement[];
|
|
191
|
+
render(): void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ProductScope {
|
|
195
|
+
readonly root: HTMLElement;
|
|
196
|
+
product: ShopifyProduct;
|
|
197
|
+
variant: ShopifyVariant | null;
|
|
198
|
+
selected: Record<string, string>;
|
|
199
|
+
quantity: number;
|
|
200
|
+
plans: ShopifySellingPlan[];
|
|
201
|
+
purchase: 'one-time' | 'subscription';
|
|
202
|
+
planId: string | null;
|
|
203
|
+
readonly plan: ShopifySellingPlan | null;
|
|
204
|
+
readonly isSubscription: boolean;
|
|
205
|
+
readonly subscriptionAvailable: boolean;
|
|
206
|
+
select(option: string, value: string): void;
|
|
207
|
+
selectVariant(id: string): void;
|
|
208
|
+
setPurchase(mode: 'one-time' | 'subscription'): void;
|
|
209
|
+
selectPlan(ref: string): void;
|
|
210
|
+
setQuantity(n: number | string): void;
|
|
211
|
+
getLine(): LineInput | null;
|
|
212
|
+
addToCart(): Promise<ShopifyCart | null>;
|
|
213
|
+
update(product: ShopifyProduct): void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface SellingPlanHelpers {
|
|
217
|
+
allPlans(product: ShopifyProduct): ShopifySellingPlan[];
|
|
218
|
+
hasPlans(product: ShopifyProduct): boolean;
|
|
219
|
+
allocationFor(variant: ShopifyVariant, planId: string | null): unknown | null;
|
|
220
|
+
plansForVariant(product: ShopifyProduct, variant: ShopifyVariant): ShopifySellingPlan[];
|
|
221
|
+
planPrice(variant: ShopifyVariant, planId: string): Money | null;
|
|
222
|
+
planCheckoutCharge(variant: ShopifyVariant, planId: string): Money | null;
|
|
223
|
+
planSavings(variant: ShopifyVariant, planId: string): { amount: Money | null; percentage: number };
|
|
224
|
+
planAdjustment(plan: ShopifySellingPlan | null | undefined): { type: 'percentage' | 'amount' | 'price'; value: number | Money } | null;
|
|
225
|
+
findPlan(product: ShopifyProduct, ref: string | null | undefined): ShopifySellingPlan | null;
|
|
226
|
+
toGid(value: string | number, type: string): string | null;
|
|
227
|
+
GID: { product(v: string | number): string | null; variant(v: string | number): string | null; sellingPlan(v: string | number): string | null };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface ProductPage {
|
|
231
|
+
collection: Record<string, unknown> | null;
|
|
232
|
+
products: ShopifyProduct[];
|
|
233
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface Localization {
|
|
237
|
+
country?: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string } };
|
|
238
|
+
language?: { isoCode: string; name: string };
|
|
239
|
+
availableCountries: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string }; availableLanguages?: { isoCode: string; name: string }[] }[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface DiagnoseReport {
|
|
243
|
+
version: string;
|
|
244
|
+
config: Record<string, unknown>;
|
|
245
|
+
apiVersion: { version: string; ok: boolean; monthsLeft?: number; next?: string; note: string };
|
|
246
|
+
market: { country?: string; language?: string; locale?: string; sources: Record<string, string> } | null;
|
|
247
|
+
reachable: boolean;
|
|
248
|
+
shop: { name?: string; url?: string; currency?: string } | null;
|
|
249
|
+
latencyMs: number | null;
|
|
250
|
+
products: { key: string; handle?: string; title?: string; plans?: number; error?: string }[];
|
|
251
|
+
sellingPlans: { product: string; group?: string; plan: string; id: string }[];
|
|
252
|
+
problems: { level: 'error' | 'warn'; message: string; element?: string }[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Events on document, as `tether:<name>` (or your prefix). Detail types for Tether.on(). */
|
|
256
|
+
export interface TetherEvents {
|
|
257
|
+
ready: { cart: ShopifyCart | null };
|
|
258
|
+
'product-ready': { product: ShopifyProduct; scope: ProductScope };
|
|
259
|
+
'product-updated': { product: ShopifyProduct; scope: ProductScope };
|
|
260
|
+
'product-not-found': { key: string | null; source: string };
|
|
261
|
+
'variant-changed': { product: ShopifyProduct; variant: ShopifyVariant; selected: Record<string, string>; purchase: 'one-time' | 'subscription'; sellingPlanId: string | null };
|
|
262
|
+
'product-added': { product: ShopifyProduct; variant: ShopifyVariant; line: LineInput };
|
|
263
|
+
added: { cart: ShopifyCart; lines: LineInput[] };
|
|
264
|
+
'group-added': { group: string | null; lines: LineInput[] };
|
|
265
|
+
'cart-updated': { cart: ShopifyCart | null; reason: string };
|
|
266
|
+
'cart-toggled': { open: boolean };
|
|
267
|
+
'cart-error': { error: Error };
|
|
268
|
+
checkout: { url: string; cart: ShopifyCart };
|
|
269
|
+
'list-ready': { products: ShopifyProduct[]; collection: Record<string, unknown> | null };
|
|
270
|
+
'country-changed': { country: string; language?: string };
|
|
271
|
+
'discount-applied': { code: string };
|
|
272
|
+
'discount-invalid': { code: string };
|
|
273
|
+
'layout-changed': { reason: string };
|
|
274
|
+
error: { error: Error; context: string; key?: string };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface Tether {
|
|
278
|
+
readonly VERSION: string;
|
|
279
|
+
/** Configure (window.TETHER_CONFIG + overrides) and load the cart. */
|
|
280
|
+
init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
|
|
281
|
+
/** Mount product scopes and lists inside root (call per page container with Barba/Swup). */
|
|
282
|
+
mount(root?: ParentNode): number;
|
|
283
|
+
/** Call before a page transition removes the page (closes the cart drawer). */
|
|
284
|
+
unmount(): void;
|
|
285
|
+
readonly config: TetherConfig;
|
|
286
|
+
readonly ready: Promise<Tether | null>;
|
|
287
|
+
/** Any Storefront API query, with @inContext added for the current market. */
|
|
288
|
+
query<T = any>(query: string, variables?: Record<string, unknown>, opts?: { context?: boolean }): Promise<T>;
|
|
289
|
+
withContext(query: string, ctx?: { country?: string; language?: string }): string;
|
|
290
|
+
cart: Cart;
|
|
291
|
+
getProduct(handleOrId: string): Promise<ShopifyProduct>;
|
|
292
|
+
getProducts(opts?: { collection?: string; query?: string; first?: number; after?: string | null; sortKey?: string; reverse?: boolean; filters?: unknown[] }): Promise<ProductPage>;
|
|
293
|
+
getRecommendations(productId: string, intent?: 'RELATED' | 'COMPLEMENTARY'): Promise<ShopifyProduct[]>;
|
|
294
|
+
getScope(el: Element | null | undefined): ProductScope | null;
|
|
295
|
+
addGroup(button: Element): Promise<ShopifyCart | null>;
|
|
296
|
+
primeProduct(product: ShopifyProduct): void;
|
|
297
|
+
normalizeProductKey(raw: string | number): string | null;
|
|
298
|
+
clearProductCache(): void;
|
|
299
|
+
formatMoney(money: Money | null | undefined): string;
|
|
300
|
+
plans: SellingPlanHelpers;
|
|
301
|
+
TetherError: typeof TetherError;
|
|
302
|
+
resolveProductKey(el: Element, location?: Location | URL): { key: string | null; fromUrl: boolean; raw: string };
|
|
303
|
+
/** Switch market: prices, currency, cart and every product on the page follow. */
|
|
304
|
+
setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
|
|
305
|
+
detectMarket(markets?: MarketsConfig): Promise<{ country?: string; language?: string; locale?: string; sources: Record<string, string> }>;
|
|
306
|
+
getLocalization(): Promise<Localization>;
|
|
307
|
+
/** Logs and returns what's wrong on this page (config, token, API version, plans, markup). */
|
|
308
|
+
diagnose(opts?: { root?: ParentNode; silent?: boolean }): Promise<DiagnoseReport>;
|
|
309
|
+
/** Listen to a Tether event; returns a function that stops listening. */
|
|
310
|
+
on<K extends keyof TetherEvents>(name: K, fn: (detail: TetherEvents[K], event: CustomEvent<TetherEvents[K]>) => void): () => void;
|
|
311
|
+
on(name: string, fn: (detail: any, event: CustomEvent) => void): () => void;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export declare const VERSION: string;
|
|
315
|
+
export declare function init(overrides?: Partial<TetherConfig>): Promise<Tether | null>;
|
|
316
|
+
export declare function mount(root?: ParentNode): number;
|
|
317
|
+
export declare function unmount(): void;
|
|
318
|
+
export declare function getScope(el: Element | null | undefined): ProductScope | null;
|
|
319
|
+
export declare function addGroup(button: Element): Promise<ShopifyCart | null>;
|
|
320
|
+
export declare function setCountry(code: string, opts?: { language?: string; remember?: boolean }): Promise<ShopifyCart | null>;
|
|
321
|
+
export declare const api: Tether;
|
|
322
|
+
declare const tether: Tether;
|
|
323
|
+
export default tether;
|
|
324
|
+
|
|
325
|
+
declare global {
|
|
326
|
+
interface Window {
|
|
327
|
+
Tether: Tether;
|
|
328
|
+
TETHER_CONFIG?: Partial<TetherConfig>;
|
|
329
|
+
TETHER_COUNTRY?: string;
|
|
330
|
+
TETHER_LANGUAGE?: string;
|
|
331
|
+
}
|
|
332
|
+
}
|