@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/dist/types.d.ts CHANGED
@@ -41,10 +41,10 @@ declare enum RedoErrorType {
41
41
  type RedoError = {
42
42
  type: RedoErrorType;
43
43
  message: string;
44
- context: any;
44
+ context: Record<string, unknown>;
45
45
  };
46
46
 
47
- declare const RedoProvider: ({ cart, storeId, children }: {
47
+ declare const RedoProvider: ({ cart, storeId, children, }: {
48
48
  cart: CartReturn | CartWithActionsDocs | OptimisticCart;
49
49
  storeId: string;
50
50
  children: ReactNode;
@@ -62,7 +62,7 @@ interface Loader<T> {
62
62
  (abort: AbortSignal): Promise<T>;
63
63
  }
64
64
  interface LoadState<T> {
65
- error?: any;
65
+ error?: unknown;
66
66
  pending: boolean;
67
67
  value?: T;
68
68
  }
@@ -78,4 +78,24 @@ interface RedoInfoModalProps {
78
78
  }
79
79
  declare const RedoInfoCard: ({ showInfoIcon, onInfoClick, infoCardImageUrl, infoModalLogoUrl, infoModalImageUrl, infoModalContent, }: RedoInfoModalProps) => react_jsx_runtime.JSX.Element;
80
80
 
81
- export { type CartAttributeKey, type CartInfoToEnable, type CartProductVariantFragment, type LoadState, type Loader, REDO_REQUIRED_HOSTNAMES, RedoCheckoutButtons, type RedoContextValue, type RedoCoverageClient, type RedoError, RedoErrorType, RedoInfoCard, RedoProvider, useLoad, useRedoCoverageClient };
81
+ /**
82
+ * React hook to disable add-to-cart buttons when a Purple Dot preorder element is present.
83
+ * Watches for DOM changes and automatically disables/enables buttons based on the presence
84
+ * of the <purple-dot-learn-more> element.
85
+ *
86
+ * @param disablePreorderButtons - When true, enables the preorder button disabling logic.
87
+ * When false, the hook does nothing.
88
+ *
89
+ * Usage:
90
+ * ```tsx
91
+ * function ProductPage() {
92
+ * const isShopOnSiteActive = true; // your condition here
93
+ * useDisablePurpleDotPreorder(isShopOnSiteActive);
94
+ * return <div>...</div>;
95
+ * }
96
+ * ```
97
+ */
98
+ declare function useDisablePurpleDotPreorder(disablePreorderButtons: boolean): void;
99
+
100
+ export { REDO_REQUIRED_HOSTNAMES, RedoCheckoutButtons, RedoErrorType, RedoInfoCard, RedoProvider, useDisablePurpleDotPreorder, useLoad, useRedoCoverageClient };
101
+ export type { CartAttributeKey, CartInfoToEnable, CartProductVariantFragment, LoadState, Loader, RedoContextValue, RedoCoverageClient, RedoError };
@@ -0,0 +1,22 @@
1
+ import tseslint from "typescript-eslint";
2
+ import prettier from "eslint-config-prettier";
3
+
4
+ export default tseslint.config(
5
+ tseslint.configs.recommended,
6
+ prettier,
7
+ { ignores: ["dist/", "node_modules/"] },
8
+ {
9
+ rules: {
10
+ "@typescript-eslint/no-unused-vars": ["error", {
11
+ argsIgnorePattern: "^_",
12
+ varsIgnorePattern: "^_",
13
+ caughtErrorsIgnorePattern: "^_",
14
+ }],
15
+ "@typescript-eslint/no-explicit-any": "error",
16
+ "prefer-const": "error",
17
+ "no-empty": ["error", { allowEmptyCatch: false }],
18
+ "no-extra-boolean-cast": "error",
19
+ eqeqeq: ["error", "always"],
20
+ },
21
+ },
22
+ );
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@redotech/redo-hydrogen",
3
- "version": "1.4.6",
3
+ "version": "2.0.0",
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
7
  "types": "dist/types.d.ts",
8
8
  "scripts": {
9
- "dev": "rollup -c --watch --bundleConfigAsCjs"
9
+ "dev": "rollup -c --watch --bundleConfigAsCjs",
10
+ "build": "rollup -c --bundleConfigAsCjs",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint src/",
13
+ "format": "prettier --write \"src/**/*.{ts,tsx}\"",
14
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
15
+ "e2e": "npm run build && cd e2e && npx playwright test"
10
16
  },
11
17
  "repository": {
12
18
  "type": "git",
@@ -17,27 +23,34 @@
17
23
  "url": "https://github.com/redoapp/redo-hydrogen/issues"
18
24
  },
19
25
  "peerDependencies": {
20
- "@remix-run/react": "^2.8.0",
21
- "@shopify/hydrogen": ">=2024.3.1",
22
- "@shopify/hydrogen-react": ">=2024.3.1",
23
- "react-dom": "^18.3.1"
26
+ "@shopify/hydrogen": ">=2025.7.0",
27
+ "@shopify/hydrogen-react": ">=2025.7.0",
28
+ "react-dom": ">=18.3.1",
29
+ "react-router": "^7.0.0"
24
30
  },
25
31
  "devDependencies": {
26
32
  "@rollup/plugin-commonjs": "^28.0.2",
27
33
  "@rollup/plugin-node-resolve": "^16.0.0",
28
34
  "@rollup/plugin-terser": "^0.4.4",
29
- "@rollup/plugin-typescript": "^12.1.2",
35
+ "@rollup/plugin-typescript": "12.1.2",
36
+ "@shopify/hydrogen": "2025.7.0",
37
+ "@shopify/hydrogen-react": "2025.7.0",
30
38
  "@svgr/rollup": "^8.1.0",
31
- "@types/react": "^19.0.8",
32
- "@types/react-dom": "^19.0.4",
39
+ "@types/react": "^18.2.22",
40
+ "@types/react-dom": "^18.2.7",
41
+ "eslint": "^10.1.0",
42
+ "eslint-config-prettier": "^10.1.8",
33
43
  "nodemon": "^3.1.9",
34
- "react": "^18.3.1",
35
- "react-dom": "^18.3.1",
44
+ "prettier": "^3.8.1",
45
+ "react": "18.3.1",
46
+ "react-dom": "18.3.1",
47
+ "react-router": "7.9.2",
36
48
  "rollup": "^4.32.1",
37
49
  "rollup-plugin-dts": "^6.1.1",
38
50
  "rollup-plugin-peer-deps-external": "^2.2.4",
39
51
  "tslib": "^2.8.1",
40
- "typescript": "^5.7.3"
52
+ "typescript": "^5.7.3",
53
+ "typescript-eslint": "^8.57.1"
41
54
  },
42
55
  "keywords": [
43
56
  "Redo",
@@ -1,11 +1,11 @@
1
- import React, { MouseEvent, ReactNode, useEffect, useState } from "react";
2
- import { CartForm, CartActionInput, CartReturn, OptimisticCart } from "@shopify/hydrogen";
1
+ import { MouseEvent, ReactNode, useEffect, useState } from "react";
2
+ import { CartReturn, OptimisticCart } from "@shopify/hydrogen";
3
3
  import { useRedoCoverageClient } from "../providers/redo-coverage-client";
4
- import { CartInfoToEnable, RedoCoverageClient } from "../types";
4
+ import { RedoCoverageClient } from "../types";
5
5
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
6
6
  import { CurrencyCode } from "@shopify/hydrogen-react/storefront-api-types";
7
7
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
8
- import { getCartLines, isCartWithActionsDocs } from "../utils/cart";
8
+ import { getCartLines } from "../utils/cart";
9
9
 
10
10
  import CircleSpinner from "../utils/circle-spinner.svg";
11
11
  import { executeWithTimeout } from "../utils/timeout";
@@ -18,23 +18,20 @@ type CheckoutButtonUIResponse = {
18
18
  const getButtonsToShow = ({
19
19
  redoCoverageClient,
20
20
  cart,
21
- storeId
21
+ storeId,
22
22
  }: {
23
- redoCoverageClient: RedoCoverageClient,
24
- cart: CartReturn | CartWithActionsDocs | OptimisticCart,
23
+ redoCoverageClient: RedoCoverageClient;
24
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart;
25
25
  storeId: string;
26
26
  }): Promise<CheckoutButtonUIResponse | null> => {
27
27
  return new Promise<CheckoutButtonUIResponse | null>((resolve, reject) => {
28
- fetch(
29
- `https://${REDO_PUBLIC_API_HOSTNAME}/v2.2/stores/${storeId}/checkout-buttons-ui`,
30
- {
31
- method: "GET",
32
- headers: {
33
- "Content-Type": "application/json",
34
- },
35
- }
36
- ).then(async (res) => {
37
- let json = await res.json();
28
+ fetch(`https://${REDO_PUBLIC_API_HOSTNAME}/v2.2/stores/${storeId}/checkout-buttons-ui`, {
29
+ method: "GET",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ },
33
+ }).then(async (res) => {
34
+ const json = await res.json();
38
35
 
39
36
  if (!json.html) {
40
37
  return resolve(null);
@@ -43,7 +40,7 @@ const getButtonsToShow = ({
43
40
  const ui = applyButtonVariables({
44
41
  redoCoverageClient,
45
42
  cart,
46
- ui: json
43
+ ui: json,
47
44
  });
48
45
 
49
46
  if (!ui) {
@@ -58,41 +55,38 @@ const getButtonsToShow = ({
58
55
  const applyButtonVariables = ({
59
56
  redoCoverageClient,
60
57
  cart,
61
- ui
58
+ ui,
62
59
  }: {
63
- redoCoverageClient: RedoCoverageClient,
64
- cart: CartReturn | CartWithActionsDocs | OptimisticCart,
65
- ui: CheckoutButtonUIResponse
60
+ redoCoverageClient: RedoCoverageClient;
61
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart;
62
+ ui: CheckoutButtonUIResponse;
66
63
  }): CheckoutButtonUIResponse | null => {
67
64
  if (!redoCoverageClient.eligible || !redoCoverageClient.price || !cart?.cost) {
68
65
  return null;
69
66
  }
70
67
 
71
68
  let currencyCode: CurrencyCode = cart.cost.subtotalAmount.currencyCode;
72
- if (currencyCode === 'XXX') {
73
- currencyCode = 'USD';
69
+ if (currencyCode === "XXX") {
70
+ currencyCode = "USD";
74
71
  }
75
72
 
76
- const cartContainsRedo = !!(getCartLines(cart).some((cartItem) => cartItem.merchandise?.product?.vendor === 're:do'));
77
- const combinedPrice = new Intl.NumberFormat('en-US', {
78
- style: 'currency',
79
- currency: currencyCode
73
+ const cartContainsRedo = !!getCartLines(cart).some((cartItem) => cartItem.merchandise?.product?.vendor === "re:do");
74
+ const combinedPrice = new Intl.NumberFormat("en-US", {
75
+ style: "currency",
76
+ currency: currencyCode,
80
77
  }).format(Number(cart.cost.subtotalAmount.amount) + (cartContainsRedo ? 0 : redoCoverageClient.price));
81
78
 
82
- if (!combinedPrice || !combinedPrice.length || combinedPrice.includes('NaN')) {
79
+ if (!combinedPrice || !combinedPrice.length || combinedPrice.includes("NaN")) {
83
80
  return null;
84
81
  }
85
82
 
86
- ui.html = ui.html.replaceAll('%combinedPrice%', combinedPrice);
83
+ ui.html = ui.html.replaceAll("%combinedPrice%", combinedPrice);
87
84
 
88
85
  return ui;
89
- }
86
+ };
90
87
 
91
- const findAncestor = (
92
- searchEl: HTMLElement | null,
93
- findFn: (el: HTMLElement) => boolean
94
- ) => {
95
- if (searchEl == null) {
88
+ const findAncestor = (searchEl: HTMLElement | null, findFn: (el: HTMLElement) => boolean) => {
89
+ if (searchEl === null) {
96
90
  return null;
97
91
  } else if (findFn(searchEl)) {
98
92
  return searchEl;
@@ -101,18 +95,12 @@ const findAncestor = (
101
95
  }
102
96
  };
103
97
 
104
- const RedoCheckoutButtons = (props: {
105
- children?: ReactNode;
106
- onClick?: (enabled: boolean) => void;
107
- }) => {
98
+ const RedoCheckoutButtons = (props: { children?: ReactNode; onClick?: (enabled: boolean) => void }) => {
108
99
  const redoCoverageClient = useRedoCoverageClient();
109
- let cart = redoCoverageClient.cart;
110
- let checkoutUrl = redoCoverageClient.cart?.checkoutUrl || '/checkout';
111
- let [redoProductToAdd, setRedoProductToAdd] =
112
- useState<CartInfoToEnable | null>(null);
113
- let [checkoutButtonsUI, setCheckoutButtonsUI] =
114
- useState<CheckoutButtonUIResponse | null>(null);
115
-
100
+ const cart = redoCoverageClient.cart;
101
+ const checkoutUrl = redoCoverageClient.cart?.checkoutUrl || "/checkout";
102
+ const [checkoutButtonsUI, setCheckoutButtonsUI] = useState<CheckoutButtonUIResponse | null>(null);
103
+
116
104
  const [buttonPending, setButtonPending] = useState(false);
117
105
 
118
106
  useEffect(() => {
@@ -128,24 +116,21 @@ const RedoCheckoutButtons = (props: {
128
116
  })();
129
117
  }, [cart, redoCoverageClient.eligible, redoCoverageClient.price, redoCoverageClient.storeId]);
130
118
 
131
- /** To avoid the inevitable spammers trying to checkout faster by clicking over and over, between the time the promise resolves and the new tab opens (or errors) */
119
+ /** To avoid the inevitable spammers trying to checkout faster by clicking over and over, between the time the promise resolves and the new tab opens (or errors) */
132
120
  const DELAY_TO_ALLOW_CLICKING_AGAIN = 2000;
133
121
  const TIMEOUT_FOR_CHECKOUTS = 8000;
134
-
122
+
135
123
  const handleCoverageCheckoutClick = async (isCoverage: boolean) => {
136
124
  if (!redoCoverageClient || !redoCoverageClient.enable || !redoCoverageClient.disable) {
137
- console.error('Required redoCoverageClient methods not available');
125
+ console.error("Required redoCoverageClient methods not available");
138
126
  return;
139
127
  }
140
128
 
141
129
  setButtonPending(true);
142
130
  try {
143
131
  const functionToCall = isCoverage ? redoCoverageClient.enable : redoCoverageClient.disable;
144
- const result = await executeWithTimeout(
145
- functionToCall(),
146
- TIMEOUT_FOR_CHECKOUTS
147
- );
148
-
132
+ const result = await executeWithTimeout(functionToCall(), TIMEOUT_FOR_CHECKOUTS);
133
+
149
134
  if (props.onClick) {
150
135
  await props.onClick(result);
151
136
  }
@@ -159,27 +144,21 @@ const RedoCheckoutButtons = (props: {
159
144
  };
160
145
 
161
146
  const wrapperClickHandler = async (e: MouseEvent) => {
162
- let clickedElement = e.target as HTMLElement;
147
+ const clickedElement = e.target as HTMLElement;
163
148
 
164
149
  if (!clickedElement.dataset) {
165
150
  return;
166
151
  }
167
152
 
168
- const isCoverageButton = findAncestor(
169
- clickedElement,
170
- (el) => el.dataset?.target == "coverage-button"
171
- );
153
+ const isCoverageButton = findAncestor(clickedElement, (el) => el.dataset?.target === "coverage-button");
172
154
 
173
- const isNonCoverageButton = findAncestor(
174
- clickedElement,
175
- (el) => el.dataset?.target == "non-coverage-button",
176
- );
155
+ const isNonCoverageButton = findAncestor(clickedElement, (el) => el.dataset?.target === "non-coverage-button");
177
156
 
178
157
  if (isCoverageButton || isNonCoverageButton) {
179
158
  try {
180
159
  await handleCoverageCheckoutClick(isCoverageButton ? true : false);
181
160
  } catch (error) {
182
- console.error('Failed to update coverage state:', error);
161
+ console.error("Failed to update coverage state:", error);
183
162
  }
184
163
  window.location.href = checkoutUrl;
185
164
  }
@@ -189,15 +168,15 @@ const RedoCheckoutButtons = (props: {
189
168
  <div>
190
169
  {checkoutButtonsUI ? (
191
170
  <div onClick={wrapperClickHandler} style={{ position: "relative" }}>
192
- {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''}
193
- <div
171
+ {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ""}
172
+ <div
194
173
  dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
195
- style={{
196
- opacity: (buttonPending) ? 0.25 : 1,
197
- transition: 'opacity 0.2s ease-in-out'
174
+ style={{
175
+ opacity: buttonPending ? 0.25 : 1,
176
+ transition: "opacity 0.2s ease-in-out",
198
177
  }}
199
178
  />
200
- {(buttonPending) && (
179
+ {buttonPending && (
201
180
  <div
202
181
  style={{
203
182
  position: "absolute",
@@ -213,7 +192,7 @@ const RedoCheckoutButtons = (props: {
213
192
  >
214
193
  <CircleSpinner />
215
194
  </div>
216
- )}
195
+ )}
217
196
  </div>
218
197
  ) : (
219
198
  props.children