@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/fragments.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The GraphQL fragments of what Tether renders. They are built at call time because the list sizes and metafields come
|
|
3
|
-
* from the config; the browser passes its validated `cfg`, the server helpers their own. Operations put these after
|
|
4
|
-
* the operation, where `withContext` expects them.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, toMetafieldIdentifier } from './config.js';
|
|
7
|
-
/** @import { TetherConfig, MetafieldIdentifier } from './config.js' */
|
|
8
|
-
/** @import { Money } from './shopify-values.js' */
|
|
9
|
-
|
|
10
|
-
/** @typedef {{ url: string, altText: string | null, width: number | null, height: number | null }} Image */
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {{ __typename: 'SellingPlanPercentagePriceAdjustment', adjustmentPercentage: number }
|
|
13
|
-
* | { __typename: 'SellingPlanFixedAmountPriceAdjustment', adjustmentAmount: Money }
|
|
14
|
-
* | { __typename: 'SellingPlanFixedPriceAdjustment', price: Money }} SellingPlanAdjustmentValue
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {object} SellingPlan
|
|
18
|
-
* @property {string} id
|
|
19
|
-
* @property {string} name
|
|
20
|
-
* @property {string | null} description
|
|
21
|
-
* @property {boolean} recurringDeliveries
|
|
22
|
-
* @property {{ name: string, value: string }[]} options
|
|
23
|
-
* @property {{ orderCount: number | null, adjustmentValue: SellingPlanAdjustmentValue }[]} priceAdjustments
|
|
24
|
-
*/
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} SellingPlanGroup
|
|
27
|
-
* @property {string} name
|
|
28
|
-
* @property {string | null} appName
|
|
29
|
-
* @property {{ name: string, values: string[] }[]} options
|
|
30
|
-
* @property {{ nodes: SellingPlan[] }} sellingPlans
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {object} SellingPlanAllocation A variant's price on one plan.
|
|
34
|
-
* @property {{ id: string }} sellingPlan
|
|
35
|
-
* @property {Money} checkoutChargeAmount What the customer pays at checkout (more for a prepaid plan).
|
|
36
|
-
* @property {Money} remainingBalanceChargeAmount
|
|
37
|
-
* @property {{ price: Money, compareAtPrice: Money, perDeliveryPrice: Money, unitPrice: Money | null }[]}
|
|
38
|
-
* priceAdjustments
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* @typedef {object} ProductVariant
|
|
42
|
-
* @property {string} id
|
|
43
|
-
* @property {string} title
|
|
44
|
-
* @property {boolean} availableForSale
|
|
45
|
-
* @property {boolean} currentlyNotInStock
|
|
46
|
-
* @property {number | null} [quantityAvailable] Absent when the store's token can't read inventory.
|
|
47
|
-
* @property {string | null} sku
|
|
48
|
-
* @property {string | null} barcode
|
|
49
|
-
* @property {{ name: string, value: string }[]} selectedOptions
|
|
50
|
-
* @property {Image | null} image
|
|
51
|
-
* @property {Money} price
|
|
52
|
-
* @property {Money | null} compareAtPrice
|
|
53
|
-
* @property {Money | null} unitPrice
|
|
54
|
-
* @property {{ nodes: SellingPlanAllocation[] }} sellingPlanAllocations
|
|
55
|
-
*/
|
|
56
|
-
/**
|
|
57
|
-
* @typedef {object} Metafield
|
|
58
|
-
* @property {string} namespace
|
|
59
|
-
* @property {string} key
|
|
60
|
-
* @property {string} type
|
|
61
|
-
* @property {string} value
|
|
62
|
-
* @property {{ image?: Image } | null} reference A file reference's image.
|
|
63
|
-
*/
|
|
64
|
-
/**
|
|
65
|
-
* @typedef {object} Product The fields of `fragment TetherProduct`.
|
|
66
|
-
* @property {string} id
|
|
67
|
-
* @property {string} handle
|
|
68
|
-
* @property {string} title
|
|
69
|
-
* @property {string} description
|
|
70
|
-
* @property {string} descriptionHtml
|
|
71
|
-
* @property {string} productType
|
|
72
|
-
* @property {string} vendor
|
|
73
|
-
* @property {string[]} tags
|
|
74
|
-
* @property {boolean} availableForSale
|
|
75
|
-
* @property {string | null} publishedAt
|
|
76
|
-
* @property {string | null} onlineStoreUrl
|
|
77
|
-
* @property {boolean} requiresSellingPlan
|
|
78
|
-
* @property {{ title: string | null, description: string | null }} seo
|
|
79
|
-
* @property {Image | null} featuredImage
|
|
80
|
-
* @property {{ nodes: Image[] }} images
|
|
81
|
-
* @property {{ name: string, optionValues: { name: string, swatch: unknown }[] }[]} options
|
|
82
|
-
* @property {{ minVariantPrice: Money, maxVariantPrice: Money }} priceRange
|
|
83
|
-
* @property {{ minVariantPrice: Money, maxVariantPrice: Money }} compareAtPriceRange
|
|
84
|
-
* @property {(Metafield | null)[]} [metafields] Present when metafields are configured; `null` for one that isn't set.
|
|
85
|
-
* @property {{ nodes: SellingPlanGroup[] }} sellingPlanGroups
|
|
86
|
-
* @property {{ nodes: ProductVariant[] }} variants
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* @typedef {Pick<TetherConfig, 'imagesFirst' | 'variantsFirst' | 'sellingPlanGroupsFirst' | 'sellingPlansFirst'
|
|
91
|
-
* | 'metafields'>} ProductFragmentConfig
|
|
92
|
-
*/
|
|
93
|
-
|
|
94
|
-
const MONEY = 'amount currencyCode';
|
|
95
|
-
const IMAGE = 'url altText width height';
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The `metafields(identifiers:)` field. A fragment can't declare variables, so the identifiers are written into the
|
|
99
|
-
* text: only valid ones (`toMetafieldIdentifier`), each quoted as a string literal.
|
|
100
|
-
*
|
|
101
|
-
* @param {ProductFragmentConfig['metafields']} metafields
|
|
102
|
-
*/
|
|
103
|
-
function metafieldsField(metafields) {
|
|
104
|
-
/** @type {MetafieldIdentifier[]} */
|
|
105
|
-
const identifiers = [];
|
|
106
|
-
for (const ref of metafields) {
|
|
107
|
-
const identifier = toMetafieldIdentifier(ref);
|
|
108
|
-
if (identifier) identifiers.push(identifier);
|
|
109
|
-
}
|
|
110
|
-
if (!identifiers.length) return '';
|
|
111
|
-
const list = identifiers
|
|
112
|
-
.map(({ namespace, key }) => `{namespace: ${JSON.stringify(namespace)}, key: ${JSON.stringify(key)}}`)
|
|
113
|
-
.join(', ');
|
|
114
|
-
return `metafields(identifiers: [${list}]) {
|
|
115
|
-
namespace key type value
|
|
116
|
-
reference { ... on MediaImage { image { ${IMAGE} } } }
|
|
117
|
-
}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const SELLING_PLAN_FIELDS = `
|
|
121
|
-
id name description recurringDeliveries
|
|
122
|
-
options { name value }
|
|
123
|
-
priceAdjustments {
|
|
124
|
-
orderCount
|
|
125
|
-
adjustmentValue {
|
|
126
|
-
__typename
|
|
127
|
-
... on SellingPlanPercentagePriceAdjustment { adjustmentPercentage }
|
|
128
|
-
... on SellingPlanFixedAmountPriceAdjustment { adjustmentAmount { ${MONEY} } }
|
|
129
|
-
... on SellingPlanFixedPriceAdjustment { price { ${MONEY} } }
|
|
130
|
-
}
|
|
131
|
-
}`;
|
|
132
|
-
|
|
133
|
-
const ALLOCATION_FIELDS = `
|
|
134
|
-
sellingPlan { id }
|
|
135
|
-
checkoutChargeAmount { ${MONEY} }
|
|
136
|
-
remainingBalanceChargeAmount { ${MONEY} }
|
|
137
|
-
priceAdjustments {
|
|
138
|
-
price { ${MONEY} }
|
|
139
|
-
compareAtPrice { ${MONEY} }
|
|
140
|
-
perDeliveryPrice { ${MONEY} }
|
|
141
|
-
unitPrice { ${MONEY} }
|
|
142
|
-
}`;
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* @param {ProductFragmentConfig} [o] The list sizes and metafields.
|
|
146
|
-
* @returns {string} `fragment TetherProduct on Product`.
|
|
147
|
-
*/
|
|
148
|
-
export function productFragment(o = cfg) {
|
|
149
|
-
return `
|
|
150
|
-
fragment TetherProduct on Product {
|
|
151
|
-
id handle title description descriptionHtml productType vendor tags
|
|
152
|
-
availableForSale publishedAt onlineStoreUrl requiresSellingPlan
|
|
153
|
-
seo { title description }
|
|
154
|
-
featuredImage { ${IMAGE} }
|
|
155
|
-
images(first: ${o.imagesFirst}) { nodes { ${IMAGE} } }
|
|
156
|
-
options { name optionValues { name swatch { color image { previewImage { ${IMAGE} } } } } }
|
|
157
|
-
priceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
|
|
158
|
-
compareAtPriceRange { minVariantPrice { ${MONEY} } maxVariantPrice { ${MONEY} } }
|
|
159
|
-
${metafieldsField(o.metafields)}
|
|
160
|
-
sellingPlanGroups(first: ${o.sellingPlanGroupsFirst}) {
|
|
161
|
-
nodes {
|
|
162
|
-
name appName
|
|
163
|
-
options { name values }
|
|
164
|
-
sellingPlans(first: ${o.sellingPlansFirst}) { nodes { ${SELLING_PLAN_FIELDS} } }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
variants(first: ${o.variantsFirst}) {
|
|
168
|
-
nodes {
|
|
169
|
-
id title availableForSale currentlyNotInStock quantityAvailable sku barcode
|
|
170
|
-
selectedOptions { name value }
|
|
171
|
-
image { ${IMAGE} }
|
|
172
|
-
price { ${MONEY} }
|
|
173
|
-
compareAtPrice { ${MONEY} }
|
|
174
|
-
unitPrice { ${MONEY} }
|
|
175
|
-
sellingPlanAllocations(first: ${o.sellingPlansFirst}) { nodes { ${ALLOCATION_FIELDS} } }
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** @returns {string} `fragment TetherCart on Cart`. */
|
|
182
|
-
export function cartFragment() {
|
|
183
|
-
return `
|
|
184
|
-
fragment TetherCart on Cart {
|
|
185
|
-
id checkoutUrl totalQuantity note createdAt updatedAt
|
|
186
|
-
attributes { key value }
|
|
187
|
-
buyerIdentity { countryCode email }
|
|
188
|
-
discountCodes { code applicable }
|
|
189
|
-
cost {
|
|
190
|
-
subtotalAmount { ${MONEY} }
|
|
191
|
-
totalAmount { ${MONEY} }
|
|
192
|
-
checkoutChargeAmount { ${MONEY} }
|
|
193
|
-
}
|
|
194
|
-
lines(first: 100) {
|
|
195
|
-
nodes {
|
|
196
|
-
id quantity
|
|
197
|
-
attributes { key value }
|
|
198
|
-
cost {
|
|
199
|
-
totalAmount { ${MONEY} }
|
|
200
|
-
subtotalAmount { ${MONEY} }
|
|
201
|
-
amountPerQuantity { ${MONEY} }
|
|
202
|
-
compareAtAmountPerQuantity { ${MONEY} }
|
|
203
|
-
}
|
|
204
|
-
discountAllocations { discountedAmount { ${MONEY} } }
|
|
205
|
-
sellingPlanAllocation {
|
|
206
|
-
sellingPlan { id name description recurringDeliveries options { name value } }
|
|
207
|
-
checkoutChargeAmount { ${MONEY} }
|
|
208
|
-
priceAdjustments { price { ${MONEY} } compareAtPrice { ${MONEY} } perDeliveryPrice { ${MONEY} } }
|
|
209
|
-
}
|
|
210
|
-
merchandise {
|
|
211
|
-
... on ProductVariant {
|
|
212
|
-
id title sku availableForSale requiresShipping quantityAvailable
|
|
213
|
-
image { ${IMAGE} }
|
|
214
|
-
price { ${MONEY} }
|
|
215
|
-
compareAtPrice { ${MONEY} }
|
|
216
|
-
selectedOptions { name value }
|
|
217
|
-
product { id title handle vendor featuredImage { ${IMAGE} } }
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
... on ComponentizableCartLine {
|
|
221
|
-
lineComponents {
|
|
222
|
-
id quantity
|
|
223
|
-
merchandise { ... on ProductVariant { id title product { title handle } } }
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** @returns {string} The collection fields Tether renders, for inside a `collection { }` selection. */
|
|
232
|
-
export function collectionFields() {
|
|
233
|
-
return `
|
|
234
|
-
id handle title description descriptionHtml
|
|
235
|
-
image { ${IMAGE} }
|
|
236
|
-
seo { title description }`;
|
|
237
|
-
}
|
package/src/index.js
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tether's entry: `window.Tether`, `init()`, `mount()` and the start of the plain `<script>` setup.
|
|
3
|
-
*
|
|
4
|
-
* <script>window.TETHER_CONFIG = { domain: 'x.myshopify.com', token: '...' }</script>
|
|
5
|
-
* <script src="tether.min.js" defer></script>
|
|
6
|
-
*
|
|
7
|
-
* With page transitions (Barba, Swup): set `autoMount: false`, call `Tether.init()` once and
|
|
8
|
-
* `Tether.mount(container)` after every page enter.
|
|
9
|
-
*/
|
|
10
|
-
import { cfg, configure, isConfigured, log, warn, reportError } from './config.js';
|
|
11
|
-
import { gql, TetherError, withContext } from './client.js';
|
|
12
|
-
import { Cart } from './cart.js';
|
|
13
|
-
import { scopes } from './product.js';
|
|
14
|
-
import { ListScope, listSelector } from './collection.js';
|
|
15
|
-
import {
|
|
16
|
-
getProduct,
|
|
17
|
-
getProducts,
|
|
18
|
-
getRecommendations,
|
|
19
|
-
clearProductCache,
|
|
20
|
-
primeProduct,
|
|
21
|
-
normalizeProductKey,
|
|
22
|
-
} from './loader.js';
|
|
23
|
-
import * as plans from './selling-plans.js';
|
|
24
|
-
import { resolveProductKey } from './status.js';
|
|
25
|
-
import { sel, emit, formatMoney, imageUrl } from './dom.js';
|
|
26
|
-
import { loadPart, coreModules } from './lazy.js';
|
|
27
|
-
import {
|
|
28
|
-
mountedLists,
|
|
29
|
-
liveLists,
|
|
30
|
-
pruneLive,
|
|
31
|
-
within,
|
|
32
|
-
mountProduct,
|
|
33
|
-
hydrateFromPage,
|
|
34
|
-
settleMarket,
|
|
35
|
-
trackMarket,
|
|
36
|
-
} from './mounting.js';
|
|
37
|
-
import { applyStartMarket, setCountry } from './page-market.js';
|
|
38
|
-
import { accountApi, mountAccountIfUsed } from './account-api.js';
|
|
39
|
-
import { bindCartControls } from './controls/cart-controls.js';
|
|
40
|
-
import { bindDiscountControls } from './controls/discount.js';
|
|
41
|
-
import { addGroup, bindExtrasControls } from './controls/extras.js';
|
|
42
|
-
import { bindNavigation, rememberDocumentMount } from './controls/navigation.js';
|
|
43
|
-
/** @import { TetherConfig, TetherOptions, MarketsConfig } from './config.js' */
|
|
44
|
-
/** @import { ShopifyCart } from './cart/store.js' */
|
|
45
|
-
/** @import { ProductScope } from './product.js' */
|
|
46
|
-
/** @import { DiagnoseOptions } from './diagnose.js' */
|
|
47
|
-
|
|
48
|
-
export { addGroup, setCountry };
|
|
49
|
-
|
|
50
|
-
export const VERSION = '0.6.8';
|
|
51
|
-
|
|
52
|
-
/** The events the analytics part reports, buffered from `init()` until the part has loaded. */
|
|
53
|
-
const ANALYTICS_EVENTS = ['product-ready', 'list-ready', 'cart-updated', 'cart-toggled', 'checkout', 'search'];
|
|
54
|
-
|
|
55
|
-
/** Milliseconds `observe` waits after the last added node, so a burst of additions mounts once. */
|
|
56
|
-
const OBSERVE_DEBOUNCE_MS = 50;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @typedef {object} TetherGlobals What the page sets on `window` for Tether, and `window.Tether` itself.
|
|
60
|
-
* @property {TetherOptions} [TETHER_CONFIG] The site's config; `configure()` validates it.
|
|
61
|
-
* @property {string} [TETHER_INTEGRATION] Set by a framework integration (Astro) that starts Tether itself.
|
|
62
|
-
* @property {TetherApi} [Tether]
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/** @returns {Window & TetherGlobals} */
|
|
66
|
-
const pageGlobals = () => window;
|
|
67
|
-
|
|
68
|
-
/** @type {Promise<TetherApi | null> | null} */
|
|
69
|
-
let initPromise = null;
|
|
70
|
-
|
|
71
|
-
/** @type {MutationObserver | null} */
|
|
72
|
-
let observer = null;
|
|
73
|
-
|
|
74
|
-
/** @type {Event[] | null} The analytics part's queue, created once by the first `init()` with `analytics`. */
|
|
75
|
-
let analyticsQueue = null;
|
|
76
|
-
|
|
77
|
-
/** @type {boolean} The document-wide listeners are bound once, by the first configured `init()`. */
|
|
78
|
-
let controlsBound = false;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Mounts the lists of `root` and the product scopes in them as they render.
|
|
82
|
-
*
|
|
83
|
-
* @param {ParentNode} root
|
|
84
|
-
* @returns {number} The lists mounted.
|
|
85
|
-
*/
|
|
86
|
-
function mountLists(root) {
|
|
87
|
-
let count = 0;
|
|
88
|
-
within(root, listSelector()).forEach((el) => {
|
|
89
|
-
if (mountedLists.has(el)) return;
|
|
90
|
-
mountedLists.add(el);
|
|
91
|
-
count++;
|
|
92
|
-
const list = new ListScope(el);
|
|
93
|
-
liveLists.add(list);
|
|
94
|
-
el.addEventListener(`${cfg.prefix}:list-rendered`, (event) => {
|
|
95
|
-
// The list emits this CustomEvent once its products are in the page.
|
|
96
|
-
mount(/** @type {CustomEvent<{ container: ParentNode }>} */ (event).detail.container);
|
|
97
|
-
});
|
|
98
|
-
void trackMarket(list.load());
|
|
99
|
-
});
|
|
100
|
-
return count;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Loads each optional part `root` uses: the country select, customer accounts and search as you type.
|
|
105
|
-
*
|
|
106
|
-
* @param {ParentNode} root
|
|
107
|
-
*/
|
|
108
|
-
function mountParts(root) {
|
|
109
|
-
if (within(root, `${sel('country-select')},${sel('country-bind')}`).length) {
|
|
110
|
-
loadPart('markets')
|
|
111
|
-
.then((part) => part.renderCountrySelects(root))
|
|
112
|
-
.catch((/** @type {unknown} */ error) => {
|
|
113
|
-
reportError(error, 'markets');
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
mountAccountIfUsed(root);
|
|
117
|
-
if (within(root, sel('search')).length) {
|
|
118
|
-
loadPart('search')
|
|
119
|
-
.then((part) => {
|
|
120
|
-
part.mountSearch(root);
|
|
121
|
-
})
|
|
122
|
-
.catch((/** @type {unknown} */ error) => {
|
|
123
|
-
reportError(error, 'search');
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Mounts every product scope and product list inside `root`. Safe to call repeatedly.
|
|
130
|
-
*
|
|
131
|
-
* @param {ParentNode} [root]
|
|
132
|
-
* @returns {number} The scopes and lists newly mounted.
|
|
133
|
-
*/
|
|
134
|
-
export function mount(root = document) {
|
|
135
|
-
if (!isConfigured()) return 0;
|
|
136
|
-
if (root === document) rememberDocumentMount();
|
|
137
|
-
// Page transitions detach old roots: they aren't kept alive.
|
|
138
|
-
pruneLive();
|
|
139
|
-
// Products the server rendered into the page need no request.
|
|
140
|
-
hydrateFromPage(root.ownerDocument ?? document);
|
|
141
|
-
let count = mountLists(root);
|
|
142
|
-
within(root, sel('product')).forEach((el) => {
|
|
143
|
-
if (mountProduct(el)) count++;
|
|
144
|
-
});
|
|
145
|
-
mountParts(root);
|
|
146
|
-
settleMarket();
|
|
147
|
-
Cart.render();
|
|
148
|
-
log(`mounted ${count} scope(s)`, root);
|
|
149
|
-
return count;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** Call before a page transition removes the page: closes the cart drawer. */
|
|
153
|
-
export function unmount() {
|
|
154
|
-
Cart.close();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* @param {Element | null | undefined} el
|
|
159
|
-
* @returns {ProductScope | null} The product scope `el` is in.
|
|
160
|
-
*/
|
|
161
|
-
export function getScope(el) {
|
|
162
|
-
const root = el?.closest(sel('product'));
|
|
163
|
-
return root ? (scopes.get(root) ?? null) : null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function bindControls() {
|
|
167
|
-
if (controlsBound) return;
|
|
168
|
-
controlsBound = true;
|
|
169
|
-
bindCartControls();
|
|
170
|
-
bindDiscountControls();
|
|
171
|
-
bindExtrasControls();
|
|
172
|
-
bindNavigation({ ready: () => initPromise ?? Promise.resolve(null), mount, unmount });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Buffers Tether's events until the analytics part has loaded, so none are missed. */
|
|
176
|
-
function startAnalyticsQueue() {
|
|
177
|
-
/** @type {Event[]} */
|
|
178
|
-
const queue = [];
|
|
179
|
-
analyticsQueue = queue;
|
|
180
|
-
ANALYTICS_EVENTS.forEach((name) => {
|
|
181
|
-
document.addEventListener(`${cfg.prefix}:${name}`, (event) => queue.push(event));
|
|
182
|
-
});
|
|
183
|
-
document.addEventListener('astro:page-load', () => queue.push(new CustomEvent(`${cfg.prefix}:page`)));
|
|
184
|
-
loadPart('analytics')
|
|
185
|
-
.then((part) => part.startAnalytics(queue))
|
|
186
|
-
.catch((/** @type {unknown} */ error) => {
|
|
187
|
-
reportError(error, 'analytics');
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* A saved cart from another market moves to this one, so its prices match the page's.
|
|
193
|
-
*
|
|
194
|
-
* @param {ShopifyCart | null} cart
|
|
195
|
-
* @returns {Promise<ShopifyCart | null>}
|
|
196
|
-
*/
|
|
197
|
-
async function cartInPageMarket(cart) {
|
|
198
|
-
const country = cfg.country?.toUpperCase();
|
|
199
|
-
const cartCountry = cart?.buyerIdentity.countryCode;
|
|
200
|
-
if (!cart || !country || !cartCountry || cartCountry === country) return cart;
|
|
201
|
-
try {
|
|
202
|
-
return await Cart.setBuyerIdentity({ countryCode: country }, { create: false });
|
|
203
|
-
} catch (error) {
|
|
204
|
-
reportError(error, 'cart country');
|
|
205
|
-
return cart;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Configures Tether (`window.TETHER_CONFIG` with `overrides` over it), applies the visitor's market and loads the
|
|
211
|
-
* cart. A second call without overrides returns the first one's promise.
|
|
212
|
-
*
|
|
213
|
-
* @param {TetherOptions} [overrides]
|
|
214
|
-
* @returns {Promise<TetherApi | null>} `null` without a domain and token, when Tether stays idle.
|
|
215
|
-
*/
|
|
216
|
-
export function init(overrides = {}) {
|
|
217
|
-
if (initPromise && !Object.keys(overrides).length) return initPromise;
|
|
218
|
-
configure({ ...pageGlobals().TETHER_CONFIG, ...overrides });
|
|
219
|
-
if (!isConfigured()) {
|
|
220
|
-
warn('TETHER_CONFIG is missing domain or token. Tether is idle.');
|
|
221
|
-
initPromise = Promise.resolve(null);
|
|
222
|
-
return initPromise;
|
|
223
|
-
}
|
|
224
|
-
bindControls();
|
|
225
|
-
if (cfg.observe) startObserver();
|
|
226
|
-
if (cfg.analytics && !analyticsQueue) startAnalyticsQueue();
|
|
227
|
-
initPromise = applyStartMarket(overrides)
|
|
228
|
-
.then(() => Cart.init())
|
|
229
|
-
.then(cartInPageMarket)
|
|
230
|
-
.then((cart) => {
|
|
231
|
-
emit('ready', { cart });
|
|
232
|
-
return api;
|
|
233
|
-
});
|
|
234
|
-
return initPromise;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** `observe`: mounts the elements added to the page later. */
|
|
238
|
-
function startObserver() {
|
|
239
|
-
if (observer || typeof MutationObserver === 'undefined') return;
|
|
240
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
241
|
-
let timer;
|
|
242
|
-
/** @type {Set<Element>} */
|
|
243
|
-
const queued = new Set();
|
|
244
|
-
observer = new MutationObserver((mutations) => {
|
|
245
|
-
mutations.forEach((mutation) => {
|
|
246
|
-
mutation.addedNodes.forEach((node) => {
|
|
247
|
-
if (node instanceof window.Element) queued.add(node);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
clearTimeout(timer);
|
|
251
|
-
timer = setTimeout(() => {
|
|
252
|
-
queued.forEach((node) => {
|
|
253
|
-
if (node.isConnected) mount(node);
|
|
254
|
-
});
|
|
255
|
-
queued.clear();
|
|
256
|
-
}, OBSERVE_DEBOUNCE_MS);
|
|
257
|
-
});
|
|
258
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* @param {MarketsConfig | null} [markets] Default `markets` of the config.
|
|
263
|
-
*/
|
|
264
|
-
const detectMarket = (markets) => loadPart('markets').then((part) => part.detectMarket(markets ?? cfg.markets));
|
|
265
|
-
|
|
266
|
-
const getLocalization = () => loadPart('markets').then((part) => part.getLocalization());
|
|
267
|
-
|
|
268
|
-
/** @param {Omit<DiagnoseOptions, 'version'>} [options] */
|
|
269
|
-
const diagnose = (options) => loadPart('diagnose').then((part) => part.diagnose({ ...options, version: VERSION }));
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Listens to the Tether event `<prefix>:<name>`.
|
|
273
|
-
*
|
|
274
|
-
* @param {string} name
|
|
275
|
-
* @param {(detail: unknown, event: CustomEvent) => void} listener
|
|
276
|
-
* @returns {() => void} Stops listening.
|
|
277
|
-
*/
|
|
278
|
-
function on(name, listener) {
|
|
279
|
-
/** @param {Event} event */
|
|
280
|
-
const handler = (event) => {
|
|
281
|
-
const custom = /** @type {CustomEvent} Tether's events are CustomEvents. */ (event);
|
|
282
|
-
listener(custom.detail, custom);
|
|
283
|
-
};
|
|
284
|
-
document.addEventListener(`${cfg.prefix}:${name}`, handler);
|
|
285
|
-
return () => {
|
|
286
|
-
document.removeEventListener(`${cfg.prefix}:${name}`, handler);
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export const api = {
|
|
291
|
-
VERSION,
|
|
292
|
-
init,
|
|
293
|
-
mount,
|
|
294
|
-
unmount,
|
|
295
|
-
/** @returns {TetherConfig} */
|
|
296
|
-
get config() {
|
|
297
|
-
return cfg;
|
|
298
|
-
},
|
|
299
|
-
/** @returns {Promise<TetherApi | null>} */
|
|
300
|
-
get ready() {
|
|
301
|
-
return initPromise ?? Promise.resolve(null);
|
|
302
|
-
},
|
|
303
|
-
query: gql,
|
|
304
|
-
withContext,
|
|
305
|
-
cart: Cart,
|
|
306
|
-
getProduct,
|
|
307
|
-
getProducts,
|
|
308
|
-
getRecommendations,
|
|
309
|
-
getScope,
|
|
310
|
-
addGroup,
|
|
311
|
-
account: accountApi,
|
|
312
|
-
primeProduct,
|
|
313
|
-
normalizeProductKey,
|
|
314
|
-
clearProductCache,
|
|
315
|
-
formatMoney,
|
|
316
|
-
imageUrl,
|
|
317
|
-
plans,
|
|
318
|
-
TetherError,
|
|
319
|
-
resolveProductKey,
|
|
320
|
-
setCountry,
|
|
321
|
-
detectMarket,
|
|
322
|
-
getLocalization,
|
|
323
|
-
diagnose,
|
|
324
|
-
__core: coreModules,
|
|
325
|
-
// The `<script>` build's parts register here when their file has loaded.
|
|
326
|
-
__parts: /** @type {Record<string, unknown>} */ ({}),
|
|
327
|
-
on,
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
/** @typedef {typeof api} TetherApi */
|
|
331
|
-
|
|
332
|
-
export default api;
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* The plain `<script>` setup starts by itself, unless `autoMount: false` or a framework integration (Astro), which
|
|
336
|
-
* starts Tether itself.
|
|
337
|
-
*/
|
|
338
|
-
function autoStart() {
|
|
339
|
-
const globals = pageGlobals();
|
|
340
|
-
globals.Tether = api;
|
|
341
|
-
if (!globals.TETHER_CONFIG || globals.TETHER_CONFIG.autoMount === false || globals.TETHER_INTEGRATION) return;
|
|
342
|
-
const start = () => {
|
|
343
|
-
init()
|
|
344
|
-
.then(() => mount(document))
|
|
345
|
-
.catch((/** @type {unknown} */ error) => {
|
|
346
|
-
reportError(error, 'init');
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
|
|
350
|
-
else start();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (typeof window !== 'undefined' && typeof document !== 'undefined') autoStart();
|
package/src/initial.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the browser script shows first for a product, as plain data, so a server renders the same and nothing flashes
|
|
3
|
-
* when Tether takes over. Public as `initialState()` of `@occluze/tether/server`; the rules are `product-state.js`'s,
|
|
4
|
-
* the ones the browser scope uses.
|
|
5
|
-
*/
|
|
6
|
-
import { DEFAULTS } from './config.js';
|
|
7
|
-
import { initialChoice, isValueAvailable, productView } from './product-state.js';
|
|
8
|
-
import { allPlans, allocationFor, planPrice, planSavings } from './selling-plans.js';
|
|
9
|
-
/** @import { Money } from './shopify-values.js' */
|
|
10
|
-
/** @import { Product, ProductVariant } from './fragments.js' */
|
|
11
|
-
/** @import { PlanInGroup } from './selling-plans.js' */
|
|
12
|
-
/** @import { Selection, ShowCondition } from './product-state.js' */
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} InitialOptions
|
|
16
|
-
* @property {string | null | undefined} [variant] The page's `?variant=`; the first variant for sale without.
|
|
17
|
-
* @property {string | null | undefined} [sellingPlan] The page's `?selling_plan=`: subscribes on that plan.
|
|
18
|
-
* @property {string | null | undefined} [defaultPurchase] `subscription` subscribes on the first plan.
|
|
19
|
-
* @property {number | undefined} [lowStock] `low-stock` at this many or fewer.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* @typedef {object} InitialPlan A plan in the plan list (`tether-plan-list`).
|
|
23
|
-
* @property {string} id
|
|
24
|
-
* @property {string} name
|
|
25
|
-
* @property {Money | null} price On this plan, for the variant.
|
|
26
|
-
* @property {number} savings In %, 0 without savings.
|
|
27
|
-
* @property {boolean} selected `is-selected`: the line is bought on this plan.
|
|
28
|
-
* @property {boolean} available The variant is on this plan; `is-unavailable` when not.
|
|
29
|
-
*/
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {object} InitialState
|
|
32
|
-
* @property {ProductVariant} variant
|
|
33
|
-
* @property {Selection} selected Option name → value of the variant: `is-selected` on those buttons.
|
|
34
|
-
* @property {'one-time' | 'subscription'} purchase
|
|
35
|
-
* @property {PlanInGroup | null} plan The plan the line is bought on, `null` when bought once.
|
|
36
|
-
* @property {InitialPlan[]} plans
|
|
37
|
-
* @property {number} savings The offered plan's savings in % (`tether-bind="subscription-savings"`), 0 without.
|
|
38
|
-
* @property {Money} price
|
|
39
|
-
* @property {Money | null} compareAtPrice Only when on sale.
|
|
40
|
-
* @property {boolean} available
|
|
41
|
-
* @property {Record<ShowCondition, boolean>} show The `tether-show` conditions: `hidden={!state.show['out-of-stock']}`.
|
|
42
|
-
* @property {Record<string, string[]>} unavailable Per option, the values without a variant for sale (given the other
|
|
43
|
-
* selected values), as plain data for frameworks that serialise props.
|
|
44
|
-
* @property {(name: string, value: string) => boolean} isUnavailable The same as a function: `is-unavailable`.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {Product | null | undefined} product
|
|
49
|
-
* @param {InitialOptions} [options]
|
|
50
|
-
* @returns {InitialState | null} `null` without a product or variants.
|
|
51
|
-
*/
|
|
52
|
-
export function initialState(product, options = {}) {
|
|
53
|
-
if (!product) return null;
|
|
54
|
-
const { variant, sellingPlan, defaultPurchase = DEFAULTS.defaultPurchase, lowStock = DEFAULTS.lowStock } = options;
|
|
55
|
-
const choice = initialChoice(product, { variant, sellingPlan, defaultPurchase });
|
|
56
|
-
const view = productView(product, { ...choice, quantity: 1 }, lowStock);
|
|
57
|
-
if (!view) return null;
|
|
58
|
-
const { selected, isSubscription } = view;
|
|
59
|
-
const isUnavailable = (/** @type {string} */ name, /** @type {string} */ value) =>
|
|
60
|
-
!isValueAvailable(product, selected, name, value);
|
|
61
|
-
return {
|
|
62
|
-
variant: view.variant,
|
|
63
|
-
selected,
|
|
64
|
-
purchase: view.purchase.kind,
|
|
65
|
-
plan: isSubscription ? view.plan : null,
|
|
66
|
-
plans: allPlans(product).map((plan) => ({
|
|
67
|
-
id: plan.id,
|
|
68
|
-
name: plan.name,
|
|
69
|
-
price: planPrice(view.variant, plan.id),
|
|
70
|
-
savings: planSavings(view.variant, plan.id).percentage,
|
|
71
|
-
selected: isSubscription && plan.id === view.purchase.planId,
|
|
72
|
-
available: Boolean(allocationFor(view.variant, plan.id)),
|
|
73
|
-
})),
|
|
74
|
-
savings: view.savings.percentage,
|
|
75
|
-
price: view.price,
|
|
76
|
-
compareAtPrice: view.compareAtPrice,
|
|
77
|
-
available: view.variant.availableForSale,
|
|
78
|
-
show: view.show,
|
|
79
|
-
unavailable: Object.fromEntries(
|
|
80
|
-
product.options.map((option) => [
|
|
81
|
-
option.name,
|
|
82
|
-
option.optionValues.map((value) => value.name).filter((value) => isUnavailable(option.name, value)),
|
|
83
|
-
]),
|
|
84
|
-
),
|
|
85
|
-
isUnavailable,
|
|
86
|
-
};
|
|
87
|
-
}
|