@occluze/tether 0.7.0 → 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 (132) hide show
  1. package/README.md +21 -21
  2. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-CXMA5RPZ.js} +1 -1
  3. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IZO7FEOZ.js} +1 -1
  4. package/dist/esm/chunks/{analytics-2V632IRV.js → analytics-PG5FPRDN.js} +1 -1
  5. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-4HD4J5JC.js} +1 -1
  6. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-6TFMLT4A.js} +2 -2
  7. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-BBVUVPMO.js} +1 -1
  8. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  9. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  10. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  11. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-N3V45JPS.js} +1 -1
  12. package/dist/esm/chunks/{chunk-YLUULATY.js → chunk-RPMBCAGS.js} +1 -1
  13. package/dist/esm/chunks/{chunk-MPDWM2NN.js → chunk-VFNHIGCS.js} +1 -1
  14. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-WT7RGGJU.js} +1 -1
  15. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  16. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  17. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-XCEBQKBO.js} +1 -1
  18. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  19. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-M4IQ62CT.js} +1 -1
  20. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/server.js +2 -2
  23. package/dist/tether.account-pages.js +13 -3
  24. package/dist/tether.account-pages.min.js +1 -1
  25. package/dist/tether.account.js +13 -3
  26. package/dist/tether.account.min.js +1 -1
  27. package/dist/tether.analytics.js +18 -3
  28. package/dist/tether.analytics.min.js +1 -1
  29. package/dist/tether.diagnose.js +18 -3
  30. package/dist/tether.diagnose.min.js +2 -2
  31. package/dist/tether.extras.js +19 -6
  32. package/dist/tether.extras.min.js +1 -1
  33. package/dist/tether.js +162 -112
  34. package/dist/tether.markets.js +18 -3
  35. package/dist/tether.markets.min.js +1 -1
  36. package/dist/tether.min.js +3 -3
  37. package/dist/tether.min.js.map +4 -4
  38. package/dist/tether.search.js +18 -3
  39. package/dist/tether.search.min.js +1 -1
  40. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  41. package/dist/types/src/cart/mutations.d.ts +6 -0
  42. package/dist/types/src/cart/store.d.ts +5 -2
  43. package/dist/types/src/cart.d.ts +2 -2
  44. package/dist/types/src/client.d.ts +53 -19
  45. package/dist/types/src/config.d.ts +6 -1
  46. package/dist/types/src/index.d.ts +2 -2
  47. package/dist/types/src/product-shape.d.ts +3 -1
  48. package/package.json +1 -2
  49. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  50. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  51. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  52. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  53. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  54. package/dist/esm/chunks/search-UJ73YBQ5.js +0 -1
  55. package/src/account/addresses-view.ts +0 -58
  56. package/src/account/answers.ts +0 -79
  57. package/src/account/markup.ts +0 -36
  58. package/src/account/operations.ts +0 -145
  59. package/src/account/order-lines.ts +0 -75
  60. package/src/account/order-list.ts +0 -107
  61. package/src/account/order-page.ts +0 -137
  62. package/src/account/page-answers.ts +0 -317
  63. package/src/account/page-events.ts +0 -253
  64. package/src/account/server-client.ts +0 -183
  65. package/src/account/subscriptions-view.ts +0 -115
  66. package/src/account-api.ts +0 -98
  67. package/src/account-marks.ts +0 -35
  68. package/src/account-pages.ts +0 -81
  69. package/src/account.ts +0 -284
  70. package/src/amounts.ts +0 -64
  71. package/src/analytics-shopify.ts +0 -246
  72. package/src/analytics.ts +0 -315
  73. package/src/api-version.ts +0 -79
  74. package/src/astro.ts +0 -272
  75. package/src/cart/cart-shape.ts +0 -25
  76. package/src/cart/checkout.ts +0 -101
  77. package/src/cart/drawer.ts +0 -125
  78. package/src/cart/mutations.ts +0 -237
  79. package/src/cart/render.ts +0 -266
  80. package/src/cart/store.ts +0 -279
  81. package/src/cart.ts +0 -345
  82. package/src/client.ts +0 -391
  83. package/src/collection.ts +0 -175
  84. package/src/config-sections.ts +0 -284
  85. package/src/config.ts +0 -480
  86. package/src/controls/cart-controls.ts +0 -123
  87. package/src/controls/discount.ts +0 -75
  88. package/src/controls/extras.ts +0 -54
  89. package/src/controls/navigation.ts +0 -77
  90. package/src/diagnose-markup.ts +0 -298
  91. package/src/diagnose.ts +0 -294
  92. package/src/dom.ts +0 -268
  93. package/src/extras.ts +0 -136
  94. package/src/fragments.ts +0 -261
  95. package/src/globals.d.ts +0 -15
  96. package/src/images.ts +0 -91
  97. package/src/index.ts +0 -415
  98. package/src/initial.ts +0 -101
  99. package/src/lazy-iife.ts +0 -197
  100. package/src/lazy.ts +0 -42
  101. package/src/loader.ts +0 -363
  102. package/src/market-codes.ts +0 -6
  103. package/src/markets-ui.ts +0 -227
  104. package/src/markets.ts +0 -246
  105. package/src/markup.ts +0 -127
  106. package/src/mounting.ts +0 -264
  107. package/src/page-market.ts +0 -110
  108. package/src/part-integrity.ts +0 -7
  109. package/src/parts/account-pages.ts +0 -7
  110. package/src/parts/account.ts +0 -7
  111. package/src/parts/analytics.ts +0 -7
  112. package/src/parts/diagnose.ts +0 -7
  113. package/src/parts/extras.ts +0 -7
  114. package/src/parts/markets.ts +0 -7
  115. package/src/parts/search.ts +0 -7
  116. package/src/presets/barba.ts +0 -194
  117. package/src/product-events.ts +0 -115
  118. package/src/product-render.ts +0 -218
  119. package/src/product-shape.ts +0 -49
  120. package/src/product-state.ts +0 -318
  121. package/src/product.ts +0 -308
  122. package/src/repeaters.ts +0 -161
  123. package/src/search.ts +0 -305
  124. package/src/selling-plans.ts +0 -145
  125. package/src/server.ts +0 -211
  126. package/src/shopify-values.ts +0 -91
  127. package/src/status.ts +0 -106
  128. package/src/storage.ts +0 -34
  129. package/src/store-html.ts +0 -359
  130. package/src/storefront/catalog.ts +0 -196
  131. package/src/storefront/json-ld.ts +0 -144
  132. package/src/variant-render.ts +0 -139
@@ -1,317 +0,0 @@
1
- /**
2
- * The account pages' answers as typed values: addresses, the order page, subscriptions and what a change returns. The
3
- * rules are answers.ts's: every selected field is kept, a missing one reads as `null`, and the ids the pages act on
4
- * are required.
5
- */
6
- import {
7
- countOrZero,
8
- field,
9
- isRecord,
10
- listOf,
11
- nodesOf,
12
- numberOrNull,
13
- textList,
14
- textOrNull,
15
- } from '../../server/src/shared/json.ts';
16
- import { imageOrNull, moneyOrNull, toOrderLine } from '../../server/src/shared/account-orders.ts';
17
- import type { AccountImage, AccountOrderLine } from '../../server/src/shared/account-orders.ts';
18
- import type { Money } from '../shopify-values.ts';
19
- import { requiredId, unexpected } from './answers.ts';
20
-
21
- /** An address as the documents select it. */
22
- export interface MailingAddress {
23
- id: string | null;
24
- firstName: string | null;
25
- lastName: string | null;
26
- company: string | null;
27
- address1: string | null;
28
- address2: string | null;
29
- city: string | null;
30
- province: string | null;
31
- zoneCode: string | null;
32
- zip: string | null;
33
- country: string | null;
34
- countryCode: string | null;
35
- phoneNumber: string | null;
36
- /** One line per entry. */
37
- formatted: string[];
38
- }
39
- /** One of the customer's saved addresses. */
40
- export type AccountAddress = MailingAddress & { id: string };
41
- export type CustomerAddress = AccountAddress & { isDefault: boolean };
42
- interface ReturnReason {
43
- id: string;
44
- name: string | null;
45
- handle: string | null;
46
- }
47
- /** A line of the order page's document. */
48
- export type OrderDetailLine = AccountOrderLine & {
49
- id: string;
50
- name: string | null;
51
- sku: string | null;
52
- price: Money | null;
53
- totalPrice: Money | null;
54
- suggestedReturnReasonDefinitions: { nodes: ReturnReason[] };
55
- };
56
- interface Tracking {
57
- company: string | null;
58
- number: string | null;
59
- url: string | null;
60
- }
61
- export interface Fulfillment {
62
- id: string | null;
63
- status: string | null;
64
- createdAt: string | null;
65
- estimatedDeliveryAt: string | null;
66
- latestShipmentStatus: string | null;
67
- trackingInformation: Tracking[];
68
- }
69
- interface ReturnableLine {
70
- quantity: number;
71
- lineItem: { id: string } | null;
72
- }
73
- /** A return Shopify opened. */
74
- export interface OpenedReturn {
75
- id: string;
76
- status: string | null;
77
- }
78
- /** The order page's document. */
79
- export interface OrderDetail {
80
- id: string;
81
- name: string | null;
82
- number: number | null;
83
- processedAt: string | null;
84
- cancelledAt: string | null;
85
- financialStatus: string | null;
86
- fulfillmentStatus: string | null;
87
- statusPageUrl: string | null;
88
- requiresShipping: boolean | null;
89
- totalPrice: Money | null;
90
- subtotal: Money | null;
91
- totalShipping: Money | null;
92
- totalTax: Money | null;
93
- shippingAddress: MailingAddress | null;
94
- billingAddress: MailingAddress | null;
95
- lineItems: { nodes: OrderDetailLine[] };
96
- fulfillments: { nodes: Fulfillment[] };
97
- returnInformation: { returnableLineItems: { nodes: ReturnableLine[] } } | null;
98
- returns: { nodes: OpenedReturn[] };
99
- }
100
- export interface SubscriptionLine {
101
- id: string | null;
102
- name: string | null;
103
- title: string | null;
104
- variantTitle: string | null;
105
- quantity: number;
106
- sku: string | null;
107
- currentPrice: Money | null;
108
- lineDiscountedPrice: Money | null;
109
- image: AccountImage | null;
110
- }
111
- interface BillingCycle {
112
- cycleIndex: number | null;
113
- skipped: boolean;
114
- billingAttemptExpectedDate: string | null;
115
- }
116
- /** The subscriptions document. */
117
- export interface SubscriptionContract {
118
- id: string;
119
- /** `ACTIVE`, `PAUSED`, `CANCELLED`, `EXPIRED` or `FAILED`. */
120
- status: string | null;
121
- nextBillingDate: string | null;
122
- currencyCode: string | null;
123
- deliveryPrice: Money | null;
124
- deliveryPolicy: { interval: string | null; intervalCount: { count: number } | null } | null;
125
- lines: { nodes: SubscriptionLine[] };
126
- upcomingBillingCycles: { nodes: BillingCycle[] };
127
- }
128
- export interface Profile {
129
- firstName: string | null;
130
- lastName: string | null;
131
- }
132
- /** What a subscription action changed: the contract (pause, resume, cancel) or its next billing cycle (skip, unskip). */
133
- export type SubscriptionChange =
134
- | { contract: { id: string | null; status: string | null } }
135
- | { billingCycle: { cycleIndex: number | null; skipped: boolean } };
136
-
137
- function toMailingAddress(value: unknown): MailingAddress {
138
- const text = (name: string) => textOrNull(field(value, name));
139
- return {
140
- id: text('id'),
141
- firstName: text('firstName'),
142
- lastName: text('lastName'),
143
- company: text('company'),
144
- address1: text('address1'),
145
- address2: text('address2'),
146
- city: text('city'),
147
- province: text('province'),
148
- zoneCode: text('zoneCode'),
149
- zip: text('zip'),
150
- country: text('country'),
151
- countryCode: text('countryCode'),
152
- phoneNumber: text('phoneNumber'),
153
- formatted: textList(field(value, 'formatted')),
154
- };
155
- }
156
-
157
- const mailingAddressOrNull = (value: unknown): MailingAddress | null =>
158
- isRecord(value) ? toMailingAddress(value) : null;
159
-
160
- /** @throws {TetherError} Without an id. */
161
- export const toAccountAddress = (value: unknown): AccountAddress => ({
162
- ...toMailingAddress(value),
163
- id: requiredId(field(value, 'id'), 'address id'),
164
- });
165
-
166
- /**
167
- * @param value The addresses operation's list, each address with the `isDefault` it adds.
168
- * @throws {TetherError} For an address without an id.
169
- */
170
- export const toCustomerAddresses = (value: unknown): CustomerAddress[] =>
171
- listOf(value, (address) => ({ ...toAccountAddress(address), isDefault: field(address, 'isDefault') === true }));
172
-
173
- const toOrderDetailLine = (value: unknown): OrderDetailLine => ({
174
- ...toOrderLine(value),
175
- id: requiredId(field(value, 'id'), 'line item id'),
176
- name: textOrNull(field(value, 'name')),
177
- sku: textOrNull(field(value, 'sku')),
178
- price: moneyOrNull(field(value, 'price')),
179
- totalPrice: moneyOrNull(field(value, 'totalPrice')),
180
- suggestedReturnReasonDefinitions: nodesOf(field(value, 'suggestedReturnReasonDefinitions'), (reason) => ({
181
- id: requiredId(field(reason, 'id'), 'return reason id'),
182
- name: textOrNull(field(reason, 'name')),
183
- handle: textOrNull(field(reason, 'handle')),
184
- })),
185
- });
186
-
187
- const toFulfillment = (value: unknown): Fulfillment => ({
188
- id: textOrNull(field(value, 'id')),
189
- status: textOrNull(field(value, 'status')),
190
- createdAt: textOrNull(field(value, 'createdAt')),
191
- estimatedDeliveryAt: textOrNull(field(value, 'estimatedDeliveryAt')),
192
- latestShipmentStatus: textOrNull(field(value, 'latestShipmentStatus')),
193
- trackingInformation: listOf(field(value, 'trackingInformation'), (tracking) => ({
194
- company: textOrNull(field(tracking, 'company')),
195
- number: textOrNull(field(tracking, 'number')),
196
- url: textOrNull(field(tracking, 'url')),
197
- })),
198
- });
199
-
200
- function toReturnableLine(value: unknown): ReturnableLine {
201
- const lineId = textOrNull(field(field(value, 'lineItem'), 'id'));
202
- return { quantity: countOrZero(field(value, 'quantity')), lineItem: lineId === null ? null : { id: lineId } };
203
- }
204
-
205
- function toReturnInformation(value: unknown): OrderDetail['returnInformation'] {
206
- if (!isRecord(value)) return null;
207
- return { returnableLineItems: nodesOf(value['returnableLineItems'], toReturnableLine) };
208
- }
209
-
210
- /**
211
- * @param value The order operation's `order`.
212
- * @returns `null` when the customer has no such order.
213
- * @throws {TetherError} For an order, line, reason or return without an id.
214
- */
215
- export function toOrderDetail(value: unknown): OrderDetail | null {
216
- if (!isRecord(value)) return null;
217
- const text = (name: string) => textOrNull(value[name]);
218
- const money = (name: string) => moneyOrNull(value[name]);
219
- const requiresShipping = value['requiresShipping'];
220
- return {
221
- id: requiredId(value['id'], 'order id'),
222
- name: text('name'),
223
- number: numberOrNull(value['number']),
224
- processedAt: text('processedAt'),
225
- cancelledAt: text('cancelledAt'),
226
- financialStatus: text('financialStatus'),
227
- fulfillmentStatus: text('fulfillmentStatus'),
228
- statusPageUrl: text('statusPageUrl'),
229
- requiresShipping: typeof requiresShipping === 'boolean' ? requiresShipping : null,
230
- totalPrice: money('totalPrice'),
231
- subtotal: money('subtotal'),
232
- totalShipping: money('totalShipping'),
233
- totalTax: money('totalTax'),
234
- shippingAddress: mailingAddressOrNull(value['shippingAddress']),
235
- billingAddress: mailingAddressOrNull(value['billingAddress']),
236
- lineItems: nodesOf(value['lineItems'], toOrderDetailLine),
237
- fulfillments: nodesOf(value['fulfillments'], toFulfillment),
238
- returnInformation: toReturnInformation(value['returnInformation']),
239
- returns: nodesOf(value['returns'], toReturn),
240
- };
241
- }
242
-
243
- const toSubscriptionLine = (value: unknown): SubscriptionLine => ({
244
- id: textOrNull(field(value, 'id')),
245
- name: textOrNull(field(value, 'name')),
246
- title: textOrNull(field(value, 'title')),
247
- variantTitle: textOrNull(field(value, 'variantTitle')),
248
- quantity: countOrZero(field(value, 'quantity')),
249
- sku: textOrNull(field(value, 'sku')),
250
- currentPrice: moneyOrNull(field(value, 'currentPrice')),
251
- lineDiscountedPrice: moneyOrNull(field(value, 'lineDiscountedPrice')),
252
- image: imageOrNull(field(value, 'image')),
253
- });
254
-
255
- const toBillingCycle = (value: unknown): BillingCycle => ({
256
- cycleIndex: numberOrNull(field(value, 'cycleIndex')),
257
- skipped: field(value, 'skipped') === true,
258
- billingAttemptExpectedDate: textOrNull(field(value, 'billingAttemptExpectedDate')),
259
- });
260
-
261
- function toDeliveryPolicy(value: unknown): SubscriptionContract['deliveryPolicy'] {
262
- if (!isRecord(value)) return null;
263
- const intervalCount = value['intervalCount'];
264
- return {
265
- interval: textOrNull(value['interval']),
266
- intervalCount: isRecord(intervalCount) ? { count: countOrZero(intervalCount['count']) } : null,
267
- };
268
- }
269
-
270
- /** @throws {TetherError} Without an id. */
271
- const toSubscriptionContract = (value: unknown): SubscriptionContract => ({
272
- id: requiredId(field(value, 'id'), 'subscription id'),
273
- status: textOrNull(field(value, 'status')),
274
- nextBillingDate: textOrNull(field(value, 'nextBillingDate')),
275
- currencyCode: textOrNull(field(value, 'currencyCode')),
276
- deliveryPrice: moneyOrNull(field(value, 'deliveryPrice')),
277
- deliveryPolicy: toDeliveryPolicy(field(value, 'deliveryPolicy')),
278
- lines: nodesOf(field(value, 'lines'), toSubscriptionLine),
279
- upcomingBillingCycles: nodesOf(field(value, 'upcomingBillingCycles'), toBillingCycle),
280
- });
281
-
282
- /**
283
- * @param value The subscriptions operation's list.
284
- * @throws {TetherError} For a contract without an id.
285
- */
286
- export const toSubscriptionContracts = (value: unknown): SubscriptionContract[] =>
287
- listOf(value, toSubscriptionContract);
288
-
289
- /**
290
- * @param result The subscription operation's result.
291
- * @throws {TetherError} With neither a contract nor a billing cycle.
292
- */
293
- export function toSubscriptionChange(result: Record<string, unknown>): SubscriptionChange {
294
- const { contract, billingCycle } = result;
295
- if (isRecord(contract)) {
296
- return { contract: { id: textOrNull(contract['id']), status: textOrNull(contract['status']) } };
297
- }
298
- if (isRecord(billingCycle)) {
299
- const { cycleIndex, skipped } = toBillingCycle(billingCycle);
300
- return { billingCycle: { cycleIndex, skipped } };
301
- }
302
- throw unexpected('changed subscription');
303
- }
304
-
305
- /** @throws {TetherError} Without an id. */
306
- export function toReturn(value: unknown): OpenedReturn {
307
- return { id: requiredId(field(value, 'id'), 'return id'), status: textOrNull(field(value, 'status')) };
308
- }
309
-
310
- /** @throws {TetherError} Without an id. */
311
- export const toDeletedId = (value: unknown): string => requiredId(value, 'deleted address id');
312
-
313
- /** @param value The profile operation's `customer`. */
314
- export const toProfile = (value: unknown): Profile => ({
315
- firstName: textOrNull(field(value, 'firstName')),
316
- lastName: textOrNull(field(value, 'lastName')),
317
- });
@@ -1,253 +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.ts';
7
- import {
8
- saveAddress,
9
- deleteAddress,
10
- subscriptionAction,
11
- requestReturn,
12
- updateProfile,
13
- pagesContext,
14
- } from './operations.ts';
15
- import type { ReturnLine } from './operations.ts';
16
- import { renderAddresses, shownAddress } from './addresses-view.ts';
17
- import { renderSubscriptions } from './subscriptions-view.ts';
18
- import { renderOrder } from './order-page.ts';
19
- import { TetherError } from '../client.ts';
20
- import type { SubscriptionAction } from '../../server/src/shared/account-routes.ts';
21
-
22
- const SUBSCRIPTION_ACTIONS: readonly SubscriptionAction[] = ['pause', 'resume', 'skip', 'unskip', 'cancel'];
23
-
24
- const isSubscriptionAction = (value: string | null): value is SubscriptionAction =>
25
- SUBSCRIPTION_ACTIONS.some((action) => action === value);
26
-
27
- const isField = (el: Element): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement =>
28
- el instanceof window.HTMLInputElement ||
29
- el instanceof window.HTMLSelectElement ||
30
- el instanceof window.HTMLTextAreaElement;
31
-
32
- const isForm = (el: Element): el is HTMLFormElement => el instanceof window.HTMLFormElement;
33
-
34
- /** @returns The form's text fields by name. */
35
- function fieldsOf(form: HTMLFormElement): Record<string, string> {
36
- const fields: Record<string, string> = {};
37
- for (const field of form.elements) {
38
- if (!isField(field)) continue;
39
- if (!field.name || field.type === 'submit' || field.type === 'button' || field.type === 'checkbox') continue;
40
- fields[field.name] = field.value;
41
- }
42
- return fields;
43
- }
44
-
45
- /** @returns What to tell the customer: Shopify's own messages when it refused the input. */
46
- function customerMessage(error: unknown): string {
47
- const userErrors = error instanceof TetherError ? (error.userErrors ?? []) : [];
48
- const message = userErrors.map((userError) => userError.message).join(' ');
49
- if (message) return message;
50
- const text = error instanceof Error && error.message ? error.message : 'Something went wrong';
51
- return text.replace(/^Customer accounts: /, '');
52
- }
53
-
54
- /**
55
- * Runs one change for `el`; a second one while it is on its way (Enter, a double click) is ignored. The failure is
56
- * shown in `el` (`tether-form-error`), the place the customer looks.
57
- */
58
- async function act(el: HTMLElement, change: () => Promise<void>): Promise<'saved' | 'error' | 'busy'> {
59
- if (el.getAttribute('aria-busy') === 'true') return 'busy';
60
- el.classList.add('is-busy');
61
- el.setAttribute('aria-busy', 'true');
62
- el.setAttribute(attr('form-state'), 'pending');
63
- const errorBoxes = qsa(el, sel('form-error'));
64
- errorBoxes.forEach((box) => {
65
- setText(box, '');
66
- });
67
- try {
68
- await change();
69
- el.setAttribute(attr('form-state'), 'saved');
70
- return 'saved';
71
- } catch (error) {
72
- el.setAttribute(attr('form-state'), 'error');
73
- errorBoxes.forEach((box) => {
74
- setText(box, customerMessage(error));
75
- });
76
- return 'error';
77
- } finally {
78
- el.classList.remove('is-busy');
79
- el.removeAttribute('aria-busy');
80
- }
81
- }
82
-
83
- /** @returns True when the button asks no `tether-confirm` question, or the visitor agreed. */
84
- const confirmed = (button: Element): boolean => {
85
- const question = get(button, 'confirm');
86
- return !question || window.confirm(question);
87
- };
88
-
89
- const addressFormReset = (form: HTMLFormElement): void => {
90
- form.reset();
91
- delete form.dataset['tetherAddressId'];
92
- };
93
-
94
- function onSubmit(event: SubmitEvent): void {
95
- const form = event.target;
96
- if (!(form instanceof window.HTMLFormElement)) return;
97
- if (form.matches(sel('profile-form'))) {
98
- event.preventDefault();
99
- void act(form, async () => {
100
- await updateProfile(fieldsOf(form));
101
- await pagesContext().bindCustomer(document);
102
- });
103
- } else if (form.matches(sel('address-form'))) {
104
- event.preventDefault();
105
- const id = form.dataset['tetherAddressId'];
106
- const box = form.elements.namedItem('isDefault');
107
- const isDefault = box instanceof window.HTMLInputElement && box.checked;
108
- void act(form, async () => {
109
- await saveAddress(fieldsOf(form), { id, isDefault });
110
- addressFormReset(form);
111
- await renderAddresses(document);
112
- });
113
- }
114
- }
115
-
116
- /** Fills the address form with a shown address, to edit it. */
117
- function editAddress(id: string): void {
118
- const address = shownAddress(id);
119
- const form = document.querySelector(sel('address-form'));
120
- if (!address || !form || !isForm(form)) return;
121
- form.dataset['tetherAddressId'] = address.id;
122
- const valuesByName: Readonly<Record<string, unknown>> = { ...address };
123
- for (const field of form.elements) {
124
- if (!isField(field)) continue;
125
- if (field.name === 'isDefault') {
126
- if (field instanceof window.HTMLInputElement) field.checked = address.isDefault;
127
- } else if (field.name && Object.hasOwn(valuesByName, field.name)) {
128
- const value = valuesByName[field.name];
129
- field.value = typeof value === 'string' ? value : '';
130
- }
131
- }
132
- form.scrollIntoView({ block: 'nearest' });
133
- }
134
-
135
- /** @returns True when the click was on an address button. */
136
- function onAddressClick(event: MouseEvent, target: Element): boolean {
137
- const row = target.closest('[data-tether-address]');
138
- const id = row instanceof window.HTMLElement ? row.dataset['tetherAddress'] : undefined;
139
- if (target.closest(sel('address-new'))) {
140
- const form = document.querySelector(sel('address-form'));
141
- if (form && isForm(form)) addressFormReset(form);
142
- return true;
143
- }
144
- if (!(row instanceof window.HTMLElement) || !id) return false;
145
- const deleteButton = target.closest(sel('address-delete'));
146
- if (deleteButton) {
147
- event.preventDefault();
148
- if (!confirmed(deleteButton)) return true;
149
- void act(row, async () => {
150
- await deleteAddress(id);
151
- await renderAddresses(document);
152
- });
153
- } else if (target.closest(sel('address-default'))) {
154
- event.preventDefault();
155
- void act(row, async () => {
156
- await saveAddress(null, { id, isDefault: true });
157
- await renderAddresses(document);
158
- });
159
- } else if (target.closest(sel('address-edit'))) {
160
- event.preventDefault();
161
- editAddress(id);
162
- } else {
163
- return false;
164
- }
165
- return true;
166
- }
167
-
168
- /** @param button A `tether-subscription-action` button. */
169
- function onSubscriptionAction(button: HTMLElement): void {
170
- const row = button.closest('[data-tether-subscription]');
171
- const action = get(button, 'subscription-action');
172
- const disabled = ('disabled' in button && button.disabled === true) || button.classList.contains('is-disabled');
173
- if (!(row instanceof window.HTMLElement) || !isSubscriptionAction(action) || disabled || !confirmed(button)) return;
174
- const id = row.dataset['tetherSubscription'] ?? '';
175
- row.setAttribute(attr('subscription-state'), 'pending');
176
- void act(row, async () => {
177
- await subscriptionAction(id, action);
178
- await renderSubscriptions(document);
179
- }).then((outcome) => {
180
- const state = outcome === 'error' ? 'error' : 'done';
181
- // The re-render may have replaced the row: mark the one that shows this subscription now.
182
- qsa(document, '[data-tether-subscription]')
183
- .filter((candidate) => candidate === row || candidate.dataset['tetherSubscription'] === id)
184
- .forEach((candidate) => {
185
- candidate.setAttribute(attr('subscription-state'), state);
186
- });
187
- });
188
- }
189
-
190
- /** @param row A line row of the order page. */
191
- function returnLineOf(row: HTMLElement): ReturnLine {
192
- const quantity = row.querySelector(sel('return-quantity'));
193
- const reason = row.querySelector(sel('return-reason'));
194
- const note = row.querySelector(sel('return-note'));
195
- const valueOf = (field: Element | null) =>
196
- field && 'value' in field && typeof field.value === 'string' ? field.value : '';
197
- const line: ReturnLine = {
198
- lineItemId: row.dataset['tetherLine'] ?? '',
199
- quantity: Number.parseInt(valueOf(quantity) || '0', 10),
200
- };
201
- const reasonId = valueOf(reason);
202
- if (reasonId) line.reasonId = reasonId;
203
- const noteText = valueOf(note);
204
- if (noteText) line.note = noteText;
205
- return line;
206
- }
207
-
208
- /** @param button A `tether-order-return` button. */
209
- function onReturnRequest(button: HTMLElement): void {
210
- const scope = button.closest(sel('order'));
211
- if (!(scope instanceof window.HTMLElement)) return;
212
- const orderId = scope.dataset['tetherOrderId'];
213
- if (!orderId) return;
214
- const lines = qsa(scope, '[data-tether-line]')
215
- .map(returnLineOf)
216
- .filter((line) => line.quantity > 0);
217
- if (!lines.length || !confirmed(button)) return;
218
- void act(scope, async () => {
219
- await requestReturn(orderId, lines);
220
- scope.dataset['tetherReturned'] = '1';
221
- await renderOrder(scope);
222
- });
223
- }
224
-
225
- function onClick(event: MouseEvent): void {
226
- const target = event.target;
227
- if (!(target instanceof window.Element)) return;
228
- if (onAddressClick(event, target)) return;
229
- const subscriptionButton = target.closest(sel('subscription-action'));
230
- if (subscriptionButton instanceof window.HTMLElement) {
231
- event.preventDefault();
232
- onSubscriptionAction(subscriptionButton);
233
- return;
234
- }
235
- const returnButton = target.closest(sel('order-return'));
236
- if (returnButton instanceof window.HTMLElement) {
237
- event.preventDefault();
238
- onReturnRequest(returnButton);
239
- }
240
- }
241
-
242
- /** Listens on the document once, for every account page rendered into it. */
243
- export function listenToPages(): void {
244
- // Capture: before a router (Astro's ClientRouter) turns the submit into a navigation with the fields in the URL.
245
- document.addEventListener('submit', onSubmit, true);
246
- document.addEventListener('input', (event) => {
247
- const target = event.target;
248
- if (target instanceof window.HTMLElement && target.closest(sel('profile-form'))) {
249
- target.dataset['tetherTouched'] = '1';
250
- }
251
- });
252
- document.addEventListener('click', onClick);
253
- }