@occluze/tether 0.6.6 → 0.6.8
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 +3 -3
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-C3NP6HFB.js +1 -0
- package/dist/esm/chunks/account-pages-WLK374FO.js +1 -0
- package/dist/esm/chunks/analytics-G3SAWMNL.js +1 -0
- package/dist/esm/chunks/chunk-27PJJZGC.js +1 -0
- package/dist/esm/chunks/chunk-2FSXS55U.js +1 -0
- package/dist/esm/chunks/chunk-FCSWQDVV.js +1 -0
- package/dist/esm/chunks/chunk-FOMSMIAM.js +1 -0
- package/dist/esm/chunks/chunk-GZJTCLLW.js +1 -0
- package/dist/esm/chunks/chunk-HEQILZVR.js +1 -0
- package/dist/esm/chunks/chunk-JOOEXYF3.js +4 -0
- package/dist/esm/chunks/chunk-MPTQYLSM.js +1 -0
- package/dist/esm/chunks/chunk-NEHZ37TU.js +3 -0
- package/dist/esm/chunks/chunk-V74KGEJF.js +1 -0
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +1 -0
- package/dist/esm/chunks/chunk-YJLWX2HK.js +1 -0
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +14 -0
- package/dist/esm/chunks/extras-GJVRDFM3.js +1 -0
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +1 -0
- package/dist/esm/chunks/search-OU75RNQL.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +56 -36
- package/dist/tether.account-pages.js +776 -432
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +655 -321
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +318 -191
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +334 -243
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +78 -33
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3171 -1998
- package/dist/tether.markets.js +231 -126
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +0 -0
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +221 -126
- package/dist/tether.search.min.js +1 -1
- package/package.json +23 -4
- package/src/account/addresses-view.js +92 -0
- package/src/account/browser-session.js +359 -0
- package/src/account/markup.js +48 -0
- package/src/account/operations.js +180 -0
- package/src/account/order-lines.js +96 -0
- package/src/account/order-list.js +91 -0
- package/src/account/order-page.js +192 -0
- package/src/account/page-events.js +288 -0
- package/src/account/server-client.js +172 -0
- package/src/account/shared.js +111 -0
- package/src/account/shopify-auth.js +150 -0
- package/src/account/subscriptions-view.js +158 -0
- package/src/account-api.js +118 -0
- package/src/account-links.js +35 -0
- package/src/account-pages.js +68 -284
- package/src/account.js +281 -317
- package/src/amounts.js +36 -0
- package/src/analytics-shopify.js +241 -0
- package/src/analytics.js +304 -184
- package/src/api-version.js +75 -25
- package/src/astro.js +233 -68
- package/src/cart/checkout.js +97 -0
- package/src/cart/drawer.js +138 -0
- package/src/cart/mutations.js +234 -0
- package/src/cart/render.js +294 -0
- package/src/cart/store.js +326 -0
- package/src/cart.js +322 -481
- package/src/client.js +214 -78
- package/src/collection.js +126 -57
- package/src/config.js +323 -59
- package/src/controls/cart-controls.js +155 -0
- package/src/controls/discount.js +96 -0
- package/src/controls/extras.js +63 -0
- package/src/controls/navigation.js +83 -0
- package/src/diagnose-markup.js +333 -0
- package/src/diagnose.js +251 -202
- package/src/dom.js +219 -220
- package/src/extras.js +119 -37
- package/src/fragments.js +115 -22
- package/src/images.js +104 -0
- package/src/index.js +245 -285
- package/src/initial.js +78 -66
- package/src/lazy-iife.js +92 -22
- package/src/lazy.js +25 -4
- package/src/loader.js +263 -124
- package/src/market-codes.js +35 -0
- package/src/markets-ui.js +169 -55
- package/src/markets.js +286 -85
- package/src/markup.js +64 -0
- package/src/mounting.js +214 -91
- package/src/page-market.js +116 -0
- package/src/parts/account-pages.js +4 -1
- package/src/parts/account.js +4 -1
- package/src/parts/analytics.js +4 -1
- package/src/parts/diagnose.js +4 -1
- package/src/parts/extras.js +4 -1
- package/src/parts/markets.js +4 -1
- package/src/parts/search.js +4 -1
- package/src/presets/barba.js +120 -30
- package/src/product-events.js +130 -0
- package/src/product-render.js +257 -0
- package/src/product-state.js +318 -0
- package/src/product.js +233 -459
- package/src/repeaters.js +173 -0
- package/src/search.js +277 -107
- package/src/selling-plans.js +107 -47
- package/src/server.js +168 -205
- package/src/shopify-values.js +89 -0
- package/src/status.js +99 -39
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +199 -0
- package/src/storefront/json-ld.js +167 -0
- package/src/variant-render.js +165 -0
- package/types/astro.d.ts +13 -2
- package/types/index.d.ts +178 -27
- package/types/server.d.ts +89 -17
- package/dist/esm/chunks/account-QUBHQG3C.js +0 -1
- package/dist/esm/chunks/account-pages-VWDHF6WA.js +0 -1
- package/dist/esm/chunks/analytics-SLATWPLC.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-L2TRRKYV.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/chunk-VCPI7MJ2.js +0 -1
- package/dist/esm/chunks/chunk-VPVJPNB4.js +0 -1
- package/dist/esm/chunks/chunk-ZIUIWEON.js +0 -1
- package/dist/esm/chunks/diagnose-VXQBSD3A.js +0 -14
- package/dist/esm/chunks/extras-R4X34VQL.js +0 -1
- package/dist/esm/chunks/markets-ui-F3R5T5EL.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DOM writes of a product scope that follow the visitor's choice, on every change: `product-state.js`'s view as
|
|
3
|
+
* text, selected and unavailable states, `tether-show` conditions and the add-to-cart buttons.
|
|
4
|
+
*/
|
|
5
|
+
import { attr, sel, get, qsa, formatMoney, formatPercent, setText, setImage, setSelected, show } from './dom.js';
|
|
6
|
+
import { STATUS_KEYS, ownedBy } from './status.js';
|
|
7
|
+
import { allocationFor } from './selling-plans.js';
|
|
8
|
+
import { isValueAvailable, planBindTexts, variantBindTexts } from './product-state.js';
|
|
9
|
+
import { isSelect, selectsIn } from './product-render.js';
|
|
10
|
+
/** @import { Formatters, ProductView } from './product-state.js' */
|
|
11
|
+
/** @import { RenderScope } from './product-render.js' */
|
|
12
|
+
|
|
13
|
+
/** @type {Formatters} */
|
|
14
|
+
const FORMAT = { money: formatMoney, percent: formatPercent };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {Element} el
|
|
18
|
+
* @returns {el is HTMLInputElement}
|
|
19
|
+
*/
|
|
20
|
+
const isInput = (el) => el.tagName === 'INPUT';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The element of an add-to-cart button that holds its label, with the label as written kept in
|
|
24
|
+
* `data-tether-label`, so mode-dependent texts (`tether-subscribe-text`, `tether-soldout-text`) can be undone.
|
|
25
|
+
*
|
|
26
|
+
* @param {HTMLElement} button
|
|
27
|
+
* @returns {{ label: HTMLElement, original: string }}
|
|
28
|
+
*/
|
|
29
|
+
export function addButtonLabel(button) {
|
|
30
|
+
const label = qsa(button, sel('add-label'))[0] ?? button;
|
|
31
|
+
const original = label.dataset['tetherLabel'] ?? (isInput(label) ? label.value : label.textContent);
|
|
32
|
+
label.dataset['tetherLabel'] = original;
|
|
33
|
+
return { label, original };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {RenderScope} scope
|
|
38
|
+
* @param {ProductView} view
|
|
39
|
+
*/
|
|
40
|
+
function renderOptionStates(scope, view) {
|
|
41
|
+
const { product } = scope;
|
|
42
|
+
const selectedValue = (/** @type {string | null} */ authored) => view.selected[scope.optName(authored)] ?? '';
|
|
43
|
+
scope.$(sel('option-selected')).forEach((el) => {
|
|
44
|
+
setText(el, selectedValue(get(el, 'option-selected')));
|
|
45
|
+
});
|
|
46
|
+
scope.$(sel('option-list')).forEach((container) => {
|
|
47
|
+
const name = scope.optName(get(container, 'option-list'));
|
|
48
|
+
qsa(container, sel('option-value')).forEach((node) => {
|
|
49
|
+
const value = get(node, 'option-value') ?? '';
|
|
50
|
+
setSelected(node, view.selected[name] === value);
|
|
51
|
+
const unavailable = !isValueAvailable(product, view.selected, name, value);
|
|
52
|
+
node.classList.toggle('is-unavailable', unavailable);
|
|
53
|
+
node.setAttribute('data-unavailable', unavailable ? 'true' : 'false');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
selectsIn(scope, sel('option')).forEach((select) => {
|
|
57
|
+
select.value = selectedValue(get(select, 'option'));
|
|
58
|
+
});
|
|
59
|
+
selectsIn(scope, sel('variant-select')).forEach((select) => {
|
|
60
|
+
select.value = view.variant.id;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The purchase toggles and the plan list and select.
|
|
66
|
+
*
|
|
67
|
+
* @param {RenderScope} scope
|
|
68
|
+
* @param {ProductView} view
|
|
69
|
+
*/
|
|
70
|
+
function renderPurchaseStates(scope, view) {
|
|
71
|
+
const { variant, purchase, isSubscription } = view;
|
|
72
|
+
scope.$(sel('purchase')).forEach((el) => {
|
|
73
|
+
if (isSelect(el)) {
|
|
74
|
+
el.value = purchase.kind;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const mode = get(el, 'purchase');
|
|
78
|
+
setSelected(el, mode === purchase.kind);
|
|
79
|
+
const unavailable = mode === 'subscription' ? !view.subscriptionAvailable : scope.product.requiresSellingPlan;
|
|
80
|
+
el.classList.toggle('is-unavailable', unavailable);
|
|
81
|
+
});
|
|
82
|
+
scope.$(sel('plan-value')).forEach((node) => {
|
|
83
|
+
const planId = get(node, 'plan-value') ?? '';
|
|
84
|
+
setSelected(node, isSubscription && planId === purchase.planId);
|
|
85
|
+
node.classList.toggle('is-unavailable', !allocationFor(variant, planId));
|
|
86
|
+
const plan = scope.plans.find((candidate) => candidate.id === planId);
|
|
87
|
+
const texts = planBindTexts(variant, planId, plan, FORMAT);
|
|
88
|
+
qsa(node, sel('plan-bind')).forEach((el) => {
|
|
89
|
+
const text = texts.get(get(el, 'plan-bind') ?? '');
|
|
90
|
+
if (text) setText(el, text());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
selectsIn(scope, sel('plan')).forEach((select) => {
|
|
94
|
+
select.value = isSubscription ? (purchase.planId ?? '') : '';
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The `tether-show` conditions, with the scope's own status: a product scope that renders is ready.
|
|
100
|
+
*
|
|
101
|
+
* @param {RenderScope} scope
|
|
102
|
+
* @param {ProductView} view
|
|
103
|
+
*/
|
|
104
|
+
function renderConditions(scope, view) {
|
|
105
|
+
/** @type {Map<string, boolean>} */
|
|
106
|
+
const conditions = new Map(Object.entries(view.show));
|
|
107
|
+
STATUS_KEYS.forEach((status) => conditions.set(status, status === 'ready'));
|
|
108
|
+
scope.$(sel('show')).forEach((el) => {
|
|
109
|
+
const key = get(el, 'show') ?? '';
|
|
110
|
+
const visible = conditions.get(key);
|
|
111
|
+
if (visible === undefined) return;
|
|
112
|
+
// State blocks of a list nested in this product belong to that list.
|
|
113
|
+
if (STATUS_KEYS.some((status) => status === key) && !ownedBy(el, scope.root)) return;
|
|
114
|
+
show(el, visible);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {RenderScope} scope
|
|
120
|
+
* @param {ProductView} view
|
|
121
|
+
*/
|
|
122
|
+
function renderAddButtons(scope, view) {
|
|
123
|
+
const disabled = !view.variant.availableForSale;
|
|
124
|
+
scope.$(sel('add-to-cart')).forEach((button) => {
|
|
125
|
+
button.classList.toggle('is-disabled', disabled);
|
|
126
|
+
button.setAttribute('aria-disabled', String(disabled));
|
|
127
|
+
// A <button> or <input> is disabled; a link or div carries only aria-disabled.
|
|
128
|
+
if ('disabled' in button) button.disabled = disabled;
|
|
129
|
+
const soldOutText = get(button, 'soldout-text');
|
|
130
|
+
const subscribeText = get(button, 'subscribe-text');
|
|
131
|
+
if (!soldOutText && !subscribeText) return;
|
|
132
|
+
const { label, original } = addButtonLabel(button);
|
|
133
|
+
let text = original;
|
|
134
|
+
if (view.isSubscription && subscribeText) text = subscribeText;
|
|
135
|
+
if (disabled && soldOutText) text = soldOutText;
|
|
136
|
+
if (button.getAttribute('aria-busy') !== 'true') setText(label, text);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Writes what follows the variant and purchase.
|
|
142
|
+
*
|
|
143
|
+
* @param {RenderScope} scope
|
|
144
|
+
* @param {ProductView} view
|
|
145
|
+
*/
|
|
146
|
+
export function writeVariant(scope, view) {
|
|
147
|
+
const { variant } = view;
|
|
148
|
+
const { product } = scope;
|
|
149
|
+
const texts = variantBindTexts(view, FORMAT);
|
|
150
|
+
scope.$(sel('bind')).forEach((el) => {
|
|
151
|
+
const key = get(el, 'bind') ?? '';
|
|
152
|
+
if (key === 'variant-image') {
|
|
153
|
+
setImage(el, variant.image?.url || product.featuredImage?.url, variant.image?.altText || product.title);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const text = texts.get(key);
|
|
157
|
+
if (text) setText(el, text());
|
|
158
|
+
});
|
|
159
|
+
renderOptionStates(scope, view);
|
|
160
|
+
renderPurchaseStates(scope, view);
|
|
161
|
+
renderConditions(scope, view);
|
|
162
|
+
scope.root.classList.toggle('is-subscription', view.isSubscription);
|
|
163
|
+
scope.root.setAttribute(attr('purchase-mode'), view.isSubscription ? 'subscription' : 'one-time');
|
|
164
|
+
renderAddButtons(scope, view);
|
|
165
|
+
}
|
package/types/astro.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { TetherConfig } from './index.js';
|
|
2
2
|
|
|
3
|
-
type Serializable<T> = {
|
|
3
|
+
type Serializable<T> = {
|
|
4
|
+
[
|
|
5
|
+
K in keyof T as T[K] extends ((...args: any[]) => any) | null | undefined
|
|
6
|
+
? Exclude<T[K], null | undefined> extends (...args: any[]) => any
|
|
7
|
+
? never
|
|
8
|
+
: K
|
|
9
|
+
: K
|
|
10
|
+
]: T[K];
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
export interface TetherAstroOptions extends Partial<Serializable<TetherConfig>> {
|
|
6
14
|
/** Add the Tether script to every page (default true). False: import and init it yourself. */
|
|
@@ -14,4 +22,7 @@ export interface TetherAstroOptions extends Partial<Serializable<TetherConfig>>
|
|
|
14
22
|
* (PUBLIC_SHOPIFY_DOMAIN, PUBLIC_SHOPIFY_STOREFRONT_TOKEN). Functions (onError, hooks) go in
|
|
15
23
|
* window.TETHER_CONFIG in your layout.
|
|
16
24
|
*/
|
|
17
|
-
export default function tether(options?: TetherAstroOptions): {
|
|
25
|
+
export default function tether(options?: TetherAstroOptions): {
|
|
26
|
+
name: string;
|
|
27
|
+
hooks: Record<string, (...args: any[]) => unknown>;
|
|
28
|
+
};
|
package/types/index.d.ts
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
// Shopify Storefront API objects are typed loosely (`ShopifyProduct` etc.): Tether passes them
|
|
3
3
|
// through as Shopify returns them. See https://shopify.dev/docs/api/storefront for the full shape.
|
|
4
4
|
|
|
5
|
-
export interface Money {
|
|
6
|
-
|
|
5
|
+
export interface Money {
|
|
6
|
+
amount: string;
|
|
7
|
+
currencyCode: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ShopifyImage {
|
|
10
|
+
url: string;
|
|
11
|
+
altText?: string | null;
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
}
|
|
7
15
|
|
|
8
16
|
export interface ShopifySellingPlan {
|
|
9
17
|
id: string;
|
|
@@ -62,7 +70,12 @@ export interface ShopifyCartLine {
|
|
|
62
70
|
merchandise: ShopifyVariant & { product: Pick<ShopifyProduct, 'id' | 'handle' | 'title'> & Record<string, unknown> };
|
|
63
71
|
sellingPlanAllocation?: { sellingPlan: { id: string; name: string } } | null;
|
|
64
72
|
attributes?: { key: string; value: string }[];
|
|
65
|
-
cost: {
|
|
73
|
+
cost: {
|
|
74
|
+
totalAmount: Money;
|
|
75
|
+
subtotalAmount: Money;
|
|
76
|
+
amountPerQuantity: Money;
|
|
77
|
+
compareAtAmountPerQuantity?: Money | null;
|
|
78
|
+
};
|
|
66
79
|
[k: string]: unknown;
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -152,9 +165,13 @@ export interface TetherConfig {
|
|
|
152
165
|
/** The *.myshopify.com domain, if domain is something else. */
|
|
153
166
|
shopDomain?: string;
|
|
154
167
|
locale?: string;
|
|
168
|
+
/** Replaces location.assign for the login redirect (popups, tests). */
|
|
169
|
+
navigate?: (url: string) => void;
|
|
155
170
|
} | null;
|
|
156
171
|
/** tether-show="low-stock" at this many or fewer (default 5). */
|
|
157
172
|
lowStock?: number;
|
|
173
|
+
/** Milliseconds the search part waits after the last keystroke (default 150). */
|
|
174
|
+
searchDelay?: number;
|
|
158
175
|
/** true: ?variant= (and selling_plan=) follow the visitor's choice on the page's main product. */
|
|
159
176
|
variantUrl?: boolean;
|
|
160
177
|
/** Your text for Shopify's cart warnings (stock, codes), e.g. translated by code. Return nothing to keep Shopify's. */
|
|
@@ -186,7 +203,8 @@ export interface TetherConfig {
|
|
|
186
203
|
debug?: boolean;
|
|
187
204
|
}
|
|
188
205
|
|
|
189
|
-
export type TetherErrorCode =
|
|
206
|
+
export type TetherErrorCode =
|
|
207
|
+
'NOT_FOUND' | 'NETWORK' | 'NOT_CONFIGURED' | 'INVALID_COUNTRY' | 'ERROR' | `HTTP_${number}`;
|
|
190
208
|
|
|
191
209
|
export declare class TetherError extends Error {
|
|
192
210
|
name: 'TetherError';
|
|
@@ -208,9 +226,32 @@ export interface LineInput {
|
|
|
208
226
|
}
|
|
209
227
|
|
|
210
228
|
/** Shopify's note on the last cart change; target is the cart line id it's about. */
|
|
211
|
-
export interface CartWarning {
|
|
229
|
+
export interface CartWarning {
|
|
230
|
+
code: string;
|
|
231
|
+
message: string;
|
|
232
|
+
target?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* What the cart is known to be: `unknown` until the saved cart has loaded, `empty` without a cart, `error` after a
|
|
237
|
+
* failed load or change (with the cart as Shopify last sent it).
|
|
238
|
+
*/
|
|
239
|
+
export type CartStatus =
|
|
240
|
+
| { kind: 'unknown' }
|
|
241
|
+
| { kind: 'empty' }
|
|
242
|
+
| { kind: 'ready'; cart: ShopifyCart }
|
|
243
|
+
| { kind: 'error'; error: unknown; cart: ShopifyCart | null };
|
|
244
|
+
|
|
245
|
+
/** The logged-in customer the cart belongs to (set by the account part). */
|
|
246
|
+
export interface CustomerLink {
|
|
247
|
+
/** The Customer Account API token: carts created from now on belong to the customer. */
|
|
248
|
+
accessToken: string | null;
|
|
249
|
+
/** The account's own checkout URL; null from it, or no function, uses the cart's. */
|
|
250
|
+
beforeCheckout: (() => Promise<string | null>) | null;
|
|
251
|
+
}
|
|
212
252
|
|
|
213
253
|
export interface Cart {
|
|
254
|
+
readonly status: CartStatus;
|
|
214
255
|
readonly state: ShopifyCart | null;
|
|
215
256
|
readonly lines: ShopifyCartLine[];
|
|
216
257
|
readonly count: number;
|
|
@@ -219,10 +260,16 @@ export interface Cart {
|
|
|
219
260
|
readonly warnings: CartWarning[];
|
|
220
261
|
/** true once the saved cart (or the absence of one) is known. */
|
|
221
262
|
readonly loaded: boolean;
|
|
263
|
+
/** Links the cart to a logged-in customer, or unlinks it with nulls. Fields left out keep their value. */
|
|
264
|
+
setCustomer(link: Partial<CustomerLink>): void;
|
|
222
265
|
id(): string | null;
|
|
223
266
|
init(): Promise<ShopifyCart | null>;
|
|
224
267
|
/** add(variantId, qty) · add({ variantId, quantity, sellingPlanId, attributes }) · add([line, line]) */
|
|
225
|
-
add(
|
|
268
|
+
add(
|
|
269
|
+
input: string | LineInput | LineInput[],
|
|
270
|
+
quantity?: number,
|
|
271
|
+
opts?: Omit<LineInput, 'variantId' | 'quantity'>,
|
|
272
|
+
): Promise<ShopifyCart>;
|
|
226
273
|
/** updateLine(id, 3) · updateLine(id, { sellingPlanId: null }) switches to one-time. */
|
|
227
274
|
updateLine(lineId: string, patch: number | (LineInput & { quantity?: number })): Promise<ShopifyCart>;
|
|
228
275
|
removeLine(lineIds: string | string[]): Promise<ShopifyCart>;
|
|
@@ -237,7 +284,10 @@ export interface Cart {
|
|
|
237
284
|
/** Rejects with code INVALID_DISCOUNT (and doesn't keep the code) when Shopify doesn't accept it. */
|
|
238
285
|
applyDiscount(code: string): Promise<ShopifyCart>;
|
|
239
286
|
removeDiscount(code: string): Promise<ShopifyCart>;
|
|
240
|
-
setBuyerIdentity(
|
|
287
|
+
setBuyerIdentity(
|
|
288
|
+
identity: { countryCode?: string; email?: string; phone?: string },
|
|
289
|
+
opts?: { create?: boolean },
|
|
290
|
+
): Promise<ShopifyCart | null>;
|
|
241
291
|
checkout(target?: '_self' | '_blank'): Promise<void>;
|
|
242
292
|
open(): void;
|
|
243
293
|
close(): void;
|
|
@@ -246,7 +296,12 @@ export interface Cart {
|
|
|
246
296
|
drawers(): HTMLElement[];
|
|
247
297
|
render(): void;
|
|
248
298
|
/** For framework islands: fn(cart, meta) now, when the saved cart has loaded and after every change. Returns unsubscribe. */
|
|
249
|
-
subscribe(
|
|
299
|
+
subscribe(
|
|
300
|
+
fn: (
|
|
301
|
+
cart: ShopifyCart | null,
|
|
302
|
+
meta: { open: boolean; count: number; loaded: boolean; warnings: CartWarning[] },
|
|
303
|
+
) => void,
|
|
304
|
+
): () => void;
|
|
250
305
|
}
|
|
251
306
|
|
|
252
307
|
export interface ProductScope {
|
|
@@ -279,10 +334,16 @@ export interface SellingPlanHelpers {
|
|
|
279
334
|
planPrice(variant: ShopifyVariant, planId: string): Money | null;
|
|
280
335
|
planCheckoutCharge(variant: ShopifyVariant, planId: string): Money | null;
|
|
281
336
|
planSavings(variant: ShopifyVariant, planId: string): { amount: Money | null; percentage: number };
|
|
282
|
-
planAdjustment(
|
|
337
|
+
planAdjustment(
|
|
338
|
+
plan: ShopifySellingPlan | null | undefined,
|
|
339
|
+
): { type: 'percentage' | 'amount' | 'price'; value: number | Money } | null;
|
|
283
340
|
findPlan(product: ShopifyProduct, ref: string | null | undefined): ShopifySellingPlan | null;
|
|
284
341
|
toGid(value: string | number, type: string): string | null;
|
|
285
|
-
GID: {
|
|
342
|
+
GID: {
|
|
343
|
+
product(v: string | number): string | null;
|
|
344
|
+
variant(v: string | number): string | null;
|
|
345
|
+
sellingPlan(v: string | number): string | null;
|
|
346
|
+
};
|
|
286
347
|
}
|
|
287
348
|
|
|
288
349
|
export interface ProductPage {
|
|
@@ -294,18 +355,41 @@ export interface ProductPage {
|
|
|
294
355
|
export interface Localization {
|
|
295
356
|
country?: { isoCode: string; name: string; currency?: { isoCode: string; symbol: string } };
|
|
296
357
|
language?: { isoCode: string; name: string };
|
|
297
|
-
availableCountries: {
|
|
358
|
+
availableCountries: {
|
|
359
|
+
isoCode: string;
|
|
360
|
+
name: string;
|
|
361
|
+
currency?: { isoCode: string; symbol: string };
|
|
362
|
+
availableLanguages?: { isoCode: string; name: string }[];
|
|
363
|
+
}[];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type MarketSource = 'saved' | 'server' | 'webflow';
|
|
367
|
+
|
|
368
|
+
/** The market fields detection found, and the source each came from (no sources with detect: false). */
|
|
369
|
+
export interface DetectedMarket {
|
|
370
|
+
country?: string;
|
|
371
|
+
language?: string;
|
|
372
|
+
locale?: string;
|
|
373
|
+
sources?: Partial<Record<'country' | 'language' | 'locale', MarketSource>>;
|
|
298
374
|
}
|
|
299
375
|
|
|
300
376
|
export interface DiagnoseReport {
|
|
301
377
|
version: string;
|
|
302
378
|
config: Record<string, unknown>;
|
|
303
379
|
apiVersion: { version: string; ok: boolean; monthsLeft?: number; next?: string; note: string };
|
|
304
|
-
market
|
|
380
|
+
/** The market in use (a visitor's choice included), and what detection would pick (null without markets.detect). */
|
|
381
|
+
market: { country: string | null; language: string | null; locale: string | null; detected: DetectedMarket | null };
|
|
305
382
|
reachable: boolean;
|
|
306
383
|
shop: { name?: string; url?: string; currency?: string } | null;
|
|
307
384
|
latencyMs: number | null;
|
|
308
|
-
products: {
|
|
385
|
+
products: {
|
|
386
|
+
key: string;
|
|
387
|
+
handle?: string;
|
|
388
|
+
title?: string;
|
|
389
|
+
plans?: number;
|
|
390
|
+
requiresSellingPlan?: boolean;
|
|
391
|
+
error?: string;
|
|
392
|
+
}[];
|
|
309
393
|
sellingPlans: { product: string; group?: string; plan: string; id: string }[];
|
|
310
394
|
problems: { level: 'error' | 'warn'; message: string; element?: string }[];
|
|
311
395
|
}
|
|
@@ -316,7 +400,13 @@ export interface TetherEvents {
|
|
|
316
400
|
'product-ready': { product: ShopifyProduct; scope: ProductScope };
|
|
317
401
|
'product-updated': { product: ShopifyProduct; scope: ProductScope };
|
|
318
402
|
'product-not-found': { key: string | null; source: string };
|
|
319
|
-
'variant-changed': {
|
|
403
|
+
'variant-changed': {
|
|
404
|
+
product: ShopifyProduct;
|
|
405
|
+
variant: ShopifyVariant;
|
|
406
|
+
selected: Record<string, string>;
|
|
407
|
+
purchase: 'one-time' | 'subscription';
|
|
408
|
+
sellingPlanId: string | null;
|
|
409
|
+
};
|
|
320
410
|
'product-added': { product: ShopifyProduct; variant: ShopifyVariant; line: LineInput };
|
|
321
411
|
added: { cart: ShopifyCart; lines: LineInput[] };
|
|
322
412
|
'group-added': { group: string | null; lines: LineInput[] };
|
|
@@ -348,27 +438,74 @@ export interface Tether {
|
|
|
348
438
|
withContext(query: string, ctx?: { country?: string; language?: string }): string;
|
|
349
439
|
cart: Cart;
|
|
350
440
|
getProduct(handleOrId: string): Promise<ShopifyProduct>;
|
|
351
|
-
getProducts(opts?: {
|
|
352
|
-
|
|
441
|
+
getProducts(opts?: {
|
|
442
|
+
collection?: string;
|
|
443
|
+
query?: string;
|
|
444
|
+
first?: number;
|
|
445
|
+
after?: string | null;
|
|
446
|
+
sortKey?: string;
|
|
447
|
+
reverse?: boolean;
|
|
448
|
+
filters?: unknown[];
|
|
449
|
+
}): Promise<ProductPage>;
|
|
450
|
+
getRecommendations(
|
|
451
|
+
product: string | Pick<ShopifyProduct, 'id'>,
|
|
452
|
+
intent?: 'RELATED' | 'COMPLEMENTARY',
|
|
453
|
+
): Promise<ShopifyProduct[]>;
|
|
353
454
|
getScope(el: Element | null | undefined): ProductScope | null;
|
|
354
455
|
addGroup(button: Element): Promise<ShopifyCart | null>;
|
|
355
456
|
/** Customer accounts (loads the account part): login(), logout(), customer(), orders(), query(gql, vars), getToken(). */
|
|
356
457
|
account: {
|
|
357
458
|
login(opts?: { returnTo?: string; locale?: string }): Promise<void>;
|
|
358
459
|
logout(opts?: { returnTo?: string }): Promise<void>;
|
|
359
|
-
customer(): Promise<{
|
|
360
|
-
|
|
460
|
+
customer(): Promise<{
|
|
461
|
+
id: string;
|
|
462
|
+
firstName?: string | null;
|
|
463
|
+
lastName?: string | null;
|
|
464
|
+
displayName?: string;
|
|
465
|
+
emailAddress?: { emailAddress: string } | null;
|
|
466
|
+
} | null>;
|
|
467
|
+
orders(opts?: { first?: number }): Promise<
|
|
468
|
+
Array<{
|
|
469
|
+
id: string;
|
|
470
|
+
name: string;
|
|
471
|
+
processedAt: string;
|
|
472
|
+
financialStatus?: string;
|
|
473
|
+
fulfillmentStatus?: string;
|
|
474
|
+
statusPageUrl: string;
|
|
475
|
+
totalPrice: Money;
|
|
476
|
+
}>
|
|
477
|
+
>;
|
|
361
478
|
query<T = any>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
362
479
|
getToken(): Promise<string | null>;
|
|
480
|
+
/** True while a customer is logged in, as far as this page knows without asking. */
|
|
481
|
+
isLoggedIn(): Promise<boolean>;
|
|
482
|
+
/** Finishes a login coming back from Shopify (browser mode); true when there was one. */
|
|
483
|
+
handleCallback(): Promise<boolean>;
|
|
484
|
+
/** Fills the account markup in root: logged-in/out content, the customer, orders and the account pages. */
|
|
485
|
+
render(root?: ParentNode): Promise<void>;
|
|
486
|
+
/** What mount() runs: finishes a login coming back on the first call, then renders root. */
|
|
487
|
+
mountAccount(root?: ParentNode): Promise<void>;
|
|
363
488
|
/** One order in full (lines, totals, addresses, fulfillments with tracking, returnable lines); null if not the customer's. */
|
|
364
489
|
order(id: string | number): Promise<Record<string, any> | null>;
|
|
365
|
-
updateProfile(fields: {
|
|
490
|
+
updateProfile(fields: {
|
|
491
|
+
firstName?: string;
|
|
492
|
+
lastName?: string;
|
|
493
|
+
}): Promise<{ firstName?: string | null; lastName?: string | null }>;
|
|
366
494
|
addresses(): Promise<Array<CustomerAddress & { isDefault: boolean }>>;
|
|
367
|
-
saveAddress(
|
|
495
|
+
saveAddress(
|
|
496
|
+
address: Partial<CustomerAddressInput> | null,
|
|
497
|
+
opts?: { id?: string; isDefault?: boolean },
|
|
498
|
+
): Promise<CustomerAddress>;
|
|
368
499
|
deleteAddress(id: string): Promise<string>;
|
|
369
500
|
subscriptions(): Promise<Array<Record<string, any>>>;
|
|
370
|
-
subscriptionAction(
|
|
371
|
-
|
|
501
|
+
subscriptionAction(
|
|
502
|
+
id: string | number,
|
|
503
|
+
action: 'pause' | 'resume' | 'skip' | 'unskip' | 'cancel',
|
|
504
|
+
): Promise<Record<string, any>>;
|
|
505
|
+
requestReturn(
|
|
506
|
+
orderId: string | number,
|
|
507
|
+
lines: Array<{ lineItemId: string; quantity: number; reasonId?: string; note?: string }>,
|
|
508
|
+
): Promise<{ id: string; status: string }>;
|
|
372
509
|
};
|
|
373
510
|
primeProduct(product: ShopifyProduct): void;
|
|
374
511
|
normalizeProductKey(raw: string | number): string | null;
|
|
@@ -381,12 +518,15 @@ export interface Tether {
|
|
|
381
518
|
resolveProductKey(el: Element, location?: Location | URL): { key: string | null; fromUrl: boolean; raw: string };
|
|
382
519
|
/** Switch market: prices, currency, cart and every product on the page follow. */
|
|
383
520
|
setCountry(code: string, opts?: SetCountryOptions): Promise<ShopifyCart | null>;
|
|
384
|
-
detectMarket(markets?: MarketsConfig): Promise<
|
|
521
|
+
detectMarket(markets?: MarketsConfig | null): Promise<DetectedMarket>;
|
|
385
522
|
getLocalization(): Promise<Localization>;
|
|
386
523
|
/** Logs and returns what's wrong on this page (config, token, API version, plans, markup). */
|
|
387
524
|
diagnose(opts?: { root?: ParentNode; silent?: boolean }): Promise<DiagnoseReport>;
|
|
388
525
|
/** Listen to a Tether event; returns a function that stops listening. */
|
|
389
|
-
on<K extends keyof TetherEvents>(
|
|
526
|
+
on<K extends keyof TetherEvents>(
|
|
527
|
+
name: K,
|
|
528
|
+
fn: (detail: TetherEvents[K], event: CustomEvent<TetherEvents[K]>) => void,
|
|
529
|
+
): () => void;
|
|
390
530
|
on(name: string, fn: (detail: any, event: CustomEvent) => void): () => void;
|
|
391
531
|
}
|
|
392
532
|
|
|
@@ -418,9 +558,20 @@ declare global {
|
|
|
418
558
|
}
|
|
419
559
|
|
|
420
560
|
export interface CustomerAddressInput {
|
|
421
|
-
firstName: string;
|
|
422
|
-
|
|
561
|
+
firstName: string;
|
|
562
|
+
lastName: string;
|
|
563
|
+
company: string;
|
|
564
|
+
address1: string;
|
|
565
|
+
address2: string;
|
|
566
|
+
city: string;
|
|
567
|
+
zip: string;
|
|
568
|
+
zoneCode: string;
|
|
569
|
+
countryCode: string;
|
|
570
|
+
phoneNumber: string;
|
|
423
571
|
}
|
|
424
572
|
export interface CustomerAddress extends Partial<CustomerAddressInput> {
|
|
425
|
-
id: string;
|
|
573
|
+
id: string;
|
|
574
|
+
province?: string | null;
|
|
575
|
+
country?: string | null;
|
|
576
|
+
formatted: string[];
|
|
426
577
|
}
|