@redotech/redo-hydrogen 1.4.7 → 1.4.8

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
  }
@@ -97,4 +97,5 @@ declare const RedoInfoCard: ({ showInfoIcon, onInfoClick, infoCardImageUrl, info
97
97
  */
98
98
  declare function useDisablePurpleDotPreorder(disablePreorderButtons: boolean): void;
99
99
 
100
- export { type CartAttributeKey, type CartInfoToEnable, type CartProductVariantFragment, type LoadState, type Loader, REDO_REQUIRED_HOSTNAMES, RedoCheckoutButtons, type RedoContextValue, type RedoCoverageClient, type RedoError, RedoErrorType, RedoInfoCard, RedoProvider, useDisablePurpleDotPreorder, useLoad, useRedoCoverageClient };
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.7",
3
+ "version": "1.4.8",
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",
@@ -30,14 +36,18 @@
30
36
  "@svgr/rollup": "^8.1.0",
31
37
  "@types/react": "^19.0.8",
32
38
  "@types/react-dom": "^19.0.4",
39
+ "eslint": "^10.1.0",
40
+ "eslint-config-prettier": "^10.1.8",
33
41
  "nodemon": "^3.1.9",
42
+ "prettier": "^3.8.1",
34
43
  "react": "^18.3.1",
35
44
  "react-dom": "^18.3.1",
36
45
  "rollup": "^4.32.1",
37
46
  "rollup-plugin-dts": "^6.1.1",
38
47
  "rollup-plugin-peer-deps-external": "^2.2.4",
39
48
  "tslib": "^2.8.1",
40
- "typescript": "^5.7.3"
49
+ "typescript": "^5.7.3",
50
+ "typescript-eslint": "^8.57.1"
41
51
  },
42
52
  "keywords": [
43
53
  "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, getCartEligibilityPriceKey } 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(() => {
@@ -126,26 +114,28 @@ const RedoCheckoutButtons = (props: {
126
114
  setCheckoutButtonsUI(buttons);
127
115
  }
128
116
  })();
129
- }, [cart, redoCoverageClient.eligible, redoCoverageClient.price, redoCoverageClient.storeId]);
130
-
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) */
117
+ }, [
118
+ getCartEligibilityPriceKey(cart),
119
+ redoCoverageClient.eligible,
120
+ redoCoverageClient.price,
121
+ redoCoverageClient.storeId,
122
+ ]);
123
+
124
+ /** 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
125
  const DELAY_TO_ALLOW_CLICKING_AGAIN = 2000;
133
126
  const TIMEOUT_FOR_CHECKOUTS = 8000;
134
-
127
+
135
128
  const handleCoverageCheckoutClick = async (isCoverage: boolean) => {
136
129
  if (!redoCoverageClient || !redoCoverageClient.enable || !redoCoverageClient.disable) {
137
- console.error('Required redoCoverageClient methods not available');
130
+ console.error("Required redoCoverageClient methods not available");
138
131
  return;
139
132
  }
140
133
 
141
134
  setButtonPending(true);
142
135
  try {
143
136
  const functionToCall = isCoverage ? redoCoverageClient.enable : redoCoverageClient.disable;
144
- const result = await executeWithTimeout(
145
- functionToCall(),
146
- TIMEOUT_FOR_CHECKOUTS
147
- );
148
-
137
+ const result = await executeWithTimeout(functionToCall(), TIMEOUT_FOR_CHECKOUTS);
138
+
149
139
  if (props.onClick) {
150
140
  await props.onClick(result);
151
141
  }
@@ -159,27 +149,21 @@ const RedoCheckoutButtons = (props: {
159
149
  };
160
150
 
161
151
  const wrapperClickHandler = async (e: MouseEvent) => {
162
- let clickedElement = e.target as HTMLElement;
152
+ const clickedElement = e.target as HTMLElement;
163
153
 
164
154
  if (!clickedElement.dataset) {
165
155
  return;
166
156
  }
167
157
 
168
- const isCoverageButton = findAncestor(
169
- clickedElement,
170
- (el) => el.dataset?.target == "coverage-button"
171
- );
158
+ const isCoverageButton = findAncestor(clickedElement, (el) => el.dataset?.target === "coverage-button");
172
159
 
173
- const isNonCoverageButton = findAncestor(
174
- clickedElement,
175
- (el) => el.dataset?.target == "non-coverage-button",
176
- );
160
+ const isNonCoverageButton = findAncestor(clickedElement, (el) => el.dataset?.target === "non-coverage-button");
177
161
 
178
162
  if (isCoverageButton || isNonCoverageButton) {
179
163
  try {
180
164
  await handleCoverageCheckoutClick(isCoverageButton ? true : false);
181
165
  } catch (error) {
182
- console.error('Failed to update coverage state:', error);
166
+ console.error("Failed to update coverage state:", error);
183
167
  }
184
168
  window.location.href = checkoutUrl;
185
169
  }
@@ -189,15 +173,15 @@ const RedoCheckoutButtons = (props: {
189
173
  <div>
190
174
  {checkoutButtonsUI ? (
191
175
  <div onClick={wrapperClickHandler} style={{ position: "relative" }}>
192
- {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''}
193
- <div
176
+ {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ""}
177
+ <div
194
178
  dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
195
- style={{
196
- opacity: (buttonPending) ? 0.25 : 1,
197
- transition: 'opacity 0.2s ease-in-out'
179
+ style={{
180
+ opacity: buttonPending ? 0.25 : 1,
181
+ transition: "opacity 0.2s ease-in-out",
198
182
  }}
199
183
  />
200
- {(buttonPending) && (
184
+ {buttonPending && (
201
185
  <div
202
186
  style={{
203
187
  position: "absolute",
@@ -213,7 +197,7 @@ const RedoCheckoutButtons = (props: {
213
197
  >
214
198
  <CircleSpinner />
215
199
  </div>
216
- )}
200
+ )}
217
201
  </div>
218
202
  ) : (
219
203
  props.children