@occluze/tether 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -2
- package/dist/presets/barba.esm.js +19 -23
- package/dist/tether.account-pages.js +320 -365
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +242 -491
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +66 -88
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +36 -33
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +28 -36
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1270 -894
- package/dist/tether.markets.js +73 -90
- 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 +44 -68
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +24 -23
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/{order-lines.js → order-lines.ts} +20 -41
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/{page-events.js → page-events.ts} +55 -90
- package/src/account/server-client.ts +183 -0
- package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/{account-pages.js → account-pages.ts} +29 -37
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
- package/src/analytics.ts +315 -0
- package/src/{api-version.js → api-version.ts} +25 -31
- package/src/{astro.js → astro.ts} +110 -104
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/{drawer.js → drawer.ts} +18 -31
- package/src/cart/mutations.ts +237 -0
- package/src/cart/{render.js → render.ts} +68 -96
- package/src/cart/store.ts +279 -0
- package/src/{cart.js → cart.ts} +82 -128
- package/src/client.ts +391 -0
- package/src/{collection.js → collection.ts} +30 -38
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
- package/src/controls/{discount.js → discount.ts} +13 -34
- package/src/controls/{extras.js → extras.ts} +15 -24
- package/src/controls/{navigation.js → navigation.ts} +24 -30
- package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
- package/src/{diagnose.js → diagnose.ts} +95 -96
- package/src/dom.ts +268 -0
- package/src/{extras.js → extras.ts} +43 -52
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/{images.js → images.ts} +30 -43
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/{loader.js → loader.ts} +149 -123
- package/src/market-codes.ts +6 -0
- package/src/{markets-ui.js → markets-ui.ts} +91 -68
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/{mounting.js → mounting.ts} +88 -101
- package/src/{page-market.js → page-market.ts} +32 -38
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/{product-events.js → product-events.ts} +33 -48
- package/src/{product-render.js → product-render.ts} +38 -77
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/{product.js → product.ts} +90 -126
- package/src/{repeaters.js → repeaters.ts} +40 -52
- package/src/{search.js → search.ts} +85 -103
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/{shopify-values.js → shopify-values.ts} +24 -22
- package/src/{status.js → status.ts} +34 -54
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
- package/src/{variant-render.js → variant-render.ts} +24 -50
- package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
- package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
- package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
- package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
- package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
- package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
- package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
- package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
- package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
- package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
- package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
- package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
- package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
- package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
- package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
- package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
- package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
- package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
- package/dist/esm/chunks/search-OU75RNQL.js +0 -1
- package/src/account/addresses-view.js +0 -92
- package/src/account/browser-session.js +0 -359
- package/src/account/markup.js +0 -48
- package/src/account/operations.js +0 -180
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/server-client.js +0 -172
- package/src/account/shared.js +0 -111
- package/src/account/shopify-auth.js +0 -150
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics.js +0 -352
- package/src/cart/checkout.js +0 -97
- package/src/cart/mutations.js +0 -234
- package/src/cart/store.js +0 -326
- package/src/client.js +0 -267
- package/src/config.js +0 -349
- package/src/dom.js +0 -287
- package/src/fragments.js +0 -237
- package/src/index.js +0 -353
- package/src/initial.js +0 -87
- package/src/lazy-iife.js +0 -116
- package/src/lazy.js +0 -39
- package/src/market-codes.js +0 -35
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/parts/account-pages.js +0 -6
- package/src/parts/account.js +0 -6
- package/src/parts/analytics.js +0 -6
- package/src/parts/diagnose.js +0 -6
- package/src/parts/extras.js +0 -6
- package/src/parts/markets.js +0 -6
- package/src/parts/search.js +0 -6
- package/src/presets/barba.js +0 -190
- package/src/product-state.js +0 -318
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/types/astro.d.ts +0 -28
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -577
- package/types/server.d.ts +0 -182
package/src/analytics.js
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analytics, as an optional part (loaded when config.analytics is set).
|
|
3
|
-
*
|
|
4
|
-
* analytics: {
|
|
5
|
-
* dataLayer: true, // GA4 / Google Tag Manager ecommerce events (window.dataLayer)
|
|
6
|
-
* shopify: true, // Shopify's own analytics (experimental, see docs/analytics.md)
|
|
7
|
-
* onEvent(name, data) {}, // any other tool: Meta, TikTok, Plausible
|
|
8
|
-
* }
|
|
9
|
-
*
|
|
10
|
-
* Events: view_item (the page's product), view_item_list (lists), add_to_cart / remove_from_cart (from the cart's
|
|
11
|
-
* changes, so quick +/- and JS calls count too), view_cart (cart opened), begin_checkout, search (search as you type).
|
|
12
|
-
* Purchases happen on Shopify's checkout: use Shopify's Google & YouTube app or a checkout pixel for those.
|
|
13
|
-
* Shopify's analytics are set up in analytics-shopify.js.
|
|
14
|
-
*/
|
|
15
|
-
import { cfg, reportError } from './config.js';
|
|
16
|
-
import { sel } from './dom.js';
|
|
17
|
-
import { Cart } from './cart.js';
|
|
18
|
-
import { startShopifyAnalytics } from './analytics-shopify.js';
|
|
19
|
-
/** @import { Money } from './shopify-values.js' */
|
|
20
|
-
/** @import { Product, ProductVariant } from './fragments.js' */
|
|
21
|
-
/** @import { AnalyticsBus, EventPayload } from './analytics-shopify.js' */
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* cart.js has no types yet; these are the fields of its cart (`fragment TetherCart`) this module reads.
|
|
25
|
-
*
|
|
26
|
-
* @typedef {object} LineProduct
|
|
27
|
-
* @property {string} id
|
|
28
|
-
* @property {string} title
|
|
29
|
-
* @property {string} handle
|
|
30
|
-
* @property {string} vendor
|
|
31
|
-
* @typedef {{ id: string, title: string, sku: string | null, price: Money, product: LineProduct }} LineVariant
|
|
32
|
-
* @typedef {{ id: string, quantity: number, merchandise: LineVariant }} CartLine
|
|
33
|
-
* @typedef {object} AnalyticsCart
|
|
34
|
-
* @property {{ nodes: CartLine[] }} lines
|
|
35
|
-
* @property {{ totalAmount: Money }} cost
|
|
36
|
-
* @property {{ code: string, applicable: boolean }[]} discountCodes
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {object} ItemProduct What a GA4 item is built from: a product, or a cart line's product.
|
|
41
|
-
* @property {string} id
|
|
42
|
-
* @property {string} title
|
|
43
|
-
* @property {string} [vendor]
|
|
44
|
-
* @property {string} [productType]
|
|
45
|
-
* @property {{ minVariantPrice: Money }} [priceRange]
|
|
46
|
-
*/
|
|
47
|
-
/** @typedef {Pick<ProductVariant, 'id' | 'title' | 'sku' | 'price'>} ItemVariant */
|
|
48
|
-
/**
|
|
49
|
-
* @typedef {object} AnalyticsItem A GA4 ecommerce item.
|
|
50
|
-
* @property {string} item_id
|
|
51
|
-
* @property {string} item_name
|
|
52
|
-
* @property {string | undefined} item_variant
|
|
53
|
-
* @property {string | undefined} item_brand
|
|
54
|
-
* @property {string | undefined} item_category
|
|
55
|
-
* @property {number} price
|
|
56
|
-
* @property {number} quantity
|
|
57
|
-
* @property {number} [index]
|
|
58
|
-
*/
|
|
59
|
-
/** @typedef {{ id: string, handle: string, title: string }} ListCollection */
|
|
60
|
-
/** @typedef {{ product: Product, scope: { root: Element, variant: ProductVariant | null } | null }} ProductReadyDetail */
|
|
61
|
-
/** @typedef {{ products: Product[] | undefined, collection: ListCollection | null }} ListReadyDetail */
|
|
62
|
-
/** @typedef {{ cart: AnalyticsCart | null, reason: string }} CartUpdatedDetail */
|
|
63
|
-
|
|
64
|
-
/** Shopify's title for the one variant of a product without options. */
|
|
65
|
-
const DEFAULT_VARIANT_TITLE = 'Default Title';
|
|
66
|
-
const CENTS_PER_UNIT = 100;
|
|
67
|
-
/** GA4 drops an event with more items than this. */
|
|
68
|
-
const MAX_LIST_ITEMS = 200;
|
|
69
|
-
|
|
70
|
-
/** @param {Money | null | undefined} money */
|
|
71
|
-
const amountOf = (money) => (money ? parseFloat(money.amount) : 0);
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param {string | null | undefined} gid
|
|
75
|
-
* @returns {string} The numeric part of a gid.
|
|
76
|
-
*/
|
|
77
|
-
const numericId = (gid) => (gid ?? '').split('/').pop() ?? '';
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @param {ItemProduct} product
|
|
81
|
-
* @param {ItemVariant | null | undefined} variant
|
|
82
|
-
* @param {number} [quantity]
|
|
83
|
-
* @param {number} [index] The position in a list.
|
|
84
|
-
* @returns {AnalyticsItem}
|
|
85
|
-
*/
|
|
86
|
-
function itemFromVariant(product, variant, quantity = 1, index) {
|
|
87
|
-
/** @type {AnalyticsItem} */
|
|
88
|
-
const item = {
|
|
89
|
-
item_id: variant?.sku || numericId(variant?.id) || numericId(product.id),
|
|
90
|
-
item_name: product.title,
|
|
91
|
-
item_variant: variant?.title === DEFAULT_VARIANT_TITLE ? undefined : variant?.title,
|
|
92
|
-
item_brand: product.vendor || undefined,
|
|
93
|
-
item_category: product.productType || undefined,
|
|
94
|
-
price: amountOf(variant?.price ?? product.priceRange?.minVariantPrice),
|
|
95
|
-
quantity,
|
|
96
|
-
};
|
|
97
|
-
if (index != null) item.index = index;
|
|
98
|
-
return item;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* @param {CartLine} line
|
|
103
|
-
* @param {number} [quantity]
|
|
104
|
-
*/
|
|
105
|
-
const itemFromLine = (line, quantity = line.quantity) =>
|
|
106
|
-
itemFromVariant(line.merchandise.product, line.merchandise, quantity);
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {AnalyticsItem[]} items
|
|
110
|
-
* @returns {number} The items' total, rounded to cents.
|
|
111
|
-
*/
|
|
112
|
-
function totalValue(items) {
|
|
113
|
-
const total = items.reduce((sum, item) => sum + item.price * (item.quantity || 1), 0);
|
|
114
|
-
return Math.round(total * CENTS_PER_UNIT) / CENTS_PER_UNIT;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @param {Product} product
|
|
119
|
-
* @param {ItemVariant} variant
|
|
120
|
-
* @param {number} [quantity]
|
|
121
|
-
*/
|
|
122
|
-
const productPayload = (product, variant, quantity = 1) => ({
|
|
123
|
-
id: product.id,
|
|
124
|
-
title: product.title,
|
|
125
|
-
price: variant.price,
|
|
126
|
-
vendor: product.vendor || '',
|
|
127
|
-
variantId: variant.id,
|
|
128
|
-
variantTitle: variant.title,
|
|
129
|
-
quantity,
|
|
130
|
-
sku: variant.sku || undefined,
|
|
131
|
-
productType: product.productType || undefined,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/** @param {CartLine} line */
|
|
135
|
-
const linePayload = (line) => ({
|
|
136
|
-
id: line.id,
|
|
137
|
-
quantity: line.quantity,
|
|
138
|
-
merchandise: {
|
|
139
|
-
id: line.merchandise.id,
|
|
140
|
-
title: line.merchandise.title,
|
|
141
|
-
price: line.merchandise.price,
|
|
142
|
-
sku: line.merchandise.sku,
|
|
143
|
-
product: {
|
|
144
|
-
id: line.merchandise.product.id,
|
|
145
|
-
title: line.merchandise.product.title,
|
|
146
|
-
vendor: line.merchandise.product.vendor || '',
|
|
147
|
-
handle: line.merchandise.product.handle,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
/** @returns {unknown[]} The GA4 / GTM data layer, created when the page has none. */
|
|
153
|
-
function dataLayer() {
|
|
154
|
-
const { dataLayer: option } = cfg.analytics ?? {};
|
|
155
|
-
const name = typeof option === 'string' ? option : 'dataLayer';
|
|
156
|
-
/** @type {unknown} */
|
|
157
|
-
const existing = Reflect.get(window, name);
|
|
158
|
-
if (Array.isArray(existing)) return existing;
|
|
159
|
-
/** @type {unknown[]} */
|
|
160
|
-
const created = [];
|
|
161
|
-
Reflect.set(window, name, created);
|
|
162
|
-
return created;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {string} event
|
|
167
|
-
* @param {Record<string, unknown> | undefined} ecommerce
|
|
168
|
-
*/
|
|
169
|
-
function pushToDataLayer(event, ecommerce) {
|
|
170
|
-
const layer = dataLayer();
|
|
171
|
-
if (!ecommerce) {
|
|
172
|
-
layer.push({ event });
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Google asks for the previous ecommerce object to be cleared first.
|
|
176
|
-
layer.push({ ecommerce: null });
|
|
177
|
-
layer.push({ event, ecommerce });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** @type {AnalyticsBus | null} */
|
|
181
|
-
let bus = null;
|
|
182
|
-
/** @type {Map<string, CartLine> | null} Variant id to line, to turn cart changes into add and remove events. */
|
|
183
|
-
let lastLines = null;
|
|
184
|
-
/** @type {AnalyticsCart | null} */
|
|
185
|
-
let lastCart = null;
|
|
186
|
-
/** @type {WeakSet<Element>} Product roots whose view_item was sent on this page. */
|
|
187
|
-
let viewed = new WeakSet();
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* @param {string} name The GA4 event.
|
|
191
|
-
* @param {Record<string, unknown>} data
|
|
192
|
-
* @param {string} [shopifyEvent]
|
|
193
|
-
* @param {EventPayload} [shopifyPayload]
|
|
194
|
-
*/
|
|
195
|
-
function send(name, data, shopifyEvent, shopifyPayload) {
|
|
196
|
-
if (cfg.analytics?.dataLayer) pushToDataLayer(name, data);
|
|
197
|
-
if (bus && shopifyEvent) bus.publish(shopifyEvent, shopifyPayload);
|
|
198
|
-
try {
|
|
199
|
-
cfg.analytics?.onEvent?.(name, data);
|
|
200
|
-
} catch (error) {
|
|
201
|
-
reportError(error, 'analytics onEvent');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** @returns {unknown} The scope of the page's own product: a product root outside every list. */
|
|
206
|
-
function mainProduct() {
|
|
207
|
-
const el = document.querySelector(sel('product'));
|
|
208
|
-
if (!el || el.closest(`${sel('collection')},${sel('products')},${sel('recommendations')}`)) return null;
|
|
209
|
-
/** @type {unknown} */
|
|
210
|
-
const tether = Reflect.get(window, 'Tether');
|
|
211
|
-
// The core sets window.Tether before it loads this part.
|
|
212
|
-
return /** @type {{ getScope: (el: Element) => unknown }} */ (tether).getScope(el);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** @param {ProductReadyDetail} detail */
|
|
216
|
-
function onProductReady({ product, scope }) {
|
|
217
|
-
if (!scope || viewed.has(scope.root) || mainProduct() !== scope) return;
|
|
218
|
-
viewed.add(scope.root);
|
|
219
|
-
const variant = scope.variant ?? product.variants.nodes[0];
|
|
220
|
-
const items = [itemFromVariant(product, variant)];
|
|
221
|
-
const products = variant ? [productPayload(product, variant)] : [];
|
|
222
|
-
send('view_item', { currency: variant?.price.currencyCode, value: totalValue(items), items }, 'product_viewed', {
|
|
223
|
-
products,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** @param {ListReadyDetail} detail */
|
|
228
|
-
function onListReady({ products = [], collection }) {
|
|
229
|
-
const items = products
|
|
230
|
-
.slice(0, MAX_LIST_ITEMS)
|
|
231
|
-
.map((product, index) => itemFromVariant(product, product.variants.nodes[0], 1, index));
|
|
232
|
-
const list = { item_list_id: collection?.handle || 'products', item_list_name: collection?.title || 'Products' };
|
|
233
|
-
if (!collection) {
|
|
234
|
-
send('view_item_list', { ...list, items });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
send('view_item_list', { ...list, items }, 'collection_viewed', {
|
|
238
|
-
collection: { id: collection.id, handle: collection.handle },
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* @param {AnalyticsCart | null} cart
|
|
244
|
-
* @param {Map<string, CartLine>} now
|
|
245
|
-
* @param {Map<string, CartLine>} before
|
|
246
|
-
*/
|
|
247
|
-
function sendCartChanges(cart, now, before) {
|
|
248
|
-
const currency = cart?.cost.totalAmount.currencyCode;
|
|
249
|
-
now.forEach((line, variantId) => {
|
|
250
|
-
const previous = before.get(variantId);
|
|
251
|
-
const added = line.quantity - (previous?.quantity ?? 0);
|
|
252
|
-
if (added <= 0) return;
|
|
253
|
-
const items = [itemFromLine(line, added)];
|
|
254
|
-
send('add_to_cart', { currency, value: totalValue(items), items }, 'product_added_to_cart', {
|
|
255
|
-
cart,
|
|
256
|
-
prevCart: lastCart,
|
|
257
|
-
currentLine: linePayload(line),
|
|
258
|
-
prevLine: previous ? linePayload(previous) : undefined,
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
before.forEach((line, variantId) => {
|
|
262
|
-
const removed = line.quantity - (now.get(variantId)?.quantity ?? 0);
|
|
263
|
-
if (removed <= 0) return;
|
|
264
|
-
const items = [itemFromLine(line, removed)];
|
|
265
|
-
send('remove_from_cart', { currency, value: totalValue(items), items });
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** @param {CartUpdatedDetail} detail */
|
|
270
|
-
function onCartUpdated({ cart, reason }) {
|
|
271
|
-
const now = new Map((cart?.lines.nodes ?? []).map((line) => [line.merchandise.id, line]));
|
|
272
|
-
// Loading the saved cart and showing a pending change aren't changes the visitor made.
|
|
273
|
-
if (lastLines && reason !== 'load' && reason !== 'pending') sendCartChanges(cart, now, lastLines);
|
|
274
|
-
lastLines = now;
|
|
275
|
-
lastCart = cart;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function onCartOpened() {
|
|
279
|
-
const items = Cart.lines.map((line) => itemFromLine(line));
|
|
280
|
-
send('view_cart', { currency: Cart.state?.cost.totalAmount.currencyCode, value: totalValue(items), items });
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** @param {{ cart: AnalyticsCart }} detail */
|
|
284
|
-
function onCheckout({ cart }) {
|
|
285
|
-
const items = cart.lines.nodes.map((line) => itemFromLine(line));
|
|
286
|
-
const coupons = cart.discountCodes.filter((discount) => discount.applicable).map((discount) => discount.code);
|
|
287
|
-
send('begin_checkout', {
|
|
288
|
-
currency: cart.cost.totalAmount.currencyCode,
|
|
289
|
-
value: totalValue(items),
|
|
290
|
-
coupon: coupons.join(',') || undefined,
|
|
291
|
-
items,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Turns one Tether event into analytics events. The details are Tether's own (`emit`), typed per event here.
|
|
297
|
-
*
|
|
298
|
-
* @param {Event} event `<prefix>:<name>`.
|
|
299
|
-
*/
|
|
300
|
-
function handle(event) {
|
|
301
|
-
const name = event.type.slice(cfg.prefix.length + 1);
|
|
302
|
-
/** @type {unknown} */
|
|
303
|
-
const detail = Reflect.get(event, 'detail');
|
|
304
|
-
switch (name) {
|
|
305
|
-
case 'product-ready':
|
|
306
|
-
onProductReady(/** @type {ProductReadyDetail} */ (detail));
|
|
307
|
-
break;
|
|
308
|
-
case 'list-ready':
|
|
309
|
-
onListReady(/** @type {ListReadyDetail} */ (detail));
|
|
310
|
-
break;
|
|
311
|
-
case 'cart-updated':
|
|
312
|
-
onCartUpdated(/** @type {CartUpdatedDetail} */ (detail));
|
|
313
|
-
break;
|
|
314
|
-
case 'cart-toggled':
|
|
315
|
-
if (/** @type {{ open: boolean }} */ (detail).open) onCartOpened();
|
|
316
|
-
break;
|
|
317
|
-
case 'checkout':
|
|
318
|
-
onCheckout(/** @type {{ cart: AnalyticsCart }} */ (detail));
|
|
319
|
-
break;
|
|
320
|
-
case 'search': {
|
|
321
|
-
const { term } = /** @type {{ term: string }} */ (detail);
|
|
322
|
-
if (term) send('search', { search_term: term }, 'search_viewed', { searchTerm: term });
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
case 'page':
|
|
326
|
-
viewed = new WeakSet();
|
|
327
|
-
bus?.publish('page_viewed', {});
|
|
328
|
-
break;
|
|
329
|
-
default:
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Called by the core with the events it buffered while this part loaded; the queue then handles events as they come.
|
|
336
|
-
*
|
|
337
|
-
* @param {Event[]} queue
|
|
338
|
-
*/
|
|
339
|
-
export async function startAnalytics(queue) {
|
|
340
|
-
if (cfg.analytics?.shopify) {
|
|
341
|
-
bus = await startShopifyAnalytics(() => mainProduct() != null).catch((/** @type {unknown} */ error) => {
|
|
342
|
-
reportError(error, 'shopify analytics');
|
|
343
|
-
return null;
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
bus?.publish('page_viewed', {});
|
|
347
|
-
queue.splice(0).forEach(handle);
|
|
348
|
-
queue.push = (...events) => {
|
|
349
|
-
events.forEach(handle);
|
|
350
|
-
return queue.length;
|
|
351
|
-
};
|
|
352
|
-
}
|
package/src/cart/checkout.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sending the visitor to Shopify's checkout. The URL comes from Shopify at click time, because the cart's checkout URL
|
|
3
|
-
* changes with its market and customer; a logged-in customer's account part may supply its own (`beforeCheckout`).
|
|
4
|
-
*/
|
|
5
|
-
import { cfg, reportError } from '../config.js';
|
|
6
|
-
import { TetherError } from '../client.js';
|
|
7
|
-
import { isHttpsUrl } from '../shopify-values.js';
|
|
8
|
-
import { emit } from '../dom.js';
|
|
9
|
-
import { fetchCheckoutUrl } from './mutations.js';
|
|
10
|
-
import { customer, currentCart, savedCartId } from './store.js';
|
|
11
|
-
/** @import { CheckoutTarget } from '../config.js' */
|
|
12
|
-
|
|
13
|
-
/** @typedef {{ kind: 'url', url: string | null } | { kind: 'failed', error: unknown }} CheckoutUrlAnswer */
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} cartId
|
|
17
|
-
* @returns {Promise<CheckoutUrlAnswer>} Shopify's answer as a value, so the request can run alongside the account's
|
|
18
|
-
* and its failure is read only when its URL is the one used.
|
|
19
|
-
*/
|
|
20
|
-
const askCheckoutUrl = (cartId) =>
|
|
21
|
-
fetchCheckoutUrl(cartId).then(
|
|
22
|
-
(url) => ({ kind: /** @type {const} */ ('url'), url }),
|
|
23
|
-
(/** @type {unknown} */ error) => ({ kind: /** @type {const} */ ('failed'), error }),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @returns {Promise<string | null>} The logged-in customer's checkout URL. When the account can't give one, the
|
|
28
|
-
* failure is reported and the cart's own checkout URL is used, which still checks out (without the login).
|
|
29
|
-
*/
|
|
30
|
-
async function accountCheckoutUrl() {
|
|
31
|
-
if (!customer.beforeCheckout) return null;
|
|
32
|
-
try {
|
|
33
|
-
return await customer.beforeCheckout();
|
|
34
|
-
} catch (error) {
|
|
35
|
-
reportError(error, 'account checkout');
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} cartId
|
|
42
|
-
* @returns {Promise<string>}
|
|
43
|
-
* @throws {TetherError} `ERROR` without a checkout URL (the cart is empty or Shopify no longer has it).
|
|
44
|
-
*/
|
|
45
|
-
async function checkoutUrl(cartId) {
|
|
46
|
-
const cartAnswer = askCheckoutUrl(cartId);
|
|
47
|
-
let url = await accountCheckoutUrl();
|
|
48
|
-
if (!url) {
|
|
49
|
-
const answer = await cartAnswer;
|
|
50
|
-
if (answer.kind === 'failed') throw answer.error;
|
|
51
|
-
url = answer.url;
|
|
52
|
-
}
|
|
53
|
-
if (!url) throw new TetherError('No checkoutUrl (cart empty or expired)');
|
|
54
|
-
if (!customer.accessToken) return url;
|
|
55
|
-
return `${url}${url.includes('?') ? '&' : '?'}logged_in=true`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* A new tab's `opener` gives the checkout page a handle on this one; a browser that doesn't let a script clear it
|
|
60
|
-
* leaves the tab as it opened.
|
|
61
|
-
*
|
|
62
|
-
* @param {Window} tab
|
|
63
|
-
*/
|
|
64
|
-
function detachOpenerIfAllowed(tab) {
|
|
65
|
-
try {
|
|
66
|
-
tab.opener = null;
|
|
67
|
-
} catch {
|
|
68
|
-
// The browser keeps `opener` read-only: see the function's comment.
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Goes to Shopify's checkout. With `_blank` the tab opens inside the click (Safari blocks `window.open` after an
|
|
74
|
-
* await) and gets its URL once there is one.
|
|
75
|
-
*
|
|
76
|
-
* @param {CheckoutTarget} target
|
|
77
|
-
* @returns {Promise<string | null>} The checkout URL, `null` with nothing to check out or after a reported failure.
|
|
78
|
-
*/
|
|
79
|
-
export async function checkout(target = cfg.checkoutTarget) {
|
|
80
|
-
const cartId = savedCartId();
|
|
81
|
-
const cart = currentCart();
|
|
82
|
-
if (!cartId || (cart && !cart.totalQuantity)) return null;
|
|
83
|
-
const tab = target === '_blank' ? window.open('', '_blank') : null;
|
|
84
|
-
if (tab) detachOpenerIfAllowed(tab);
|
|
85
|
-
try {
|
|
86
|
-
const url = await checkoutUrl(cartId);
|
|
87
|
-
if (!isHttpsUrl(url)) throw new TetherError('The checkout URL is not https: not opened');
|
|
88
|
-
emit('checkout', { url, cart: currentCart() });
|
|
89
|
-
if (tab && !tab.closed) tab.location.href = url;
|
|
90
|
-
else window.location.href = url;
|
|
91
|
-
return url;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
tab?.close();
|
|
94
|
-
reportError(error, 'checkout failed');
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/cart/mutations.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Storefront Cart API: loading the saved cart, creating one, and the mutations that change it. Shopify forgets a
|
|
3
|
-
* cart some days after its last change and once it has been checked out; a change that needs a cart then creates a
|
|
4
|
-
* new one with the same input, so the visitor's click still lands.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, log } from '../config.js';
|
|
7
|
-
import { gql, unwrap, TetherError } from '../client.js';
|
|
8
|
-
import { cartFragment } from '../fragments.js';
|
|
9
|
-
import { GID } from '../selling-plans.js';
|
|
10
|
-
import { toAttributes } from '../dom.js';
|
|
11
|
-
import { customer, savedCartId, forgetSavedCart, setWarnings } from './store.js';
|
|
12
|
-
/** @import { CartWarning } from '../config.js' */
|
|
13
|
-
/** @import { UserError } from '../client.js' */
|
|
14
|
-
/** @import { AttributeValue } from '../dom.js' */
|
|
15
|
-
/** @import { ShopifyCart, Attribute } from './store.js' */
|
|
16
|
-
|
|
17
|
-
/** @typedef {Record<string, AttributeValue> | { key: string | number, value?: AttributeValue }[]} Attributes */
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {object} LineSpec A line to add, in any of the shapes `Cart.add` takes.
|
|
20
|
-
* @property {string | number} [variantId]
|
|
21
|
-
* @property {string | number} [merchandiseId]
|
|
22
|
-
* @property {string | number} [id]
|
|
23
|
-
* @property {number | string} [quantity]
|
|
24
|
-
* @property {string | number | null} [sellingPlanId]
|
|
25
|
-
* @property {string | number | null} [sellingPlan]
|
|
26
|
-
* @property {Attributes | null} [attributes]
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {object} CartLineInput Shopify's `CartLineInput`.
|
|
30
|
-
* @property {string} merchandiseId
|
|
31
|
-
* @property {number} quantity
|
|
32
|
-
* @property {string} [sellingPlanId]
|
|
33
|
-
* @property {Attribute[]} [attributes]
|
|
34
|
-
*/
|
|
35
|
-
/**
|
|
36
|
-
* @typedef {object} CartPayload What every cart mutation answers.
|
|
37
|
-
* @property {ShopifyCart | null} cart
|
|
38
|
-
* @property {UserError[]} userErrors
|
|
39
|
-
* @property {CartWarning[]} [warnings]
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Shopify answers a cart id it can't resolve, in a mutation, with a top-level GraphQL error whose only detail is
|
|
44
|
-
* this message (no `extensions.code`). A user error on the `cartId` field is recognised by its shape instead.
|
|
45
|
-
*/
|
|
46
|
-
const UNKNOWN_CART_MESSAGE = /does not exist|not found|invalid id/i;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {unknown} error
|
|
50
|
-
* @returns {boolean} True when `error` says Shopify doesn't have the cart, false for any other failure (the network,
|
|
51
|
-
* the token, a line it refused), after which the saved cart is still good.
|
|
52
|
-
*/
|
|
53
|
-
export function isCartGone(error) {
|
|
54
|
-
if (!(error instanceof TetherError)) return false;
|
|
55
|
-
if (error.userErrors?.some((userError) => userError.field?.[0] === 'cartId')) return true;
|
|
56
|
-
return (error.errors ?? []).some((graphQLError) => UNKNOWN_CART_MESSAGE.test(graphQLError.message));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param {string | number | LineSpec | null | undefined} input A variant id, or a line.
|
|
61
|
-
* @param {number} [quantity] For a variant id.
|
|
62
|
-
* @param {Omit<LineSpec, 'variantId' | 'quantity'>} [options] For a variant id.
|
|
63
|
-
* @returns {CartLineInput}
|
|
64
|
-
* @throws {TetherError} `ERROR` without a variant id.
|
|
65
|
-
*/
|
|
66
|
-
export function toLineInput(input, quantity = 1, options = {}) {
|
|
67
|
-
const spec = typeof input === 'object' && input !== null ? input : { ...options, variantId: input, quantity };
|
|
68
|
-
const merchandiseId = GID.variant(spec.merchandiseId || spec.variantId || spec.id);
|
|
69
|
-
if (!merchandiseId) throw new TetherError('add(): variantId is required');
|
|
70
|
-
const asked = parseInt(String(spec.quantity ?? 1), 10);
|
|
71
|
-
/** @type {CartLineInput} */
|
|
72
|
-
const line = { merchandiseId, quantity: Math.max(1, asked || 1) };
|
|
73
|
-
const planId = GID.sellingPlan(spec.sellingPlanId || spec.sellingPlan);
|
|
74
|
-
if (planId) line.sellingPlanId = planId;
|
|
75
|
-
const attributes = toAttributes(spec.attributes);
|
|
76
|
-
if (attributes.length) line.attributes = attributes;
|
|
77
|
-
return line;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @param {string} name The mutation, which is also its payload's field.
|
|
82
|
-
* @param {string} args The operation's variable definitions.
|
|
83
|
-
* @param {string} inner The mutation field's arguments.
|
|
84
|
-
*/
|
|
85
|
-
function mutationText(name, args, inner) {
|
|
86
|
-
return `mutation Tether_${name}(${args}) {
|
|
87
|
-
${name}(${inner}) { cart { ...TetherCart } userErrors { field message } warnings { code message target } }
|
|
88
|
-
}
|
|
89
|
-
${cartFragment()}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param {string} name
|
|
94
|
-
* @param {string} args
|
|
95
|
-
* @param {string} inner
|
|
96
|
-
* @param {Record<string, unknown>} variables
|
|
97
|
-
* @returns {Promise<ShopifyCart | null>} The cart in Shopify's answer, after keeping its warnings.
|
|
98
|
-
* @throws {TetherError} For a failed request, or the mutation's user errors.
|
|
99
|
-
*/
|
|
100
|
-
async function send(name, args, inner, variables) {
|
|
101
|
-
/** @type {Record<string, CartPayload | null | undefined>} */
|
|
102
|
-
const data = await gql(mutationText(name, args, inner), variables);
|
|
103
|
-
const payload = unwrap(data[name], name);
|
|
104
|
-
setWarnings(payload.warnings ?? []);
|
|
105
|
-
return payload.cart;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* A new cart in the visitor's market, for the logged-in customer if there is one.
|
|
110
|
-
*
|
|
111
|
-
* @param {Record<string, unknown>} input More of Shopify's `CartInput`, e.g. the first lines.
|
|
112
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
113
|
-
*/
|
|
114
|
-
function createCart(input) {
|
|
115
|
-
const buyerIdentity = {
|
|
116
|
-
...(cfg.country ? { countryCode: cfg.country.toUpperCase() } : {}),
|
|
117
|
-
...(customer.accessToken ? { customerAccessToken: customer.accessToken } : {}),
|
|
118
|
-
};
|
|
119
|
-
const withIdentity = Object.keys(buyerIdentity).length ? { buyerIdentity, ...input } : input;
|
|
120
|
-
return send('cartCreate', '$input: CartInput!', 'input: $input', { input: withIdentity });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @typedef {object} CartMutation A mutation on the saved cart.
|
|
125
|
-
* @property {string} name The mutation, e.g. `cartLinesAdd`.
|
|
126
|
-
* @property {string} args Its variable definitions besides `$cartId`.
|
|
127
|
-
* @property {string} inner Its arguments besides `cartId`.
|
|
128
|
-
* @property {Record<string, unknown>} variables
|
|
129
|
-
* @property {Record<string, unknown> | null} createInput The `CartInput` of a new cart that does what the mutation
|
|
130
|
-
* asks, when there is no cart (or Shopify no longer has it); `null` leaves the visitor without a cart instead.
|
|
131
|
-
*/
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Runs a mutation on the saved cart, or creates a cart from `createInput` when there is none to change.
|
|
135
|
-
*
|
|
136
|
-
* @param {CartMutation} mutation
|
|
137
|
-
* @returns {Promise<ShopifyCart | null>} The changed cart; `null` without a cart and without a `createInput`.
|
|
138
|
-
* @throws {TetherError} For a failed request or Shopify's user errors; also when the cart is gone and there is no
|
|
139
|
-
* `createInput` (the saved cart is forgotten first).
|
|
140
|
-
*/
|
|
141
|
-
export async function mutateCart({ name, args, inner, variables, createInput }) {
|
|
142
|
-
const cartId = savedCartId();
|
|
143
|
-
if (!cartId) return createInput ? createCart(createInput) : null;
|
|
144
|
-
try {
|
|
145
|
-
return await send(name, `$cartId: ID!, ${args}`, `cartId: $cartId, ${inner}`, { ...variables, cartId });
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (!isCartGone(error)) throw error;
|
|
148
|
-
forgetSavedCart();
|
|
149
|
-
if (!createInput) throw error;
|
|
150
|
-
log('cart expired, creating a new one');
|
|
151
|
-
return createCart(createInput);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* @param {string} id
|
|
157
|
-
* @returns {Promise<ShopifyCart | null>} The cart, `null` when Shopify doesn't have it (expired or checked out).
|
|
158
|
-
*/
|
|
159
|
-
export async function fetchCart(id) {
|
|
160
|
-
/** @type {{ cart: ShopifyCart | null }} */
|
|
161
|
-
const data = await gql(`query TetherCart($id: ID!) { cart(id: $id) { ...TetherCart } }\n${cartFragment()}`, { id });
|
|
162
|
-
return data.cart;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* @param {string} id
|
|
167
|
-
* @returns {Promise<string | null>} The cart's current checkout URL, `null` when Shopify doesn't have the cart.
|
|
168
|
-
*/
|
|
169
|
-
export async function fetchCheckoutUrl(id) {
|
|
170
|
-
/** @type {{ cart: { checkoutUrl: string } | null }} */
|
|
171
|
-
const data = await gql('query TetherCheckout($id: ID!) { cart(id: $id) { checkoutUrl } }', { id });
|
|
172
|
-
return data.cart?.checkoutUrl ?? null;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* @typedef {object} LinePatch A change to one line, for `updateLine`.
|
|
177
|
-
* @property {number} [quantity] 0 or less removes the line.
|
|
178
|
-
* @property {string | number | null} [sellingPlanId] `null` switches the line back to one-time purchase.
|
|
179
|
-
* @property {Attributes} [attributes]
|
|
180
|
-
* @property {string | number} [merchandiseId]
|
|
181
|
-
* @property {string | number} [variantId]
|
|
182
|
-
*/
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @param {string} lineId
|
|
186
|
-
* @param {number | LinePatch} patch
|
|
187
|
-
* @returns {Record<string, unknown>} Shopify's `CartLineUpdateInput`.
|
|
188
|
-
*/
|
|
189
|
-
export function toLineUpdate(lineId, patch) {
|
|
190
|
-
const change = typeof patch === 'number' ? { quantity: patch } : patch;
|
|
191
|
-
/** @type {Record<string, unknown>} */
|
|
192
|
-
const line = { id: lineId };
|
|
193
|
-
if (change.quantity !== undefined) line['quantity'] = change.quantity;
|
|
194
|
-
if ('sellingPlanId' in change)
|
|
195
|
-
line['sellingPlanId'] = change.sellingPlanId ? GID.sellingPlan(change.sellingPlanId) : null;
|
|
196
|
-
if (change.attributes) line['attributes'] = toAttributes(change.attributes);
|
|
197
|
-
const variantId = change.merchandiseId || change.variantId;
|
|
198
|
-
if (variantId) line['merchandiseId'] = GID.variant(variantId);
|
|
199
|
-
return line;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* @param {Record<string, unknown>[]} lines
|
|
204
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
205
|
-
*/
|
|
206
|
-
export const updateLines = (lines) =>
|
|
207
|
-
mutateCart({
|
|
208
|
-
name: 'cartLinesUpdate',
|
|
209
|
-
args: '$lines: [CartLineUpdateInput!]!',
|
|
210
|
-
inner: 'lines: $lines',
|
|
211
|
-
variables: { lines },
|
|
212
|
-
createInput: null,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* @param {string[]} lineIds
|
|
217
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
218
|
-
*/
|
|
219
|
-
export const removeLines = (lineIds) =>
|
|
220
|
-
mutateCart({
|
|
221
|
-
name: 'cartLinesRemove',
|
|
222
|
-
args: '$lineIds: [ID!]!',
|
|
223
|
-
inner: 'lineIds: $lineIds',
|
|
224
|
-
variables: { lineIds },
|
|
225
|
-
createInput: null,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @param {string} lineId
|
|
230
|
-
* @param {number} target
|
|
231
|
-
* @returns {Promise<ShopifyCart | null>} The cart once `lineId` has `target` (0 removes it).
|
|
232
|
-
*/
|
|
233
|
-
export const setLineQuantity = (lineId, target) =>
|
|
234
|
-
target > 0 ? updateLines([{ id: lineId, quantity: target }]) : removeLines([lineId]);
|