@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.
Files changed (215) hide show
  1. package/README.md +28 -28
  2. package/dist/esm/astro.js +2 -2
  3. package/dist/esm/chunks/account-CXMA5RPZ.js +1 -0
  4. package/dist/esm/chunks/account-pages-IZO7FEOZ.js +1 -0
  5. package/dist/esm/chunks/analytics-PG5FPRDN.js +1 -0
  6. package/dist/esm/chunks/chunk-4HD4J5JC.js +1 -0
  7. package/dist/esm/chunks/chunk-6TFMLT4A.js +4 -0
  8. package/dist/esm/chunks/chunk-BBVUVPMO.js +1 -0
  9. package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
  10. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  11. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  12. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  13. package/dist/esm/chunks/chunk-N3V45JPS.js +1 -0
  14. package/dist/esm/chunks/chunk-RPMBCAGS.js +1 -0
  15. package/dist/esm/chunks/chunk-VFNHIGCS.js +1 -0
  16. package/dist/esm/chunks/chunk-WT7RGGJU.js +1 -0
  17. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  18. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  19. package/dist/esm/chunks/diagnose-XCEBQKBO.js +14 -0
  20. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  21. package/dist/esm/chunks/markets-ui-M4IQ62CT.js +1 -0
  22. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/server.js +2 -2
  25. package/dist/presets/barba.esm.js +19 -23
  26. package/dist/tether.account-pages.js +332 -367
  27. package/dist/tether.account-pages.min.js +1 -1
  28. package/dist/tether.account.js +253 -492
  29. package/dist/tether.account.min.js +1 -1
  30. package/dist/tether.analytics.js +82 -89
  31. package/dist/tether.analytics.min.js +1 -1
  32. package/dist/tether.diagnose.js +52 -34
  33. package/dist/tether.diagnose.min.js +2 -2
  34. package/dist/tether.extras.js +42 -37
  35. package/dist/tether.extras.min.js +1 -1
  36. package/dist/tether.js +1361 -935
  37. package/dist/tether.markets.js +89 -91
  38. package/dist/tether.markets.min.js +1 -1
  39. package/dist/tether.min.js +0 -0
  40. package/dist/tether.min.js.map +4 -4
  41. package/dist/tether.search.js +60 -69
  42. package/dist/tether.search.min.js +1 -1
  43. package/dist/types/server/src/shared/account-orders.d.ts +44 -0
  44. package/dist/types/server/src/shared/account-routes.d.ts +153 -0
  45. package/dist/types/server/src/shared/api-versions.d.ts +13 -0
  46. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  47. package/dist/types/server/src/shared/error-message.d.ts +2 -0
  48. package/dist/types/server/src/shared/json.d.ts +25 -0
  49. package/dist/types/server/src/shared/market-codes.d.ts +16 -0
  50. package/dist/types/src/account/addresses-view.d.ts +5 -0
  51. package/dist/types/src/account/answers.d.ts +40 -0
  52. package/dist/types/src/account/markup.d.ts +19 -0
  53. package/dist/types/src/account/operations.d.ts +41 -0
  54. package/dist/types/src/account/order-lines.d.ts +13 -0
  55. package/dist/types/src/account/order-list.d.ts +12 -0
  56. package/dist/types/src/account/order-page.d.ts +6 -0
  57. package/dist/types/src/account/page-answers.d.ts +182 -0
  58. package/dist/types/src/account/page-events.d.ts +2 -0
  59. package/dist/types/src/account/server-client.d.ts +60 -0
  60. package/dist/types/src/account/subscriptions-view.d.ts +2 -0
  61. package/dist/types/src/account-api.d.ts +18 -0
  62. package/dist/types/src/account-marks.d.ts +15 -0
  63. package/dist/types/src/account-pages.d.ts +8 -0
  64. package/dist/types/src/account.d.ts +64 -0
  65. package/dist/types/src/amounts.d.ts +12 -0
  66. package/dist/types/src/analytics-shopify.d.ts +41 -0
  67. package/dist/types/src/analytics.d.ts +2 -0
  68. package/dist/types/src/api-version.d.ts +28 -0
  69. package/dist/types/src/astro.d.ts +42 -0
  70. package/dist/types/src/cart/cart-shape.d.ts +43 -0
  71. package/dist/types/src/cart/checkout.d.ts +16 -0
  72. package/dist/types/src/cart/drawer.d.ts +18 -0
  73. package/dist/types/src/cart/mutations.d.ts +85 -0
  74. package/dist/types/src/cart/render.d.ts +17 -0
  75. package/dist/types/src/cart/store.d.ts +185 -0
  76. package/dist/types/src/cart.d.ts +106 -0
  77. package/dist/types/src/client.d.ts +134 -0
  78. package/dist/types/src/collection.d.ts +25 -0
  79. package/dist/types/src/config-sections.d.ts +40 -0
  80. package/dist/types/src/config.d.ts +190 -0
  81. package/dist/types/src/controls/cart-controls.d.ts +2 -0
  82. package/dist/types/src/controls/discount.d.ts +2 -0
  83. package/dist/types/src/controls/extras.d.ts +5 -0
  84. package/dist/types/src/controls/navigation.d.ts +12 -0
  85. package/dist/types/src/diagnose-markup.d.ts +19 -0
  86. package/dist/types/src/diagnose.d.ts +60 -0
  87. package/dist/types/src/dom.d.ts +75 -0
  88. package/dist/types/src/extras.d.ts +21 -0
  89. package/dist/types/src/fragments.d.ts +158 -0
  90. package/dist/types/src/globals.d.ts +15 -0
  91. package/dist/types/src/images.d.ts +20 -0
  92. package/dist/types/src/index.d.ts +167 -0
  93. package/dist/types/src/initial.d.ts +56 -0
  94. package/dist/types/src/lazy.d.ts +28 -0
  95. package/dist/types/src/loader.d.ts +84 -0
  96. package/dist/types/src/market-codes.d.ts +6 -0
  97. package/dist/types/src/markets-ui.d.ts +32 -0
  98. package/dist/types/src/markets.d.ts +72 -0
  99. package/dist/types/src/markup.d.ts +131 -0
  100. package/dist/types/src/mounting.d.ts +46 -0
  101. package/dist/types/src/page-market.d.ts +27 -0
  102. package/dist/types/src/presets/barba.d.ts +56 -0
  103. package/dist/types/src/product-events.d.ts +2 -0
  104. package/dist/types/src/product-render.d.ts +17 -0
  105. package/dist/types/src/product-shape.d.ts +90 -0
  106. package/dist/types/src/product-state.d.ts +110 -0
  107. package/dist/types/src/product.d.ts +66 -0
  108. package/dist/types/src/repeaters.d.ts +16 -0
  109. package/dist/types/src/search.d.ts +33 -0
  110. package/dist/types/src/selling-plans.d.ts +64 -0
  111. package/dist/types/src/server.d.ts +88 -0
  112. package/dist/types/src/shopify-values.d.ts +46 -0
  113. package/dist/types/src/status.d.ts +33 -0
  114. package/dist/types/src/storage.d.ts +12 -0
  115. package/dist/types/src/store-html.d.ts +39 -0
  116. package/dist/types/src/storefront/catalog.d.ts +38 -0
  117. package/dist/types/src/storefront/json-ld.d.ts +20 -0
  118. package/dist/types/src/variant-render.d.ts +14 -0
  119. package/package.json +24 -24
  120. package/dist/esm/chunks/account-C3NP6HFB.js +0 -1
  121. package/dist/esm/chunks/account-pages-WLK374FO.js +0 -1
  122. package/dist/esm/chunks/analytics-G3SAWMNL.js +0 -1
  123. package/dist/esm/chunks/chunk-27PJJZGC.js +0 -1
  124. package/dist/esm/chunks/chunk-2FSXS55U.js +0 -1
  125. package/dist/esm/chunks/chunk-FCSWQDVV.js +0 -1
  126. package/dist/esm/chunks/chunk-FOMSMIAM.js +0 -1
  127. package/dist/esm/chunks/chunk-GZJTCLLW.js +0 -1
  128. package/dist/esm/chunks/chunk-HEQILZVR.js +0 -1
  129. package/dist/esm/chunks/chunk-JOOEXYF3.js +0 -4
  130. package/dist/esm/chunks/chunk-MPTQYLSM.js +0 -1
  131. package/dist/esm/chunks/chunk-NEHZ37TU.js +0 -3
  132. package/dist/esm/chunks/chunk-V74KGEJF.js +0 -1
  133. package/dist/esm/chunks/chunk-VT4CDUJJ.js +0 -1
  134. package/dist/esm/chunks/chunk-YJLWX2HK.js +0 -1
  135. package/dist/esm/chunks/diagnose-6VVGZNTY.js +0 -14
  136. package/dist/esm/chunks/extras-GJVRDFM3.js +0 -1
  137. package/dist/esm/chunks/markets-ui-UNB4PTA7.js +0 -1
  138. package/dist/esm/chunks/search-OU75RNQL.js +0 -1
  139. package/src/account/addresses-view.js +0 -92
  140. package/src/account/browser-session.js +0 -359
  141. package/src/account/markup.js +0 -48
  142. package/src/account/operations.js +0 -180
  143. package/src/account/order-lines.js +0 -96
  144. package/src/account/order-list.js +0 -91
  145. package/src/account/order-page.js +0 -192
  146. package/src/account/page-events.js +0 -288
  147. package/src/account/server-client.js +0 -172
  148. package/src/account/shared.js +0 -111
  149. package/src/account/shopify-auth.js +0 -150
  150. package/src/account/subscriptions-view.js +0 -158
  151. package/src/account-api.js +0 -118
  152. package/src/account-links.js +0 -35
  153. package/src/account-pages.js +0 -89
  154. package/src/account.js +0 -342
  155. package/src/amounts.js +0 -36
  156. package/src/analytics-shopify.js +0 -241
  157. package/src/analytics.js +0 -352
  158. package/src/api-version.js +0 -85
  159. package/src/astro.js +0 -266
  160. package/src/cart/checkout.js +0 -97
  161. package/src/cart/drawer.js +0 -138
  162. package/src/cart/mutations.js +0 -234
  163. package/src/cart/render.js +0 -294
  164. package/src/cart/store.js +0 -326
  165. package/src/cart.js +0 -391
  166. package/src/client.js +0 -267
  167. package/src/collection.js +0 -183
  168. package/src/config.js +0 -349
  169. package/src/controls/cart-controls.js +0 -155
  170. package/src/controls/discount.js +0 -96
  171. package/src/controls/extras.js +0 -63
  172. package/src/controls/navigation.js +0 -83
  173. package/src/diagnose-markup.js +0 -333
  174. package/src/diagnose.js +0 -295
  175. package/src/dom.js +0 -287
  176. package/src/extras.js +0 -145
  177. package/src/fragments.js +0 -237
  178. package/src/images.js +0 -104
  179. package/src/index.js +0 -353
  180. package/src/initial.js +0 -87
  181. package/src/lazy-iife.js +0 -116
  182. package/src/lazy.js +0 -39
  183. package/src/loader.js +0 -337
  184. package/src/market-codes.js +0 -35
  185. package/src/markets-ui.js +0 -204
  186. package/src/markets.js +0 -334
  187. package/src/markup.js +0 -64
  188. package/src/mounting.js +0 -277
  189. package/src/page-market.js +0 -116
  190. package/src/parts/account-pages.js +0 -6
  191. package/src/parts/account.js +0 -6
  192. package/src/parts/analytics.js +0 -6
  193. package/src/parts/diagnose.js +0 -6
  194. package/src/parts/extras.js +0 -6
  195. package/src/parts/markets.js +0 -6
  196. package/src/parts/search.js +0 -6
  197. package/src/presets/barba.js +0 -190
  198. package/src/product-events.js +0 -130
  199. package/src/product-render.js +0 -257
  200. package/src/product-state.js +0 -318
  201. package/src/product.js +0 -344
  202. package/src/repeaters.js +0 -173
  203. package/src/search.js +0 -323
  204. package/src/selling-plans.js +0 -154
  205. package/src/server.js +0 -215
  206. package/src/shopify-values.js +0 -89
  207. package/src/status.js +0 -126
  208. package/src/store-html.js +0 -0
  209. package/src/storefront/catalog.js +0 -199
  210. package/src/storefront/json-ld.js +0 -167
  211. package/src/variant-render.js +0 -165
  212. package/types/astro.d.ts +0 -28
  213. package/types/barba.d.ts +0 -16
  214. package/types/index.d.ts +0 -577
  215. package/types/server.d.ts +0 -182
@@ -1,288 +0,0 @@
1
- /**
2
- * What the visitor does on the account pages: the profile and address forms, the address buttons, subscription
3
- * actions and return requests. Each change marks its form or row busy (`aria-busy`, `is-busy`), then sets
4
- * `tether-form-state` to `saved` or `error` with Shopify's message in `tether-form-error`.
5
- */
6
- import { attr, get, qsa, sel, setText } from '../dom.js';
7
- import {
8
- saveAddress,
9
- deleteAddress,
10
- subscriptionAction,
11
- requestReturn,
12
- updateProfile,
13
- pagesContext,
14
- } from './operations.js';
15
- import { renderAddresses, shownAddress } from './addresses-view.js';
16
- import { renderSubscriptions } from './subscriptions-view.js';
17
- import { renderOrder } from './order-page.js';
18
- import { TetherError } from '../client.js';
19
- /** @import { ReturnLine } from './operations.js' */
20
- /** @import { SubscriptionAction } from '../../server/src/shared/account-ops.js' */
21
-
22
- /** @type {readonly SubscriptionAction[]} */
23
- const SUBSCRIPTION_ACTIONS = ['pause', 'resume', 'skip', 'unskip', 'cancel'];
24
-
25
- /**
26
- * @param {string | null} value
27
- * @returns {value is SubscriptionAction}
28
- */
29
- const isSubscriptionAction = (value) => SUBSCRIPTION_ACTIONS.some((action) => action === value);
30
-
31
- /**
32
- * @param {Element} el
33
- * @returns {el is HTMLInputElement}
34
- */
35
- const isInput = (el) => el.tagName === 'INPUT';
36
-
37
- /**
38
- * @param {Element} el
39
- * @returns {el is HTMLFormElement}
40
- */
41
- const isForm = (el) => el.tagName === 'FORM';
42
-
43
- /**
44
- * @param {HTMLFormElement} form
45
- * @returns {Record<string, string>} The form's text fields by name.
46
- */
47
- function fieldsOf(form) {
48
- /** @type {Record<string, string>} */
49
- const fields = {};
50
- for (const field of form.elements) {
51
- if (!isInput(field) && field.tagName !== 'SELECT' && field.tagName !== 'TEXTAREA') continue;
52
- const input = /** @type {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} */ (field);
53
- if (!input.name || input.type === 'submit' || input.type === 'button' || input.type === 'checkbox') continue;
54
- fields[input.name] = input.value;
55
- }
56
- return fields;
57
- }
58
-
59
- /**
60
- * @param {unknown} error
61
- * @returns {string} What to tell the customer: Shopify's own messages when it refused the input.
62
- */
63
- function messageOf(error) {
64
- const userErrors = error instanceof TetherError ? (error.userErrors ?? []) : [];
65
- const message = userErrors.map((userError) => userError.message).join(' ');
66
- if (message) return message;
67
- const text = error instanceof Error && error.message ? error.message : 'Something went wrong';
68
- return text.replace(/^Customer accounts: /, '');
69
- }
70
-
71
- /**
72
- * Runs one change for `el`; a second one while it is on its way (Enter, a double click) is ignored. The failure is
73
- * shown in `el` (`tether-form-error`), the place the customer looks.
74
- *
75
- * @param {HTMLElement} el
76
- * @param {() => Promise<void>} change
77
- * @returns {Promise<'saved' | 'error' | 'busy'>}
78
- */
79
- async function act(el, change) {
80
- if (el.getAttribute('aria-busy') === 'true') return 'busy';
81
- el.classList.add('is-busy');
82
- el.setAttribute('aria-busy', 'true');
83
- el.setAttribute(attr('form-state'), 'pending');
84
- const errorBoxes = qsa(el, sel('form-error'));
85
- errorBoxes.forEach((box) => {
86
- setText(box, '');
87
- });
88
- try {
89
- await change();
90
- el.setAttribute(attr('form-state'), 'saved');
91
- return 'saved';
92
- } catch (error) {
93
- el.setAttribute(attr('form-state'), 'error');
94
- errorBoxes.forEach((box) => {
95
- setText(box, messageOf(error));
96
- });
97
- return 'error';
98
- } finally {
99
- el.classList.remove('is-busy');
100
- el.removeAttribute('aria-busy');
101
- }
102
- }
103
-
104
- /**
105
- * @param {Element} button
106
- * @returns {boolean} True when the button asks no `tether-confirm` question, or the visitor agreed.
107
- */
108
- const confirmed = (button) => {
109
- const question = get(button, 'confirm');
110
- return !question || window.confirm(question);
111
- };
112
-
113
- /** @param {HTMLFormElement} form */
114
- const addressFormReset = (form) => {
115
- form.reset();
116
- delete form.dataset['tetherAddressId'];
117
- };
118
-
119
- /** @param {SubmitEvent} event */
120
- function onSubmit(event) {
121
- const form = event.target;
122
- if (!(form instanceof window.HTMLFormElement)) return;
123
- if (form.matches(sel('profile-form'))) {
124
- event.preventDefault();
125
- void act(form, async () => {
126
- await updateProfile(fieldsOf(form));
127
- await pagesContext().bindCustomer(document);
128
- });
129
- } else if (form.matches(sel('address-form'))) {
130
- event.preventDefault();
131
- const id = form.dataset['tetherAddressId'];
132
- const box = form.elements.namedItem('isDefault');
133
- const isDefault = box instanceof window.HTMLInputElement && box.checked;
134
- void act(form, async () => {
135
- await saveAddress(fieldsOf(form), { id, isDefault });
136
- addressFormReset(form);
137
- await renderAddresses(document);
138
- });
139
- }
140
- }
141
-
142
- /**
143
- * Fills the address form with a shown address, to edit it.
144
- *
145
- * @param {string} id
146
- */
147
- function editAddress(id) {
148
- const address = shownAddress(id);
149
- const form = document.querySelector(sel('address-form'));
150
- if (!address || !form || !isForm(form)) return;
151
- form.dataset['tetherAddressId'] = address.id;
152
- for (const field of form.elements) {
153
- if (!isInput(field) && field.tagName !== 'SELECT' && field.tagName !== 'TEXTAREA') continue;
154
- const input = /** @type {HTMLInputElement} */ (field);
155
- if (input.name === 'isDefault') input.checked = address.isDefault;
156
- else if (input.name && Object.hasOwn(address, input.name)) {
157
- const value = Object.entries(address).find(([key]) => key === input.name)?.[1];
158
- input.value = typeof value === 'string' ? value : '';
159
- }
160
- }
161
- form.scrollIntoView({ block: 'nearest' });
162
- }
163
-
164
- /**
165
- * @param {MouseEvent} event
166
- * @param {Element} target
167
- * @returns {boolean} True when the click was on an address button.
168
- */
169
- function onAddressClick(event, target) {
170
- const row = target.closest('[data-tether-address]');
171
- const id = row instanceof window.HTMLElement ? row.dataset['tetherAddress'] : undefined;
172
- if (target.closest(sel('address-new'))) {
173
- const form = document.querySelector(sel('address-form'));
174
- if (form && isForm(form)) addressFormReset(form);
175
- return true;
176
- }
177
- if (!(row instanceof window.HTMLElement) || !id) return false;
178
- const deleteButton = target.closest(sel('address-delete'));
179
- if (deleteButton) {
180
- event.preventDefault();
181
- if (!confirmed(deleteButton)) return true;
182
- void act(row, async () => {
183
- await deleteAddress(id);
184
- await renderAddresses(document);
185
- });
186
- } else if (target.closest(sel('address-default'))) {
187
- event.preventDefault();
188
- void act(row, async () => {
189
- await saveAddress(null, { id, isDefault: true });
190
- await renderAddresses(document);
191
- });
192
- } else if (target.closest(sel('address-edit'))) {
193
- event.preventDefault();
194
- editAddress(id);
195
- } else {
196
- return false;
197
- }
198
- return true;
199
- }
200
-
201
- /** @param {HTMLElement} button A `tether-subscription-action` button. */
202
- function onSubscriptionAction(button) {
203
- const row = button.closest('[data-tether-subscription]');
204
- const action = get(button, 'subscription-action');
205
- const disabled = ('disabled' in button && button.disabled === true) || button.classList.contains('is-disabled');
206
- if (!(row instanceof window.HTMLElement) || !isSubscriptionAction(action) || disabled || !confirmed(button)) return;
207
- const id = row.dataset['tetherSubscription'] ?? '';
208
- row.setAttribute(attr('subscription-state'), 'pending');
209
- void act(row, async () => {
210
- await subscriptionAction(id, action);
211
- await renderSubscriptions(document);
212
- }).then((outcome) => {
213
- const state = outcome === 'error' ? 'error' : 'done';
214
- // The re-render may have replaced the row: mark the one that shows this subscription now.
215
- qsa(document, '[data-tether-subscription]')
216
- .filter((candidate) => candidate === row || candidate.dataset['tetherSubscription'] === id)
217
- .forEach((candidate) => {
218
- candidate.setAttribute(attr('subscription-state'), state);
219
- });
220
- });
221
- }
222
-
223
- /**
224
- * @param {HTMLElement} row A line row of the order page.
225
- * @returns {ReturnLine}
226
- */
227
- function returnLineOf(row) {
228
- const quantity = row.querySelector(sel('return-quantity'));
229
- const reason = row.querySelector(sel('return-reason'));
230
- const note = row.querySelector(sel('return-note'));
231
- /** @param {Element | null} field */
232
- const valueOf = (field) => (field && 'value' in field && typeof field.value === 'string' ? field.value : '');
233
- /** @type {ReturnLine} */
234
- const line = { lineItemId: row.dataset['tetherLine'] ?? '', quantity: Number.parseInt(valueOf(quantity) || '0', 10) };
235
- const reasonId = valueOf(reason);
236
- if (reasonId) line.reasonId = reasonId;
237
- const noteText = valueOf(note);
238
- if (noteText) line.note = noteText;
239
- return line;
240
- }
241
-
242
- /** @param {HTMLElement} button A `tether-order-return` button. */
243
- function onReturnRequest(button) {
244
- const scope = button.closest(sel('order'));
245
- if (!(scope instanceof window.HTMLElement)) return;
246
- const orderId = scope.dataset['tetherOrderId'];
247
- if (!orderId) return;
248
- const lines = qsa(scope, '[data-tether-line]')
249
- .map(returnLineOf)
250
- .filter((line) => line.quantity > 0);
251
- if (!lines.length || !confirmed(button)) return;
252
- void act(scope, async () => {
253
- await requestReturn(orderId, lines);
254
- scope.dataset['tetherReturned'] = '1';
255
- await renderOrder(scope);
256
- });
257
- }
258
-
259
- /** @param {MouseEvent} event */
260
- function onClick(event) {
261
- const target = event.target;
262
- if (!(target instanceof window.Element)) return;
263
- if (onAddressClick(event, target)) return;
264
- const subscriptionButton = target.closest(sel('subscription-action'));
265
- if (subscriptionButton instanceof window.HTMLElement) {
266
- event.preventDefault();
267
- onSubscriptionAction(subscriptionButton);
268
- return;
269
- }
270
- const returnButton = target.closest(sel('order-return'));
271
- if (returnButton instanceof window.HTMLElement) {
272
- event.preventDefault();
273
- onReturnRequest(returnButton);
274
- }
275
- }
276
-
277
- /** Listens on the document once, for every account page rendered into it. */
278
- export function listenToPages() {
279
- // Capture: before a router (Astro's ClientRouter) turns the submit into a navigation with the fields in the URL.
280
- document.addEventListener('submit', onSubmit, true);
281
- document.addEventListener('input', (event) => {
282
- const target = event.target;
283
- if (target instanceof window.HTMLElement && target.closest(sel('profile-form'))) {
284
- target.dataset['tetherTouched'] = '1';
285
- }
286
- });
287
- document.addEventListener('click', onClick);
288
- }
@@ -1,172 +0,0 @@
1
- /**
2
- * Server mode (`account.server`): the site's Worker holds the Shopify tokens, and this browser only a session cookie
3
- * its scripts can't read. This module is the browser's side of the Worker's account routes.
4
- */
5
- import { cfg } from '../config.js';
6
- import { TetherError } from '../client.js';
7
- import { Cart } from '../cart.js';
8
- import { emit } from '../dom.js';
9
- import {
10
- isRecord,
11
- loginCountry,
12
- loginLanguage,
13
- navigate,
14
- readJsonObjectOrEmpty,
15
- setLoginHint,
16
- textOrNull,
17
- } from './shared.js';
18
- /** @import { UserError } from '../client.js' */
19
-
20
- /**
21
- * @typedef {{ kind: 'answer', data: Record<string, unknown> } | { kind: 'logged-out' }} ServerAnswer The Worker's
22
- * answer, or a 401: the session cookie is missing, has expired, or Shopify ended the login.
23
- */
24
- /**
25
- * @typedef {object} ServerCustomer The profile the Worker's `/me` sends.
26
- * @property {string | null} firstName
27
- * @property {string | null} lastName
28
- * @property {string | null} displayName
29
- * @property {string | null} email
30
- * @property {string[] | null} address The default address, one line per entry.
31
- */
32
- /** @typedef {{ loggedIn: true, customer: ServerCustomer } | { loggedIn: false }} ServerLogin */
33
- /** @typedef {{ method?: 'GET' | 'POST', body?: Record<string, unknown> }} ServerRequest */
34
-
35
- /** @type {ServerLogin | null} The Worker's `/me` answer, until a change of the customer's data. */
36
- let known = null;
37
-
38
- /**
39
- * @returns {string} The Worker's account routes, without a trailing slash; `''` in browser mode. `account.server` is a
40
- * full URL or a path on this site.
41
- */
42
- export function serverBase() {
43
- const server = cfg.account?.server;
44
- if (!server) return '';
45
- return new URL(server, window.location.href).href.replace(/\/$/, '');
46
- }
47
-
48
- /**
49
- * One request to the Worker, with the session cookie. A POST carries the header the Worker requires against
50
- * cross-site forms.
51
- *
52
- * @param {string} path
53
- * @param {ServerRequest} [request]
54
- * @returns {Promise<ServerAnswer>}
55
- * @throws {TetherError} `HTTP_<status>` for any failure but a 401, with the Worker's `userErrors`.
56
- */
57
- export async function serverFetch(path, { method = 'GET', body } = {}) {
58
- /** @type {RequestInit} */
59
- const init = { method, credentials: 'include' };
60
- if (method === 'POST') init.headers = { 'Content-Type': 'application/json', 'X-Tether-Account': '1' };
61
- if (body) init.body = JSON.stringify(body);
62
- const response = await fetch(`${serverBase()}${path}`, init);
63
- if (response.status === 401) return { kind: 'logged-out' };
64
- const data = await readJsonObjectOrEmpty(response);
65
- if (!response.ok) {
66
- const reason = textOrNull(data['error']) ?? String(response.status);
67
- /** @type {TetherError['userErrors']} */
68
- const userErrors = Array.isArray(data['userErrors'])
69
- ? // The Worker sends Shopify's own `userErrors` list unchanged.
70
- /** @type {UserError[]} */ (data['userErrors'])
71
- : undefined;
72
- throw new TetherError(`Customer accounts: ${reason}`, {
73
- status: response.status,
74
- ...(userErrors && { userErrors }),
75
- });
76
- }
77
- return { kind: 'answer', data };
78
- }
79
-
80
- /**
81
- * @param {unknown} value
82
- * @returns {ServerCustomer}
83
- */
84
- function toServerCustomer(value) {
85
- const customer = isRecord(value) ? value : {};
86
- const address = customer['address'];
87
- return {
88
- firstName: textOrNull(customer['firstName']),
89
- lastName: textOrNull(customer['lastName']),
90
- displayName: textOrNull(customer['displayName']),
91
- email: textOrNull(customer['email']),
92
- address: Array.isArray(address) ? address.filter((line) => typeof line === 'string') : null,
93
- };
94
- }
95
-
96
- /**
97
- * Who is logged in, asked once and then kept; the "may be logged in" hint follows the answer.
98
- *
99
- * @returns {Promise<ServerLogin>}
100
- * @throws {TetherError} When the Worker can't be reached or fails: that says nothing about the login.
101
- */
102
- export async function serverLogin() {
103
- if (!known) {
104
- const answer = await serverFetch('/me');
105
- known =
106
- answer.kind === 'answer' && answer.data['loggedIn'] === true
107
- ? { loggedIn: true, customer: toServerCustomer(answer.data['customer']) }
108
- : { loggedIn: false };
109
- }
110
- setLoginHint(known.loggedIn);
111
- return known;
112
- }
113
-
114
- /** @returns {boolean} True when the Worker last said a customer is logged in. */
115
- export const isServerLoggedIn = () => known?.loggedIn === true;
116
-
117
- /** Asks the Worker again on the next `serverLogin()`, after a change of the customer's data. */
118
- export function forgetServerLogin() {
119
- known = null;
120
- }
121
-
122
- /**
123
- * To the Worker's login, which comes back to `returnTo`. The cart is linked later, at checkout, through the Worker's
124
- * protected `POST /cart`.
125
- *
126
- * @param {{ returnTo: string, locale?: string | undefined }} options
127
- */
128
- export function serverLoginRedirect({ returnTo, locale }) {
129
- const url = new URL(`${serverBase()}/login`);
130
- url.searchParams.set('returnTo', returnTo);
131
- const language = loginLanguage(locale);
132
- if (language) url.searchParams.set('locale', language);
133
- const country = loginCountry();
134
- if (country) url.searchParams.set('country', country);
135
- navigate(url.toString());
136
- }
137
-
138
- /**
139
- * The logged-in customer's checkout, for `Cart.beforeCheckout`: the Worker links the cart to the customer and answers
140
- * with its checkout URL.
141
- *
142
- * @returns {Promise<string | null>} `null` when nobody is logged in or the Worker linked no cart, so the cart's own
143
- * checkout URL is used.
144
- * @throws {TetherError} When the Worker fails; the cart reports it and checks out without the login.
145
- */
146
- export async function serverCheckoutUrl() {
147
- const cartId = Cart.id();
148
- if (!(await serverLogin()).loggedIn || !cartId) return null;
149
- const answer = await serverFetch('/cart', { method: 'POST', body: { cartId } });
150
- if (answer.kind === 'logged-out') return null;
151
- const url = textOrNull(answer.data['checkoutUrl']);
152
- if (!url) return null;
153
- return `${url}${url.includes('?') ? '&' : '?'}logged_in=true`;
154
- }
155
-
156
- /**
157
- * Ends the session at the Worker, which also unlinks the cart, then goes on to Shopify's logout when the Worker says
158
- * where. Sends `tether:account-logout`.
159
- *
160
- * @param {string} returnTo
161
- * @returns {Promise<'redirected' | 'stayed'>}
162
- */
163
- export async function serverLogout(returnTo) {
164
- const answer = await serverFetch('/logout', { method: 'POST', body: { returnTo, cartId: Cart.id() } });
165
- known = null;
166
- setLoginHint(false);
167
- emit('account-logout', {});
168
- const logoutUrl = answer.kind === 'answer' ? textOrNull(answer.data['logoutUrl']) : null;
169
- if (!logoutUrl) return 'stayed';
170
- navigate(logoutUrl);
171
- return 'redirected';
172
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * What both account modes share: the storage keys, the redirect to Shopify's pages, the login language, and reading
3
- * the JSON that Shopify and the Worker answer with. Storage throws in private mode and where the visitor blocks site
4
- * data; a login then lasts as long as the page.
5
- */
6
- import { cfg } from '../config.js';
7
-
8
- /** @typedef {'session' | 'local'} StorageArea */
9
-
10
- /** @returns {string} The browser-mode session's key. */
11
- export const sessionKey = () => `${cfg.prefix}:account:${cfg.domain}`;
12
-
13
- /** @returns {string} The key of the login in progress (browser mode), between leaving for Shopify and coming back. */
14
- export const pendingLoginKey = () => `${cfg.prefix}:account-login:${cfg.domain}`;
15
-
16
- /**
17
- * Server mode's "may be logged in" mark: it makes the core load this part on the next page, and holds no secret.
18
- *
19
- * @returns {string}
20
- */
21
- const hintKey = () => `${cfg.prefix}:account-hint:${cfg.domain}`;
22
-
23
- /** @returns {StorageArea} Where the browser-mode session lives: `localStorage` with `account.remember`. */
24
- export const sessionArea = () => (cfg.account?.remember ? 'local' : 'session');
25
-
26
- /** @param {StorageArea} area */
27
- const storageOf = (area) => (area === 'local' ? window.localStorage : window.sessionStorage);
28
-
29
- /**
30
- * @param {StorageArea} area
31
- * @param {string} key
32
- * @returns {unknown} The stored JSON value; `null` without one, for text that isn't JSON, or with storage blocked.
33
- */
34
- export function readJsonIfAllowed(area, key) {
35
- try {
36
- return JSON.parse(storageOf(area).getItem(key) ?? 'null');
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- /**
43
- * Keeps `value` as JSON; without storage nothing is kept (see the file's comment).
44
- *
45
- * @param {StorageArea} area
46
- * @param {string} key
47
- * @param {unknown} value `null` removes the key.
48
- */
49
- export function writeJsonIfAllowed(area, key, value) {
50
- try {
51
- if (value === null) storageOf(area).removeItem(key);
52
- else storageOf(area).setItem(key, JSON.stringify(value));
53
- } catch {
54
- // Private mode or blocked storage: see the file's comment.
55
- }
56
- }
57
-
58
- /** @param {boolean} loggedIn */
59
- export function setLoginHint(loggedIn) {
60
- writeJsonIfAllowed('local', hintKey(), loggedIn ? 1 : null);
61
- }
62
-
63
- /**
64
- * @param {unknown} value
65
- * @returns {value is Record<string, unknown>}
66
- */
67
- export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
68
-
69
- /**
70
- * @param {unknown} value
71
- * @returns {string | null}
72
- */
73
- export const textOrNull = (value) => (typeof value === 'string' ? value : null);
74
-
75
- /**
76
- * An error page that isn't JSON reads as an empty answer: the response's status then says what went wrong.
77
- *
78
- * @param {Response} response
79
- * @returns {Promise<Record<string, unknown>>}
80
- */
81
- export async function readJsonObjectOrEmpty(response) {
82
- /** @type {unknown} */
83
- let json;
84
- try {
85
- json = await response.json();
86
- } catch {
87
- return {};
88
- }
89
- return isRecord(json) ? json : {};
90
- }
91
-
92
- /**
93
- * Goes to one of Shopify's (or the Worker's) pages, through `account.navigate` when the site replaces the redirect.
94
- *
95
- * @param {string} url
96
- */
97
- export function navigate(url) {
98
- const replacement = cfg.account?.navigate;
99
- if (replacement) replacement(url);
100
- else window.location.assign(url);
101
- }
102
-
103
- /**
104
- * @param {string | undefined} locale The caller's choice.
105
- * @returns {string} The language of Shopify's login page: the caller's, `account.locale`, else the site's; `''` for
106
- * Shopify's default.
107
- */
108
- export const loginLanguage = (locale) => locale || cfg.account?.locale || (cfg.locale || '').split('-')[0] || '';
109
-
110
- /** @returns {string} The market's country for Shopify's login page, `''` without one. */
111
- export const loginCountry = () => (cfg.country ? cfg.country.toUpperCase() : '');