@ensofinance/checkout-widget 0.0.12 → 0.0.13

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.
@@ -1,6 +1,6 @@
1
1
  import { Box, Icon, Skeleton } from "@chakra-ui/react";
2
2
  import { X } from "lucide-react";
3
- import { useContext, useMemo } from "react";
3
+ import { useContext, useMemo, useState } from "react";
4
4
  import { useAccount } from "wagmi";
5
5
  import { IconButton } from "../ui";
6
6
  import {
@@ -16,18 +16,31 @@ import Modal from "../modal";
16
16
  import { CheckoutContext } from "../Checkout";
17
17
  import { WalletCard, OptionCard } from "../cards";
18
18
  import { WalletStatus } from "../cards/WalletCard";
19
- import { useWalletIcon } from "@/util/enso-hooks";
19
+ import { useWalletIcon, useSmartAccountBalances } from "@/util/enso-hooks";
20
+ import ExchangeFlow, { WithdrawalStep } from "@/components/steps/ExchangeFlow";
21
+ import WalletFlow from "@/components/steps/WalletFlow/WalletFlow";
22
+
20
23
  import BinanceIcon from "../../assets/BinanceBadge.svg";
21
24
 
22
- const InitialStep = () => {
23
- const { handleClose, setStep, setFlow, enableExchanges } =
24
- useContext(CheckoutContext);
25
+ const FLOWS = {
26
+ exchangeFlow: ExchangeFlow,
27
+ walletFlow: WalletFlow,
28
+ };
29
+
30
+ const FlowSelector = () => {
31
+ const { handleClose, enableExchanges } = useContext(CheckoutContext);
32
+ const [flow, setFlow] = useState("");
33
+ const [initialStep, setInitialStep] = useState<string | number>("");
25
34
 
26
35
  const { total, isLoading } = useWalletBalance();
27
36
  const { address } = useAccount();
28
37
 
29
38
  const { walletIcon, walletDisplayName } = useWalletIcon();
30
39
 
40
+ // Get smart account balances
41
+ const { total: smartAccountTotal, isLoading: isLoadingSmartAccount } =
42
+ useSmartAccountBalances(1);
43
+
31
44
  const formattedBalance = useMemo(() => {
32
45
  if (isLoading)
33
46
  return (
@@ -52,7 +65,7 @@ const InitialStep = () => {
52
65
  delay: string;
53
66
  icons: string[];
54
67
  flow: string;
55
- firstStep: string;
68
+ firstStep: string | number;
56
69
  disabled?: boolean;
57
70
  }[] = [
58
71
  // {
@@ -77,18 +90,37 @@ const InitialStep = () => {
77
90
  // },
78
91
  ];
79
92
 
93
+ // Add Smart Balance option if balance > $20
94
+ if (!isLoadingSmartAccount && smartAccountTotal > 20) {
95
+ options.push({
96
+ id: "smart-balance",
97
+ title: "Smart Account Balance",
98
+ limit: formatUSD(smartAccountTotal),
99
+ delay: "2 min",
100
+ icons: [],
101
+ flow: "exchangeFlow",
102
+ firstStep: WithdrawalStep.ChooseBalanceAsset,
103
+ });
104
+ }
105
+
80
106
  if (enableExchanges?.includes?.("binance"))
81
107
  options.unshift({
82
- id: "1",
108
+ id: "binance-exchange",
83
109
  title: "Connect Exchange",
84
110
  limit: "No Limit",
85
111
  delay: "2 min",
86
112
  icons: [BinanceIcon],
87
113
  flow: "exchangeFlow",
88
- firstStep: "connectExchange",
114
+ firstStep: WithdrawalStep.CheckSessionKey,
89
115
  });
90
116
  return options;
91
- }, [address, enableExchanges]);
117
+ }, [address, enableExchanges, smartAccountTotal, isLoadingSmartAccount]);
118
+
119
+ const FlowComponent = flow && FLOWS[flow];
120
+
121
+ if (FlowComponent) {
122
+ return <FlowComponent setFlow={setFlow} initialStep={initialStep} />;
123
+ }
92
124
 
93
125
  return (
94
126
  <>
@@ -124,8 +156,7 @@ const InitialStep = () => {
124
156
  status={WalletStatus.CONNECTED}
125
157
  badge={walletDisplayName}
126
158
  onClick={() => {
127
- setFlow("mainFlow");
128
- setStep("selectToken");
159
+ setFlow("walletFlow");
129
160
  }}
130
161
  />
131
162
  }
@@ -135,24 +166,20 @@ const InitialStep = () => {
135
166
  <>
136
167
  <Divider />
137
168
  <ListWrapper>
138
- {OPTIONS.map((option) => {
139
- const optionCard = (
140
- <OptionCard
141
- key={option.id}
142
- title={option.title}
143
- limit={option.limit}
144
- delay={option.delay}
145
- icons={option.icons}
146
- onClick={() => {
147
- if (option.disabled) return;
148
- setFlow(option.flow);
149
- setStep(option.firstStep);
150
- }}
151
- />
152
- );
153
-
154
- return optionCard;
155
- })}
169
+ {OPTIONS.map((option) => (
170
+ <OptionCard
171
+ key={option.id}
172
+ title={option.title}
173
+ limit={option.limit}
174
+ delay={option.delay}
175
+ icons={option.icons}
176
+ onClick={() => {
177
+ if (option.disabled) return;
178
+ setFlow(option.flow);
179
+ setInitialStep(option.firstStep);
180
+ }}
181
+ />
182
+ ))}
156
183
  </ListWrapper>
157
184
  </>
158
185
  )}
@@ -162,4 +189,4 @@ const InitialStep = () => {
162
189
  );
163
190
  };
164
191
 
165
- export default InitialStep;
192
+ export default FlowSelector;
@@ -0,0 +1,215 @@
1
+ import { Box, Icon, Text } from "@chakra-ui/react";
2
+ import { BodyWrapper } from "@/components/ui/styled";
3
+ import { ArrowDownUpIcon } from "lucide-react";
4
+ import { useState, useEffect, useMemo } from "react";
5
+ import { Address } from "viem";
6
+ import { useAppStore } from "@/store";
7
+ import { Button, IconButton, Tab, Input } from "@/components/ui";
8
+ import CurrencySwapDisplay from "@/components/CurrencySwapDisplay";
9
+ import { useEnsoPrice, useEnsoToken } from "@/enso-api/api";
10
+ import {
11
+ normalizeValue,
12
+ denormalizeValue,
13
+ formatNumber,
14
+ formatUSD,
15
+ } from "@/util";
16
+ import { useTokenBalance } from "@/util/wallet";
17
+
18
+ type InputMode = "usd" | "token";
19
+
20
+ const percentageOptions = [
21
+ { label: "25%", value: 25 },
22
+ { label: "50%", value: 50 },
23
+ { label: "75%", value: 75 },
24
+ { label: "Max", value: 100 },
25
+ ];
26
+
27
+ const WalletAmountStep = ({ setStep }: { setStep: (step: string) => void }) => {
28
+ const [usdValue, setUsdValue] = useState<string>("10.10");
29
+ // const [tokenAmount, setTokenAmount] = useState<string>("");
30
+ const [inputMode, setInputMode] = useState<InputMode>("usd");
31
+
32
+ const tokenIn = useAppStore((state) => state.tokenIn);
33
+ const tokenOut = useAppStore((state) => state.tokenOut);
34
+ const chainIdOut = useAppStore((state) => state.chainIdOut);
35
+ const chainIdIn = useAppStore((state) => state.chainIdIn);
36
+ const setAmountIn = useAppStore((state) => state.setAmountIn);
37
+ const amountIn = useAppStore((state) => state.amountIn);
38
+ const [initialLoad, setInitialLoad] = useState(true);
39
+
40
+ const {
41
+ data: [tokenDetails],
42
+ } = useEnsoToken(tokenIn, chainIdIn);
43
+ const {
44
+ data: [tokenOutDetails],
45
+ } = useEnsoToken(tokenOut, chainIdOut);
46
+ const { data: priceData } = useEnsoPrice(chainIdIn, tokenIn);
47
+
48
+ const tokenValue = useMemo(() => {
49
+ return normalizeValue(amountIn, tokenDetails?.decimals);
50
+ }, [amountIn, tokenDetails?.decimals]);
51
+
52
+ const balanceIn = useTokenBalance(tokenIn as Address, chainIdIn);
53
+
54
+ // Handle percentage selection
55
+ const handlePercentageSelect = (percent: number) => {
56
+ if (!balanceIn || !priceData || !tokenDetails?.decimals) {
57
+ setUsdValue("0.00");
58
+ return;
59
+ }
60
+
61
+ const amountToSet = (
62
+ (BigInt(balanceIn) * BigInt(percent)) /
63
+ BigInt(100)
64
+ ).toString();
65
+
66
+ setAmountIn(amountToSet);
67
+ setUsdValue(
68
+ (
69
+ +normalizeValue(amountToSet, tokenDetails?.decimals) * priceData
70
+ ).toFixed(2),
71
+ );
72
+ };
73
+
74
+ useEffect(() => {
75
+ if (initialLoad && priceData && tokenDetails && +balanceIn > 0) {
76
+ setInitialLoad(false);
77
+ handlePercentageSelect(100);
78
+ }
79
+ }, [balanceIn, initialLoad, priceData, tokenDetails, balanceIn]);
80
+
81
+ // Handle input change based on current mode
82
+ const handleInputChange = (value: string) => {
83
+ if (inputMode === "usd") {
84
+ const cleanUsd = value.replace("$", "");
85
+ // Clean the input from usd sign
86
+ console.log(cleanUsd, priceData, tokenDetails?.decimals);
87
+ setUsdValue(cleanUsd);
88
+ setAmountIn(
89
+ denormalizeValue(
90
+ (parseFloat(cleanUsd) / priceData).toString(),
91
+ tokenDetails?.decimals,
92
+ ),
93
+ );
94
+ } else {
95
+ setAmountIn(denormalizeValue(value, tokenDetails?.decimals));
96
+ setUsdValue((parseFloat(value) * priceData).toFixed(2));
97
+ }
98
+ };
99
+
100
+ // Toggle between USD and token input modes
101
+ const handleToggleMode = () => {
102
+ setInputMode(inputMode === "usd" ? "token" : "usd");
103
+ };
104
+
105
+ // Get input placeholder and display value
106
+ const getInputDisplay = () => {
107
+ const formattedValue = formatNumber(tokenValue);
108
+ const safeUsdValue = parseFloat(usdValue) > 0 ? usdValue : 0;
109
+
110
+ if (inputMode === "usd") {
111
+ return {
112
+ placeholder: "$10.00",
113
+ displayValue: safeUsdValue ? `$${safeUsdValue}` : "",
114
+ equivalentValue: tokenValue
115
+ ? `${formattedValue} ${tokenDetails?.symbol}`
116
+ : "—",
117
+ };
118
+ }
119
+
120
+ return {
121
+ placeholder: "0.00",
122
+ displayValue: formattedValue,
123
+ equivalentValue: formatUSD(safeUsdValue),
124
+ };
125
+ };
126
+
127
+ const { placeholder, displayValue, equivalentValue } = getInputDisplay();
128
+ const notEnoughBalance = +balanceIn < +amountIn;
129
+
130
+ return (
131
+ <BodyWrapper>
132
+ <Box display={"flex"} flexDirection={"column"} gap={"8px"}>
133
+ <Box
134
+ display={"flex"}
135
+ flexDirection={"column"}
136
+ gap={"8px"}
137
+ alignItems={"center"}
138
+ padding={"25.5px"}
139
+ >
140
+ {/* Main Input */}
141
+ <Input
142
+ inputMode="decimal"
143
+ marginY={"8px"}
144
+ variant={"text"}
145
+ placeholder={placeholder}
146
+ value={displayValue}
147
+ onChange={(e) => handleInputChange(e.target.value)}
148
+ />
149
+
150
+ {/* Toggle Button and Equivalent Display */}
151
+ <Box
152
+ display={"flex"}
153
+ gap={"3"}
154
+ alignItems={"center"}
155
+ onClick={handleToggleMode}
156
+ _hover={{ background: "bg.subtle" }}
157
+ cursor={"pointer"}
158
+ borderRadius={"lg"}
159
+ px={"3"}
160
+ >
161
+ <IconButton
162
+ minWidth={"24px"}
163
+ minHeight={"24px"}
164
+ maxWidth={"24px"}
165
+ background={"transparent"}
166
+ >
167
+ <Icon
168
+ as={ArrowDownUpIcon}
169
+ color="gray"
170
+ width={"16px"}
171
+ height={"16px"}
172
+ />
173
+ </IconButton>
174
+
175
+ {/* Small equivalent value display */}
176
+ <Text fontSize="sm" color="fg.muted">
177
+ {equivalentValue}
178
+ </Text>
179
+ </Box>
180
+ </Box>
181
+ <Box
182
+ display={"flex"}
183
+ gap={"4px"}
184
+ justifyContent={"center"}
185
+ paddingBottom={"35px"}
186
+ >
187
+ {percentageOptions.map((option) => (
188
+ <Tab
189
+ key={option.label}
190
+ onClick={() => handlePercentageSelect(option.value)}
191
+ >
192
+ {option.label}
193
+ </Tab>
194
+ ))}
195
+ </Box>
196
+ </Box>
197
+
198
+ <CurrencySwapDisplay
199
+ tokenOut={tokenOutDetails}
200
+ tokenIn={tokenDetails}
201
+ chainIdIn={chainIdIn}
202
+ chainIdOut={chainIdOut}
203
+ />
204
+
205
+ <Button
206
+ onClick={() => setStep("quote")}
207
+ disabled={notEnoughBalance}
208
+ >
209
+ Continue
210
+ </Button>
211
+ </BodyWrapper>
212
+ );
213
+ };
214
+
215
+ export default WalletAmountStep;
@@ -1,25 +1,23 @@
1
- import { Box, Icon, Table, Text, Image } from "@chakra-ui/react";
2
- import { HeaderWrapper, HeaderTitle, BodyWrapper } from "../ui/styled";
3
- import { ChevronLeft, X } from "lucide-react";
4
- import { useContext, useEffect, useState, useMemo, useCallback } from "react";
1
+ import { Box, Table, Text, Image } from "@chakra-ui/react";
2
+ import { useEffect, useState, useMemo, useCallback } from "react";
5
3
  import { useAccount } from "wagmi";
6
4
  import { Address } from "viem";
7
- import Modal from "../modal";
8
- import { CheckoutContext } from "../Checkout";
9
- import { Button, IconButton } from "../ui";
10
- import { CircleTimer } from "../CircleTimer";
11
- import { TransactionDetailRow } from "../TransactionDetailRow";
12
- import DepositProcessing from "../DepositProcessing";
5
+ import { BodyWrapper } from "@/components/ui/styled";
6
+ import { Button } from "@/components/ui";
7
+ import { CircleTimer } from "@/components/CircleTimer";
8
+ import { TransactionDetailRow } from "@/components/TransactionDetailRow";
9
+ import DepositProcessing from "@/components/DepositProcessing";
13
10
  import { useSendTxns, useRouteData, useAppDetails } from "@/util/enso-hooks";
14
- import { getChainIcon } from "@/util";
11
+ import { compareCaseInsensitive, getChainIcon } from "@/util";
15
12
  import { getChainEtherscanUrl } from "@/util/common";
16
13
  import { useTrackTx, useTxByHash } from "@/util/tx-tracker";
17
- import QuoteParameters from "../QuoteParameters";
14
+ import QuoteParameters from "@/components/QuoteParameters";
18
15
  import { useApproveData } from "@/enso-api/api";
19
16
  import { useIsApproveNeeded } from "@/util/wallet";
17
+ import { ETH_ADDRESS } from "@/util/constants";
20
18
 
21
- import SuccessIcon from "../../assets/success.svg";
22
- import FailIcon from "../../assets/fail.svg";
19
+ import SuccessIcon from "@/assets/success.svg";
20
+ import FailIcon from "@/assets/fail.svg";
23
21
 
24
22
  type TransferStatus = "processing" | "success" | "failed" | "idle";
25
23
 
@@ -42,7 +40,13 @@ const useOperationsCalls = () => {
42
40
  );
43
41
 
44
42
  const calls = useMemo(() => {
45
- if (!allowanceFetched || !routeFetched || !approveFetched) return [];
43
+ if (
44
+ (!compareCaseInsensitive(tokenIn, ETH_ADDRESS) &&
45
+ !allowanceFetched) ||
46
+ !routeFetched ||
47
+ !approveFetched
48
+ )
49
+ return [];
46
50
 
47
51
  if (approveNeeded) {
48
52
  return [approveData?.tx, routeData?.tx];
@@ -63,8 +67,11 @@ const useOperationsCalls = () => {
63
67
  };
64
68
  };
65
69
 
66
- const WalletConfirmStep = () => {
67
- const { handleClose, setFlow, setStep } = useContext(CheckoutContext);
70
+ const WalletConfirmStep = ({
71
+ setStep,
72
+ }: {
73
+ setStep: (step: string) => void;
74
+ }) => {
68
75
  const [status, setStatus] = useState<TransferStatus>("idle");
69
76
  const [isTimerFinished, setIsTimerFinished] = useState(false);
70
77
  const { address } = useAccount();
@@ -113,6 +120,7 @@ const WalletConfirmStep = () => {
113
120
 
114
121
  // Initiate the transaction when wallet is ready
115
122
  useEffect(() => {
123
+ console.log(called, ready);
116
124
  if (!called && ready) {
117
125
  sendTxns(onTxSend, onFail);
118
126
  setCalled(true);
@@ -125,7 +133,6 @@ const WalletConfirmStep = () => {
125
133
 
126
134
  const handleNewDeposit = () => {
127
135
  // Reset to initial step or handle new deposit logic
128
- setFlow("mainFlow");
129
136
  setStep("selectToken");
130
137
  };
131
138
 
@@ -133,7 +140,6 @@ const WalletConfirmStep = () => {
133
140
  // Reset to previous step to retry the transfer
134
141
  // setStatus("processing");
135
142
  // setIsTimerFinished(false);
136
- setFlow("mainFlow");
137
143
  setStep("quote");
138
144
  };
139
145
 
@@ -258,146 +264,91 @@ const WalletConfirmStep = () => {
258
264
  };
259
265
 
260
266
  return (
261
- <>
262
- <Modal.Header>
263
- <HeaderWrapper>
264
- <IconButton
265
- minWidth={"16px"}
266
- minHeight={"16px"}
267
- maxWidth={"16px"}
268
- onClick={() => {
269
- setFlow("mainFlow");
270
- setStep("quote");
271
- }}
272
- >
273
- <Icon
274
- as={ChevronLeft}
275
- color="gray"
276
- width={"16px"}
277
- height={"16px"}
278
- />
279
- </IconButton>
280
-
281
- <Box display="flex" flexDirection="column" gap={"4px"}>
282
- <HeaderTitle>Top up your wallet</HeaderTitle>
283
- </Box>
284
-
285
- {handleClose && (
286
- <IconButton
287
- onClick={handleClose}
288
- width={"40px"}
289
- marginLeft={"auto"}
267
+ <BodyWrapper>
268
+ <Box
269
+ display={"flex"}
270
+ flexDirection={"column"}
271
+ paddingBottom={"16px"}
272
+ alignItems={"center"}
273
+ width={"100%"}
274
+ >
275
+ {renderStatusIcon()}
276
+ {renderProcessingText()}
277
+ </Box>
278
+
279
+ <Table.Root
280
+ key={"status"}
281
+ size="sm"
282
+ variant={"outline"}
283
+ width={"100%"}
284
+ >
285
+ <Table.Body>
286
+ <Table.Row>
287
+ <Table.Cell>Status</Table.Cell>
288
+ <Table.Cell
289
+ display="flex"
290
+ textAlign="end"
291
+ justifyContent="end"
290
292
  >
291
- <Icon
292
- as={X}
293
- color="gray"
294
- width={"16px"}
295
- height={"16px"}
296
- />
297
- </IconButton>
298
- )}
299
- </HeaderWrapper>
300
- </Modal.Header>
301
- <Modal.Body>
302
- <BodyWrapper>
303
- <Box
304
- display={"flex"}
305
- flexDirection={"column"}
306
- paddingBottom={"16px"}
307
- alignItems={"center"}
308
- width={"100%"}
309
- >
310
- {renderStatusIcon()}
311
- {renderProcessingText()}
312
- </Box>
313
-
314
- <Table.Root
315
- key={"status"}
316
- size="sm"
317
- variant={"outline"}
318
- width={"100%"}
319
- >
320
- <Table.Body>
321
- <Table.Row>
322
- <Table.Cell>Status</Table.Cell>
323
- <Table.Cell
324
- display="flex"
325
- textAlign="end"
326
- justifyContent="end"
327
- >
328
- <Text color={getStatusColor()}>
329
- {getStatusText()}
330
- </Text>
331
- </Table.Cell>
332
- </Table.Row>
333
- {["success", "processing"].includes(status) && (
334
- <Table.Row>
335
- <Table.Cell>Transaction</Table.Cell>
336
- <Table.Cell
337
- display="flex"
338
- textAlign="end"
339
- justifyContent="end"
340
- >
341
- {explorerUrl ? (
342
- <a
343
- href={explorerUrl}
344
- target="_blank"
345
- rel="noreferrer"
346
- >
347
- <Text color="blue.500">
348
- View on{" "}
349
- {isCrosschain
350
- ? "LayerZero"
351
- : "Etherscan"}
352
- </Text>
353
- </a>
354
- ) : (
355
- <Text>-</Text>
356
- )}
357
- </Table.Cell>
358
- </Table.Row>
359
- )}
360
- </Table.Body>
361
- </Table.Root>
362
-
363
- <QuoteParameters />
364
-
365
- {status === "processing" ? (
366
- <DepositProcessing
367
- currencyIcon={getChainIcon(chainIdOut)}
368
- />
369
- ) : (
370
- <TransactionDetailRow />
371
- )}
372
-
373
- {status === "success" && (
374
- <Box display="flex" gap={"12px"} width="100%">
375
- <Button visual={"lightGray"} onClick={handleClose}>
376
- Close
377
- </Button>
378
- <Button visual={"solid"} onClick={handleNewDeposit}>
379
- New Deposit
380
- </Button>
381
- </Box>
382
- )}
383
-
384
- {status === "failed" && (
385
- <Box display="flex" gap={"12px"} width="100%">
386
- <Button
387
- // variant={"surface"}
388
- visual={"lightGray"}
389
- onClick={handleClose}
293
+ <Text color={getStatusColor()}>
294
+ {getStatusText()}
295
+ </Text>
296
+ </Table.Cell>
297
+ </Table.Row>
298
+ {["success", "processing"].includes(status) && (
299
+ <Table.Row>
300
+ <Table.Cell>Transaction</Table.Cell>
301
+ <Table.Cell
302
+ display="flex"
303
+ textAlign="end"
304
+ justifyContent="end"
390
305
  >
391
- Close
392
- </Button>
393
- <Button visual={"solid"} onClick={handleRetry}>
394
- Retry Deposit
395
- </Button>
396
- </Box>
306
+ {explorerUrl ? (
307
+ <a
308
+ href={explorerUrl}
309
+ target="_blank"
310
+ rel="noreferrer"
311
+ >
312
+ <Text color="blue.500">
313
+ View on{" "}
314
+ {isCrosschain
315
+ ? "LayerZero"
316
+ : "Etherscan"}
317
+ </Text>
318
+ </a>
319
+ ) : (
320
+ <Text>-</Text>
321
+ )}
322
+ </Table.Cell>
323
+ </Table.Row>
397
324
  )}
398
- </BodyWrapper>
399
- </Modal.Body>
400
- </>
325
+ </Table.Body>
326
+ </Table.Root>
327
+
328
+ <QuoteParameters />
329
+
330
+ {status === "processing" ? (
331
+ <DepositProcessing currencyIcon={getChainIcon(chainIdOut)} />
332
+ ) : (
333
+ <TransactionDetailRow />
334
+ )}
335
+
336
+ {status === "success" && (
337
+ <Button
338
+ visual={"solid"}
339
+ onClick={handleNewDeposit}
340
+ width="100%"
341
+ >
342
+ New Deposit
343
+ </Button>
344
+ )}
345
+
346
+ {status === "failed" && (
347
+ <Button visual={"solid"} onClick={handleRetry} width="100%">
348
+ Retry Deposit
349
+ </Button>
350
+ )}
351
+ </BodyWrapper>
401
352
  );
402
353
  };
403
354