@redotech/redo-hydrogen 1.4.6 → 2.0.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.
package/src/utils/cart.ts CHANGED
@@ -1,20 +1,23 @@
1
- import { FetcherWithComponents, useFetcher } from "@remix-run/react";
1
+ import { FetcherWithComponents, useFetcher } from "react-router";
2
2
  import { CartInfoToEnable } from "../types";
3
- import { CartForm, CartReturn, OptimisticCart, OptimisticCartLine } from "@shopify/hydrogen";
4
- import type { AppData } from '@remix-run/react/dist/data';
5
- import React, { useCallback, useEffect, useRef } from 'react'
3
+ import { CartForm, CartReturn, OptimisticCart } from "@shopify/hydrogen";
4
+ import React, { useCallback, useEffect, useRef } from "react";
6
5
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
7
6
  import { CartLine, ComponentizableCartLine } from "@shopify/hydrogen-react/storefront-api-types";
8
7
 
9
- const DEFAULT_REDO_ENABLED_CART_ATTRIBUTE = 'redo_opted_in_from_cart';
8
+ const DEFAULT_REDO_ENABLED_CART_ATTRIBUTE = "redo_opted_in_from_cart";
10
9
  const CONCIERGE_ATTRIBUTION_CART_ATTRIBUTE_KEY = "redo.conciergeAssisted";
11
10
  const CONCIERGE_CONVERSATION_IDS_STORAGE_KEY = "redoConciergeConversationIds";
12
11
 
13
- const isCartWithActionsDocs = (cart: CartReturn | CartWithActionsDocs| OptimisticCart): cart is CartWithActionsDocs => {
14
- return (Array.isArray(cart.lines) && 'linesAdd' in cart && typeof cart.linesAdd === 'function');
15
- }
12
+ const isCartWithActionsDocs = (
13
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart,
14
+ ): cart is CartWithActionsDocs => {
15
+ return Array.isArray(cart.lines) && "linesAdd" in cart && typeof cart.linesAdd === "function";
16
+ };
16
17
 
17
- const getCartLines = (cart: CartReturn | CartWithActionsDocs | OptimisticCart): Array<CartLine | ComponentizableCartLine> => {
18
+ const getCartLines = (
19
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart,
20
+ ): Array<CartLine | ComponentizableCartLine> => {
18
21
  if (isOptimisticCart(cart)) {
19
22
  return cart.lines.nodes;
20
23
  } else if (isCartWithActionsDocs(cart)) {
@@ -22,85 +25,51 @@ const getCartLines = (cart: CartReturn | CartWithActionsDocs | OptimisticCart):
22
25
  } else {
23
26
  return cart.lines.nodes ?? cart.lines.edges.map((edge) => edge.node);
24
27
  }
25
- }
28
+ };
26
29
 
27
30
  // https://shopify.dev/docs/api/hydrogen/2025-01/hooks/useoptimisticcart
28
31
  const isOptimisticCart = (cart: CartReturn | CartWithActionsDocs | OptimisticCart): cart is OptimisticCart => {
29
- return 'isOptimistic' in cart && (cart.isOptimistic ?? false);
30
- }
31
-
32
- const isRedoInCart = ({
33
- cart
34
- }: {
35
- cart: CartReturn | CartWithActionsDocs | OptimisticCart
36
- }): boolean => {
37
- if(!cart) {
38
- return false;
39
- }
40
-
41
- return getCartLines(cart).some((cartLine) => {
42
- return cartLine.merchandise.product.vendor === 're:do';
43
- });
44
- }
45
-
46
- const waitForConditionsMetOrTimeout = ({
47
- conditions,
48
- timeoutMs
49
- }: {
50
- conditions: (() => boolean)[];
51
- timeoutMs: number;
52
- }): Promise<boolean> => {
53
- return new Promise((resolve, reject) => {
54
- let start = Date.now();
55
- let interval = setInterval(() => {
56
- if((Date.now() - start) > timeoutMs) {
57
- clearInterval(interval);
58
- return resolve(false);
59
- }
60
-
61
- let conditionsMet = conditions.every((conditionCallback) => conditionCallback());
62
-
63
- if(conditionsMet) {
64
- clearInterval(interval);
65
- return resolve(true);
66
- }
67
- }, 100);
68
- })
69
- }
32
+ return "isOptimistic" in cart && (cart.isOptimistic ?? false);
33
+ };
70
34
 
71
35
  const addProductToCartIfNeeded = async ({
72
36
  cart,
73
37
  fetcher,
74
38
  waitCartIdle,
75
- cartInfoToEnable
39
+ cartInfoToEnable,
76
40
  }: {
77
- cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
78
- fetcher: FetcherWithComponents<unknown>,
41
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
42
+ fetcher: FetcherWithComponents<unknown>;
79
43
  waitCartIdle: WaitCartIdleCallback;
80
- cartInfoToEnable: CartInfoToEnable
44
+ cartInfoToEnable: CartInfoToEnable;
81
45
  }) => {
82
- if(!cart) {
46
+ if (!cart) {
83
47
  return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
84
48
  }
85
49
 
86
50
  const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
87
- return cartLine.merchandise.product.vendor === 're:do';
51
+ return cartLine.merchandise.product.vendor === "re:do";
88
52
  });
89
53
  const correctRedoProductInCart = redoProductsInCart?.filter((cartLine) => {
90
54
  return cartLine.merchandise.id === `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`;
91
55
  });
92
- if(redoProductsInCart.length === 0) {
56
+ if (redoProductsInCart.length === 0) {
93
57
  return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
94
- } else if (redoProductsInCart.length === 1 && correctRedoProductInCart.length === 1 && correctRedoProductInCart[0].quantity === 1) {
58
+ } else if (
59
+ redoProductsInCart.length === 1 &&
60
+ correctRedoProductInCart.length === 1 &&
61
+ correctRedoProductInCart[0].quantity === 1
62
+ ) {
95
63
  // No action needed
96
64
  return;
97
65
  } else {
98
- let isSuccess = true;
99
-
100
- await removeLinesFromCart({ cart, fetcher, waitCartIdle, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
66
+ await removeLinesFromCart({
67
+ cart,
68
+ fetcher,
69
+ waitCartIdle,
70
+ lineIds: redoProductsInCart.map((cartLine) => cartLine.id),
71
+ });
101
72
  await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
102
-
103
- return;
104
73
  }
105
74
  };
106
75
 
@@ -108,7 +77,7 @@ const removeLinesFromCart = async ({
108
77
  cart,
109
78
  fetcher,
110
79
  waitCartIdle,
111
- lineIds
80
+ lineIds,
112
81
  }: {
113
82
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
114
83
  fetcher: FetcherWithComponents<unknown>;
@@ -118,11 +87,11 @@ const removeLinesFromCart = async ({
118
87
  const formInput = {
119
88
  action: CartForm.ACTIONS.LinesRemove,
120
89
  inputs: {
121
- lineIds
122
- }
123
- }
90
+ lineIds,
91
+ },
92
+ };
124
93
 
125
- if(cart && isCartWithActionsDocs(cart)) {
94
+ if (cart && isCartWithActionsDocs(cart)) {
126
95
  cart.linesRemove(lineIds);
127
96
  await waitCartIdle();
128
97
  } else {
@@ -130,7 +99,7 @@ const removeLinesFromCart = async ({
130
99
  {
131
100
  [CartForm.INPUT_NAME]: JSON.stringify(formInput),
132
101
  },
133
- {method: 'POST', action: '/cart'},
102
+ { method: "POST", action: "/cart" },
134
103
  );
135
104
  }
136
105
  };
@@ -139,25 +108,28 @@ const removeProductFromCartIfNeeded = async ({
139
108
  cart,
140
109
  fetcher,
141
110
  waitCartIdle,
142
- cartInfoToEnable
143
111
  }: {
144
- cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
145
- fetcher: FetcherWithComponents<unknown>,
146
- waitCartIdle: WaitCartIdleCallback
147
- cartInfoToEnable: CartInfoToEnable
112
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
113
+ fetcher: FetcherWithComponents<unknown>;
114
+ waitCartIdle: WaitCartIdleCallback;
115
+ cartInfoToEnable: CartInfoToEnable;
148
116
  }) => {
149
- if(!cart) {
150
- console.error('No cart');
117
+ if (!cart) {
118
+ console.error("No cart");
151
119
  return;
152
120
  }
153
121
 
154
122
  const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
155
- return cartLine.merchandise.product.vendor === 're:do';
123
+ return cartLine.merchandise.product.vendor === "re:do";
156
124
  });
157
125
 
158
- if(redoProductsInCart.length !== 0) {
159
- await removeLinesFromCart({ cart, fetcher, waitCartIdle, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
160
- } else {
126
+ if (redoProductsInCart.length !== 0) {
127
+ await removeLinesFromCart({
128
+ cart,
129
+ fetcher,
130
+ waitCartIdle,
131
+ lineIds: redoProductsInCart.map((cartLine) => cartLine.id),
132
+ });
161
133
  }
162
134
  };
163
135
 
@@ -168,25 +140,23 @@ const addProductToCart = async ({
168
140
  cartInfoToEnable,
169
141
  }: {
170
142
  waitCartIdle: WaitCartIdleCallback;
171
- cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
172
- fetcher: FetcherWithComponents<unknown>,
173
- cartInfoToEnable: CartInfoToEnable
143
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
144
+ fetcher: FetcherWithComponents<unknown>;
145
+ cartInfoToEnable: CartInfoToEnable;
174
146
  }) => {
175
147
  const redoProductLine = {
176
- "merchandiseId": `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`,
177
- "quantity": 1,
148
+ merchandiseId: `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`,
149
+ quantity: 1,
178
150
  };
179
151
 
180
152
  const formInput = {
181
153
  action: CartForm.ACTIONS.LinesAdd,
182
154
  inputs: {
183
- lines: [
184
- redoProductLine
185
- ]
186
- }
187
- }
155
+ lines: [redoProductLine],
156
+ },
157
+ };
188
158
 
189
- if(cart && isCartWithActionsDocs(cart)) {
159
+ if (cart && isCartWithActionsDocs(cart)) {
190
160
  cart.linesAdd([redoProductLine]);
191
161
  await waitCartIdle();
192
162
  } else {
@@ -194,7 +164,7 @@ const addProductToCart = async ({
194
164
  {
195
165
  [CartForm.INPUT_NAME]: JSON.stringify(formInput),
196
166
  },
197
- {method: 'POST', action: '/cart'},
167
+ { method: "POST", action: "/cart" },
198
168
  );
199
169
  }
200
170
  };
@@ -211,8 +181,7 @@ function getConciergeConversationIdsFromStorage(): string[] | null {
211
181
  return null;
212
182
  }
213
183
 
214
- const conversationIdsWithExpiry: ConversationIdWithExpiry[] =
215
- JSON.parse(stored);
184
+ const conversationIdsWithExpiry: ConversationIdWithExpiry[] = JSON.parse(stored);
216
185
  const now = Date.now();
217
186
 
218
187
  const validConversationIds = conversationIdsWithExpiry
@@ -220,7 +189,7 @@ function getConciergeConversationIdsFromStorage(): string[] | null {
220
189
  .map((item) => item.conversationId);
221
190
 
222
191
  return validConversationIds.length > 0 ? validConversationIds : null;
223
- } catch (error) {
192
+ } catch (_error) {
224
193
  return null;
225
194
  }
226
195
  }
@@ -230,7 +199,7 @@ const setCartRedoEnabledAttribute = async ({
230
199
  fetcher,
231
200
  waitCartIdle,
232
201
  cartInfoToEnable,
233
- enabled
202
+ enabled,
234
203
  }: {
235
204
  cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
236
205
  fetcher: FetcherWithComponents<unknown>;
@@ -240,15 +209,13 @@ const setCartRedoEnabledAttribute = async ({
240
209
  }) => {
241
210
  const redoCartAttribute = {
242
211
  key: cartInfoToEnable?.cartAttribute || DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
243
- value: enabled.toString()
212
+ value: enabled.toString(),
244
213
  };
245
214
 
246
215
  const existingAttributes = cart?.attributes || [];
247
-
248
- const existingAttributesMap = new Map(
249
- existingAttributes.map(attr => [attr.key, attr.value])
250
- );
251
-
216
+
217
+ const existingAttributesMap = new Map(existingAttributes.map((attr) => [attr.key, attr.value]));
218
+
252
219
  existingAttributesMap.set(redoCartAttribute.key, redoCartAttribute.value);
253
220
  const conciergeConversationIds = getConciergeConversationIdsFromStorage();
254
221
  if (conciergeConversationIds && conciergeConversationIds.length > 0) {
@@ -256,23 +223,23 @@ const setCartRedoEnabledAttribute = async ({
256
223
  CONCIERGE_ATTRIBUTION_CART_ATTRIBUTE_KEY,
257
224
  JSON.stringify({
258
225
  conciergeConversationIds: conciergeConversationIds,
259
- })
226
+ }),
260
227
  );
261
228
  }
262
229
 
263
230
  const updatedAttributes = Array.from(existingAttributesMap.entries()).map(([key, value]) => ({
264
231
  key,
265
- value: value ?? ""
232
+ value: value ?? "",
266
233
  }));
267
234
 
268
235
  const formInput = {
269
236
  action: CartForm.ACTIONS.AttributesUpdateInput,
270
237
  inputs: {
271
- attributes: updatedAttributes
272
- }
273
- }
238
+ attributes: updatedAttributes,
239
+ },
240
+ };
274
241
 
275
- if(cart && isCartWithActionsDocs(cart)) {
242
+ if (cart && isCartWithActionsDocs(cart)) {
276
243
  cart.cartAttributesUpdate(updatedAttributes);
277
244
  await waitCartIdle();
278
245
  } else {
@@ -280,49 +247,49 @@ const setCartRedoEnabledAttribute = async ({
280
247
  {
281
248
  [CartForm.INPUT_NAME]: JSON.stringify(formInput),
282
249
  },
283
- {method: 'POST', action: '/cart'},
250
+ { method: "POST", action: "/cart" },
284
251
  );
285
252
  }
286
253
  };
287
254
 
288
- type FetcherData<T> = NonNullable<T | unknown> // FIXME: used to use SerializeFrom which is deprecated. Can this be better typed?
289
- type ResolveFunction<T> = (value: FetcherData<T>) => void
255
+ type FetcherData<T> = NonNullable<T | unknown>; // FIXME: used to use SerializeFrom which is deprecated. Can this be better typed?
256
+ type ResolveFunction<T> = (value: FetcherData<T>) => void;
290
257
 
291
- function useFetcherWithPromise<TData = AppData>(opts?: Parameters<typeof useFetcher>[0]) {
292
- const fetcher = useFetcher<TData>(opts)
293
- const resolveRef = React.useRef<ResolveFunction<TData>>(null)
294
- const promiseRef = React.useRef<Promise<FetcherData<TData>>>(null)
258
+ function useFetcherWithPromise<TData = unknown>(opts?: Parameters<typeof useFetcher>[0]) {
259
+ const fetcher = useFetcher<TData>(opts);
260
+ const resolveRef = React.useRef<ResolveFunction<TData> | null>(null);
261
+ const promiseRef = React.useRef<Promise<FetcherData<TData>> | null>(null);
295
262
 
296
263
  if (!promiseRef.current) {
297
264
  promiseRef.current = new Promise<FetcherData<TData>>((resolve) => {
298
- resolveRef.current = resolve
299
- })
265
+ resolveRef.current = resolve;
266
+ });
300
267
  }
301
268
 
302
269
  const resetResolver = React.useCallback(() => {
303
270
  promiseRef.current = new Promise((resolve) => {
304
- resolveRef.current = resolve
305
- })
306
- }, [promiseRef, resolveRef])
271
+ resolveRef.current = resolve;
272
+ });
273
+ }, [promiseRef, resolveRef]);
307
274
 
308
275
  const submit = React.useCallback(
309
- async (...args: Parameters<typeof fetcher.submit>) => {
276
+ async (...args: Parameters<typeof fetcher.submit>): Promise<void> => {
310
277
  fetcher.submit(...args);
311
- return promiseRef.current
278
+ await promiseRef.current;
312
279
  },
313
- [fetcher, promiseRef]
314
- )
280
+ [fetcher, promiseRef],
281
+ );
315
282
 
316
283
  React.useEffect(() => {
317
- if (fetcher.state === 'idle') {
284
+ if (fetcher.state === "idle") {
318
285
  if (fetcher.data) {
319
- resolveRef.current?.(fetcher.data)
286
+ resolveRef.current?.(fetcher.data);
320
287
  }
321
- resetResolver()
288
+ resetResolver();
322
289
  }
323
- }, [fetcher, resetResolver])
290
+ }, [fetcher, resetResolver]);
324
291
 
325
- return { ...fetcher, submit }
292
+ return { ...fetcher, submit };
326
293
  }
327
294
 
328
295
  type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs | OptimisticCart>;
@@ -330,49 +297,46 @@ type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs | Opt
330
297
  // This function allows us to await a cart idle state without breaking React rules.
331
298
  // It returns a function, which returns a promise, which will resolve once the cart value passed in reaches an idle state.
332
299
  // Not intended for use with CartReturn, but will accept that value if passed in to avoid breaking rules of hooks
333
- const useWaitCartIdle = (cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined) => {
334
- const resolveRef = useRef<any>(null)
335
- const promiseRef = useRef<any>(null)
300
+ type CartUnion = CartReturn | CartWithActionsDocs | OptimisticCart;
301
+
302
+ const useWaitCartIdle = (cart: CartUnion | undefined) => {
303
+ const resolveRef = useRef<((value: CartUnion) => void) | null>(null);
304
+ const promiseRef = useRef<Promise<CartUnion>>(null!);
336
305
 
337
306
  if (!promiseRef.current) {
338
307
  promiseRef.current = new Promise<CartReturn | CartWithActionsDocs | OptimisticCart>((resolve) => {
339
- resolveRef.current = resolve
340
- })
308
+ resolveRef.current = resolve;
309
+ });
341
310
  }
342
311
 
343
312
  const resetResolver = useCallback(() => {
344
313
  promiseRef.current = new Promise((resolve) => {
345
- resolveRef.current = resolve
346
- })
314
+ resolveRef.current = resolve;
315
+ });
347
316
  }, [promiseRef, resolveRef]);
348
317
 
349
- const waitCartIdle = useCallback(
350
- async () => {
351
- return promiseRef.current
352
- },
353
- [cart, promiseRef]
354
- )
318
+ const waitCartIdle = useCallback(async () => {
319
+ return promiseRef.current;
320
+ }, [cart, promiseRef]);
355
321
 
356
322
  useEffect(() => {
357
- if(!cart) {
323
+ if (!cart) {
358
324
  return;
359
325
  }
360
- if(!isCartWithActionsDocs(cart)) {
326
+ if (!isCartWithActionsDocs(cart)) {
361
327
  // Wrong type of cart. Just resolve.
362
328
  resolveRef.current?.(cart);
363
329
  resetResolver();
364
- } else if(cart.status === 'idle') {
365
- resolveRef.current?.(cart)
330
+ } else if (cart.status === "idle") {
331
+ resolveRef.current?.(cart);
366
332
  resetResolver();
367
333
  }
368
334
  }, [cart, resetResolver]);
369
335
 
370
336
  return waitCartIdle;
371
- }
337
+ };
372
338
 
373
- export type {
374
- WaitCartIdleCallback
375
- }
339
+ export type { WaitCartIdleCallback };
376
340
 
377
341
  export {
378
342
  DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
@@ -383,5 +347,5 @@ export {
383
347
  useWaitCartIdle,
384
348
  isCartWithActionsDocs,
385
349
  getCartLines,
386
- isOptimisticCart
387
- };
350
+ isOptimisticCart,
351
+ };
@@ -0,0 +1,86 @@
1
+ const DISABLED_ATTR = "data-redo-preorder-disabled";
2
+ const ORIGINAL_DISABLED_ATTR = "data-redo-original-disabled";
3
+
4
+ /**
5
+ * Finds add-to-cart buttons on the page
6
+ */
7
+ function findAddToCartButtons(): HTMLButtonElement[] {
8
+ const buttons = document.querySelectorAll<HTMLButtonElement>(
9
+ 'button[name="add"], button[type="submit"], .product-form__submit',
10
+ );
11
+
12
+ return Array.from(buttons).filter((button) => {
13
+ const text = button.textContent?.toLowerCase().trim() ?? "";
14
+ return text.includes("add to cart") || text.includes("add to bag");
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Checks for Purple Dot preorder element and updates button states accordingly.
20
+ * Disables add-to-cart buttons when purple-dot-learn-more element is present.
21
+ * Re-enables them when the element is removed.
22
+ */
23
+ export function updatePurpleDotButtons(): void {
24
+ const hasPurpleDot = document.querySelector("purple-dot-learn-more") !== null;
25
+ const addToCartButtons = findAddToCartButtons();
26
+
27
+ for (const button of addToCartButtons) {
28
+ if (hasPurpleDot) {
29
+ // Disable the button
30
+ if (!button.hasAttribute(DISABLED_ATTR)) {
31
+ // Preserve original disabled state
32
+ if (!button.hasAttribute(ORIGINAL_DISABLED_ATTR)) {
33
+ button.setAttribute(ORIGINAL_DISABLED_ATTR, String(button.disabled));
34
+ }
35
+
36
+ button.setAttribute(DISABLED_ATTR, "true");
37
+ button.disabled = true;
38
+ button.style.opacity = "0.5";
39
+ button.style.cursor = "not-allowed";
40
+ button.style.pointerEvents = "none";
41
+
42
+ // Add message if not already present
43
+ if (!button.parentElement?.querySelector(".redo-preorder-msg")) {
44
+ const msg = document.createElement("div");
45
+ msg.className = "redo-preorder-msg";
46
+ msg.textContent = "Preorder items cannot be added during exchanges";
47
+ msg.style.cssText = "color: #d63031; font-size: 12px; margin-top: 8px; font-weight: 500;";
48
+ button.parentElement?.insertBefore(msg, button.nextSibling);
49
+ }
50
+ }
51
+ } else {
52
+ // Re-enable the button if we disabled it
53
+ if (button.hasAttribute(DISABLED_ATTR)) {
54
+ const wasDisabled = button.getAttribute(ORIGINAL_DISABLED_ATTR) === "true";
55
+ button.removeAttribute(DISABLED_ATTR);
56
+ button.removeAttribute(ORIGINAL_DISABLED_ATTR);
57
+ button.disabled = wasDisabled;
58
+ button.style.opacity = "";
59
+ button.style.cursor = "";
60
+ button.style.pointerEvents = "";
61
+ button.parentElement?.querySelector(".redo-preorder-msg")?.remove();
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Cleans up all disabled buttons (used on unmount).
69
+ * Restores original disabled state.
70
+ */
71
+ export function cleanupPurpleDotButtons(): void {
72
+ const disabledButtons = Array.from(document.querySelectorAll<HTMLButtonElement>(`button[${DISABLED_ATTR}]`));
73
+
74
+ for (const button of disabledButtons) {
75
+ const wasDisabled = button.getAttribute(ORIGINAL_DISABLED_ATTR) === "true";
76
+ button.removeAttribute(DISABLED_ATTR);
77
+ button.removeAttribute(ORIGINAL_DISABLED_ATTR);
78
+ button.disabled = wasDisabled;
79
+ button.style.opacity = "";
80
+ button.style.cursor = "";
81
+ button.style.pointerEvents = "";
82
+ }
83
+
84
+ // Remove all messages
85
+ document.querySelectorAll(".redo-preorder-msg").forEach((msg) => msg.remove());
86
+ }
@@ -1,17 +1,11 @@
1
- import {
2
- DependencyList,
3
- useCallback,
4
- useEffect,
5
- useRef,
6
- useState,
7
- } from "react";
1
+ import { DependencyList, useEffect, useState } from "react";
8
2
 
9
3
  export interface Loader<T> {
10
4
  (abort: AbortSignal): Promise<T>;
11
5
  }
12
6
 
13
7
  export interface LoadState<T> {
14
- error?: any;
8
+ error?: unknown;
15
9
  pending: boolean;
16
10
  value?: T;
17
11
  }
@@ -24,12 +18,14 @@ export function useLoad<T>(fn: Loader<T>, deps: DependencyList): LoadState<T> {
24
18
  setState((state) => ({ ...state, pending: true }));
25
19
  fn(abortController.signal).then(
26
20
  (value) => setState({ pending: false, value }),
27
- (error) => {
21
+ (error: unknown) => {
22
+ const message = error instanceof Error ? error.message : "";
23
+ const code = (error as Record<string, unknown>)?.code;
28
24
  if (
29
25
  !(
30
- error.message.includes("Request aborted for RPC method") ||
31
- error.code === "ERR_CANCELED" ||
32
- error.message === "Another request is in flight"
26
+ message.includes("Request aborted for RPC method") ||
27
+ code === "ERR_CANCELED" ||
28
+ message === "Another request is in flight"
33
29
  )
34
30
  ) {
35
31
  setState({ pending: false, error });
@@ -43,8 +39,7 @@ export function useLoad<T>(fn: Loader<T>, deps: DependencyList): LoadState<T> {
43
39
  // The way useLoad() is designed, we have no choice but to trust that the user gave us the correct deps for fn().
44
40
  // We could fix this by marking useLoad() as a custom hook, and then exhaustive-deps would enforce that for us.
45
41
  // https://www.npmjs.com/package/eslint-plugin-react-hooks#advanced-configuration
46
- // eslint-disable-next-line react-hooks/exhaustive-deps
47
42
  }, deps);
48
43
 
49
44
  return state;
50
- }
45
+ }
@@ -1,10 +1,5 @@
1
- const REDO_PUBLIC_API_HOSTNAME = 'api.getredo.com';
1
+ const REDO_PUBLIC_API_HOSTNAME = "api.getredo.com";
2
2
 
3
- const REDO_REQUIRED_HOSTNAMES = [
4
- REDO_PUBLIC_API_HOSTNAME
5
- ];
3
+ const REDO_REQUIRED_HOSTNAMES = [REDO_PUBLIC_API_HOSTNAME];
6
4
 
7
- export {
8
- REDO_REQUIRED_HOSTNAMES,
9
- REDO_PUBLIC_API_HOSTNAME
10
- };
5
+ export { REDO_REQUIRED_HOSTNAMES, REDO_PUBLIC_API_HOSTNAME };
@@ -3,10 +3,5 @@ export async function executeWithTimeout<T, E extends Error>(
3
3
  timeoutMs: number,
4
4
  error: E = new Error("timeout") as E,
5
5
  ): Promise<T> {
6
- return Promise.race([
7
- promise,
8
- new Promise<never>((_, reject) =>
9
- setTimeout(() => reject(error), timeoutMs),
10
- ),
11
- ]);
6
+ return Promise.race([promise, new Promise<never>((_, reject) => setTimeout(() => reject(error), timeoutMs))]);
12
7
  }