@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/account-api.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The core's side of customer accounts, which are an optional part: whether a page needs the part, and
|
|
3
|
-
* `Tether.account`, whose functions load it on first call. Every function returns a promise, the part's own sync ones
|
|
4
|
-
* (`isLoggedIn`) too, because the part may still have to load.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, reportError } from './config.js';
|
|
7
|
-
import { attr } from './dom.js';
|
|
8
|
-
import { loadPart } from './lazy.js';
|
|
9
|
-
import { within } from './mounting.js';
|
|
10
|
-
/** @import { Parts } from './lazy.js' */
|
|
11
|
-
|
|
12
|
-
/** @typedef {Parts['account']} AccountPart */
|
|
13
|
-
/**
|
|
14
|
-
* @template {keyof AccountPart} Name
|
|
15
|
-
* @typedef {Awaited<ReturnType<AccountPart[Name]>>} AccountResult
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* @template {keyof AccountPart} Name
|
|
19
|
-
* @typedef {(...args: Parameters<AccountPart[Name]>) => Promise<AccountResult<Name>>} AccountFunction
|
|
20
|
-
*/
|
|
21
|
-
/** @typedef {{ [Name in keyof AccountPart]: AccountFunction<Name> }} AccountApi */
|
|
22
|
-
|
|
23
|
-
/** The attributes of the account markup: a page with one of them loads the part. */
|
|
24
|
-
const ACCOUNT_ATTRIBUTES = [
|
|
25
|
-
'account-login',
|
|
26
|
-
'account-bind',
|
|
27
|
-
'account-link',
|
|
28
|
-
'orders',
|
|
29
|
-
'order',
|
|
30
|
-
'addresses',
|
|
31
|
-
'subscriptions',
|
|
32
|
-
'profile-form',
|
|
33
|
-
'address-form',
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
/** @returns {string} A selector for every element the account part fills in or shows. */
|
|
37
|
-
const accountSelector = () =>
|
|
38
|
-
[...ACCOUNT_ATTRIBUTES.map((name) => `[${attr(name)}]`), `[${attr('show')}^="logged-"]`].join(',');
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Whether this browser keeps an account session or the hint of one, so a page without account markup still finishes
|
|
42
|
-
* a login. Storage throws where the browser blocks it, and then there is no session.
|
|
43
|
-
*
|
|
44
|
-
* @returns {boolean}
|
|
45
|
-
*/
|
|
46
|
-
function hasSavedSession() {
|
|
47
|
-
const sessionKey = `${cfg.prefix}:account:${cfg.domain}`;
|
|
48
|
-
const hintKey = `${cfg.prefix}:account-hint:${cfg.domain}`;
|
|
49
|
-
try {
|
|
50
|
-
return Boolean(
|
|
51
|
-
sessionStorage.getItem(sessionKey) || localStorage.getItem(sessionKey) || localStorage.getItem(hintKey),
|
|
52
|
-
);
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Loads the account part and mounts it on `root`, when accounts are configured and the page needs them: account
|
|
60
|
-
* markup in `root`, a login coming back from Shopify (`?code=`), or a saved session.
|
|
61
|
-
*
|
|
62
|
-
* @param {ParentNode} root
|
|
63
|
-
*/
|
|
64
|
-
export function mountAccountIfUsed(root) {
|
|
65
|
-
if (!cfg.account?.clientId && !cfg.account?.server) return;
|
|
66
|
-
const needed =
|
|
67
|
-
within(root, accountSelector()).length > 0 || /[?&]code=/.test(window.location.search) || hasSavedSession();
|
|
68
|
-
if (!needed) return;
|
|
69
|
-
loadPart('account')
|
|
70
|
-
.then((part) => part.mountAccount(root))
|
|
71
|
-
.catch((/** @type {unknown} */ error) => {
|
|
72
|
-
reportError(error, 'account');
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @template {keyof AccountPart} Name
|
|
78
|
-
* @param {Name} name
|
|
79
|
-
* @returns {AccountFunction<Name>} `name` of the account part, called once the part has loaded.
|
|
80
|
-
*/
|
|
81
|
-
function fromPart(name) {
|
|
82
|
-
return (...args) =>
|
|
83
|
-
loadPart('account').then((part) => {
|
|
84
|
-
// `Name` picks one function of the part, and `args` are that function's parameters.
|
|
85
|
-
const call =
|
|
86
|
-
/**
|
|
87
|
-
* @type {(...callArgs: Parameters<AccountPart[Name]>) => AccountResult<Name> | PromiseLike<AccountResult<Name>>}
|
|
88
|
-
*/ (part[name]);
|
|
89
|
-
return call(...args);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* `Tether.account`: the account part's functions, each loading the part on first call. A plain object, so `await
|
|
95
|
-
* Tether.account` and `Object.keys(Tether.account)` behave as for any object.
|
|
96
|
-
*
|
|
97
|
-
* @type {AccountApi}
|
|
98
|
-
*/
|
|
99
|
-
export const accountApi = {
|
|
100
|
-
login: fromPart('login'),
|
|
101
|
-
logout: fromPart('logout'),
|
|
102
|
-
handleCallback: fromPart('handleCallback'),
|
|
103
|
-
getToken: fromPart('getToken'),
|
|
104
|
-
isLoggedIn: fromPart('isLoggedIn'),
|
|
105
|
-
query: fromPart('query'),
|
|
106
|
-
customer: fromPart('customer'),
|
|
107
|
-
orders: fromPart('orders'),
|
|
108
|
-
order: fromPart('order'),
|
|
109
|
-
updateProfile: fromPart('updateProfile'),
|
|
110
|
-
addresses: fromPart('addresses'),
|
|
111
|
-
saveAddress: fromPart('saveAddress'),
|
|
112
|
-
deleteAddress: fromPart('deleteAddress'),
|
|
113
|
-
subscriptions: fromPart('subscriptions'),
|
|
114
|
-
subscriptionAction: fromPart('subscriptionAction'),
|
|
115
|
-
requestReturn: fromPart('requestReturn'),
|
|
116
|
-
render: fromPart('render'),
|
|
117
|
-
mountAccount: fromPart('mountAccount'),
|
|
118
|
-
};
|
package/src/account-links.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/** Order links and dates, shared by the account part's order list and the account pages part. */
|
|
2
|
-
import { cfg } from './config.js';
|
|
3
|
-
import { isHttpsUrl } from './shopify-values.js';
|
|
4
|
-
|
|
5
|
-
export { isHttpsUrl };
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* An ISO date in the configured locale; the input itself for a locale `Intl` doesn't know (a RangeError).
|
|
9
|
-
*
|
|
10
|
-
* @param {string} iso
|
|
11
|
-
*/
|
|
12
|
-
export function localDate(iso) {
|
|
13
|
-
try {
|
|
14
|
-
return new Date(iso).toLocaleDateString(cfg.locale);
|
|
15
|
-
} catch (error) {
|
|
16
|
-
if (error instanceof RangeError) return iso;
|
|
17
|
-
throw error;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The order's page: the site's own (`account.orderUrl`, e.g. `/account/order?id={id}`, with the numeric order id),
|
|
23
|
-
* else Shopify's status page when it is https.
|
|
24
|
-
*
|
|
25
|
-
* @param {{ id: string, statusPageUrl?: string | null }} order
|
|
26
|
-
* @returns {string | null}
|
|
27
|
-
*/
|
|
28
|
-
export function orderUrl(order) {
|
|
29
|
-
const template = cfg.account?.orderUrl;
|
|
30
|
-
if (template) {
|
|
31
|
-
const numericId = order.id.split('/').pop()?.split('?')[0] ?? '';
|
|
32
|
-
return template.replace('{id}', encodeURIComponent(numericId));
|
|
33
|
-
}
|
|
34
|
-
return isHttpsUrl(order.statusPageUrl) ? (order.statusPageUrl ?? null) : null;
|
|
35
|
-
}
|
package/src/account.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Customer accounts (Shopify Customer Account API), as an optional part: loaded only when `account` is configured
|
|
3
|
-
* and the page uses it, or a login is in progress, or the visitor may be logged in.
|
|
4
|
-
*
|
|
5
|
-
* window.TETHER_CONFIG = { …, account: { server: '/_tether/account' } } // or { clientId: '…' } for browser mode
|
|
6
|
-
*
|
|
7
|
-
* <button tether-account-login>Log in</button>
|
|
8
|
-
* <button tether-account-logout tether-show="logged-in">Log out</button>
|
|
9
|
-
* <span tether-account-bind="first-name"></span> first-name, last-name, name, email, address
|
|
10
|
-
* <div tether-orders> the customer's orders (first child = template)
|
|
11
|
-
* <a tether-order-bind="url"><span tether-order-bind="name"></span> · <span tether-order-bind="date"></span></a>
|
|
12
|
-
* </div>
|
|
13
|
-
* <p tether-show="logged-out">…</p>
|
|
14
|
-
*
|
|
15
|
-
* Two modes, one interface: server mode (`account/server-client.js`), where the site's Worker holds the tokens, and
|
|
16
|
-
* browser mode (`account/browser-session.js`), where this browser does the OAuth login itself. Shopify needs HTTPS
|
|
17
|
-
* (no localhost: use a tunnel) and the site's origin, callback and logout URLs registered in the Headless channel's
|
|
18
|
-
* Customer Account API settings. The order page, profile, addresses, subscriptions and returns are the
|
|
19
|
-
* `account-pages` part, loaded only on pages that use them.
|
|
20
|
-
*/
|
|
21
|
-
import { cfg, log, reportError } from './config.js';
|
|
22
|
-
import { TetherError } from './client.js';
|
|
23
|
-
import { sel, get, qsa, setText, show, emit } from './dom.js';
|
|
24
|
-
import { Cart } from './cart.js';
|
|
25
|
-
import { loadPart } from './lazy.js';
|
|
26
|
-
import { ORDERS_QUERY } from '../server/src/shared/account-documents.js';
|
|
27
|
-
import {
|
|
28
|
-
browserCustomer,
|
|
29
|
-
browserLoginRedirect,
|
|
30
|
-
browserLogout,
|
|
31
|
-
forgetBrowserCustomer,
|
|
32
|
-
getToken,
|
|
33
|
-
handleCallback,
|
|
34
|
-
hasBrowserLogin,
|
|
35
|
-
query as browserQuery,
|
|
36
|
-
} from './account/browser-session.js';
|
|
37
|
-
import {
|
|
38
|
-
forgetServerLogin,
|
|
39
|
-
isServerLoggedIn,
|
|
40
|
-
serverBase,
|
|
41
|
-
serverCheckoutUrl,
|
|
42
|
-
serverFetch,
|
|
43
|
-
serverLogin,
|
|
44
|
-
serverLoginRedirect,
|
|
45
|
-
serverLogout,
|
|
46
|
-
} from './account/server-client.js';
|
|
47
|
-
import { renderOrderList } from './account/order-list.js';
|
|
48
|
-
import { ownValue } from './account/markup.js';
|
|
49
|
-
/** @import { AccountCustomer } from './account/browser-session.js' */
|
|
50
|
-
/** @import { AccountOrder } from './account/order-list.js' */
|
|
51
|
-
/** @import { Parts } from './lazy.js' */
|
|
52
|
-
/** @import { AddressFields, ReturnLine } from './account/operations.js' */
|
|
53
|
-
/** @import { SubscriptionAction } from '../server/src/shared/account-ops.js' */
|
|
54
|
-
|
|
55
|
-
export { getToken, handleCallback };
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @typedef {{ kind: 'logged-in' } | { kind: 'logged-out' } | { kind: 'unknown', error: unknown }} LoginState
|
|
59
|
-
* `unknown` when the Worker or Shopify can't say: the page shows its logged-out content, and the account pages their
|
|
60
|
-
* error state.
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/** The attributes of the account pages, which are their own part. */
|
|
64
|
-
const PAGE_ATTRIBUTES = ['order', 'addresses', 'subscriptions', 'profile-form', 'address-form'];
|
|
65
|
-
|
|
66
|
-
/** @returns {boolean} True while a customer is logged in, as far as this page knows without asking. */
|
|
67
|
-
export function isLoggedIn() {
|
|
68
|
-
return serverBase() ? isServerLoggedIn() : hasBrowserLogin();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* To Shopify's login page; the visitor comes back to `account.callbackUrl` (default: this page).
|
|
73
|
-
*
|
|
74
|
-
* @param {{ returnTo?: string, locale?: string }} [options]
|
|
75
|
-
* @returns {Promise<void>}
|
|
76
|
-
* @throws {TetherError} `NOT_CONFIGURED` in browser mode without `account.clientId`.
|
|
77
|
-
*/
|
|
78
|
-
export async function login({ returnTo = window.location.href, locale } = {}) {
|
|
79
|
-
if (serverBase()) serverLoginRedirect({ returnTo, locale });
|
|
80
|
-
else await browserLoginRedirect({ returnTo, locale });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Any Customer Account API query, in browser mode.
|
|
85
|
-
*
|
|
86
|
-
* @template T The shape of the query's `data`.
|
|
87
|
-
* @param {string} document
|
|
88
|
-
* @param {Record<string, unknown>} [variables]
|
|
89
|
-
* @returns {Promise<T>}
|
|
90
|
-
* @throws {TetherError} `NOT_ALLOWED` in server mode, which answers only its fixed operations; `NOT_LOGGED_IN`.
|
|
91
|
-
*/
|
|
92
|
-
export async function query(document, variables = {}) {
|
|
93
|
-
if (serverBase()) {
|
|
94
|
-
throw new TetherError('Customer accounts in server mode only answer customer() and orders()', {
|
|
95
|
-
code: 'NOT_ALLOWED',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
return browserQuery(document, variables);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** @returns {Promise<AccountCustomer | null>} The logged-in customer, `null` when nobody is. */
|
|
102
|
-
export async function customer() {
|
|
103
|
-
if (!serverBase()) return browserCustomer();
|
|
104
|
-
const me = await serverLogin();
|
|
105
|
-
if (!me.loggedIn) return null;
|
|
106
|
-
return {
|
|
107
|
-
...me.customer,
|
|
108
|
-
emailAddress: { emailAddress: me.customer.email },
|
|
109
|
-
defaultAddress: { formatted: me.customer.address },
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {{ first?: number }} [options]
|
|
115
|
-
* @returns {Promise<AccountOrder[]>} The customer's most recent orders, newest first; none when nobody is logged in
|
|
116
|
-
* (server mode).
|
|
117
|
-
*/
|
|
118
|
-
export async function orders({ first = 10 } = {}) {
|
|
119
|
-
if (serverBase()) {
|
|
120
|
-
const answer = await serverFetch(`/orders?first=${first}`);
|
|
121
|
-
if (answer.kind === 'logged-out') return [];
|
|
122
|
-
const list = answer.data['orders'];
|
|
123
|
-
// The Worker sends the nodes of the same `ORDERS_QUERY`.
|
|
124
|
-
return Array.isArray(list) ? /** @type {AccountOrder[]} */ (list) : [];
|
|
125
|
-
}
|
|
126
|
-
/** @type {{ customer: { orders: { nodes: AccountOrder[] } } | null }} */
|
|
127
|
-
const data = await browserQuery(ORDERS_QUERY, { first });
|
|
128
|
-
return data.customer?.orders.nodes ?? [];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Logs out here and at Shopify, and unlinks the cart. Sends `tether:account-logout`.
|
|
133
|
-
*
|
|
134
|
-
* @param {{ returnTo?: string }} [options] Where Shopify sends the visitor afterwards.
|
|
135
|
-
* @returns {Promise<void>}
|
|
136
|
-
*/
|
|
137
|
-
export async function logout({ returnTo = cfg.account?.logoutUrl || `${window.location.origin}/` } = {}) {
|
|
138
|
-
const outcome = serverBase() ? await serverLogout(returnTo) : await browserLogout(returnTo);
|
|
139
|
-
if (outcome === 'stayed') await render(document);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** @returns {Promise<LoginState>} */
|
|
143
|
-
async function loginState() {
|
|
144
|
-
try {
|
|
145
|
-
if (serverBase()) return (await serverLogin()).loggedIn ? { kind: 'logged-in' } : { kind: 'logged-out' };
|
|
146
|
-
if (hasBrowserLogin() || (await getToken())) return { kind: 'logged-in' };
|
|
147
|
-
return { kind: 'logged-out' };
|
|
148
|
-
} catch (error) {
|
|
149
|
-
return { kind: 'unknown', error };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** @param {ParentNode} root */
|
|
154
|
-
const usesPages = (root) => PAGE_ATTRIBUTES.some((name) => qsa(root, sel(name)).length > 0);
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* `tether-account-link`: Shopify's own pages, for what has to stay there: `subscriptions` and `payment`
|
|
158
|
-
* (`account.subscriptionsUrl` / `paymentUrl`), or the account itself.
|
|
159
|
-
*
|
|
160
|
-
* @param {ParentNode} root
|
|
161
|
-
*/
|
|
162
|
-
function linkShopifyPages(root) {
|
|
163
|
-
const account = cfg.account;
|
|
164
|
-
qsa(root, sel('account-link')).forEach((link) => {
|
|
165
|
-
const page = get(link, 'account-link');
|
|
166
|
-
const subscriptions = account?.subscriptionsUrl;
|
|
167
|
-
const url =
|
|
168
|
-
(page === 'payment' && (account?.paymentUrl || subscriptions)) || (page === 'subscriptions' && subscriptions);
|
|
169
|
-
link.setAttribute('href', url || `https://${cfg.domain}/account`);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Fills the account markup in `root`: what shows when logged in or out, the customer, their orders, and the account
|
|
175
|
-
* pages.
|
|
176
|
-
*
|
|
177
|
-
* @param {ParentNode} [root]
|
|
178
|
-
* @returns {Promise<void>}
|
|
179
|
-
*/
|
|
180
|
-
export async function render(root = document) {
|
|
181
|
-
const state = await loginState();
|
|
182
|
-
if (state.kind === 'unknown') reportError(state.error, 'account');
|
|
183
|
-
const loggedIn = state.kind === 'logged-in';
|
|
184
|
-
qsa(root, sel('show')).forEach((el) => {
|
|
185
|
-
const condition = get(el, 'show');
|
|
186
|
-
if (condition === 'logged-in') show(el, loggedIn);
|
|
187
|
-
if (condition === 'logged-out') show(el, !loggedIn);
|
|
188
|
-
});
|
|
189
|
-
linkShopifyPages(root);
|
|
190
|
-
if (state.kind === 'unknown' && usesPages(root)) {
|
|
191
|
-
(await pages()).showUnavailable(root);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (!loggedIn) return;
|
|
195
|
-
await bindCustomer(root);
|
|
196
|
-
await Promise.all([
|
|
197
|
-
renderOrderList(root, (first) => orders({ first })),
|
|
198
|
-
usesPages(root) ? pages().then((part) => part.renderPages(root)) : null,
|
|
199
|
-
]);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* `tether-account-bind`: the customer's name, email, default address. A failure is reported and leaves the binds as
|
|
204
|
-
* they are.
|
|
205
|
-
*
|
|
206
|
-
* @param {ParentNode} root
|
|
207
|
-
*/
|
|
208
|
-
async function bindCustomer(root) {
|
|
209
|
-
const binds = qsa(root, sel('account-bind'));
|
|
210
|
-
if (!binds.length) return;
|
|
211
|
-
const profile = await customer().catch((/** @type {unknown} */ error) => {
|
|
212
|
-
reportError(error, 'account');
|
|
213
|
-
return null;
|
|
214
|
-
});
|
|
215
|
-
/** @type {Record<string, string | null | undefined>} */
|
|
216
|
-
const texts = {
|
|
217
|
-
'first-name': profile?.firstName,
|
|
218
|
-
'last-name': profile?.lastName,
|
|
219
|
-
name: profile?.displayName,
|
|
220
|
-
email: profile?.emailAddress?.emailAddress,
|
|
221
|
-
address: profile?.defaultAddress?.formatted?.join(', '),
|
|
222
|
-
};
|
|
223
|
-
binds.forEach((el) => {
|
|
224
|
-
const text = ownValue(texts, get(el, 'account-bind') ?? '');
|
|
225
|
-
if (text != null) setText(el, text);
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** @type {Promise<Parts['account-pages']> | null} */
|
|
230
|
-
let pagesPart = null;
|
|
231
|
-
|
|
232
|
-
/** @returns {Promise<Parts['account-pages']>} The account pages part, sharing this part's session. */
|
|
233
|
-
function pages() {
|
|
234
|
-
pagesPart ??= loadPart('account-pages').then((part) => {
|
|
235
|
-
part.setup({
|
|
236
|
-
serverBase,
|
|
237
|
-
serverFetch,
|
|
238
|
-
query,
|
|
239
|
-
customer,
|
|
240
|
-
bindCustomer,
|
|
241
|
-
reset: () => {
|
|
242
|
-
forgetBrowserCustomer();
|
|
243
|
-
forgetServerLogin();
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
return part;
|
|
247
|
-
});
|
|
248
|
-
return pagesPart;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* One of the customer's orders, in full (lines, totals, addresses, fulfillments, returns).
|
|
253
|
-
*
|
|
254
|
-
* @param {string | number} id
|
|
255
|
-
*/
|
|
256
|
-
export const order = (id) => pages().then((part) => part.order(id));
|
|
257
|
-
/** @param {{ firstName?: string, lastName?: string }} [fields] */
|
|
258
|
-
export const updateProfile = (fields) => pages().then((part) => part.updateProfile(fields));
|
|
259
|
-
export const addresses = () => pages().then((part) => part.addresses());
|
|
260
|
-
/**
|
|
261
|
-
* Creates an address, or updates the one with `id`.
|
|
262
|
-
*
|
|
263
|
-
* @param {AddressFields | null} address
|
|
264
|
-
* @param {{ id?: string, isDefault?: boolean }} [options]
|
|
265
|
-
*/
|
|
266
|
-
export const saveAddress = (address, options) => pages().then((part) => part.saveAddress(address, options));
|
|
267
|
-
/** @param {string} id */
|
|
268
|
-
export const deleteAddress = (id) => pages().then((part) => part.deleteAddress(id));
|
|
269
|
-
export const subscriptions = () => pages().then((part) => part.subscriptions());
|
|
270
|
-
/**
|
|
271
|
-
* @param {string | number} id
|
|
272
|
-
* @param {SubscriptionAction} action
|
|
273
|
-
*/
|
|
274
|
-
export const subscriptionAction = (id, action) => pages().then((part) => part.subscriptionAction(id, action));
|
|
275
|
-
/**
|
|
276
|
-
* @param {string | number} orderId
|
|
277
|
-
* @param {ReturnLine[]} lines
|
|
278
|
-
*/
|
|
279
|
-
export const requestReturn = (orderId, lines) => pages().then((part) => part.requestReturn(orderId, lines));
|
|
280
|
-
|
|
281
|
-
/** @param {MouseEvent} event */
|
|
282
|
-
function onClick(event) {
|
|
283
|
-
const target = event.target;
|
|
284
|
-
if (!(target instanceof window.Element)) return;
|
|
285
|
-
if (target.closest(sel('account-login'))) {
|
|
286
|
-
event.preventDefault();
|
|
287
|
-
login().catch((/** @type {unknown} */ error) => {
|
|
288
|
-
reportError(error, 'login');
|
|
289
|
-
});
|
|
290
|
-
} else if (target.closest(sel('account-logout'))) {
|
|
291
|
-
event.preventDefault();
|
|
292
|
-
logout().catch((/** @type {unknown} */ error) => {
|
|
293
|
-
reportError(error, 'logout');
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Back from the Worker's login (`?account=in`): drops the mark, and lets the Worker link the cart at checkout. */
|
|
299
|
-
function startServerMode() {
|
|
300
|
-
const url = new URL(window.location.href);
|
|
301
|
-
if (url.searchParams.get('account')) {
|
|
302
|
-
url.searchParams.delete('account');
|
|
303
|
-
window.history.replaceState(window.history.state, '', url);
|
|
304
|
-
emit('account-login', {});
|
|
305
|
-
}
|
|
306
|
-
Cart.setCustomer({ beforeCheckout: serverCheckoutUrl });
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Finishes a login that's coming back from Shopify, and links the cart of a customer who is logged in. */
|
|
310
|
-
async function startBrowserMode() {
|
|
311
|
-
try {
|
|
312
|
-
await handleCallback();
|
|
313
|
-
} catch (error) {
|
|
314
|
-
reportError(error, 'login');
|
|
315
|
-
}
|
|
316
|
-
if (Cart.customerAccessToken) return;
|
|
317
|
-
try {
|
|
318
|
-
const accessToken = await getToken();
|
|
319
|
-
if (accessToken) Cart.setCustomer({ accessToken });
|
|
320
|
-
} catch (error) {
|
|
321
|
-
reportError(error, 'account');
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let started = false;
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Called by the core on mount: on the first one, finishes a login that's coming back; then fills the page.
|
|
329
|
-
*
|
|
330
|
-
* @param {ParentNode} [root]
|
|
331
|
-
* @returns {Promise<void>}
|
|
332
|
-
*/
|
|
333
|
-
export async function mountAccount(root = document) {
|
|
334
|
-
if (!started) {
|
|
335
|
-
started = true;
|
|
336
|
-
document.addEventListener('click', onClick);
|
|
337
|
-
if (serverBase()) startServerMode();
|
|
338
|
-
else await startBrowserMode();
|
|
339
|
-
log('account', isLoggedIn() ? 'logged in' : 'logged out');
|
|
340
|
-
}
|
|
341
|
-
await render(root);
|
|
342
|
-
}
|
package/src/amounts.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify sends money amounts as decimal strings (`'27.50'`). Comparing them as floats can misjudge two amounts that
|
|
3
|
-
* differ only past float precision, so they are compared digit by digit, as integers of the smallest unit present.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const DECIMAL = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {string} amount
|
|
10
|
-
* @param {number} fractionDigits Digits after the point to scale to.
|
|
11
|
-
* @returns {bigint | null} The amount in units of 10^-fractionDigits, `null` when it isn't a decimal number.
|
|
12
|
-
*/
|
|
13
|
-
function toScaledInteger(amount, fractionDigits) {
|
|
14
|
-
const match = DECIMAL.exec(amount.trim());
|
|
15
|
-
if (!match) return null;
|
|
16
|
-
const [, sign = '', whole = '0', fraction = ''] = match;
|
|
17
|
-
const digits = BigInt(`${whole}${fraction.padEnd(fractionDigits, '0')}`);
|
|
18
|
-
return sign === '-' ? -digits : digits;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** @param {string} amount */
|
|
22
|
-
const fractionLength = (amount) => amount.split('.')[1]?.length ?? 0;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {string | null | undefined} amount
|
|
26
|
-
* @param {string | null | undefined} other
|
|
27
|
-
* @returns {boolean} True when both are decimal numbers and `amount` is the larger one.
|
|
28
|
-
*/
|
|
29
|
-
export function isGreaterAmount(amount, other) {
|
|
30
|
-
if (amount == null || other == null) return false;
|
|
31
|
-
const fractionDigits = Math.max(fractionLength(amount.trim()), fractionLength(other.trim()));
|
|
32
|
-
const scaled = toScaledInteger(amount, fractionDigits);
|
|
33
|
-
const scaledOther = toScaledInteger(other, fractionDigits);
|
|
34
|
-
if (scaled === null || scaledOther === null) return false;
|
|
35
|
-
return scaled > scaledOther;
|
|
36
|
-
}
|