@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/.claude/settings.local.json +34 -0
- package/.github/workflows/ci.yml +56 -0
- package/.github/workflows/e2e.yml +72 -0
- package/.github/workflows/publish.yml +36 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +4 -0
- 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 +5 -4
- package/eslint.config.mjs +22 -0
- package/package.json +13 -3
- package/src/components/redo-checkout-buttons.tsx +58 -74
- package/src/components/redo-info-modal.tsx +486 -345
- package/src/hooks/use-purple-dot-preorder.ts +1 -4
- package/src/index.ts +12 -4
- package/src/providers/redo-coverage-client.tsx +69 -70
- package/src/svg.d.ts +2 -2
- package/src/types.ts +30 -23
- package/src/utils/cart.ts +137 -147
- package/src/utils/purple-dot.ts +5 -11
- package/src/utils/react-utils.ts +9 -14
- package/src/utils/security.ts +3 -8
- package/src/utils/timeout.ts +1 -6
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:
|
|
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?:
|
|
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 {
|
|
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.
|
|
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
|
|
2
|
-
import {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 ===
|
|
73
|
-
currencyCode =
|
|
69
|
+
if (currencyCode === "XXX") {
|
|
70
|
+
currencyCode = "USD";
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
const cartContainsRedo = !!
|
|
77
|
-
const combinedPrice = new Intl.NumberFormat(
|
|
78
|
-
style:
|
|
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(
|
|
79
|
+
if (!combinedPrice || !combinedPrice.length || combinedPrice.includes("NaN")) {
|
|
83
80
|
return null;
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
ui.html = ui.html.replaceAll(
|
|
83
|
+
ui.html = ui.html.replaceAll("%combinedPrice%", combinedPrice);
|
|
87
84
|
|
|
88
85
|
return ui;
|
|
89
|
-
}
|
|
86
|
+
};
|
|
90
87
|
|
|
91
|
-
const findAncestor = (
|
|
92
|
-
searchEl
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
}, [
|
|
130
|
-
|
|
131
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
193
|
-
|
|
176
|
+
{checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ""}
|
|
177
|
+
<div
|
|
194
178
|
dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
|
|
195
|
-
style={{
|
|
196
|
-
opacity:
|
|
197
|
-
transition:
|
|
179
|
+
style={{
|
|
180
|
+
opacity: buttonPending ? 0.25 : 1,
|
|
181
|
+
transition: "opacity 0.2s ease-in-out",
|
|
198
182
|
}}
|
|
199
183
|
/>
|
|
200
|
-
{
|
|
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
|