@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/.github/workflows/ci.yml +58 -0
- package/.github/workflows/e2e.yml +72 -0
- package/.github/workflows/publish.yml +68 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +36 -24
- package/README.md +37 -2
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/types.d.ts +24 -4
- package/eslint.config.mjs +22 -0
- package/package.json +25 -12
- package/src/components/redo-checkout-buttons.tsx +51 -72
- package/src/components/redo-info-modal.tsx +486 -345
- package/src/hooks/use-purple-dot-preorder.ts +39 -0
- package/src/index.ts +14 -4
- package/src/providers/redo-coverage-client.tsx +67 -69
- package/src/svg.d.ts +2 -2
- package/src/types.ts +30 -23
- package/src/utils/cart.ts +117 -153
- package/src/utils/purple-dot.ts +86 -0
- package/src/utils/react-utils.ts +9 -14
- package/src/utils/security.ts +3 -8
- package/src/utils/timeout.ts +1 -6
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { updatePurpleDotButtons, cleanupPurpleDotButtons } from "../utils/purple-dot";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook to disable add-to-cart buttons when a Purple Dot preorder element is present.
|
|
6
|
+
* Watches for DOM changes and automatically disables/enables buttons based on the presence
|
|
7
|
+
* of the <purple-dot-learn-more> element.
|
|
8
|
+
*
|
|
9
|
+
* @param disablePreorderButtons - When true, enables the preorder button disabling logic.
|
|
10
|
+
* When false, the hook does nothing.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function ProductPage() {
|
|
15
|
+
* const isShopOnSiteActive = true; // your condition here
|
|
16
|
+
* useDisablePurpleDotPreorder(isShopOnSiteActive);
|
|
17
|
+
* return <div>...</div>;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useDisablePurpleDotPreorder(disablePreorderButtons: boolean): void {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!disablePreorderButtons) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Initial check
|
|
28
|
+
updatePurpleDotButtons();
|
|
29
|
+
|
|
30
|
+
// Watch for DOM changes (variant selection, page navigation, etc.)
|
|
31
|
+
const observer = new MutationObserver(updatePurpleDotButtons);
|
|
32
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
observer.disconnect();
|
|
36
|
+
cleanupPurpleDotButtons();
|
|
37
|
+
};
|
|
38
|
+
}, [disablePreorderButtons]);
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { RedoProvider, useRedoCoverageClient } from "./providers/redo-coverage-client";
|
|
2
2
|
import { RedoCheckoutButtons } from "./components/redo-checkout-buttons";
|
|
3
3
|
import { REDO_REQUIRED_HOSTNAMES } from "./utils/security";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
CartProductVariantFragment,
|
|
6
|
+
CartAttributeKey,
|
|
7
|
+
CartInfoToEnable,
|
|
8
|
+
RedoContextValue,
|
|
9
|
+
RedoCoverageClient,
|
|
10
|
+
RedoError,
|
|
11
|
+
RedoErrorType,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { LoadState, Loader, useLoad } from "./utils/react-utils";
|
|
6
14
|
import { RedoInfoCard } from "./components/redo-info-modal";
|
|
15
|
+
import { useDisablePurpleDotPreorder } from "./hooks/use-purple-dot-preorder";
|
|
7
16
|
|
|
8
17
|
export {
|
|
9
18
|
RedoCheckoutButtons,
|
|
@@ -12,7 +21,8 @@ export {
|
|
|
12
21
|
useLoad,
|
|
13
22
|
REDO_REQUIRED_HOSTNAMES,
|
|
14
23
|
RedoErrorType,
|
|
15
|
-
RedoInfoCard
|
|
24
|
+
RedoInfoCard,
|
|
25
|
+
useDisablePurpleDotPreorder,
|
|
16
26
|
};
|
|
17
27
|
|
|
18
28
|
export type {
|
|
@@ -23,5 +33,5 @@ export type {
|
|
|
23
33
|
RedoCoverageClient,
|
|
24
34
|
LoadState,
|
|
25
35
|
Loader,
|
|
26
|
-
RedoError
|
|
36
|
+
RedoError,
|
|
27
37
|
};
|
|
@@ -1,52 +1,56 @@
|
|
|
1
|
-
import { useFetcher } from "@remix-run/react";
|
|
2
1
|
import { CartReturn, OptimisticCart } from "@shopify/hydrogen";
|
|
3
|
-
import { createContext, ReactNode,
|
|
4
|
-
import {
|
|
2
|
+
import { createContext, ReactNode, useContext, useEffect, useState } from "react";
|
|
3
|
+
import { CartInfoToEnable, RedoContextValue, RedoCoverageClient, RedoError, RedoErrorType } from "../types";
|
|
5
4
|
import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
addProductToCartIfNeeded,
|
|
7
|
+
removeProductFromCartIfNeeded,
|
|
8
|
+
setCartRedoEnabledAttribute,
|
|
9
|
+
useFetcherWithPromise,
|
|
10
|
+
getCartLines,
|
|
11
|
+
useWaitCartIdle,
|
|
12
|
+
isOptimisticCart,
|
|
13
|
+
} from "../utils/cart";
|
|
7
14
|
import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
|
|
8
15
|
|
|
9
16
|
const DEFAULT_REDO_CONTEXT_VALUE: RedoContextValue = {
|
|
10
17
|
enabled: false,
|
|
11
18
|
loading: true,
|
|
12
|
-
}
|
|
19
|
+
};
|
|
13
20
|
|
|
14
21
|
const RedoContext = createContext<RedoContextValue>(DEFAULT_REDO_CONTEXT_VALUE);
|
|
15
22
|
|
|
16
23
|
const RedoProvider = ({
|
|
17
24
|
cart,
|
|
18
25
|
storeId,
|
|
19
|
-
children
|
|
26
|
+
children,
|
|
20
27
|
}: {
|
|
21
|
-
cart: CartReturn | CartWithActionsDocs | OptimisticCart
|
|
22
|
-
storeId: string
|
|
23
|
-
children: ReactNode
|
|
28
|
+
cart: CartReturn | CartWithActionsDocs | OptimisticCart;
|
|
29
|
+
storeId: string;
|
|
30
|
+
children: ReactNode;
|
|
24
31
|
}): ReactNode => {
|
|
25
|
-
const [cartProduct, setCartProduct] = useState();
|
|
26
|
-
const [cartAttribute, setCartAttribute] = useState<CartAttributeKey>();
|
|
27
32
|
const [cartInfoToEnable, setCartInfoToEnable] = useState<CartInfoToEnable>();
|
|
28
33
|
const [loading, setLoading] = useState<boolean>(true);
|
|
29
34
|
const [errors, setErrors] = useState<RedoError[]>([]);
|
|
30
35
|
|
|
31
36
|
const logUniqueError = (newError: RedoError) => {
|
|
32
|
-
if(errors.find((err) => err.type === newError.type)) {
|
|
33
|
-
} else {
|
|
37
|
+
if (!errors.find((err) => err.type === newError.type)) {
|
|
34
38
|
setErrors([...errors, newError]);
|
|
35
39
|
}
|
|
36
40
|
return newError;
|
|
37
|
-
}
|
|
41
|
+
};
|
|
38
42
|
|
|
39
43
|
useEffect(() => {
|
|
40
|
-
if(!cart || !storeId || isOptimisticCart(cart)) {
|
|
44
|
+
if (!cart || !storeId || isOptimisticCart(cart)) {
|
|
41
45
|
return;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
const cartLines = getCartLines(cart);
|
|
45
49
|
|
|
46
50
|
fetch(`https://${REDO_PUBLIC_API_HOSTNAME}/v2.2/stores/${storeId}/coverage-products`, {
|
|
47
|
-
method:
|
|
51
|
+
method: "POST",
|
|
48
52
|
headers: {
|
|
49
|
-
"Content-Type": "application/json"
|
|
53
|
+
"Content-Type": "application/json",
|
|
50
54
|
},
|
|
51
55
|
body: JSON.stringify({
|
|
52
56
|
cart: {
|
|
@@ -54,88 +58,85 @@ const RedoProvider = ({
|
|
|
54
58
|
id: cartLine.id,
|
|
55
59
|
originalPrice: {
|
|
56
60
|
amount: cartLine.merchandise?.price?.amount,
|
|
57
|
-
currency: cartLine.merchandise?.price?.currencyCode
|
|
61
|
+
currency: cartLine.merchandise?.price?.currencyCode,
|
|
58
62
|
},
|
|
59
63
|
priceTotal: {
|
|
60
64
|
amount: cartLine.cost?.totalAmount?.amount,
|
|
61
|
-
currency: cartLine.cost?.totalAmount?.currencyCode
|
|
65
|
+
currency: cartLine.cost?.totalAmount?.currencyCode,
|
|
62
66
|
},
|
|
63
67
|
product: {
|
|
64
|
-
id: cartLine.merchandise?.product?.id
|
|
68
|
+
id: cartLine.merchandise?.product?.id,
|
|
65
69
|
},
|
|
66
70
|
variant: {
|
|
67
|
-
id: cartLine.merchandise?.id
|
|
71
|
+
id: cartLine.merchandise?.id,
|
|
68
72
|
},
|
|
69
73
|
quantity: cartLine.quantity,
|
|
70
74
|
})),
|
|
71
75
|
priceTotal: {
|
|
72
76
|
amount: cart.cost?.totalAmount?.amount,
|
|
73
|
-
currency: cart.cost?.totalAmount?.currencyCode
|
|
77
|
+
currency: cart.cost?.totalAmount?.currencyCode,
|
|
74
78
|
},
|
|
75
79
|
},
|
|
76
80
|
customer: {
|
|
77
|
-
id: cart.buyerIdentity?.customer?.id ||
|
|
78
|
-
country: cart.buyerIdentity?.countryCode
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
if(res.status === 500) {
|
|
81
|
+
id: cart.buyerIdentity?.customer?.id || "",
|
|
82
|
+
country: cart.buyerIdentity?.countryCode,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
}).then(async (res) => {
|
|
86
|
+
if (res.status === 500) {
|
|
84
87
|
logUniqueError({
|
|
85
88
|
type: RedoErrorType.ApiServerError,
|
|
86
|
-
message:
|
|
89
|
+
message:
|
|
90
|
+
"Internal server error occured when getting available coverage products from Redo API.. Check your inputs are correct and storeId have been configured. Reach out to Redo support if the issue persists.",
|
|
87
91
|
context: {
|
|
88
|
-
json: await res.json()
|
|
89
|
-
}
|
|
92
|
+
json: await res.json(),
|
|
93
|
+
},
|
|
90
94
|
});
|
|
91
95
|
return;
|
|
92
|
-
} else if(res.status === 400) {
|
|
96
|
+
} else if (res.status === 400) {
|
|
93
97
|
logUniqueError({
|
|
94
98
|
type: RedoErrorType.ApiBadRequest,
|
|
95
|
-
message:
|
|
99
|
+
message:
|
|
100
|
+
"Bad request when getting available coverage products from Redo API. Check that the passed in cart is of the correct type Cart/CartReturn and includes all of the correct cart information.",
|
|
96
101
|
context: {
|
|
97
|
-
json: await res.json()
|
|
98
|
-
}
|
|
102
|
+
json: await res.json(),
|
|
103
|
+
},
|
|
99
104
|
});
|
|
100
105
|
return;
|
|
101
|
-
} else if(res.status !== 200) {
|
|
106
|
+
} else if (res.status !== 200) {
|
|
102
107
|
logUniqueError({
|
|
103
108
|
type: RedoErrorType.ApiUnknownError,
|
|
104
109
|
message: "Unkown error occured while getting available coverage products from Redo API.",
|
|
105
110
|
context: {
|
|
106
111
|
status: res.status,
|
|
107
|
-
json: await res.json()
|
|
108
|
-
}
|
|
112
|
+
json: await res.json(),
|
|
113
|
+
},
|
|
109
114
|
});
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
|
|
118
|
+
const json = await res.json();
|
|
114
119
|
|
|
115
120
|
setLoading(false);
|
|
116
|
-
|
|
117
|
-
if(!json?.coverageProducts?.[0]?.cartInfoToEnable) {
|
|
121
|
+
|
|
122
|
+
if (!json?.coverageProducts?.[0]?.cartInfoToEnable) {
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
setCartInfoToEnable(json.coverageProducts[0].cartInfoToEnable);
|
|
122
|
-
})
|
|
127
|
+
});
|
|
123
128
|
}, [cart, storeId]);
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
const contextVal: RedoContextValue = {
|
|
126
131
|
enabled: true,
|
|
127
132
|
loading,
|
|
128
133
|
storeId,
|
|
129
134
|
cartInfoToEnable,
|
|
130
135
|
cart,
|
|
131
|
-
errors:
|
|
136
|
+
errors: errors?.length && errors.length > 0 ? errors : undefined,
|
|
132
137
|
};
|
|
133
138
|
|
|
134
|
-
return
|
|
135
|
-
<RedoContext.Provider value={contextVal}>
|
|
136
|
-
{children}
|
|
137
|
-
</RedoContext.Provider>
|
|
138
|
-
);
|
|
139
|
+
return <RedoContext.Provider value={contextVal}>{children}</RedoContext.Provider>;
|
|
139
140
|
};
|
|
140
141
|
|
|
141
142
|
const useRedoCoverageClient = (): RedoCoverageClient => {
|
|
@@ -144,23 +145,23 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
|
|
|
144
145
|
const waitCartIdle = useWaitCartIdle(redoContext.cart);
|
|
145
146
|
|
|
146
147
|
useEffect(() => {
|
|
147
|
-
if(redoContext.loading || !redoContext.cartInfoToEnable) {
|
|
148
|
+
if (redoContext.loading || !redoContext.cartInfoToEnable) {
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
151
|
removeProductFromCartIfNeeded({
|
|
151
152
|
cart: redoContext.cart,
|
|
152
153
|
fetcher,
|
|
153
154
|
waitCartIdle,
|
|
154
|
-
cartInfoToEnable: redoContext.cartInfoToEnable
|
|
155
|
+
cartInfoToEnable: redoContext.cartInfoToEnable,
|
|
155
156
|
});
|
|
156
157
|
}, [redoContext.loading]);
|
|
157
|
-
|
|
158
|
+
|
|
158
159
|
return {
|
|
159
160
|
enable: async () => {
|
|
160
|
-
if(redoContext.loading || !redoContext.cartInfoToEnable) {
|
|
161
|
+
if (redoContext.loading || !redoContext.cartInfoToEnable) {
|
|
161
162
|
return false;
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
await addProductToCartIfNeeded({
|
|
164
165
|
fetcher,
|
|
165
166
|
waitCartIdle,
|
|
166
167
|
cart: redoContext.cart,
|
|
@@ -171,26 +172,26 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
|
|
|
171
172
|
fetcher,
|
|
172
173
|
waitCartIdle,
|
|
173
174
|
cartInfoToEnable: redoContext.cartInfoToEnable,
|
|
174
|
-
enabled: true
|
|
175
|
+
enabled: true,
|
|
175
176
|
});
|
|
176
177
|
return true;
|
|
177
178
|
},
|
|
178
179
|
disable: async () => {
|
|
179
|
-
if(!redoContext.cartInfoToEnable) {
|
|
180
|
+
if (!redoContext.cartInfoToEnable) {
|
|
180
181
|
return false;
|
|
181
182
|
}
|
|
182
183
|
await removeProductFromCartIfNeeded({
|
|
183
184
|
fetcher,
|
|
184
185
|
waitCartIdle,
|
|
185
186
|
cart: redoContext.cart,
|
|
186
|
-
cartInfoToEnable: redoContext.cartInfoToEnable
|
|
187
|
+
cartInfoToEnable: redoContext.cartInfoToEnable,
|
|
187
188
|
});
|
|
188
189
|
await setCartRedoEnabledAttribute({
|
|
189
190
|
cart: redoContext.cart,
|
|
190
191
|
fetcher,
|
|
191
192
|
waitCartIdle,
|
|
192
193
|
cartInfoToEnable: redoContext.cartInfoToEnable,
|
|
193
|
-
enabled: false
|
|
194
|
+
enabled: false,
|
|
194
195
|
});
|
|
195
196
|
return true;
|
|
196
197
|
},
|
|
@@ -204,8 +205,8 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
|
|
|
204
205
|
return redoContext.enabled;
|
|
205
206
|
},
|
|
206
207
|
get price() {
|
|
207
|
-
|
|
208
|
-
if(!priceToEnable || Number(priceToEnable).toString() ===
|
|
208
|
+
const priceToEnable = redoContext.cartInfoToEnable?.selectedVariant?.price?.amount;
|
|
209
|
+
if (!priceToEnable || Number(priceToEnable).toString() === "NaN") {
|
|
209
210
|
return undefined;
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -218,18 +219,15 @@ const useRedoCoverageClient = (): RedoCoverageClient => {
|
|
|
218
219
|
return redoContext.cartInfoToEnable?.selectedVariant;
|
|
219
220
|
},
|
|
220
221
|
get cartAttribute() {
|
|
221
|
-
return redoContext.cartInfoToEnable?.cartAttribute
|
|
222
|
+
return redoContext.cartInfoToEnable?.cartAttribute;
|
|
222
223
|
},
|
|
223
224
|
get storeId() {
|
|
224
225
|
return redoContext.storeId;
|
|
225
226
|
},
|
|
226
227
|
get errors() {
|
|
227
228
|
return redoContext.errors;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
230
231
|
};
|
|
231
232
|
|
|
232
|
-
export {
|
|
233
|
-
RedoProvider,
|
|
234
|
-
useRedoCoverageClient
|
|
235
|
-
}
|
|
233
|
+
export { RedoProvider, useRedoCoverageClient };
|
package/src/svg.d.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -2,8 +2,17 @@ import { CartReturn, OptimisticCart } from "@shopify/hydrogen";
|
|
|
2
2
|
import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
|
|
3
3
|
import { ProductVariant } from "@shopify/hydrogen-react/storefront-api-types";
|
|
4
4
|
|
|
5
|
-
type CartProductVariantFragment = Omit<
|
|
6
|
-
|
|
5
|
+
type CartProductVariantFragment = Omit<
|
|
6
|
+
ProductVariant,
|
|
7
|
+
| "components"
|
|
8
|
+
| "metafields"
|
|
9
|
+
| "quantityPriceBreaks"
|
|
10
|
+
| "quantityRule"
|
|
11
|
+
| "requiresComponents"
|
|
12
|
+
| "requiresShipping"
|
|
13
|
+
| "storeAvailability"
|
|
14
|
+
| "taxable"
|
|
15
|
+
| "weightUnit"
|
|
7
16
|
>;
|
|
8
17
|
|
|
9
18
|
type CartAttributeKey = string;
|
|
@@ -23,36 +32,34 @@ interface RedoCoverageClient {
|
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
type CartInfoToEnable = {
|
|
26
|
-
productId: string
|
|
27
|
-
variantId: string
|
|
28
|
-
cartAttribute: CartAttributeKey
|
|
29
|
-
selectedVariant: CartProductVariantFragment
|
|
30
|
-
}
|
|
35
|
+
productId: string;
|
|
36
|
+
variantId: string;
|
|
37
|
+
cartAttribute: CartAttributeKey;
|
|
38
|
+
selectedVariant: CartProductVariantFragment;
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
type RedoContextValue = {
|
|
33
|
-
enabled: boolean
|
|
34
|
-
loading: boolean
|
|
35
|
-
storeId?: string
|
|
36
|
-
cartInfoToEnable?: CartInfoToEnable
|
|
37
|
-
cart?: CartReturn | CartWithActionsDocs | OptimisticCart
|
|
38
|
-
errors?: RedoError[]
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
loading: boolean;
|
|
44
|
+
storeId?: string;
|
|
45
|
+
cartInfoToEnable?: CartInfoToEnable;
|
|
46
|
+
cart?: CartReturn | CartWithActionsDocs | OptimisticCart;
|
|
47
|
+
errors?: RedoError[];
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
enum RedoErrorType {
|
|
42
51
|
ApiBadRequest = "API_BAD_REQUEST",
|
|
43
52
|
ApiServerError = "API_SERVER_ERROR",
|
|
44
|
-
ApiUnknownError = "API_UNKNOWN_ERROR"
|
|
45
|
-
}
|
|
53
|
+
ApiUnknownError = "API_UNKNOWN_ERROR",
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
type RedoError = {
|
|
48
|
-
type: RedoErrorType
|
|
49
|
-
message: string
|
|
50
|
-
context:
|
|
57
|
+
type: RedoErrorType;
|
|
58
|
+
message: string;
|
|
59
|
+
context: Record<string, unknown>;
|
|
51
60
|
};
|
|
52
61
|
|
|
53
|
-
export {
|
|
54
|
-
RedoErrorType,
|
|
55
|
-
}
|
|
62
|
+
export { RedoErrorType };
|
|
56
63
|
|
|
57
64
|
export type {
|
|
58
65
|
CartAttributeKey,
|
|
@@ -60,5 +67,5 @@ export type {
|
|
|
60
67
|
RedoContextValue,
|
|
61
68
|
RedoCoverageClient,
|
|
62
69
|
CartProductVariantFragment,
|
|
63
|
-
RedoError
|
|
64
|
-
}
|
|
70
|
+
RedoError,
|
|
71
|
+
};
|