@redotech/redo-hydrogen 1.2.2 → 1.3.1

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,13 @@ 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;
56
55
  children?: ReactNode;
57
56
  onClick?: (enabled: boolean) => void;
58
57
  }) => 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.1",
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,6 @@ const findAncestor = (
103
102
  };
104
103
 
105
104
  const RedoCheckoutButtons = (props: {
106
- cart: CartReturn | CartWithActionsDocs;
107
105
  children?: ReactNode;
108
106
  onClick?: (enabled: boolean) => void;
109
107
  }) => {
@@ -112,23 +110,54 @@ const RedoCheckoutButtons = (props: {
112
110
  let checkoutUrl = redoCoverageClient.cart?.checkoutUrl || '/checkout';
113
111
  let [redoProductToAdd, setRedoProductToAdd] =
114
112
  useState<CartInfoToEnable | null>(null);
115
- let [checkoutButtonsUI, setCheckoutButtonsUI] = useState<CheckoutButtonUIResponse | null>(
116
- null
117
- );
113
+ let [checkoutButtonsUI, setCheckoutButtonsUI] =
114
+ useState<CheckoutButtonUIResponse | null>(null);
115
+
116
+ const [buttonPending, setButtonPending] = useState(false);
118
117
 
119
118
  useEffect(() => {
120
119
  (async () => {
121
- if(!redoCoverageClient.eligible || !cart || !redoCoverageClient.storeId) {
120
+ if (!redoCoverageClient.eligible || !cart || !redoCoverageClient.storeId) {
122
121
  return;
123
122
  }
124
123
 
125
124
  const buttons = await getButtonsToShow({ redoCoverageClient, cart, storeId: redoCoverageClient.storeId });
126
- if(buttons) {
125
+ if (buttons) {
127
126
  setCheckoutButtonsUI(buttons);
128
127
  }
129
128
  })();
130
129
  }, [cart, redoCoverageClient.eligible, redoCoverageClient.price, redoCoverageClient.storeId]);
131
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) */
132
+ const DELAY_TO_ALLOW_CLICKING_AGAIN = 2000;
133
+ const TIMEOUT_FOR_CHECKOUTS = 8000;
134
+
135
+ const handleCoverageCheckoutClick = async (isCoverage: boolean) => {
136
+ if (!redoCoverageClient || !redoCoverageClient.enable || !redoCoverageClient.disable) {
137
+ console.error('Required redoCoverageClient methods not available');
138
+ return;
139
+ }
140
+
141
+ setButtonPending(true);
142
+ try {
143
+ const functionToCall = isCoverage ? redoCoverageClient.enable : redoCoverageClient.disable;
144
+ const result = await executeWithTimeout(
145
+ functionToCall(),
146
+ TIMEOUT_FOR_CHECKOUTS
147
+ );
148
+
149
+ if (props.onClick) {
150
+ await props.onClick(result);
151
+ }
152
+ } catch (e) {
153
+ console.error(e);
154
+ } finally {
155
+ setTimeout(() => {
156
+ setButtonPending(false);
157
+ }, DELAY_TO_ALLOW_CLICKING_AGAIN);
158
+ }
159
+ };
160
+
132
161
  const wrapperClickHandler = async (e: MouseEvent) => {
133
162
  let clickedElement = e.target as HTMLElement;
134
163
 
@@ -136,26 +165,21 @@ const RedoCheckoutButtons = (props: {
136
165
  return;
137
166
  }
138
167
 
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);
168
+ const isCoverageButton = findAncestor(
169
+ clickedElement,
170
+ (el) => el.dataset?.target == "coverage-button"
171
+ );
172
+
173
+ const isNonCoverageButton = findAncestor(
174
+ clickedElement,
175
+ (el) => el.dataset?.target == "non-coverage-button",
176
+ );
177
+
178
+ if (isCoverageButton || isNonCoverageButton) {
179
+ try {
180
+ await handleCoverageCheckoutClick(isCoverageButton ? true : false);
181
+ } catch (error) {
182
+ console.error('Failed to update coverage state:', error);
159
183
  }
160
184
  window.location.href = checkoutUrl;
161
185
  }
@@ -164,11 +188,32 @@ const RedoCheckoutButtons = (props: {
164
188
  return (
165
189
  <div>
166
190
  {checkoutButtonsUI ? (
167
- <div onClick={wrapperClickHandler}>
168
- {
169
- checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''
170
- }
171
- <div dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }} />
191
+ <div onClick={wrapperClickHandler} style={{ position: "relative" }}>
192
+ {checkoutButtonsUI.css ? <style>{checkoutButtonsUI.css}</style> : ''}
193
+ <div
194
+ dangerouslySetInnerHTML={{ __html: checkoutButtonsUI.html }}
195
+ style={{
196
+ opacity: (buttonPending) ? 0.25 : 1,
197
+ transition: 'opacity 0.2s ease-in-out'
198
+ }}
199
+ />
200
+ {(buttonPending) && (
201
+ <div
202
+ style={{
203
+ position: "absolute",
204
+ top: 0,
205
+ left: 0,
206
+ width: "100%",
207
+ height: "100%",
208
+ display: "flex",
209
+ justifyContent: "center",
210
+ alignItems: "center",
211
+ zIndex: 1,
212
+ }}
213
+ >
214
+ <CircleSpinner />
215
+ </div>
216
+ )}
172
217
  </div>
173
218
  ) : (
174
219
  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
  }