@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.
Files changed (259) hide show
  1. package/README.md +11 -11
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
  4. package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
  5. package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
  6. package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
  7. package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
  8. package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
  9. package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
  10. package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
  11. package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
  12. package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
  13. package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
  14. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  15. package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
  16. package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
  17. package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
  18. package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
  19. package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
  20. package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
  21. package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
  22. package/dist/esm/index.js +1 -1
  23. package/dist/esm/server.js +2 -2
  24. package/dist/presets/barba.esm.js +19 -23
  25. package/dist/tether.account-pages.js +320 -365
  26. package/dist/tether.account-pages.min.js +1 -1
  27. package/dist/tether.account.js +242 -491
  28. package/dist/tether.account.min.js +1 -1
  29. package/dist/tether.analytics.js +66 -88
  30. package/dist/tether.analytics.min.js +1 -1
  31. package/dist/tether.diagnose.js +36 -33
  32. package/dist/tether.diagnose.min.js +2 -2
  33. package/dist/tether.extras.js +28 -36
  34. package/dist/tether.extras.min.js +1 -1
  35. package/dist/tether.js +1270 -894
  36. package/dist/tether.markets.js +73 -90
  37. package/dist/tether.markets.min.js +1 -1
  38. package/dist/tether.min.js +0 -0
  39. package/dist/tether.min.js.map +4 -4
  40. package/dist/tether.search.js +44 -68
  41. package/dist/tether.search.min.js +1 -1
  42. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  43. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  44. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  45. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  46. package/dist/types/server/src/shared/json.d.ts +25 -0
  47. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  48. package/dist/types/src/account/addresses-view.d.ts +5 -0
  49. package/dist/types/src/account/answers.d.ts +40 -0
  50. package/dist/types/src/account/markup.d.ts +19 -0
  51. package/dist/types/src/account/operations.d.ts +41 -0
  52. package/dist/types/src/account/order-lines.d.ts +13 -0
  53. package/dist/types/src/account/order-list.d.ts +12 -0
  54. package/dist/types/src/account/order-page.d.ts +6 -0
  55. package/dist/types/src/account/page-answers.d.ts +182 -0
  56. package/dist/types/src/account/page-events.d.ts +2 -0
  57. package/dist/types/src/account/server-client.d.ts +60 -0
  58. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  59. package/dist/types/src/account-api.d.ts +18 -0
  60. package/dist/types/src/account-marks.d.ts +15 -0
  61. package/dist/types/src/account-pages.d.ts +8 -0
  62. package/dist/types/src/account.d.ts +64 -0
  63. package/dist/types/src/amounts.d.ts +12 -0
  64. package/dist/types/src/analytics-shopify.d.ts +41 -0
  65. package/dist/types/src/analytics.d.ts +2 -0
  66. package/dist/types/src/api-version.d.ts +28 -0
  67. package/dist/types/src/astro.d.ts +42 -0
  68. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  69. package/dist/types/src/cart/checkout.d.ts +16 -0
  70. package/dist/types/src/cart/drawer.d.ts +18 -0
  71. package/dist/types/src/cart/mutations.d.ts +79 -0
  72. package/dist/types/src/cart/render.d.ts +17 -0
  73. package/dist/types/src/cart/store.d.ts +182 -0
  74. package/dist/types/src/cart.d.ts +106 -0
  75. package/dist/types/src/client.d.ts +100 -0
  76. package/dist/types/src/collection.d.ts +25 -0
  77. package/dist/types/src/config-sections.d.ts +40 -0
  78. package/dist/types/src/config.d.ts +185 -0
  79. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  80. package/dist/types/src/controls/discount.d.ts +2 -0
  81. package/dist/types/src/controls/extras.d.ts +5 -0
  82. package/dist/types/src/controls/navigation.d.ts +12 -0
  83. package/dist/types/src/diagnose-markup.d.ts +19 -0
  84. package/dist/types/src/diagnose.d.ts +60 -0
  85. package/dist/types/src/dom.d.ts +75 -0
  86. package/dist/types/src/extras.d.ts +21 -0
  87. package/dist/types/src/fragments.d.ts +158 -0
  88. package/dist/types/src/globals.d.ts +15 -0
  89. package/dist/types/src/images.d.ts +20 -0
  90. package/dist/types/src/index.d.ts +167 -0
  91. package/dist/types/src/initial.d.ts +56 -0
  92. package/dist/types/src/lazy.d.ts +28 -0
  93. package/dist/types/src/loader.d.ts +84 -0
  94. package/dist/types/src/market-codes.d.ts +6 -0
  95. package/dist/types/src/markets-ui.d.ts +32 -0
  96. package/dist/types/src/markets.d.ts +72 -0
  97. package/dist/types/src/markup.d.ts +131 -0
  98. package/dist/types/src/mounting.d.ts +46 -0
  99. package/dist/types/src/page-market.d.ts +27 -0
  100. package/dist/types/src/presets/barba.d.ts +56 -0
  101. package/dist/types/src/product-events.d.ts +2 -0
  102. package/dist/types/src/product-render.d.ts +17 -0
  103. package/dist/types/src/product-shape.d.ts +88 -0
  104. package/dist/types/src/product-state.d.ts +110 -0
  105. package/dist/types/src/product.d.ts +66 -0
  106. package/dist/types/src/repeaters.d.ts +16 -0
  107. package/dist/types/src/search.d.ts +33 -0
  108. package/dist/types/src/selling-plans.d.ts +64 -0
  109. package/dist/types/src/server.d.ts +88 -0
  110. package/dist/types/src/shopify-values.d.ts +46 -0
  111. package/dist/types/src/status.d.ts +33 -0
  112. package/dist/types/src/storage.d.ts +12 -0
  113. package/dist/types/src/store-html.d.ts +39 -0
  114. package/dist/types/src/storefront/catalog.d.ts +38 -0
  115. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  116. package/dist/types/src/variant-render.d.ts +14 -0
  117. package/package.json +24 -23
  118. package/src/account/addresses-view.ts +58 -0
  119. package/src/account/answers.ts +79 -0
  120. package/src/account/markup.ts +36 -0
  121. package/src/account/operations.ts +145 -0
  122. package/src/account/{order-lines.js → order-lines.ts} +20 -41
  123. package/src/account/order-list.ts +107 -0
  124. package/src/account/order-page.ts +137 -0
  125. package/src/account/page-answers.ts +317 -0
  126. package/src/account/{page-events.js → page-events.ts} +55 -90
  127. package/src/account/server-client.ts +183 -0
  128. package/src/account/{subscriptions-view.js → subscriptions-view.ts} +25 -68
  129. package/src/account-api.ts +98 -0
  130. package/src/account-marks.ts +35 -0
  131. package/src/{account-pages.js → account-pages.ts} +29 -37
  132. package/src/account.ts +284 -0
  133. package/src/amounts.ts +64 -0
  134. package/src/{analytics-shopify.js → analytics-shopify.ts} +112 -107
  135. package/src/analytics.ts +315 -0
  136. package/src/{api-version.js → api-version.ts} +25 -31
  137. package/src/{astro.js → astro.ts} +110 -104
  138. package/src/cart/cart-shape.ts +25 -0
  139. package/src/cart/checkout.ts +101 -0
  140. package/src/cart/{drawer.js → drawer.ts} +18 -31
  141. package/src/cart/mutations.ts +237 -0
  142. package/src/cart/{render.js → render.ts} +68 -96
  143. package/src/cart/store.ts +279 -0
  144. package/src/{cart.js → cart.ts} +82 -128
  145. package/src/client.ts +391 -0
  146. package/src/{collection.js → collection.ts} +30 -38
  147. package/src/config-sections.ts +284 -0
  148. package/src/config.ts +480 -0
  149. package/src/controls/{cart-controls.js → cart-controls.ts} +22 -54
  150. package/src/controls/{discount.js → discount.ts} +13 -34
  151. package/src/controls/{extras.js → extras.ts} +15 -24
  152. package/src/controls/{navigation.js → navigation.ts} +24 -30
  153. package/src/{diagnose-markup.js → diagnose-markup.ts} +37 -72
  154. package/src/{diagnose.js → diagnose.ts} +95 -96
  155. package/src/dom.ts +268 -0
  156. package/src/{extras.js → extras.ts} +43 -52
  157. package/src/fragments.ts +261 -0
  158. package/src/globals.d.ts +15 -0
  159. package/src/{images.js → images.ts} +30 -43
  160. package/src/index.ts +415 -0
  161. package/src/initial.ts +101 -0
  162. package/src/lazy-iife.ts +197 -0
  163. package/src/lazy.ts +42 -0
  164. package/src/{loader.js → loader.ts} +149 -123
  165. package/src/market-codes.ts +6 -0
  166. package/src/{markets-ui.js → markets-ui.ts} +91 -68
  167. package/src/markets.ts +246 -0
  168. package/src/markup.ts +127 -0
  169. package/src/{mounting.js → mounting.ts} +88 -101
  170. package/src/{page-market.js → page-market.ts} +32 -38
  171. package/src/part-integrity.ts +7 -0
  172. package/src/parts/account-pages.ts +7 -0
  173. package/src/parts/account.ts +7 -0
  174. package/src/parts/analytics.ts +7 -0
  175. package/src/parts/diagnose.ts +7 -0
  176. package/src/parts/extras.ts +7 -0
  177. package/src/parts/markets.ts +7 -0
  178. package/src/parts/search.ts +7 -0
  179. package/src/presets/barba.ts +194 -0
  180. package/src/{product-events.js → product-events.ts} +33 -48
  181. package/src/{product-render.js → product-render.ts} +38 -77
  182. package/src/product-shape.ts +49 -0
  183. package/src/product-state.ts +318 -0
  184. package/src/{product.js → product.ts} +90 -126
  185. package/src/{repeaters.js → repeaters.ts} +40 -52
  186. package/src/{search.js → search.ts} +85 -103
  187. package/src/selling-plans.ts +145 -0
  188. package/src/server.ts +211 -0
  189. package/src/{shopify-values.js → shopify-values.ts} +24 -22
  190. package/src/{status.js → status.ts} +34 -54
  191. package/src/storage.ts +34 -0
  192. package/src/store-html.ts +359 -0
  193. package/src/storefront/catalog.ts +196 -0
  194. package/src/storefront/{json-ld.js → json-ld.ts} +45 -68
  195. package/src/{variant-render.js → variant-render.ts} +24 -50
  196. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  197. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  198. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  199. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  200. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  201. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  202. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  203. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  204. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  205. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  206. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  207. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  208. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  209. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  210. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  211. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  212. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  213. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  214. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  215. package/src/account/addresses-view.js +0 -92
  216. package/src/account/browser-session.js +0 -359
  217. package/src/account/markup.js +0 -48
  218. package/src/account/operations.js +0 -180
  219. package/src/account/order-list.js +0 -91
  220. package/src/account/order-page.js +0 -192
  221. package/src/account/server-client.js +0 -172
  222. package/src/account/shared.js +0 -111
  223. package/src/account/shopify-auth.js +0 -150
  224. package/src/account-api.js +0 -118
  225. package/src/account-links.js +0 -35
  226. package/src/account.js +0 -342
  227. package/src/amounts.js +0 -36
  228. package/src/analytics.js +0 -352
  229. package/src/cart/checkout.js +0 -97
  230. package/src/cart/mutations.js +0 -234
  231. package/src/cart/store.js +0 -326
  232. package/src/client.js +0 -267
  233. package/src/config.js +0 -349
  234. package/src/dom.js +0 -287
  235. package/src/fragments.js +0 -237
  236. package/src/index.js +0 -353
  237. package/src/initial.js +0 -87
  238. package/src/lazy-iife.js +0 -116
  239. package/src/lazy.js +0 -39
  240. package/src/market-codes.js +0 -35
  241. package/src/markets.js +0 -334
  242. package/src/markup.js +0 -64
  243. package/src/parts/account-pages.js +0 -6
  244. package/src/parts/account.js +0 -6
  245. package/src/parts/analytics.js +0 -6
  246. package/src/parts/diagnose.js +0 -6
  247. package/src/parts/extras.js +0 -6
  248. package/src/parts/markets.js +0 -6
  249. package/src/parts/search.js +0 -6
  250. package/src/presets/barba.js +0 -190
  251. package/src/product-state.js +0 -318
  252. package/src/selling-plans.js +0 -154
  253. package/src/server.js +0 -215
  254. package/src/store-html.js +0 -0
  255. package/src/storefront/catalog.js +0 -199
  256. package/types/astro.d.ts +0 -28
  257. package/types/barba.d.ts +0 -16
  258. package/types/index.d.ts +0 -577
  259. package/types/server.d.ts +0 -182
@@ -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
- };
@@ -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
- }