@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
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shopify's side of the browser-mode login: the OAuth and Customer Account API endpoints, found through the shop's
|
|
3
|
-
* `.well-known` documents (a shop with its own accounts domain has other ones), the token endpoint, and the check of
|
|
4
|
-
* the id token it answers with.
|
|
5
|
-
*/
|
|
6
|
-
import { cfg } from '../config.js';
|
|
7
|
-
import { TetherError } from '../client.js';
|
|
8
|
-
import { fromBase64Url } from '../../server/src/shared/base64url.js';
|
|
9
|
-
import { isRecord, readJsonObjectOrEmpty, textOrNull } from './shared.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {object} Endpoints
|
|
13
|
-
* @property {string} issuer The id token's `iss`.
|
|
14
|
-
* @property {string} authorizationEndpoint
|
|
15
|
-
* @property {string} tokenEndpoint
|
|
16
|
-
* @property {string} endSessionEndpoint
|
|
17
|
-
* @property {string} graphql The Customer Account API.
|
|
18
|
-
*/
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {object} TokenAnswer
|
|
21
|
-
* @property {string} access
|
|
22
|
-
* @property {string | null} refresh Left out of some refresh answers.
|
|
23
|
-
* @property {string | null} idToken Left out of refresh answers.
|
|
24
|
-
* @property {number} expiresInSeconds
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/** Shopify's access tokens last an hour; an answer without `expires_in` is taken to say so. */
|
|
28
|
-
const DEFAULT_TOKEN_SECONDS = 3600;
|
|
29
|
-
|
|
30
|
-
/** @type {Endpoints | null} */
|
|
31
|
-
let endpoints = null;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {string} document `openid-configuration` or `customer-account-api`.
|
|
35
|
-
* @returns {Promise<Record<string, unknown>>}
|
|
36
|
-
*/
|
|
37
|
-
async function wellKnown(document) {
|
|
38
|
-
const shop = cfg.account?.shopDomain || cfg.domain;
|
|
39
|
-
const response = await fetch(`https://${shop}/.well-known/${document}`);
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new TetherError(`Customer accounts: ${document} answered ${response.status}`, { status: response.status });
|
|
42
|
-
}
|
|
43
|
-
const json = /** @type {unknown} */ (await response.json());
|
|
44
|
-
return isRecord(json) ? json : {};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {Record<string, unknown>} json
|
|
49
|
-
* @param {string} name
|
|
50
|
-
* @returns {string}
|
|
51
|
-
* @throws {TetherError} When Shopify's document lacks the endpoint.
|
|
52
|
-
*/
|
|
53
|
-
function endpointIn(json, name) {
|
|
54
|
-
const url = textOrNull(json[name]);
|
|
55
|
-
if (!url) throw new TetherError(`Customer accounts: Shopify named no ${name}`);
|
|
56
|
-
return url;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @returns {Promise<Endpoints>} The shop's endpoints, asked once per page.
|
|
61
|
-
* @throws {TetherError} When Shopify's documents can't be read.
|
|
62
|
-
*/
|
|
63
|
-
export async function discover() {
|
|
64
|
-
if (endpoints) return endpoints;
|
|
65
|
-
const [openid, api] = await Promise.all([wellKnown('openid-configuration'), wellKnown('customer-account-api')]);
|
|
66
|
-
endpoints = {
|
|
67
|
-
issuer: endpointIn(openid, 'issuer'),
|
|
68
|
-
authorizationEndpoint: endpointIn(openid, 'authorization_endpoint'),
|
|
69
|
-
tokenEndpoint: endpointIn(openid, 'token_endpoint'),
|
|
70
|
-
endSessionEndpoint: endpointIn(openid, 'end_session_endpoint'),
|
|
71
|
-
graphql: endpointIn(api, 'graphql_api'),
|
|
72
|
-
};
|
|
73
|
-
return endpoints;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* A token request, as a "simple" request (a form body, no custom header) so the browser sends it without a CORS
|
|
78
|
-
* preflight. `account.tokenUrl` replaces Shopify's endpoint for a browser that can't reach it; it must be the shop's
|
|
79
|
-
* own first-party endpoint passing the form on to Shopify, because the id token in the answer is trusted unsigned
|
|
80
|
-
* (`idTokenClaims`).
|
|
81
|
-
*
|
|
82
|
-
* @param {Record<string, string>} params
|
|
83
|
-
* @returns {Promise<TokenAnswer>}
|
|
84
|
-
* @throws {TetherError} `SESSION_ENDED` when Shopify refuses the code or refresh token (400, 401), else `TOKEN_FAILED`.
|
|
85
|
-
*/
|
|
86
|
-
export async function requestToken(params) {
|
|
87
|
-
const { tokenEndpoint } = await discover();
|
|
88
|
-
const response = await fetch(cfg.account?.tokenUrl || tokenEndpoint, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
body: new URLSearchParams(params),
|
|
91
|
-
});
|
|
92
|
-
const data = await readJsonObjectOrEmpty(response);
|
|
93
|
-
const access = textOrNull(data['access_token']);
|
|
94
|
-
if (!response.ok || !access) {
|
|
95
|
-
const ended = response.status === 400 || response.status === 401;
|
|
96
|
-
throw new TetherError(`Customer accounts: token ${response.status} ${textOrNull(data['error']) ?? ''}`.trim(), {
|
|
97
|
-
status: response.status,
|
|
98
|
-
code: ended ? 'SESSION_ENDED' : 'TOKEN_FAILED',
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
access,
|
|
103
|
-
refresh: textOrNull(data['refresh_token']) || null,
|
|
104
|
-
idToken: textOrNull(data['id_token']) || null,
|
|
105
|
-
expiresInSeconds: Number(data['expires_in']) || DEFAULT_TOKEN_SECONDS,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The id token's claims. Its signature isn't checked: the token comes straight from Shopify's token endpoint over
|
|
111
|
-
* TLS, in answer to this browser's own code and verifier, so nobody else could have written it. That holds only while
|
|
112
|
-
* `account.tokenUrl` (when set) is the shop's own first-party endpoint passing the request on to Shopify (see
|
|
113
|
-
* `requestToken`).
|
|
114
|
-
*
|
|
115
|
-
* @param {string} idToken
|
|
116
|
-
* @returns {Record<string, unknown> | null}
|
|
117
|
-
*/
|
|
118
|
-
function idTokenClaims(idToken) {
|
|
119
|
-
const payload = idToken.split('.')[1];
|
|
120
|
-
if (!payload) return null;
|
|
121
|
-
/** @type {unknown} */
|
|
122
|
-
let claims;
|
|
123
|
-
try {
|
|
124
|
-
claims = JSON.parse(new TextDecoder().decode(fromBase64Url(payload)));
|
|
125
|
-
} catch {
|
|
126
|
-
// Not base64url JSON: not an id token, which the caller refuses.
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
return isRecord(claims) ? claims : null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @param {string | null} idToken
|
|
134
|
-
* @param {{ nonce: string, issuer: string, client: string }} expected The login's nonce, Shopify's issuer, our client.
|
|
135
|
-
* @returns {boolean} True when the id token answers this login: the expected claims, and not expired.
|
|
136
|
-
*/
|
|
137
|
-
export function answersLogin(idToken, { nonce, issuer, client }) {
|
|
138
|
-
const claims = idToken ? idTokenClaims(idToken) : null;
|
|
139
|
-
if (!claims) return false;
|
|
140
|
-
const audience = claims['aud'];
|
|
141
|
-
const audiences = Array.isArray(audience) ? audience : [audience];
|
|
142
|
-
const expiresAt = claims['exp'];
|
|
143
|
-
return (
|
|
144
|
-
claims['nonce'] === nonce &&
|
|
145
|
-
claims['iss'] === issuer &&
|
|
146
|
-
audiences.includes(client) &&
|
|
147
|
-
typeof expiresAt === 'number' &&
|
|
148
|
-
expiresAt * 1000 >= Date.now()
|
|
149
|
-
);
|
|
150
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The customer's subscriptions (`tether-subscriptions`), with `tether-show="subscriptions-loading"`,
|
|
3
|
-
* `subscriptions-empty` and `subscriptions-error`. Each row's `tether-subscription-show` conditions and action buttons
|
|
4
|
-
* follow the contract's status and its next billing cycle.
|
|
5
|
-
*/
|
|
6
|
-
import { reportError } from '../config.js';
|
|
7
|
-
import { attr, formatMoney, get, qsa, repeat, sel, setImage, setText } from '../dom.js';
|
|
8
|
-
import { bindsIn, enumText, ownValue, showIn } from './markup.js';
|
|
9
|
-
import { subscriptions } from './operations.js';
|
|
10
|
-
import { localDate } from '../account-links.js';
|
|
11
|
-
/** @import { Money } from '../shopify-values.js' */
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {object} SubscriptionLine
|
|
15
|
-
* @property {string} title
|
|
16
|
-
* @property {string | null} variantTitle
|
|
17
|
-
* @property {number} quantity
|
|
18
|
-
* @property {Money} currentPrice
|
|
19
|
-
* @property {Money | null} lineDiscountedPrice
|
|
20
|
-
* @property {{ url: string, altText: string | null } | null} image
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {object} SubscriptionContract The subscriptions document in account-ops.js.
|
|
24
|
-
* @property {string} id
|
|
25
|
-
* @property {'ACTIVE' | 'PAUSED' | 'CANCELLED' | 'EXPIRED' | 'FAILED'} status
|
|
26
|
-
* @property {string | null} nextBillingDate
|
|
27
|
-
* @property {string} currencyCode
|
|
28
|
-
* @property {Money | null} deliveryPrice
|
|
29
|
-
* @property {{ interval: string | null, intervalCount: { count: number } | null } | null} deliveryPolicy
|
|
30
|
-
* @property {{ nodes: SubscriptionLine[] }} lines
|
|
31
|
-
* @property {{ nodes: { cycleIndex: number, skipped: boolean }[] }} upcomingBillingCycles
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param {SubscriptionContract} contract
|
|
36
|
-
* @returns {Record<string, boolean>} The row's `tether-subscription-show` conditions, `can-<action>` among them.
|
|
37
|
-
*/
|
|
38
|
-
function conditionsOf(contract) {
|
|
39
|
-
const next = contract.upcomingBillingCycles.nodes[0];
|
|
40
|
-
const status = contract.status;
|
|
41
|
-
const active = status === 'ACTIVE';
|
|
42
|
-
return {
|
|
43
|
-
active,
|
|
44
|
-
paused: status === 'PAUSED',
|
|
45
|
-
cancelled: status === 'CANCELLED',
|
|
46
|
-
expired: status === 'EXPIRED',
|
|
47
|
-
failed: status === 'FAILED',
|
|
48
|
-
skipped: Boolean(next?.skipped),
|
|
49
|
-
'can-pause': active,
|
|
50
|
-
'can-resume': status === 'PAUSED' || status === 'FAILED',
|
|
51
|
-
'can-skip': active && next !== undefined && !next.skipped,
|
|
52
|
-
'can-unskip': active && Boolean(next?.skipped),
|
|
53
|
-
'can-cancel': active || status === 'PAUSED' || status === 'FAILED',
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @param {SubscriptionContract} contract
|
|
59
|
-
* @returns {string} `month`, `3 months`; `''` without a delivery policy.
|
|
60
|
-
*/
|
|
61
|
-
function intervalText(contract) {
|
|
62
|
-
const unit = enumText(contract.deliveryPolicy?.interval);
|
|
63
|
-
if (!unit) return '';
|
|
64
|
-
const count = contract.deliveryPolicy?.intervalCount?.count ?? 1;
|
|
65
|
-
return count <= 1 ? unit : `${count} ${unit}s`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @param {SubscriptionContract} contract
|
|
70
|
-
* @returns {Record<string, string>} The text of each `tether-subscription-bind` value.
|
|
71
|
-
*/
|
|
72
|
-
function contractTexts(contract) {
|
|
73
|
-
const lines = contract.lines.nodes;
|
|
74
|
-
const total = lines.reduce((sum, line) => sum + Number.parseFloat(line.lineDiscountedPrice?.amount ?? '0'), 0);
|
|
75
|
-
return {
|
|
76
|
-
status: enumText(contract.status),
|
|
77
|
-
next: contract.nextBillingDate ? localDate(contract.nextBillingDate) : '',
|
|
78
|
-
interval: intervalText(contract),
|
|
79
|
-
products: lines.map((line) => `${line.quantity}× ${line.title}`).join(', '),
|
|
80
|
-
total: lines.length ? formatMoney({ amount: String(total), currencyCode: contract.currencyCode }) : '',
|
|
81
|
-
delivery: formatMoney(contract.deliveryPrice),
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @param {HTMLElement} row
|
|
87
|
-
* @param {SubscriptionLine} line
|
|
88
|
-
*/
|
|
89
|
-
function fillLine(row, line) {
|
|
90
|
-
/** @type {Record<string, string>} */
|
|
91
|
-
const texts = {
|
|
92
|
-
title: line.title,
|
|
93
|
-
'variant-title': line.variantTitle || '',
|
|
94
|
-
quantity: String(line.quantity),
|
|
95
|
-
price: formatMoney(line.currentPrice),
|
|
96
|
-
};
|
|
97
|
-
bindsIn(row, 'subscription-line').forEach((el) => {
|
|
98
|
-
const key = get(el, 'subscription-line') ?? '';
|
|
99
|
-
if (key === 'image') {
|
|
100
|
-
if (line.image) setImage(el, line.image.url, line.image.altText || line.title);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const text = ownValue(texts, key);
|
|
104
|
-
if (text !== undefined) setText(el, text);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {HTMLElement} row
|
|
110
|
-
* @param {SubscriptionContract} contract
|
|
111
|
-
*/
|
|
112
|
-
function fillContract(row, contract) {
|
|
113
|
-
row.dataset['tetherSubscription'] = contract.id;
|
|
114
|
-
row.setAttribute(attr('subscription-status'), enumText(contract.status));
|
|
115
|
-
const texts = contractTexts(contract);
|
|
116
|
-
qsa(row, sel('subscription-bind')).forEach((el) => {
|
|
117
|
-
const text = ownValue(texts, get(el, 'subscription-bind') ?? '');
|
|
118
|
-
if (text !== undefined) setText(el, text);
|
|
119
|
-
});
|
|
120
|
-
qsa(row, sel('subscription-lines')).forEach((box) => {
|
|
121
|
-
repeat(box, contract.lines.nodes, fillLine);
|
|
122
|
-
});
|
|
123
|
-
const conditions = conditionsOf(contract);
|
|
124
|
-
showIn(row, 'subscription-show', conditions);
|
|
125
|
-
qsa(row, sel('subscription-action')).forEach((button) => {
|
|
126
|
-
const allowed = ownValue(conditions, `can-${get(button, 'subscription-action') ?? ''}`) === true;
|
|
127
|
-
if ('disabled' in button) button.disabled = !allowed;
|
|
128
|
-
button.classList.toggle('is-disabled', !allowed);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Loads and renders the subscriptions. A failure is reported and shows `subscriptions-error`.
|
|
134
|
-
*
|
|
135
|
-
* @param {ParentNode} root
|
|
136
|
-
*/
|
|
137
|
-
export async function renderSubscriptions(root) {
|
|
138
|
-
const boxes = qsa(root, sel('subscriptions'));
|
|
139
|
-
if (!boxes.length) return;
|
|
140
|
-
showIn(root, 'show', { 'subscriptions-loading': true, 'subscriptions-empty': false });
|
|
141
|
-
/** @type {SubscriptionContract[]} */
|
|
142
|
-
let list;
|
|
143
|
-
try {
|
|
144
|
-
list = await subscriptions();
|
|
145
|
-
} catch (error) {
|
|
146
|
-
reportError(error, 'subscriptions');
|
|
147
|
-
showIn(root, 'show', { 'subscriptions-loading': false, 'subscriptions-error': true });
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
showIn(root, 'show', {
|
|
151
|
-
'subscriptions-loading': false,
|
|
152
|
-
'subscriptions-empty': list.length === 0,
|
|
153
|
-
'subscriptions-error': false,
|
|
154
|
-
});
|
|
155
|
-
boxes.forEach((box) => {
|
|
156
|
-
repeat(box, list, fillContract);
|
|
157
|
-
});
|
|
158
|
-
}
|
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-pages.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Customer account pages, an optional part the account part loads on pages that use them: the order page with
|
|
3
|
-
* returns, the profile form, addresses and subscriptions. The operations and their input checks are shared with the
|
|
4
|
-
* Worker (server/src/shared/account-ops.js), so both modes do the same.
|
|
5
|
-
*/
|
|
6
|
-
import { reportError } from './config.js';
|
|
7
|
-
import { qsa, sel } from './dom.js';
|
|
8
|
-
import { pagesContext } from './account/operations.js';
|
|
9
|
-
import { renderOrder, showOrderState } from './account/order-page.js';
|
|
10
|
-
import { renderAddresses } from './account/addresses-view.js';
|
|
11
|
-
import { renderSubscriptions } from './account/subscriptions-view.js';
|
|
12
|
-
import { listenToPages } from './account/page-events.js';
|
|
13
|
-
import { showIn } from './account/markup.js';
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
setup,
|
|
17
|
-
order,
|
|
18
|
-
updateProfile,
|
|
19
|
-
addresses,
|
|
20
|
-
saveAddress,
|
|
21
|
-
deleteAddress,
|
|
22
|
-
subscriptions,
|
|
23
|
-
subscriptionAction,
|
|
24
|
-
requestReturn,
|
|
25
|
-
} from './account/operations.js';
|
|
26
|
-
|
|
27
|
-
/** The profile fields a `tether-profile-form` is filled with. */
|
|
28
|
-
const PROFILE_FIELDS = /** @type {const} */ (['firstName', 'lastName']);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Fills the profile form with the customer's name, leaving a field the visitor has typed in alone.
|
|
32
|
-
*
|
|
33
|
-
* @param {HTMLElement} form
|
|
34
|
-
*/
|
|
35
|
-
async function renderProfile(form) {
|
|
36
|
-
const customer = await pagesContext()
|
|
37
|
-
.customer()
|
|
38
|
-
.catch((/** @type {unknown} */ error) => {
|
|
39
|
-
// The form stays empty and still saves: only the prefill is missing.
|
|
40
|
-
reportError(error, 'account');
|
|
41
|
-
return null;
|
|
42
|
-
});
|
|
43
|
-
if (!customer || form.tagName !== 'FORM') return;
|
|
44
|
-
const fields = /** @type {HTMLFormElement} */ (form).elements;
|
|
45
|
-
PROFILE_FIELDS.forEach((name) => {
|
|
46
|
-
const input = fields.namedItem(name);
|
|
47
|
-
const isTextField = input instanceof window.HTMLInputElement || input instanceof window.HTMLTextAreaElement;
|
|
48
|
-
if (isTextField && !input.dataset['tetherTouched']) input.value = customer[name] ?? '';
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let listening = false;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Renders the order page, profile form, addresses and subscriptions in `root`: only what it has.
|
|
56
|
-
*
|
|
57
|
-
* @param {ParentNode} [root]
|
|
58
|
-
* @returns {Promise<void>}
|
|
59
|
-
*/
|
|
60
|
-
export async function renderPages(root = document) {
|
|
61
|
-
if (!listening) {
|
|
62
|
-
listening = true;
|
|
63
|
-
listenToPages();
|
|
64
|
-
}
|
|
65
|
-
await Promise.all([
|
|
66
|
-
...qsa(root, sel('order')).map((el) => renderOrder(el)),
|
|
67
|
-
...qsa(root, sel('profile-form')).map((form) => renderProfile(form)),
|
|
68
|
-
renderAddresses(root),
|
|
69
|
-
renderSubscriptions(root),
|
|
70
|
-
]);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* The error state of every account page in `root`, for when nobody can say whether a customer is logged in (the
|
|
75
|
-
* Worker or Shopify can't be reached).
|
|
76
|
-
*
|
|
77
|
-
* @param {ParentNode} root
|
|
78
|
-
*/
|
|
79
|
-
export function showUnavailable(root) {
|
|
80
|
-
qsa(root, sel('order')).forEach((el) => {
|
|
81
|
-
showOrderState(el, 'error');
|
|
82
|
-
});
|
|
83
|
-
showIn(root, 'show', {
|
|
84
|
-
'addresses-loading': false,
|
|
85
|
-
'addresses-error': true,
|
|
86
|
-
'subscriptions-loading': false,
|
|
87
|
-
'subscriptions-error': true,
|
|
88
|
-
});
|
|
89
|
-
}
|