@redotech/redo-hydrogen 1.2.2 → 1.3.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
@@ -1,4 +1,4 @@
1
- import { CartReturn } from '@shopify/hydrogen';
1
+ import { CartReturn, OptimisticCart } from '@shopify/hydrogen';
2
2
  import { ReactNode, DependencyList } from 'react';
3
3
  import { CartWithActionsDocs } from '@shopify/hydrogen-react/dist/types/cart-types';
4
4
  import { ProductVariant } from '@shopify/hydrogen-react/storefront-api-types';
@@ -14,7 +14,7 @@ interface RedoCoverageClient {
14
14
  get eligible(): boolean;
15
15
  get price(): number | undefined;
16
16
  get storeId(): string | undefined;
17
- get cart(): CartReturn | CartWithActionsDocs | undefined;
17
+ get cart(): CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
18
18
  get cartProduct(): CartProductVariantFragment | undefined;
19
19
  get cartAttribute(): CartAttributeKey | undefined;
20
20
  get errors(): RedoError[] | undefined;
@@ -30,7 +30,7 @@ type RedoContextValue = {
30
30
  loading: boolean;
31
31
  storeId?: string;
32
32
  cartInfoToEnable?: CartInfoToEnable;
33
- cart?: CartReturn | CartWithActionsDocs;
33
+ cart?: CartReturn | CartWithActionsDocs | OptimisticCart;
34
34
  errors?: RedoError[];
35
35
  };
36
36
  declare enum RedoErrorType {
@@ -45,14 +45,14 @@ type RedoError = {
45
45
  };
46
46
 
47
47
  declare const RedoProvider: ({ cart, storeId, children }: {
48
- cart: CartReturn | CartWithActionsDocs;
48
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart;
49
49
  storeId: string;
50
50
  children: ReactNode;
51
51
  }) => ReactNode;
52
52
  declare const useRedoCoverageClient: () => RedoCoverageClient;
53
53
 
54
54
  declare const RedoCheckoutButtons: (props: {
55
- cart: CartReturn | CartWithActionsDocs;
55
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart;
56
56
  children?: ReactNode;
57
57
  onClick?: (enabled: boolean) => void;
58
58
  }) => react_jsx_runtime.JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redotech/redo-hydrogen",
3
- "version": "1.2.2",
3
+ "version": "1.3.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",
@@ -26,6 +26,7 @@
26
26
  "@rollup/plugin-node-resolve": "^16.0.0",
27
27
  "@rollup/plugin-terser": "^0.4.4",
28
28
  "@rollup/plugin-typescript": "^12.1.2",
29
+ "@svgr/rollup": "^8.1.0",
29
30
  "@types/react": "^19.0.8",
30
31
  "nodemon": "^3.1.9",
31
32
  "react": "^18.3.1",
package/rollup.config.js CHANGED
@@ -4,6 +4,7 @@ import typescript from "@rollup/plugin-typescript";
4
4
  import dts from "rollup-plugin-dts";
5
5
  import terser from "@rollup/plugin-terser";
6
6
  import peerDepsExternal from "rollup-plugin-peer-deps-external";
7
+ import svgr from '@svgr/rollup';
7
8
 
8
9
  const packageJson = require("./package.json");
9
10
 
@@ -30,6 +31,7 @@ export default [
30
31
  commonjs(),
31
32
  typescript({ tsconfig: "./tsconfig.json" }),
32
33
  terser(),
34
+ svgr(),
33
35
  ],
34
36
  external: ["react", "react-dom"],
35
37
  },
@@ -1,9 +1,5 @@
1
1
  import React, { MouseEvent, ReactNode, useEffect, useState } from "react";
2
- import {
3
- CartForm,
4
- CartActionInput,
5
- CartReturn,
6
- } from "@shopify/hydrogen";
2
+ import { CartForm, CartActionInput, CartReturn, OptimisticCart } from "@shopify/hydrogen";
7
3
  import { useRedoCoverageClient } from "../providers/redo-coverage-client";
8
4
  import { CartInfoToEnable, RedoCoverageClient } from "../types";
9
5
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
@@ -11,6 +7,9 @@ import { CurrencyCode } from "@shopify/hydrogen-react/storefront-api-types";
11
7
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
12
8
  import { getCartLines, isCartWithActionsDocs } from "../utils/cart";
13
9
 
10
+ import CircleSpinner from "../utils/circle-spinner.svg";
11
+ import { executeWithTimeout } from "../utils/timeout";
12
+
14
13
  type CheckoutButtonUIResponse = {
15
14
  html: string;
16
15
  css: string;
@@ -22,7 +21,7 @@ const getButtonsToShow = ({
22
21
  storeId
23
22
  }: {
24
23
  redoCoverageClient: RedoCoverageClient,
25
- cart: CartReturn | CartWithActionsDocs,
24
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart,
26
25
  storeId: string;
27
26
  }): Promise<CheckoutButtonUIResponse | null> => {
28
27
  return new Promise<CheckoutButtonUIResponse | null>((resolve, reject) => {
@@ -47,7 +46,7 @@ const getButtonsToShow = ({
47
46
  ui: json
48
47
  });
49
48
 
50
- if(!ui) {
49
+ if (!ui) {
51
50
  return reject(null);
52
51
  }
53
52
 
@@ -62,15 +61,15 @@ const applyButtonVariables = ({
62
61
  ui
63
62
  }: {
64
63
  redoCoverageClient: RedoCoverageClient,
65
- cart: CartReturn | CartWithActionsDocs,
64
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart,
66
65
  ui: CheckoutButtonUIResponse
67
66
  }): CheckoutButtonUIResponse | null => {
68
- if(!redoCoverageClient.eligible || !redoCoverageClient.price || !cart?.cost) {
67
+ if (!redoCoverageClient.eligible || !redoCoverageClient.price || !cart?.cost) {
69
68
  return null;
70
69
  }
71
70
 
72
- let currencyCode: CurrencyCode = cart.cost.totalAmount.currencyCode;
73
- if(currencyCode === 'XXX') {
71
+ let currencyCode: CurrencyCode = cart.cost.subtotalAmount.currencyCode;
72
+ if (currencyCode === 'XXX') {
74
73
  currencyCode = 'USD';
75
74
  }
76
75
 
@@ -78,9 +77,9 @@ const applyButtonVariables = ({
78
77
  const combinedPrice = new Intl.NumberFormat('en-US', {
79
78
  style: 'currency',
80
79
  currency: currencyCode
81
- }).format(Number(cart.cost.totalAmount.amount) + (cartContainsRedo ? 0 : redoCoverageClient.price));
80
+ }).format(Number(cart.cost.subtotalAmount.amount) + (cartContainsRedo ? 0 : redoCoverageClient.price));
82
81
 
83
- if(!combinedPrice || !combinedPrice.length || combinedPrice.includes('NaN')) {
82
+ if (!combinedPrice || !combinedPrice.length || combinedPrice.includes('NaN')) {
84
83
  return null;
85
84
  }
86
85
 
@@ -103,7 +102,7 @@ const findAncestor = (
103
102
  };
104
103
 
105
104
  const RedoCheckoutButtons = (props: {
106
- cart: CartReturn | CartWithActionsDocs;
105
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart;
107
106
  children?: ReactNode;
108
107
  onClick?: (enabled: boolean) => void;
109
108
  }) => {
@@ -112,23 +111,54 @@ const RedoCheckoutButtons = (props: {
112
111
  let checkoutUrl = redoCoverageClient.cart?.checkoutUrl || '/checkout';
113
112
  let [redoProductToAdd, setRedoProductToAdd] =
114
113
  useState<CartInfoToEnable | null>(null);
115
- let [checkoutButtonsUI, setCheckoutButtonsUI] = useState<CheckoutButtonUIResponse | null>(
116
- null
117
- );
114
+ let [checkoutButtonsUI, setCheckoutButtonsUI] =
115
+ useState<CheckoutButtonUIResponse | null>(null);
116
+
117
+ const [buttonPending, setButtonPending] = useState(false);
118
118
 
119
119
  useEffect(() => {
120
120
  (async () => {
121
- if(!redoCoverageClient.eligible || !cart || !redoCoverageClient.storeId) {
121
+ if (!redoCoverageClient.eligible || !cart || !redoCoverageClient.storeId) {
122
122
  return;
123
123
  }
124
124
 
125
125
  const buttons = await getButtonsToShow({ redoCoverageClient, cart, storeId: redoCoverageClient.storeId });
126
- if(buttons) {
126
+ if (buttons) {
127
127
  setCheckoutButtonsUI(buttons);
128
128
  }
129
129
  })();
130
130
  }, [cart, redoCoverageClient.eligible, redoCoverageClient.price, redoCoverageClient.storeId]);
131
131
 
132
+ /** 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) */
133
+ const DELAY_TO_ALLOW_CLICKING_AGAIN = 2000;
134
+ const TIMEOUT_FOR_CHECKOUTS = 8000;
135
+
136
+ const handleCoverageCheckoutClick = async (isCoverage: boolean) => {
137
+ if (!redoCoverageClient || !redoCoverageClient.enable || !redoCoverageClient.disable) {
138
+ console.error('Required redoCoverageClient methods not available');
139
+ return;
140
+ }
141
+
142
+ setButtonPending(true);
143
+ try {
144
+ const functionToCall = isCoverage ? redoCoverageClient.enable : redoCoverageClient.disable;
145
+ const result = await executeWithTimeout(
146
+ functionToCall(),
147
+ TIMEOUT_FOR_CHECKOUTS
148
+ );
149
+
150
+ if (props.onClick) {
151
+ await props.onClick(result);
152
+ }
153
+ } catch (e) {
154
+ console.error(e);
155
+ } finally {
156
+ setTimeout(() => {
157
+ setButtonPending(false);
158
+ }, DELAY_TO_ALLOW_CLICKING_AGAIN);
159
+ }
160
+ };
161
+
132
162
  const wrapperClickHandler = async (e: MouseEvent) => {
133
163
  let clickedElement = e.target as HTMLElement;
134
164
 
@@ -136,26 +166,21 @@ const RedoCheckoutButtons = (props: {
136
166
  return;
137
167
  }
138
168
 
139
- if (
140
- findAncestor(
141
- clickedElement,
142
- (el) => el.dataset?.target == "coverage-button"
143
- )
144
- ) {
145
- const attachResult = await redoCoverageClient.enable();
146
- if (props.onClick) {
147
- await props.onClick(attachResult);
148
- }
149
- window.location.href = checkoutUrl;
150
- } else if (
151
- findAncestor(
152
- clickedElement,
153
- (el) => el.dataset.target == "non-coverage-button"
154
- )
155
- ) {
156
- await redoCoverageClient.disable();
157
- if (props.onClick) {
158
- await props.onClick(false);
169
+ const isCoverageButton = findAncestor(
170
+ clickedElement,
171
+ (el) => el.dataset?.target == "coverage-button"
172
+ );
173
+
174
+ const isNonCoverageButton = findAncestor(
175
+ clickedElement,
176
+ (el) => el.dataset?.target == "non-coverage-button",
177
+ );
178
+
179
+ if (isCoverageButton || isNonCoverageButton) {
180
+ try {
181
+ await handleCoverageCheckoutClick(isCoverageButton ? true : false);
182
+ } catch (error) {
183
+ console.error('Failed to update coverage state:', error);
159
184
  }
160
185
  window.location.href = checkoutUrl;
161
186
  }
@@ -164,11 +189,32 @@ const RedoCheckoutButtons = (props: {
164
189
  return (
165
190
  <div>
166
191
  {checkoutButtonsUI ? (
167
- <div onClick={wrapperClickHandler}>
168
- {
169
- checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''
170
- }
171
- <div dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }} />
192
+ <div onClick={wrapperClickHandler} style={{ position: "relative" }}>
193
+ {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''}
194
+ <div
195
+ dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
196
+ style={{
197
+ opacity: (buttonPending) ? 0.25 : 1,
198
+ transition: 'opacity 0.2s ease-in-out'
199
+ }}
200
+ />
201
+ {(buttonPending) && (
202
+ <div
203
+ style={{
204
+ position: "absolute",
205
+ top: 0,
206
+ left: 0,
207
+ width: "100%",
208
+ height: "100%",
209
+ display: "flex",
210
+ justifyContent: "center",
211
+ alignItems: "center",
212
+ zIndex: 1,
213
+ }}
214
+ >
215
+ <CircleSpinner />
216
+ </div>
217
+ )}
172
218
  </div>
173
219
  ) : (
174
220
  props.children
@@ -1,9 +1,9 @@
1
1
  import { useFetcher } from "@remix-run/react";
2
- import { CartReturn } from "@shopify/hydrogen";
2
+ import { CartReturn, OptimisticCart } from "@shopify/hydrogen";
3
3
  import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { CartProductVariantFragment, CartAttributeKey, CartInfoToEnable, RedoContextValue, RedoCoverageClient, RedoError, RedoErrorType } from "../types";
5
5
  import { REDO_PUBLIC_API_HOSTNAME } from "../utils/security";
6
- import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise, isCartWithActionsDocs, getCartLines, useWaitCartIdle } from "../utils/cart";
6
+ import { addProductToCartIfNeeded, removeProductFromCartIfNeeded, setCartRedoEnabledAttribute, useFetcherWithPromise, isCartWithActionsDocs, getCartLines, useWaitCartIdle, isOptimisticCart } from "../utils/cart";
7
7
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
8
8
 
9
9
  const DEFAULT_REDO_CONTEXT_VALUE: RedoContextValue = {
@@ -18,7 +18,7 @@ const RedoProvider = ({
18
18
  storeId,
19
19
  children
20
20
  }: {
21
- cart: CartReturn | CartWithActionsDocs,
21
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart,
22
22
  storeId: string,
23
23
  children: ReactNode,
24
24
  }): ReactNode => {
@@ -37,7 +37,7 @@ const RedoProvider = ({
37
37
  }
38
38
 
39
39
  useEffect(() => {
40
- if(!cart || !storeId) {
40
+ if(!cart || !storeId || isOptimisticCart(cart)) {
41
41
  return;
42
42
  }
43
43
 
package/src/svg.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.svg' {
2
+ const content: string;
3
+ export default content;
4
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CartReturn } from "@shopify/hydrogen";
1
+ 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
 
@@ -16,7 +16,7 @@ interface RedoCoverageClient {
16
16
  get eligible(): boolean;
17
17
  get price(): number | undefined;
18
18
  get storeId(): string | undefined;
19
- get cart(): CartReturn | CartWithActionsDocs | undefined;
19
+ get cart(): CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
20
20
  get cartProduct(): CartProductVariantFragment | undefined;
21
21
  get cartAttribute(): CartAttributeKey | undefined;
22
22
  get errors(): RedoError[] | undefined;
@@ -34,7 +34,7 @@ type RedoContextValue = {
34
34
  loading: boolean,
35
35
  storeId?: string,
36
36
  cartInfoToEnable?: CartInfoToEnable,
37
- cart?: CartReturn | CartWithActionsDocs,
37
+ cart?: CartReturn | CartWithActionsDocs | OptimisticCart,
38
38
  errors?: RedoError[],
39
39
  };
40
40
 
package/src/utils/cart.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { FetcherWithComponents, useFetcher } from "@remix-run/react";
2
2
  import { CartInfoToEnable } from "../types";
3
- import { CartForm, CartReturn } from "@shopify/hydrogen";
3
+ import { CartForm, CartReturn, OptimisticCart, OptimisticCartLine } from "@shopify/hydrogen";
4
4
  import type { AppData } from '@remix-run/react/dist/data';
5
5
  import React, { useCallback, useEffect, useRef } from 'react'
6
6
  import { CartWithActionsDocs } from "@shopify/hydrogen-react/dist/types/cart-types";
@@ -8,22 +8,29 @@ import { CartLine, ComponentizableCartLine } from "@shopify/hydrogen-react/store
8
8
 
9
9
  const DEFAULT_REDO_ENABLED_CART_ATTRIBUTE = 'redo_opted_in_from_cart';
10
10
 
11
- const isCartWithActionsDocs = (cart: CartReturn | CartWithActionsDocs): cart is CartWithActionsDocs => {
11
+ const isCartWithActionsDocs = (cart: CartReturn | CartWithActionsDocs| OptimisticCart): cart is CartWithActionsDocs => {
12
12
  return (Array.isArray(cart.lines) && 'linesAdd' in cart && typeof cart.linesAdd === 'function');
13
13
  }
14
14
 
15
- const getCartLines = (cart: CartReturn | CartWithActionsDocs): Array<CartLine | ComponentizableCartLine> => {
16
- if(isCartWithActionsDocs(cart)) {
15
+ const getCartLines = (cart: CartReturn | CartWithActionsDocs | OptimisticCart): Array<CartLine | ComponentizableCartLine> => {
16
+ if (isOptimisticCart(cart)) {
17
+ return cart.lines.nodes;
18
+ } else if (isCartWithActionsDocs(cart)) {
17
19
  return cart.lines;
18
20
  } else {
19
21
  return cart.lines.nodes ?? cart.lines.edges.map((edge) => edge.node);
20
22
  }
21
23
  }
22
24
 
25
+ // https://shopify.dev/docs/api/hydrogen/2025-01/hooks/useoptimisticcart
26
+ const isOptimisticCart = (cart: CartReturn | CartWithActionsDocs | OptimisticCart): cart is OptimisticCart => {
27
+ return 'isOptimistic' in cart && (cart.isOptimistic ?? false);
28
+ }
29
+
23
30
  const isRedoInCart = ({
24
31
  cart
25
32
  }: {
26
- cart: CartReturn | CartWithActionsDocs
33
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart
27
34
  }): boolean => {
28
35
  if(!cart) {
29
36
  return false;
@@ -65,7 +72,7 @@ const addProductToCartIfNeeded = async ({
65
72
  waitCartIdle,
66
73
  cartInfoToEnable
67
74
  }: {
68
- cart: CartReturn | CartWithActionsDocs | undefined,
75
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
69
76
  fetcher: FetcherWithComponents<unknown>,
70
77
  waitCartIdle: WaitCartIdleCallback;
71
78
  cartInfoToEnable: CartInfoToEnable
@@ -101,7 +108,7 @@ const removeLinesFromCart = async ({
101
108
  waitCartIdle,
102
109
  lineIds
103
110
  }: {
104
- cart: CartReturn | CartWithActionsDocs | undefined;
111
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
105
112
  fetcher: FetcherWithComponents<unknown>;
106
113
  waitCartIdle: WaitCartIdleCallback;
107
114
  lineIds: string[];
@@ -132,7 +139,7 @@ const removeProductFromCartIfNeeded = async ({
132
139
  waitCartIdle,
133
140
  cartInfoToEnable
134
141
  }: {
135
- cart: CartReturn | CartWithActionsDocs | undefined,
142
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
136
143
  fetcher: FetcherWithComponents<unknown>,
137
144
  waitCartIdle: WaitCartIdleCallback
138
145
  cartInfoToEnable: CartInfoToEnable
@@ -159,7 +166,7 @@ const addProductToCart = async ({
159
166
  cartInfoToEnable,
160
167
  }: {
161
168
  waitCartIdle: WaitCartIdleCallback;
162
- cart: CartReturn | CartWithActionsDocs | undefined,
169
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined,
163
170
  fetcher: FetcherWithComponents<unknown>,
164
171
  cartInfoToEnable: CartInfoToEnable
165
172
  }) => {
@@ -197,7 +204,7 @@ const setCartRedoEnabledAttribute = async ({
197
204
  cartInfoToEnable,
198
205
  enabled
199
206
  }: {
200
- cart: CartReturn | CartWithActionsDocs | undefined;
207
+ cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined;
201
208
  fetcher: FetcherWithComponents<unknown>;
202
209
  waitCartIdle: WaitCartIdleCallback;
203
210
  cartInfoToEnable: CartInfoToEnable | null;
@@ -270,17 +277,17 @@ function useFetcherWithPromise<TData = AppData>(opts?: Parameters<typeof useFetc
270
277
  return { ...fetcher, submit }
271
278
  }
272
279
 
273
- type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs>;
280
+ type WaitCartIdleCallback = () => Promise<CartReturn | CartWithActionsDocs | OptimisticCart>;
274
281
 
275
282
  // This function allows us to await a cart idle state without breaking React rules.
276
283
  // It returns a function, which returns a promise, which will resolve once the cart value passed in reaches an idle state.
277
284
  // Not intended for use with CartReturn, but will accept that value if passed in to avoid breaking rules of hooks
278
- const useWaitCartIdle = (cart: CartReturn | CartWithActionsDocs | undefined) => {
285
+ const useWaitCartIdle = (cart: CartReturn | CartWithActionsDocs | OptimisticCart | undefined) => {
279
286
  const resolveRef = useRef<any>(null)
280
287
  const promiseRef = useRef<any>(null)
281
288
 
282
289
  if (!promiseRef.current) {
283
- promiseRef.current = new Promise<CartReturn | CartWithActionsDocs>((resolve) => {
290
+ promiseRef.current = new Promise<CartReturn | CartWithActionsDocs | OptimisticCart>((resolve) => {
284
291
  resolveRef.current = resolve
285
292
  })
286
293
  }
@@ -327,5 +334,6 @@ export {
327
334
  useFetcherWithPromise,
328
335
  useWaitCartIdle,
329
336
  isCartWithActionsDocs,
330
- getCartLines
337
+ getCartLines,
338
+ isOptimisticCart
331
339
  };
@@ -0,0 +1,24 @@
1
+ <svg
2
+ width="24"
3
+ height="24"
4
+ viewBox="0 0 24 24"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ >
7
+ <path
8
+ d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
9
+ fill="currentColor"
10
+ opacity=".25"
11
+ />
12
+ <path
13
+ d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
14
+ fill="currentColor"
15
+ >
16
+ <animateTransform
17
+ attributeName="transform"
18
+ type="rotate"
19
+ dur="0.75s"
20
+ values="0 12 12;360 12 12"
21
+ repeatCount="indefinite"
22
+ />
23
+ </path>
24
+ </svg>
@@ -0,0 +1,12 @@
1
+ export async function executeWithTimeout<T, E extends Error>(
2
+ promise: Promise<T>,
3
+ timeoutMs: number,
4
+ error: E = new Error("timeout") as E,
5
+ ): Promise<T> {
6
+ return Promise.race([
7
+ promise,
8
+ new Promise<never>((_, reject) =>
9
+ setTimeout(() => reject(error), timeoutMs),
10
+ ),
11
+ ]);
12
+ }
package/tsconfig.json CHANGED
@@ -16,5 +16,5 @@
16
16
  "noEmit": true,
17
17
  "jsx": "react-jsx"
18
18
  },
19
- "include": ["src", "src/index.ts"]
19
+ "include": ["src", "src/index.ts", "src/svg.d.ts"]
20
20
  }