@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/presets/barba.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preset for sites with page transitions (Barba, Swup), smooth scroll (Lenis) and GSAP. Nothing is read from globals
|
|
3
|
-
* except GSAP and ScrollTrigger by default: pass in what your site has.
|
|
4
|
-
*
|
|
5
|
-
* import Tether from "@occluze/tether";
|
|
6
|
-
* import { barbaPreset } from "@occluze/tether/presets/barba";
|
|
7
|
-
*
|
|
8
|
-
* Tether.init({
|
|
9
|
-
* ...window.TETHER_CONFIG,
|
|
10
|
-
* ...barbaPreset({
|
|
11
|
-
* lenis: () => myLenis, // resized after async renders
|
|
12
|
-
* lockScroll: (drawer) => myLock.lock(drawer), // when the cart opens
|
|
13
|
-
* unlockScroll: () => myLock.unlock(), // when the cart closes
|
|
14
|
-
* onValueChange: (el, previous) => {}, // e.g. a text scramble
|
|
15
|
-
* }),
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* // once per page container, e.g. in Barba's beforeEnter AND once():
|
|
19
|
-
* Tether.mount(container);
|
|
20
|
-
* // when leaving a page:
|
|
21
|
-
* Tether.unmount();
|
|
22
|
-
*/
|
|
23
|
-
/** @import { TetherConfig } from '../config.js' */
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* GSAP isn't a dependency, so the two calls this preset makes are described here.
|
|
27
|
-
*
|
|
28
|
-
* @typedef {Record<string, string | number | (() => void)>} TweenVars
|
|
29
|
-
* @typedef {object} Gsap
|
|
30
|
-
* @property {(target: HTMLElement, from: TweenVars, to: TweenVars) => unknown} fromTo
|
|
31
|
-
* @property {(target: HTMLElement, to: TweenVars) => unknown} to
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {object} BarbaPresetOptions Each getter may also be the value itself.
|
|
36
|
-
* @property {() => unknown} [gsap] Default `window.gsap`.
|
|
37
|
-
* @property {(() => unknown) | null} [lenis] Resized after async renders.
|
|
38
|
-
* @property {() => unknown} [scrollTrigger] Default `window.ScrollTrigger`, refreshed after async renders.
|
|
39
|
-
* @property {((drawer: HTMLElement) => void) | null} [lockScroll] When the cart opens.
|
|
40
|
-
* @property {(() => void) | null} [unlockScroll] When the cart closes.
|
|
41
|
-
* @property {(() => void) | null} [onResize] After a cart line animated in or out.
|
|
42
|
-
* @property {((el: HTMLElement, previousText: string) => void) | null} [onValueChange]
|
|
43
|
-
* @property {boolean} [animateLines] Animate cart lines in and out (default true).
|
|
44
|
-
* @property {number} [lineDuration] Seconds.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @typedef {Pick<TetherConfig, 'autoMount' | 'onLayoutChange' | 'onCartOpen' | 'onCartClose' | 'onLineEnter'
|
|
49
|
-
* | 'onLineLeave' | 'onValueChange'>} BarbaPresetConfig
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
const LINE_DURATION_SECONDS = 0.3;
|
|
53
|
-
/** @returns {TweenVars} A new object each time: GSAP writes into the vars it is given. */
|
|
54
|
-
const collapsed = () => ({
|
|
55
|
-
height: 0,
|
|
56
|
-
paddingTop: 0,
|
|
57
|
-
paddingBottom: 0,
|
|
58
|
-
borderBottomWidth: 0,
|
|
59
|
-
opacity: 0,
|
|
60
|
-
overflow: 'hidden',
|
|
61
|
-
});
|
|
62
|
-
const ANIMATED_PROPS = 'height,paddingTop,paddingBottom,borderBottomWidth,opacity,overflow';
|
|
63
|
-
|
|
64
|
-
const reducedMotion = () =>
|
|
65
|
-
typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {unknown} option
|
|
69
|
-
* @returns {unknown} What a getter option returns, or the option itself when it is the value.
|
|
70
|
-
*/
|
|
71
|
-
function current(option) {
|
|
72
|
-
if (typeof option !== 'function') return option;
|
|
73
|
-
// Every function option of this preset is a getter that takes no arguments.
|
|
74
|
-
const getter = /** @type {() => unknown} */ (option);
|
|
75
|
-
return getter();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Calls `target[method]()` when it has one: Lenis and ScrollTrigger are optional and versions differ.
|
|
80
|
-
*
|
|
81
|
-
* @param {unknown} target
|
|
82
|
-
* @param {string} method
|
|
83
|
-
*/
|
|
84
|
-
function callIfPresent(target, method) {
|
|
85
|
-
if (typeof target !== 'object' || target === null) return;
|
|
86
|
-
/** @type {unknown} */
|
|
87
|
-
const fn = Reflect.get(target, method);
|
|
88
|
-
// A method of `target`, called on it.
|
|
89
|
-
if (typeof fn === 'function') /** @type {(this: unknown) => unknown} */ (fn).call(target);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param {unknown} value
|
|
94
|
-
* @returns {value is Gsap}
|
|
95
|
-
*/
|
|
96
|
-
const isGsap = (value) =>
|
|
97
|
-
typeof value === 'object' &&
|
|
98
|
-
value !== null &&
|
|
99
|
-
typeof Reflect.get(value, 'fromTo') === 'function' &&
|
|
100
|
-
typeof Reflect.get(value, 'to') === 'function';
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Hooks for sites with page transitions, smooth scroll and GSAP: spread into `Tether.init()`.
|
|
104
|
-
*
|
|
105
|
-
* @param {BarbaPresetOptions} [options]
|
|
106
|
-
* @returns {BarbaPresetConfig}
|
|
107
|
-
*/
|
|
108
|
-
export function barbaPreset(options = {}) {
|
|
109
|
-
const {
|
|
110
|
-
gsap = () => /** @type {unknown} */ (Reflect.get(window, 'gsap')),
|
|
111
|
-
lenis = null,
|
|
112
|
-
scrollTrigger = () => /** @type {unknown} */ (Reflect.get(window, 'ScrollTrigger')),
|
|
113
|
-
lockScroll = null,
|
|
114
|
-
unlockScroll = null,
|
|
115
|
-
onResize = null,
|
|
116
|
-
onValueChange = null,
|
|
117
|
-
animateLines = true,
|
|
118
|
-
lineDuration = LINE_DURATION_SECONDS,
|
|
119
|
-
} = options;
|
|
120
|
-
|
|
121
|
-
/** Whether this preset holds the scroll lock, so a modal's own lock stays intact. */
|
|
122
|
-
let locked = false;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @param {HTMLElement} node
|
|
126
|
-
* @returns {Gsap | null} GSAP, when this line should animate.
|
|
127
|
-
*/
|
|
128
|
-
const animator = (node) => {
|
|
129
|
-
if (!animateLines || reducedMotion() || node.offsetHeight <= 0) return null;
|
|
130
|
-
const animation = current(gsap);
|
|
131
|
-
return isGsap(animation) ? animation : null;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
autoMount: false,
|
|
136
|
-
|
|
137
|
-
onLayoutChange() {
|
|
138
|
-
callIfPresent(current(lenis), 'resize');
|
|
139
|
-
callIfPresent(current(scrollTrigger), 'refresh');
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
onCartOpen(drawers) {
|
|
143
|
-
const drawer = drawers[0];
|
|
144
|
-
if (locked || typeof lockScroll !== 'function' || !drawer) return;
|
|
145
|
-
lockScroll(drawer);
|
|
146
|
-
locked = true;
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
onCartClose() {
|
|
150
|
-
if (locked && typeof unlockScroll === 'function') unlockScroll();
|
|
151
|
-
locked = false;
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
onLineEnter(node) {
|
|
155
|
-
const animation = animator(node);
|
|
156
|
-
if (!animation) return;
|
|
157
|
-
const style = getComputedStyle(node);
|
|
158
|
-
animation.fromTo(node, collapsed(), {
|
|
159
|
-
height: node.offsetHeight,
|
|
160
|
-
paddingTop: style.paddingTop,
|
|
161
|
-
paddingBottom: style.paddingBottom,
|
|
162
|
-
borderBottomWidth: style.borderBottomWidth,
|
|
163
|
-
opacity: 1,
|
|
164
|
-
duration: lineDuration,
|
|
165
|
-
clearProps: ANIMATED_PROPS,
|
|
166
|
-
onComplete: () => onResize?.(),
|
|
167
|
-
});
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
onLineLeave(node, done) {
|
|
171
|
-
const animation = animator(node);
|
|
172
|
-
if (!animation) {
|
|
173
|
-
done();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
animation.to(node, {
|
|
177
|
-
...collapsed(),
|
|
178
|
-
duration: lineDuration,
|
|
179
|
-
onComplete: () => {
|
|
180
|
-
done();
|
|
181
|
-
onResize?.();
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
},
|
|
185
|
-
|
|
186
|
-
onValueChange: typeof onValueChange === 'function' ? onValueChange : null,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export default barbaPreset;
|
package/src/product-state.js
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a product scope shows for the visitor's choice: the variant, the purchase it is bought as, its prices, the
|
|
3
|
-
* text of every `tether-bind` and every `tether-show` condition. Pure, no DOM and no config: the browser scope
|
|
4
|
-
* (`product.js`) and the server's `initialState()` (`initial.js`) derive their view here, so a server-rendered page
|
|
5
|
-
* shows what the script shows when it takes over.
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
GID,
|
|
9
|
-
allPlans,
|
|
10
|
-
allocationFor,
|
|
11
|
-
findPlan,
|
|
12
|
-
planAdjustment,
|
|
13
|
-
planCheckoutCharge,
|
|
14
|
-
planPrice,
|
|
15
|
-
planSavings,
|
|
16
|
-
} from './selling-plans.js';
|
|
17
|
-
/** @import { Money } from './shopify-values.js' */
|
|
18
|
-
/** @import { Product, ProductVariant } from './fragments.js' */
|
|
19
|
-
/** @import { PlanInGroup } from './selling-plans.js' */
|
|
20
|
-
|
|
21
|
-
/** @typedef {Record<string, string>} Selection Option name → chosen value. */
|
|
22
|
-
/**
|
|
23
|
-
* How the visitor buys. Buying once still names the product's plan when it has plans: the plan the subscription
|
|
24
|
-
* binds (`subscription-price`, `plan-name`) offer next to the one-time price, and the one subscribing switches to.
|
|
25
|
-
*
|
|
26
|
-
* @typedef {{ kind: 'one-time', planId: string | null } | { kind: 'subscription', planId: string }} Purchase
|
|
27
|
-
*/
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {object} ProductChoice What the visitor chose, before it is checked against the product.
|
|
30
|
-
* @property {Selection} selected
|
|
31
|
-
* @property {Purchase} purchase
|
|
32
|
-
* @property {number} quantity
|
|
33
|
-
*/
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {'in-stock' | 'out-of-stock' | 'backorder' | 'on-sale' | 'not-on-sale' | 'has-variants' | 'single-variant'
|
|
36
|
-
* | 'low-stock' | 'plenty-in-stock' | 'subscription-available' | 'subscription-unavailable' | 'subscription-only'
|
|
37
|
-
* | 'one-time-available' | 'subscription-selected' | 'one-time-selected' | 'has-savings'} ShowCondition
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {object} ProductView
|
|
41
|
-
* @property {ProductVariant} variant
|
|
42
|
-
* @property {Selection} selected The selection, matching `variant`.
|
|
43
|
-
* @property {Purchase} purchase The choice, made valid for the product and `variant`.
|
|
44
|
-
* @property {PlanInGroup | null} plan The purchase's plan, offered or chosen.
|
|
45
|
-
* @property {boolean} isSubscription The line is bought on `plan`: subscription chosen and the variant is on it.
|
|
46
|
-
* @property {boolean} subscriptionAvailable The variant is on at least one plan.
|
|
47
|
-
* @property {number} quantity
|
|
48
|
-
* @property {Money} price What one item costs as bought.
|
|
49
|
-
* @property {Money | null} compareAtPrice Only when on sale.
|
|
50
|
-
* @property {{ amount: Money | null, percentage: number }} savings What `plan` saves on the one-time price.
|
|
51
|
-
* @property {Record<ShowCondition, boolean>} show
|
|
52
|
-
*/
|
|
53
|
-
/**
|
|
54
|
-
* @typedef {object} Formatters How the caller writes values as text.
|
|
55
|
-
* @property {(money: Money | null) => string} money
|
|
56
|
-
* @property {(percentage: number) => string} percent
|
|
57
|
-
*/
|
|
58
|
-
/** @typedef {string | number | null | undefined} BindText What a bind writes; `null` and `undefined` clear it. */
|
|
59
|
-
|
|
60
|
-
/** Money arithmetic works in hundredths, the precision Tether shows Shopify's amounts in. */
|
|
61
|
-
const MINOR_UNITS_PER_UNIT = 100;
|
|
62
|
-
const PERCENT = 100;
|
|
63
|
-
|
|
64
|
-
/** @param {Money} money */
|
|
65
|
-
const toMinorUnits = (money) => Math.round(parseFloat(money.amount) * MINOR_UNITS_PER_UNIT);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {number} minorUnits
|
|
69
|
-
* @param {string} currencyCode
|
|
70
|
-
* @returns {Money}
|
|
71
|
-
*/
|
|
72
|
-
const fromMinorUnits = (minorUnits, currencyCode) => ({
|
|
73
|
-
amount: (minorUnits / MINOR_UNITS_PER_UNIT).toFixed(2),
|
|
74
|
-
currencyCode,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @param {Money} money
|
|
79
|
-
* @param {number} quantity
|
|
80
|
-
*/
|
|
81
|
-
const timesQuantity = (money, quantity) => fromMinorUnits(toMinorUnits(money) * quantity, money.currencyCode);
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @param {ProductVariant} variant
|
|
85
|
-
* @returns {Selection}
|
|
86
|
-
*/
|
|
87
|
-
export const selectionOf = (variant) =>
|
|
88
|
-
Object.fromEntries(variant.selectedOptions.map((option) => [option.name, option.value]));
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @param {Product} product
|
|
92
|
-
* @param {Selection} selected
|
|
93
|
-
* @returns {ProductVariant | null} The variant with exactly these option values.
|
|
94
|
-
*/
|
|
95
|
-
function findVariant(product, selected) {
|
|
96
|
-
return (
|
|
97
|
-
product.variants.nodes.find((variant) =>
|
|
98
|
-
variant.selectedOptions.every((option) => selected[option.name] === option.value),
|
|
99
|
-
) ?? null
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* @param {Product} product
|
|
105
|
-
* @param {Selection} selected
|
|
106
|
-
* @param {string} name An option name.
|
|
107
|
-
* @param {string} value
|
|
108
|
-
* @returns {boolean} True when a variant for sale has this value and the other selected values.
|
|
109
|
-
*/
|
|
110
|
-
export function isValueAvailable(product, selected, name, value) {
|
|
111
|
-
return product.variants.nodes.some(
|
|
112
|
-
(variant) =>
|
|
113
|
-
variant.availableForSale &&
|
|
114
|
-
variant.selectedOptions.every((option) =>
|
|
115
|
-
option.name === name ? option.value === value : selected[option.name] === option.value,
|
|
116
|
-
),
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The purchase made valid for the product and variant: one-time without plans, a subscription when the product
|
|
122
|
-
* requires one, and a plan the variant is on when it is on any (else a plan of the product, for the binds to offer).
|
|
123
|
-
*
|
|
124
|
-
* @param {Product} product
|
|
125
|
-
* @param {ProductVariant | null} variant
|
|
126
|
-
* @param {{ kind: Purchase['kind'], planId: string | null }} wanted
|
|
127
|
-
* @returns {Purchase}
|
|
128
|
-
*/
|
|
129
|
-
export function normalisePurchase(product, variant, wanted) {
|
|
130
|
-
const plans = allPlans(product);
|
|
131
|
-
const known = plans.find((plan) => plan.id === wanted.planId) ?? plans[0];
|
|
132
|
-
if (!known) return { kind: 'one-time', planId: null };
|
|
133
|
-
const onVariant = allocationFor(variant, known.id)
|
|
134
|
-
? known
|
|
135
|
-
: (plans.find((plan) => allocationFor(variant, plan.id)) ?? known);
|
|
136
|
-
if (product.requiresSellingPlan || wanted.kind === 'subscription')
|
|
137
|
-
return { kind: 'subscription', planId: onVariant.id };
|
|
138
|
-
return { kind: 'one-time', planId: onVariant.id };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @typedef {object} InitialChoiceOptions
|
|
143
|
-
* @property {string | null | undefined} [variant] A variant id or gid (`?variant=`); the first variant for sale without.
|
|
144
|
-
* @property {string | null | undefined} [sellingPlan] A plan to subscribe on (`?selling_plan=`).
|
|
145
|
-
* @property {string | null | undefined} [defaultPlan] The plan offered first, by id, name or option value.
|
|
146
|
-
* @property {string | null | undefined} [defaultPurchase] `subscription` subscribes; anything else buys once.
|
|
147
|
-
*/
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* What a product starts with before the visitor chooses anything.
|
|
151
|
-
*
|
|
152
|
-
* @param {Product} product
|
|
153
|
-
* @param {InitialChoiceOptions} options
|
|
154
|
-
* @returns {Omit<ProductChoice, 'quantity'>}
|
|
155
|
-
*/
|
|
156
|
-
export function initialChoice(product, { variant, sellingPlan, defaultPlan, defaultPurchase }) {
|
|
157
|
-
const variants = product.variants.nodes;
|
|
158
|
-
const wantedId = GID.variant(variant);
|
|
159
|
-
const start =
|
|
160
|
-
variants.find((candidate) => candidate.id === wantedId) ??
|
|
161
|
-
variants.find((candidate) => candidate.availableForSale) ??
|
|
162
|
-
variants[0] ??
|
|
163
|
-
null;
|
|
164
|
-
const fromUrl = findPlan(product, sellingPlan);
|
|
165
|
-
const offered = fromUrl ?? findPlan(product, defaultPlan);
|
|
166
|
-
const kind = fromUrl || defaultPurchase === 'subscription' ? 'subscription' : 'one-time';
|
|
167
|
-
return {
|
|
168
|
-
selected: start ? selectionOf(start) : {},
|
|
169
|
-
purchase: normalisePurchase(product, start, { kind, planId: offered?.id ?? null }),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @param {ProductVariant} variant
|
|
175
|
-
* @param {number} lowStock
|
|
176
|
-
* @param {number} variantCount
|
|
177
|
-
* @param {Pick<ProductView, 'isSubscription' | 'subscriptionAvailable' | 'compareAtPrice' | 'savings'>} sale
|
|
178
|
-
* @param {boolean} requiresSellingPlan
|
|
179
|
-
* @returns {Record<ShowCondition, boolean>}
|
|
180
|
-
*/
|
|
181
|
-
function showConditions(variant, lowStock, variantCount, sale, requiresSellingPlan) {
|
|
182
|
-
const stock = variant.quantityAvailable;
|
|
183
|
-
const low = stock != null && stock > 0 && stock <= lowStock;
|
|
184
|
-
const onSale = sale.compareAtPrice !== null;
|
|
185
|
-
return {
|
|
186
|
-
'in-stock': variant.availableForSale,
|
|
187
|
-
'out-of-stock': !variant.availableForSale,
|
|
188
|
-
backorder: variant.availableForSale && variant.currentlyNotInStock,
|
|
189
|
-
'on-sale': onSale,
|
|
190
|
-
'not-on-sale': !onSale,
|
|
191
|
-
'has-variants': variantCount > 1,
|
|
192
|
-
'single-variant': variantCount === 1,
|
|
193
|
-
'low-stock': low,
|
|
194
|
-
'plenty-in-stock': variant.availableForSale && !low,
|
|
195
|
-
'subscription-available': sale.subscriptionAvailable,
|
|
196
|
-
'subscription-unavailable': !sale.subscriptionAvailable,
|
|
197
|
-
'subscription-only': requiresSellingPlan,
|
|
198
|
-
'one-time-available': !requiresSellingPlan,
|
|
199
|
-
'subscription-selected': sale.isSubscription,
|
|
200
|
-
'one-time-selected': !sale.isSubscription,
|
|
201
|
-
'has-savings': sale.isSubscription ? sale.savings.percentage > 0 : onSale,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* The view for the visitor's choice. A selection no variant has falls back to the first variant.
|
|
207
|
-
*
|
|
208
|
-
* @param {Product} product
|
|
209
|
-
* @param {ProductChoice} choice
|
|
210
|
-
* @param {number} lowStock `low-stock` at this many or fewer.
|
|
211
|
-
* @returns {ProductView | null} `null` for a product without variants.
|
|
212
|
-
*/
|
|
213
|
-
export function productView(product, choice, lowStock) {
|
|
214
|
-
const exact = findVariant(product, choice.selected);
|
|
215
|
-
const variant = exact ?? product.variants.nodes[0];
|
|
216
|
-
if (!variant) return null;
|
|
217
|
-
const purchase = normalisePurchase(product, variant, choice.purchase);
|
|
218
|
-
const { planId } = purchase;
|
|
219
|
-
const plans = allPlans(product);
|
|
220
|
-
const allocation = purchase.kind === 'subscription' ? allocationFor(variant, planId) : null;
|
|
221
|
-
// The plan helpers fall back to the one-time price only without a variant, which `variant` rules out.
|
|
222
|
-
const price = allocation ? (planPrice(variant, planId) ?? variant.price) : variant.price;
|
|
223
|
-
const compareAt = allocation?.priceAdjustments[0]?.compareAtPrice ?? variant.compareAtPrice;
|
|
224
|
-
const sale = {
|
|
225
|
-
isSubscription: allocation !== null,
|
|
226
|
-
subscriptionAvailable: plans.some((plan) => allocationFor(variant, plan.id)),
|
|
227
|
-
compareAtPrice: compareAt && parseFloat(compareAt.amount) > parseFloat(price.amount) ? compareAt : null,
|
|
228
|
-
savings: planSavings(variant, planId),
|
|
229
|
-
};
|
|
230
|
-
return {
|
|
231
|
-
...sale,
|
|
232
|
-
variant,
|
|
233
|
-
selected: exact ? choice.selected : selectionOf(variant),
|
|
234
|
-
purchase,
|
|
235
|
-
plan: plans.find((plan) => plan.id === planId) ?? null,
|
|
236
|
-
quantity: choice.quantity,
|
|
237
|
-
price,
|
|
238
|
-
show: showConditions(variant, lowStock, product.variants.nodes.length, sale, product.requiresSellingPlan),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* The `tether-bind` values that follow the variant and purchase. Thunks, so only the binds on the page are computed.
|
|
244
|
-
*
|
|
245
|
-
* @param {ProductView} view
|
|
246
|
-
* @param {Formatters} format
|
|
247
|
-
* @returns {Map<string, () => BindText>}
|
|
248
|
-
*/
|
|
249
|
-
export function variantBindTexts(view, format) {
|
|
250
|
-
const { variant, plan, price, compareAtPrice, quantity } = view;
|
|
251
|
-
const planId = view.purchase.planId;
|
|
252
|
-
// Prices for the quantity count one item when it is 0 (an optional bundle item).
|
|
253
|
-
const forQuantity = (/** @type {Money} */ money) => timesQuantity(money, Math.max(1, quantity));
|
|
254
|
-
const checkoutCharge = () => {
|
|
255
|
-
const charge = view.isSubscription ? (planCheckoutCharge(variant, planId) ?? price) : price;
|
|
256
|
-
return format.money(forQuantity(charge));
|
|
257
|
-
};
|
|
258
|
-
const discountPercentage = () => {
|
|
259
|
-
if (!compareAtPrice) return '0%';
|
|
260
|
-
return `${Math.round((1 - parseFloat(price.amount) / parseFloat(compareAtPrice.amount)) * PERCENT)}%`;
|
|
261
|
-
};
|
|
262
|
-
const discountAmount = () => {
|
|
263
|
-
if (!compareAtPrice) return '';
|
|
264
|
-
return format.money(fromMinorUnits(toMinorUnits(compareAtPrice) - toMinorUnits(price), price.currencyCode));
|
|
265
|
-
};
|
|
266
|
-
/** @type {Record<string, () => BindText>} */
|
|
267
|
-
const texts = {
|
|
268
|
-
price: () => format.money(price),
|
|
269
|
-
'total-price': () => format.money(forQuantity(price)),
|
|
270
|
-
'one-time-price': () => format.money(variant.price),
|
|
271
|
-
'subscription-price': () => format.money(planId ? planPrice(variant, planId) : null),
|
|
272
|
-
'subscription-savings': () => format.percent(view.savings.percentage),
|
|
273
|
-
'subscription-savings-amount': () => format.money(view.savings.amount),
|
|
274
|
-
'checkout-charge': checkoutCharge,
|
|
275
|
-
'plan-name': () => plan?.name,
|
|
276
|
-
'plan-description': () => plan?.description,
|
|
277
|
-
'plan-group': () => plan?.group.name,
|
|
278
|
-
'compare-at-price': () => format.money(compareAtPrice),
|
|
279
|
-
'unit-price': () => format.money(variant.unitPrice),
|
|
280
|
-
'discount-percentage': discountPercentage,
|
|
281
|
-
'discount-amount': discountAmount,
|
|
282
|
-
'variant-title': () => variant.title,
|
|
283
|
-
'variant-id': () => variant.id,
|
|
284
|
-
sku: () => variant.sku,
|
|
285
|
-
barcode: () => variant.barcode,
|
|
286
|
-
stock: () => variant.quantityAvailable,
|
|
287
|
-
quantity: () => quantity,
|
|
288
|
-
};
|
|
289
|
-
return new Map(Object.entries(texts));
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* The `tether-plan-bind` values of one plan in a plan list, for the variant.
|
|
293
|
-
*
|
|
294
|
-
* @param {ProductVariant} variant
|
|
295
|
-
* @param {string} planId The list item's `tether-plan-value`.
|
|
296
|
-
* @param {PlanInGroup | undefined} plan The product's plan with that id.
|
|
297
|
-
* @param {Formatters} format
|
|
298
|
-
* @returns {Map<string, () => BindText>}
|
|
299
|
-
*/
|
|
300
|
-
export function planBindTexts(variant, planId, plan, format) {
|
|
301
|
-
const adjustmentText = () => {
|
|
302
|
-
const adjustment = planAdjustment(plan);
|
|
303
|
-
if (!adjustment) return '';
|
|
304
|
-
return adjustment.type === 'percentage' ? `${adjustment.value}%` : format.money(adjustment.value);
|
|
305
|
-
};
|
|
306
|
-
/** @type {Record<string, () => BindText>} */
|
|
307
|
-
const texts = {
|
|
308
|
-
name: () => plan?.name,
|
|
309
|
-
// The option values alone ("1 month"), without the price text a store may put in the plan name.
|
|
310
|
-
option: () => (plan?.options ?? []).map((option) => option.value).join(', '),
|
|
311
|
-
description: () => plan?.description,
|
|
312
|
-
price: () => format.money(planPrice(variant, planId)),
|
|
313
|
-
savings: () => format.percent(planSavings(variant, planId).percentage),
|
|
314
|
-
'savings-amount': () => format.money(planSavings(variant, planId).amount),
|
|
315
|
-
adjustment: adjustmentText,
|
|
316
|
-
};
|
|
317
|
-
return new Map(Object.entries(texts));
|
|
318
|
-
}
|
package/src/selling-plans.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify selling plans (subscriptions): which plans a product and a variant have, and what each costs. Pure, no DOM,
|
|
3
|
-
* shared by the browser script and the server helpers.
|
|
4
|
-
*/
|
|
5
|
-
/** @import { Money } from './shopify-values.js' */
|
|
6
|
-
/** @import { SellingPlan, SellingPlanGroup, SellingPlanAllocation } from './fragments.js' */
|
|
7
|
-
|
|
8
|
-
/** @typedef {SellingPlan & { group: SellingPlanGroup }} PlanInGroup A plan with the group it belongs to. */
|
|
9
|
-
/** @typedef {{ sellingPlanGroups?: { nodes: SellingPlanGroup[] } | null }} ProductWithPlans */
|
|
10
|
-
/**
|
|
11
|
-
* A variant as far as plans go. A cart line's variant has no allocations.
|
|
12
|
-
*
|
|
13
|
-
* @typedef {{ price: Money, sellingPlanAllocations?: { nodes: SellingPlanAllocation[] } | null }} VariantWithPlans
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {{ type: 'percentage', value: number } | { type: 'amount', value: Money } | { type: 'price', value: Money }}
|
|
17
|
-
* PlanAdjustment
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {unknown} value A gid, a numeric id, or anything else.
|
|
22
|
-
* @param {string} type The Shopify type, e.g. `ProductVariant`.
|
|
23
|
-
* @returns {string | null} The gid of a numeric id; a gid or other value as written; `null` when empty.
|
|
24
|
-
*/
|
|
25
|
-
export function toGid(value, type) {
|
|
26
|
-
const text = typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';
|
|
27
|
-
if (!text) return null;
|
|
28
|
-
if (text.startsWith('gid://')) return text;
|
|
29
|
-
if (/^\d+$/.test(text)) return `gid://shopify/${type}/${text}`;
|
|
30
|
-
return text;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const GID = {
|
|
34
|
-
/** @param {unknown} value */
|
|
35
|
-
product: (value) => toGid(value, 'Product'),
|
|
36
|
-
/** @param {unknown} value */
|
|
37
|
-
variant: (value) => toGid(value, 'ProductVariant'),
|
|
38
|
-
/** @param {unknown} value */
|
|
39
|
-
sellingPlan: (value) => toGid(value, 'SellingPlan'),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {ProductWithPlans | null | undefined} product
|
|
44
|
-
* @returns {PlanInGroup[]} All plans of the product, flattened.
|
|
45
|
-
*/
|
|
46
|
-
export function allPlans(product) {
|
|
47
|
-
const groups = product?.sellingPlanGroups?.nodes ?? [];
|
|
48
|
-
return groups.flatMap((group) => group.sellingPlans.nodes.map((plan) => ({ ...plan, group })));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** @param {ProductWithPlans | null | undefined} product */
|
|
52
|
-
export function hasPlans(product) {
|
|
53
|
-
return allPlans(product).length > 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @param {VariantWithPlans | null | undefined} variant
|
|
58
|
-
* @param {string | null | undefined} planId
|
|
59
|
-
* @returns {SellingPlanAllocation | null} The variant's allocation on the plan; `null` when the variant isn't in it.
|
|
60
|
-
*/
|
|
61
|
-
export function allocationFor(variant, planId) {
|
|
62
|
-
if (!variant || !planId) return null;
|
|
63
|
-
const allocations = variant.sellingPlanAllocations?.nodes ?? [];
|
|
64
|
-
return allocations.find((allocation) => allocation.sellingPlan.id === planId) ?? null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @param {ProductWithPlans | null | undefined} product
|
|
69
|
-
* @param {VariantWithPlans | null | undefined} variant
|
|
70
|
-
* @returns {PlanInGroup[]} The plans this variant can be bought on.
|
|
71
|
-
*/
|
|
72
|
-
export function plansForVariant(product, variant) {
|
|
73
|
-
return allPlans(product).filter((plan) => allocationFor(variant, plan.id));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @param {VariantWithPlans | null | undefined} variant
|
|
78
|
-
* @param {string | null | undefined} planId
|
|
79
|
-
* @returns {Money | null} The price per delivery on the plan; the one-time price when the variant isn't on it.
|
|
80
|
-
*/
|
|
81
|
-
export function planPrice(variant, planId) {
|
|
82
|
-
const adjustment = allocationFor(variant, planId)?.priceAdjustments[0];
|
|
83
|
-
return adjustment?.perDeliveryPrice ?? adjustment?.price ?? variant?.price ?? null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param {VariantWithPlans | null | undefined} variant
|
|
88
|
-
* @param {string | null | undefined} planId
|
|
89
|
-
* @returns {Money | null} What the customer pays at checkout on the plan (prepaid plans charge more up front).
|
|
90
|
-
*/
|
|
91
|
-
export function planCheckoutCharge(variant, planId) {
|
|
92
|
-
const allocation = allocationFor(variant, planId);
|
|
93
|
-
return allocation?.checkoutChargeAmount ?? allocation?.priceAdjustments[0]?.price ?? variant?.price ?? null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {VariantWithPlans | null | undefined} variant
|
|
98
|
-
* @param {string | null | undefined} planId
|
|
99
|
-
* @returns {{ amount: Money | null, percentage: number }} What the plan saves on the one-time price; no amount and
|
|
100
|
-
* 0% when it saves nothing.
|
|
101
|
-
*/
|
|
102
|
-
export function planSavings(variant, planId) {
|
|
103
|
-
const oneTime = parseFloat(variant?.price.amount ?? '');
|
|
104
|
-
const onPlan = parseFloat(planPrice(variant, planId)?.amount ?? '');
|
|
105
|
-
if (!variant || !Number.isFinite(oneTime) || !Number.isFinite(onPlan) || oneTime <= 0 || onPlan >= oneTime) {
|
|
106
|
-
return { amount: null, percentage: 0 };
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
amount: { amount: (oneTime - onPlan).toFixed(2), currencyCode: variant.price.currencyCode },
|
|
110
|
-
percentage: Math.round((1 - onPlan / oneTime) * 100),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {Pick<SellingPlan, 'priceAdjustments'> | null | undefined} plan
|
|
116
|
-
* @returns {PlanAdjustment | null} The plan's own discount, whatever the variant: a percentage, a fixed amount off or
|
|
117
|
-
* a fixed price.
|
|
118
|
-
*/
|
|
119
|
-
export function planAdjustment(plan) {
|
|
120
|
-
const value = plan?.priceAdjustments[0]?.adjustmentValue;
|
|
121
|
-
if (!value) return null;
|
|
122
|
-
switch (value.__typename) {
|
|
123
|
-
case 'SellingPlanPercentagePriceAdjustment':
|
|
124
|
-
return { type: 'percentage', value: value.adjustmentPercentage };
|
|
125
|
-
case 'SellingPlanFixedAmountPriceAdjustment':
|
|
126
|
-
return { type: 'amount', value: value.adjustmentAmount };
|
|
127
|
-
case 'SellingPlanFixedPriceAdjustment':
|
|
128
|
-
return { type: 'price', value: value.price };
|
|
129
|
-
default:
|
|
130
|
-
// An adjustment type newer than the API version Tether was written for.
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Finds a plan by id, numeric id, name or option value (case-insensitive), so Webflow editors can write
|
|
137
|
-
* `tether-default-plan="Every month"`.
|
|
138
|
-
*
|
|
139
|
-
* @param {ProductWithPlans | null | undefined} product
|
|
140
|
-
* @param {string | null | undefined} ref
|
|
141
|
-
* @returns {PlanInGroup | null}
|
|
142
|
-
*/
|
|
143
|
-
export function findPlan(product, ref) {
|
|
144
|
-
if (!ref) return null;
|
|
145
|
-
const plans = allPlans(product);
|
|
146
|
-
const gid = GID.sellingPlan(ref);
|
|
147
|
-
const needle = ref.trim().toLowerCase();
|
|
148
|
-
return (
|
|
149
|
-
plans.find((plan) => plan.id === gid) ??
|
|
150
|
-
plans.find((plan) => plan.name.toLowerCase() === needle) ??
|
|
151
|
-
plans.find((plan) => plan.options.some((option) => option.value.toLowerCase() === needle)) ??
|
|
152
|
-
null
|
|
153
|
-
);
|
|
154
|
-
}
|