@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/CHANGELOG.md +8 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/package.json +2 -2
- package/src/providers/redo-coverage-client.tsx +10 -4
- package/src/utils/cart.ts +101 -14
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redotech/redo-hydrogen",
|
|
3
|
-
"version": "1.2.
|
|
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/
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|