@redotech/redo-hydrogen 1.4.7 → 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 +8 -0
- 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 +5 -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 +1 -4
- package/src/index.ts +12 -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 +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": "
|
|
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
|
-
"@
|
|
21
|
-
"@shopify/hydrogen": ">=
|
|
22
|
-
"
|
|
23
|
-
"react-
|
|
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": "
|
|
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": "^
|
|
32
|
-
"@types/react-dom": "^
|
|
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
|
-
"
|
|
35
|
-
"react
|
|
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
|
|
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 } 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(() => {
|
|
@@ -128,24 +116,21 @@ const RedoCheckoutButtons = (props: {
|
|
|
128
116
|
})();
|
|
129
117
|
}, [cart, redoCoverageClient.eligible, redoCoverageClient.price, redoCoverageClient.storeId]);
|
|
130
118
|
|
|
131
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
193
|
-
|
|
171
|
+
{checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ""}
|
|
172
|
+
<div
|
|
194
173
|
dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
|
|
195
|
-
style={{
|
|
196
|
-
opacity:
|
|
197
|
-
transition:
|
|
174
|
+
style={{
|
|
175
|
+
opacity: buttonPending ? 0.25 : 1,
|
|
176
|
+
transition: "opacity 0.2s ease-in-out",
|
|
198
177
|
}}
|
|
199
178
|
/>
|
|
200
|
-
{
|
|
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
|