@redotech/redo-hydrogen 1.2.0 → 1.2.2

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@redotech/redo-hydrogen",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Utilities to enable and disable Redo coverage on Hydrogen stores",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
7
- "types": "dist/index.d.ts",
7
+ "types": "dist/types.d.ts",
8
8
  "scripts": {
9
9
  "dev": "rollup -c --watch --bundleConfigAsCjs"
10
10
  },
@@ -1,9 +1,9 @@
1
1
  import { useFetcher } from "@remix-run/react";
2
2
  import { CartReturn } from "@shopify/hydrogen";
3
- import { createContext, ReactNode, useContext, useEffect, useState } from "react";
3
+ import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { CartProductVariantFragment, CartAttributeKey, CartInfoToEnable, RedoContextValue, RedoCoverageClient, RedoError, RedoErrorType } from "../types";
5
5
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
6
- import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise, isCartWithActionsDocs, getCartLines } from "../utils/cart";
6
+ import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise, isCartWithActionsDocs, getCartLines, useWaitCartIdle } from "../utils/cart";
7
7
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
8
8
 
9
9
  const DEFAULT_REDO_CONTEXT_VALUE: RedoContextValue = {
@@ -141,17 +141,19 @@ const RedoProvider = ({
141
141
  const useRedoCoverageClient = (): RedoCoverageClient => {
142
142
  const redoContext = useContext(RedoContext);
143
143
  const fetcher = useFetcherWithPromise();
144
+ const waitCartIdle = useWaitCartIdle(redoContext.cart);
144
145
 
145
146
  useEffect(() => {
146
147
  if(redoContext.loading || !redoContext.cartInfoToEnable) {
147
148
  return;
148
149
  }
149
150
  removeProductFromCartIfNeeded({
150
- fetcher,
151
151
  cart: redoContext.cart,
152
+ fetcher,
153
+ waitCartIdle,
152
154
  cartInfoToEnable: redoContext.cartInfoToEnable
153
155
  });
154
- }, [redoContext.loading])
156
+ }, [redoContext.loading]);
155
157
 
156
158
  return {
157
159
  enable: async () => {
@@ -160,12 +162,14 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
160
162
  }
161
163
  let addProductResult = await addProductToCartIfNeeded({
162
164
  fetcher,
165
+ waitCartIdle,
163
166
  cart: redoContext.cart,
164
167
  cartInfoToEnable: redoContext.cartInfoToEnable,
165
168
  });
166
169
  await setCartRedoEnabledAttribute({
167
170
  cart: redoContext.cart,
168
171
  fetcher,
172
+ waitCartIdle,
169
173
  cartInfoToEnable: redoContext.cartInfoToEnable,
170
174
  enabled: true
171
175
  });
@@ -177,12 +181,14 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
177
181
  }
178
182
  await removeProductFromCartIfNeeded({
179
183
  fetcher,
184
+ waitCartIdle,
180
185
  cart: redoContext.cart,
181
186
  cartInfoToEnable: redoContext.cartInfoToEnable
182
187
  });
183
188
  await setCartRedoEnabledAttribute({
184
189
  cart: redoContext.cart,
185
190
  fetcher,
191
+ waitCartIdle,
186
192
  cartInfoToEnable: redoContext.cartInfoToEnable,
187
193
  enabled: false
188
194
  });
package/src/utils/cart.ts CHANGED
@@ -2,7 +2,7 @@ import { FetcherWithComponents, useFetcher } from "@remix-run/react";
2
2
  import { CartInfoToEnable } from "../types";
3
3
  import { CartForm, CartReturn } from "@shopify/hydrogen";
4
4
  import type { AppData } from '@remix-run/react/dist/data';
5
- import React from 'react'
5
+ import React, { useCallback, useEffect, useRef } from 'react'
6
6
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
7
7
  import { CartLine, ComponentizableCartLine } from "@shopify/hydrogen-react/storefront-api-types";
8
8
 
@@ -20,28 +20,58 @@ const getCartLines = (cart: CartReturn | CartWithActionsDocs): Array<CartLine |
20
20
  }
21
21
  }
22
22
 
23
- const waitUntilCartIdle = (cart: CartWithActionsDocs): Promise<void> => {
23
+ const isRedoInCart = ({
24
+ cart
25
+ }: {
26
+ cart: CartReturn | CartWithActionsDocs
27
+ }): boolean => {
28
+ if(!cart) {
29
+ return false;
30
+ }
31
+
32
+ return getCartLines(cart).some((cartLine) => {
33
+ return cartLine.merchandise.product.vendor === 're:do';
34
+ });
35
+ }
36
+
37
+ const waitForConditionsMetOrTimeout = ({
38
+ conditions,
39
+ timeoutMs
40
+ }: {
41
+ conditions: (() => boolean)[];
42
+ timeoutMs: number;
43
+ }): Promise<boolean> => {
24
44
  return new Promise((resolve, reject) => {
45
+ let start = Date.now();
25
46
  let interval = setInterval(() => {
26
- if(cart.status === 'idle') {
47
+ if((Date.now() - start) > timeoutMs) {
27
48
  clearInterval(interval);
28
- return resolve();
49
+ return resolve(false);
50
+ }
51
+
52
+ let conditionsMet = conditions.every((conditionCallback) => conditionCallback());
53
+
54
+ if(conditionsMet) {
55
+ clearInterval(interval);
56
+ return resolve(true);
29
57
  }
30
58
  }, 100);
31
- });
59
+ })
32
60
  }
33
61
 
34
62
  const addProductToCartIfNeeded = async ({
35
63
  cart,
36
64
  fetcher,
65
+ waitCartIdle,
37
66
  cartInfoToEnable
38
67
  }: {
39
68
  cart: CartReturn | CartWithActionsDocs | undefined,
40
69
  fetcher: FetcherWithComponents<unknown>,
70
+ waitCartIdle: WaitCartIdleCallback;
41
71
  cartInfoToEnable: CartInfoToEnable
42
72
  }) => {
43
73
  if(!cart) {
44
- return await addProductToCart({ cart, fetcher, cartInfoToEnable });
74
+ return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
45
75
  }
46
76
 
47
77
  const redoProductsInCart = getCartLines(cart).filter((cartLine) => {
@@ -51,15 +81,15 @@ const addProductToCartIfNeeded = async ({
51
81
  return cartLine.merchandise.id === `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`;
52
82
  });
53
83
  if(redoProductsInCart.length === 0) {
54
- return await addProductToCart({ cart, fetcher, cartInfoToEnable });
84
+ return await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
55
85
  } else if (redoProductsInCart.length === 1 && correctRedoProductInCart.length === 1 && correctRedoProductInCart[0].quantity === 1) {
56
86
  // No action needed
57
87
  return;
58
88
  } else {
59
89
  let isSuccess = true;
60
90
 
61
- await removeLinesFromCart({ cart, fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
62
- await addProductToCart({ cart, fetcher, cartInfoToEnable });
91
+ await removeLinesFromCart({ cart, fetcher, waitCartIdle, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
92
+ await addProductToCart({ cart, fetcher, waitCartIdle, cartInfoToEnable });
63
93
 
64
94
  return;
65
95
  }
@@ -68,10 +98,12 @@ const addProductToCartIfNeeded = async ({
68
98
  const removeLinesFromCart = async ({
69
99
  cart,
70
100
  fetcher,
101
+ waitCartIdle,
71
102
  lineIds
72
103
  }: {
73
104
  cart: CartReturn | CartWithActionsDocs | undefined;
74
105
  fetcher: FetcherWithComponents<unknown>;
106
+ waitCartIdle: WaitCartIdleCallback;
75
107
  lineIds: string[];
76
108
  }) => {
77
109
  const formInput = {
@@ -83,7 +115,7 @@ const removeLinesFromCart = async ({
83
115
 
84
116
  if(cart && isCartWithActionsDocs(cart)) {
85
117
  cart.linesRemove(lineIds);
86
- await waitUntilCartIdle(cart);
118
+ await waitCartIdle();
87
119
  } else {
88
120
  await fetcher.submit(
89
121
  {
@@ -97,10 +129,12 @@ const removeLinesFromCart = async ({
97
129
  const removeProductFromCartIfNeeded = async ({
98
130
  cart,
99
131
  fetcher,
132
+ waitCartIdle,
100
133
  cartInfoToEnable
101
134
  }: {
102
135
  cart: CartReturn | CartWithActionsDocs | undefined,
103
136
  fetcher: FetcherWithComponents<unknown>,
137
+ waitCartIdle: WaitCartIdleCallback
104
138
  cartInfoToEnable: CartInfoToEnable
105
139
  }) => {
106
140
  if(!cart) {
@@ -113,16 +147,18 @@ const removeProductFromCartIfNeeded = async ({
113
147
  });
114
148
 
115
149
  if(redoProductsInCart.length !== 0) {
116
- await removeLinesFromCart({ cart, fetcher, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
150
+ await removeLinesFromCart({ cart, fetcher, waitCartIdle, lineIds: redoProductsInCart.map((cartLine) => cartLine.id) });
117
151
  } else {
118
152
  }
119
153
  };
120
154
 
121
155
  const addProductToCart = async ({
156
+ waitCartIdle,
122
157
  cart,
123
158
  fetcher,
124
159
  cartInfoToEnable,
125
160
  }: {
161
+ waitCartIdle: WaitCartIdleCallback;
126
162
  cart: CartReturn | CartWithActionsDocs | undefined,
127
163
  fetcher: FetcherWithComponents<unknown>,
128
164
  cartInfoToEnable: CartInfoToEnable
@@ -130,7 +166,6 @@ const addProductToCart = async ({
130
166
  const redoProductLine = {
131
167
  "merchandiseId": `gid://shopify/ProductVariant/${cartInfoToEnable.variantId}`,
132
168
  "quantity": 1,
133
- "selectedVariant": cartInfoToEnable.selectedVariant
134
169
  };
135
170
 
136
171
  const formInput = {
@@ -144,7 +179,7 @@ const addProductToCart = async ({
144
179
 
145
180
  if(cart && isCartWithActionsDocs(cart)) {
146
181
  cart.linesAdd([redoProductLine]);
147
- await waitUntilCartIdle(cart);
182
+ await waitCartIdle();
148
183
  } else {
149
184
  await fetcher.submit(
150
185
  {
@@ -158,11 +193,13 @@ const addProductToCart = async ({
158
193
  const setCartRedoEnabledAttribute = async ({
159
194
  cart,
160
195
  fetcher,
196
+ waitCartIdle,
161
197
  cartInfoToEnable,
162
198
  enabled
163
199
  }: {
164
200
  cart: CartReturn | CartWithActionsDocs | undefined;
165
201
  fetcher: FetcherWithComponents<unknown>;
202
+ waitCartIdle: WaitCartIdleCallback;
166
203
  cartInfoToEnable: CartInfoToEnable | null;
167
204
  enabled: boolean;
168
205
  }) => {
@@ -182,7 +219,7 @@ const setCartRedoEnabledAttribute = async ({
182
219
 
183
220
  if(cart && isCartWithActionsDocs(cart)) {
184
221
  cart.cartAttributesUpdate([redoCartAttribute]);
185
- await waitUntilCartIdle(cart);
222
+ await waitCartIdle();
186
223
  } else {
187
224
  await fetcher.submit(
188
225
  {
@@ -233,12 +270,62 @@ function useFetcherWithPromise<TData = AppData>(opts?: Parameters<typeof useFetc
233
270
  return { ...fetcher, submit }
234
271
  }
235
272
 
273
+ type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs>;
274
+
275
+ // This function allows us to await a cart idle state without breaking React rules.
276
+ // It returns a function, which returns a promise, which will resolve once the cart value passed in reaches an idle state.
277
+ // Not intended for use with CartReturn, but will accept that value if passed in to avoid breaking rules of hooks
278
+ const useWaitCartIdle = (cart: CartReturn | CartWithActionsDocs | undefined) => {
279
+ const resolveRef = useRef<any>(null)
280
+ const promiseRef = useRef<any>(null)
281
+
282
+ if (!promiseRef.current) {
283
+ promiseRef.current = new Promise<CartReturn | CartWithActionsDocs>((resolve) => {
284
+ resolveRef.current = resolve
285
+ })
286
+ }
287
+
288
+ const resetResolver = useCallback(() => {
289
+ promiseRef.current = new Promise((resolve) => {
290
+ resolveRef.current = resolve
291
+ })
292
+ }, [promiseRef, resolveRef]);
293
+
294
+ const waitCartIdle = useCallback(
295
+ async () => {
296
+ return promiseRef.current
297
+ },
298
+ [cart, promiseRef]
299
+ )
300
+
301
+ useEffect(() => {
302
+ if(!cart) {
303
+ return;
304
+ }
305
+ if(!isCartWithActionsDocs(cart)) {
306
+ // Wrong type of cart. Just resolve.
307
+ resolveRef.current?.(cart);
308
+ resetResolver();
309
+ } else if(cart.status === 'idle') {
310
+ resolveRef.current?.(cart)
311
+ resetResolver();
312
+ }
313
+ }, [cart, resetResolver]);
314
+
315
+ return waitCartIdle;
316
+ }
317
+
318
+ export type {
319
+ WaitCartIdleCallback
320
+ }
321
+
236
322
  export {
237
323
  DEFAULT_REDO_ENABLED_CART_ATTRIBUTE,
238
324
  addProductToCartIfNeeded,
239
325
  removeProductFromCartIfNeeded,
240
326
  setCartRedoEnabledAttribute,
241
327
  useFetcherWithPromise,
328
+ useWaitCartIdle,
242
329
  isCartWithActionsDocs,
243
330
  getCartLines
244
331
  };