@occluze/tether 0.6.8 → 0.7.1
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 +28 -28
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
- package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
- package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
- package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
- package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
- package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
- package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
- package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
- package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
- package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
- package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
- package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
- package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
- package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
- package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
- package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
- package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
- package/dist/esm/chunks/search-A5DS2IOS.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 +332 -367
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +253 -492
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +82 -89
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +52 -34
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +42 -37
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +1361 -935
- package/dist/tether.markets.js +89 -91
- 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 +60 -69
- 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/cart-id.d.ts +2 -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 +85 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +185 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +134 -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 +190 -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 +90 -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 -24
- 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-lines.js +0 -96
- package/src/account/order-list.js +0 -91
- package/src/account/order-page.js +0 -192
- package/src/account/page-events.js +0 -288
- 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/subscriptions-view.js +0 -158
- package/src/account-api.js +0 -118
- package/src/account-links.js +0 -35
- package/src/account-pages.js +0 -89
- package/src/account.js +0 -342
- package/src/amounts.js +0 -36
- package/src/analytics-shopify.js +0 -241
- package/src/analytics.js +0 -352
- package/src/api-version.js +0 -85
- package/src/astro.js +0 -266
- package/src/cart/checkout.js +0 -97
- package/src/cart/drawer.js +0 -138
- package/src/cart/mutations.js +0 -234
- package/src/cart/render.js +0 -294
- package/src/cart/store.js +0 -326
- package/src/cart.js +0 -391
- package/src/client.js +0 -267
- package/src/collection.js +0 -183
- package/src/config.js +0 -349
- package/src/controls/cart-controls.js +0 -155
- package/src/controls/discount.js +0 -96
- package/src/controls/extras.js +0 -63
- package/src/controls/navigation.js +0 -83
- package/src/diagnose-markup.js +0 -333
- package/src/diagnose.js +0 -295
- package/src/dom.js +0 -287
- package/src/extras.js +0 -145
- package/src/fragments.js +0 -237
- package/src/images.js +0 -104
- 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/loader.js +0 -337
- package/src/market-codes.js +0 -35
- package/src/markets-ui.js +0 -204
- package/src/markets.js +0 -334
- package/src/markup.js +0 -64
- package/src/mounting.js +0 -277
- package/src/page-market.js +0 -116
- 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-events.js +0 -130
- package/src/product-render.js +0 -257
- package/src/product-state.js +0 -318
- package/src/product.js +0 -344
- package/src/repeaters.js +0 -173
- package/src/search.js +0 -323
- package/src/selling-plans.js +0 -154
- package/src/server.js +0 -215
- package/src/shopify-values.js +0 -89
- package/src/status.js +0 -126
- package/src/store-html.js +0 -0
- package/src/storefront/catalog.js +0 -199
- package/src/storefront/json-ld.js +0 -167
- package/src/variant-render.js +0 -165
- 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.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
|
-
}
|
package/src/analytics-shopify.js
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify's own analytics for the analytics part. Tether sets up the same event bus Hydrogen uses
|
|
3
|
-
* (`window.Shopify.analytics`) and loads Shopify's consent script and shopify.js, which build and send the events.
|
|
4
|
-
* Nothing is delivered before the Customer Privacy API allows analytics.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg, log, reportError } from './config.js';
|
|
7
|
-
import { gql } from './client.js';
|
|
8
|
-
|
|
9
|
-
/** @typedef {Record<string, unknown>} EventPayload */
|
|
10
|
-
/** @typedef {{ uniqueToken: string, visitToken: string }} TrackingValues */
|
|
11
|
-
/** @typedef {{ getTrackingValues: () => TrackingValues }} DestinationContext */
|
|
12
|
-
/** @typedef {(payload: EventPayload, context: DestinationContext) => void} Subscriber */
|
|
13
|
-
/** @typedef {(event: string, subscriber: Subscriber) => () => void} Subscribe */
|
|
14
|
-
/** @typedef {{ shop: { shopId: string, channel: 'headless' }, consent: { mode: string } }} BusConfig */
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {object} DestinationSpec What shopify.js registers.
|
|
17
|
-
* @property {string} name
|
|
18
|
-
* @property {(api: { subscribe: Subscribe, getConfig: () => BusConfig }) => unknown} [setup]
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {object} AnalyticsBus Hydrogen's `window.Shopify.analytics`.
|
|
22
|
-
* @property {() => BusConfig} getConfig
|
|
23
|
-
* @property {(event: string, payload?: EventPayload) => void} publish
|
|
24
|
-
* @property {(destination: DestinationSpec) => () => void} addDestination
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The parts of Shopify's Customer Privacy API (`window.Shopify.customerPrivacy`) that decide tracking.
|
|
29
|
-
*
|
|
30
|
-
* @typedef {object} CustomerPrivacy
|
|
31
|
-
* @property {string} [consentStatus]
|
|
32
|
-
* @property {() => { analytics?: string } | undefined} [currentVisitorConsent]
|
|
33
|
-
* @property {() => boolean} [analyticsProcessingAllowed]
|
|
34
|
-
* @property {{ uniqueToken?: TokenReader, visitToken?: TokenReader }} [__internal]
|
|
35
|
-
* @property {object} [config]
|
|
36
|
-
*/
|
|
37
|
-
/** @typedef {(options: { generateFallback: boolean, tag: string }) => string} TokenReader */
|
|
38
|
-
/**
|
|
39
|
-
* `window.Shopify`, which Shopify's scripts read and extend.
|
|
40
|
-
*
|
|
41
|
-
* @typedef {object} ShopifyGlobal
|
|
42
|
-
* @property {AnalyticsBus} [analytics]
|
|
43
|
-
* @property {CustomerPrivacy} [customerPrivacy]
|
|
44
|
-
* @property {string} [country]
|
|
45
|
-
* @property {string} [locale]
|
|
46
|
-
* @property {string} [shop]
|
|
47
|
-
* @property {object} [routes]
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
/** @typedef {{ event: string, payload: EventPayload }} BufferedEvent */
|
|
51
|
-
/**
|
|
52
|
-
* @typedef {object} Destination
|
|
53
|
-
* @property {string} name
|
|
54
|
-
* @property {number} next The index of the next buffered event it gets.
|
|
55
|
-
* @property {Map<string, Set<Subscriber>>} subscribers
|
|
56
|
-
* @property {DestinationContext} context
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/** Events kept for destinations that register late (shopify.js loads after the first page view). */
|
|
60
|
-
const BUFFER_SIZE = 500;
|
|
61
|
-
const CONSENT_SCRIPTS = {
|
|
62
|
-
'default-banner': 'https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js',
|
|
63
|
-
other: 'https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js',
|
|
64
|
-
};
|
|
65
|
-
const ANALYTICS_SCRIPT = 'https://cdn.shopify.com/storefront/analytics/shopify.js';
|
|
66
|
-
|
|
67
|
-
/** @returns {ShopifyGlobal | undefined} `window.Shopify`, whose shape Shopify's scripts define. */
|
|
68
|
-
function shopifyGlobal() {
|
|
69
|
-
/** @type {unknown} */
|
|
70
|
-
const shopify = Reflect.get(window, 'Shopify');
|
|
71
|
-
return /** @type {ShopifyGlobal | undefined} */ (shopify);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** @returns {{ consent?: string, proxy?: string }} The `analytics.shopify` options; `true` sets none. */
|
|
75
|
-
function shopifyOptions() {
|
|
76
|
-
const shopify = cfg.analytics?.shopify;
|
|
77
|
-
return typeof shopify === 'object' ? shopify : {};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Shopify's privacy API can throw while its script is still setting up. An answer it can't give counts as "not
|
|
82
|
-
* allowed", so nothing is sent without consent; the next consent event asks again.
|
|
83
|
-
*
|
|
84
|
-
* @returns {boolean}
|
|
85
|
-
*/
|
|
86
|
-
function trackingAllowed() {
|
|
87
|
-
try {
|
|
88
|
-
const privacy = shopifyGlobal()?.customerPrivacy;
|
|
89
|
-
return (
|
|
90
|
-
privacy?.consentStatus === 'loaded' &&
|
|
91
|
-
privacy.currentVisitorConsent?.()?.analytics !== 'no' &&
|
|
92
|
-
Boolean(privacy.analyticsProcessingAllowed?.())
|
|
93
|
-
);
|
|
94
|
-
} catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @param {string} name The destination's name, Hydrogen tags its tokens with it.
|
|
101
|
-
* @returns {DestinationContext}
|
|
102
|
-
*/
|
|
103
|
-
function destinationContext(name) {
|
|
104
|
-
const tag = `hydrogen:${name}`;
|
|
105
|
-
return {
|
|
106
|
-
getTrackingValues: () => {
|
|
107
|
-
const internal = trackingAllowed() ? shopifyGlobal()?.customerPrivacy?.__internal : undefined;
|
|
108
|
-
return {
|
|
109
|
-
uniqueToken: internal?.uniqueToken?.({ generateFallback: true, tag }) ?? '',
|
|
110
|
-
visitToken: internal?.visitToken?.({ generateFallback: true, tag }) ?? '',
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @param {Destination} destination
|
|
118
|
-
* @returns {Subscribe}
|
|
119
|
-
*/
|
|
120
|
-
const subscriberOf = (destination) => (event, subscriber) => {
|
|
121
|
-
const subscribers = destination.subscribers.get(event) ?? new Set();
|
|
122
|
-
destination.subscribers.set(event, subscribers);
|
|
123
|
-
subscribers.add(subscriber);
|
|
124
|
-
return () => destination.subscribers.get(event)?.delete(subscriber);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {string} shopId
|
|
129
|
-
* @returns {AnalyticsBus} A bus that buffers events and delivers them to each destination once consent allows.
|
|
130
|
-
*/
|
|
131
|
-
function createBus(shopId) {
|
|
132
|
-
/** @type {Destination[]} */
|
|
133
|
-
const destinations = [];
|
|
134
|
-
/** @type {BufferedEvent[]} */
|
|
135
|
-
const buffer = [];
|
|
136
|
-
/** @type {BusConfig} */
|
|
137
|
-
const config = {
|
|
138
|
-
shop: { shopId, channel: 'headless' },
|
|
139
|
-
consent: { mode: shopifyOptions().consent || 'no-banner' },
|
|
140
|
-
};
|
|
141
|
-
const deliver = () => {
|
|
142
|
-
if (!trackingAllowed()) return;
|
|
143
|
-
for (const destination of destinations) {
|
|
144
|
-
for (; destination.next < buffer.length; destination.next += 1) {
|
|
145
|
-
const buffered = buffer[destination.next];
|
|
146
|
-
if (buffered) deliverTo(destination, buffered);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
/** @type {AnalyticsBus} */
|
|
151
|
-
const bus = {
|
|
152
|
-
getConfig: () => config,
|
|
153
|
-
publish(event, payload = {}) {
|
|
154
|
-
buffer.push({ event, payload: { shop: config.shop, url: window.location.href, ...payload } });
|
|
155
|
-
if (buffer.length > BUFFER_SIZE) {
|
|
156
|
-
buffer.shift();
|
|
157
|
-
for (const destination of destinations) destination.next = Math.max(0, destination.next - 1);
|
|
158
|
-
}
|
|
159
|
-
deliver();
|
|
160
|
-
},
|
|
161
|
-
addDestination(spec) {
|
|
162
|
-
/** @type {Destination} */
|
|
163
|
-
const destination = { name: spec.name, next: 0, subscribers: new Map(), context: destinationContext(spec.name) };
|
|
164
|
-
Promise.resolve(spec.setup?.({ subscribe: subscriberOf(destination), getConfig: bus.getConfig }))
|
|
165
|
-
.then(() => {
|
|
166
|
-
destinations.push(destination);
|
|
167
|
-
deliver();
|
|
168
|
-
})
|
|
169
|
-
.catch((/** @type {unknown} */ error) => {
|
|
170
|
-
reportError(error, `analytics ${spec.name}`);
|
|
171
|
-
});
|
|
172
|
-
return () => {
|
|
173
|
-
const index = destinations.indexOf(destination);
|
|
174
|
-
if (index >= 0) destinations.splice(index, 1);
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
document.addEventListener('consentTrackingApiLoaded', deliver);
|
|
179
|
-
document.addEventListener('visitorConsentCollected', deliver);
|
|
180
|
-
return bus;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* @param {Destination} destination
|
|
185
|
-
* @param {BufferedEvent} buffered
|
|
186
|
-
*/
|
|
187
|
-
function deliverTo(destination, { event, payload }) {
|
|
188
|
-
destination.subscribers.get(event)?.forEach((subscriber) => {
|
|
189
|
-
try {
|
|
190
|
-
subscriber(payload, destination.context);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
reportError(error, `analytics ${destination.name}`);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** @param {string} src */
|
|
198
|
-
function loadScript(src) {
|
|
199
|
-
const script = document.createElement('script');
|
|
200
|
-
script.src = src;
|
|
201
|
-
script.async = true;
|
|
202
|
-
document.head.appendChild(script);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Sets up `window.Shopify` for Shopify's scripts and loads them.
|
|
207
|
-
*
|
|
208
|
-
* @param {() => boolean} isProductPage Asked by Shopify's script for the page template.
|
|
209
|
-
* @returns {Promise<AnalyticsBus>} The bus, the page's own when a theme already set one up.
|
|
210
|
-
*/
|
|
211
|
-
export async function startShopifyAnalytics(isProductPage) {
|
|
212
|
-
const existing = shopifyGlobal();
|
|
213
|
-
if (existing?.analytics) return existing.analytics;
|
|
214
|
-
/** @type {{ shop: { id: string } }} */
|
|
215
|
-
const data = await gql('query TetherShopId { shop { id } }', {}, { context: false });
|
|
216
|
-
const shopify = shopifyGlobal() ?? {};
|
|
217
|
-
Reflect.set(window, 'Shopify', shopify);
|
|
218
|
-
shopify.country = (cfg.country || 'US').toUpperCase();
|
|
219
|
-
// Shopify's script requires a language, not a full locale.
|
|
220
|
-
shopify.locale = (cfg.locale || 'en').split('-')[0]?.toLowerCase() ?? 'en';
|
|
221
|
-
shopify.shop = cfg.domain;
|
|
222
|
-
shopify.routes = {
|
|
223
|
-
root: '/',
|
|
224
|
-
match: () => ({ pageTemplateName: isProductPage() ? 'product' : 'page' }),
|
|
225
|
-
...shopify.routes,
|
|
226
|
-
};
|
|
227
|
-
// A same-origin Storefront API path gives Shopify exact visitor counts.
|
|
228
|
-
const { proxy, consent } = shopifyOptions();
|
|
229
|
-
shopify.customerPrivacy = {
|
|
230
|
-
...shopify.customerPrivacy,
|
|
231
|
-
config: proxy
|
|
232
|
-
? { isHeadless: true, asyncConsent: true, asyncVisitorState: true, consentDomain: window.location.host }
|
|
233
|
-
: { isHeadless: true, storefrontAccessToken: cfg.token, checkoutRootDomain: cfg.domain },
|
|
234
|
-
};
|
|
235
|
-
const bus = createBus(data.shop.id);
|
|
236
|
-
shopify.analytics = bus;
|
|
237
|
-
loadScript(consent === 'default-banner' ? CONSENT_SCRIPTS['default-banner'] : CONSENT_SCRIPTS.other);
|
|
238
|
-
loadScript(ANALYTICS_SCRIPT);
|
|
239
|
-
log('shopify analytics ready', data.shop.id);
|
|
240
|
-
return bus;
|
|
241
|
-
}
|