@ensofinance/checkout-widget 0.1.2 → 0.1.3
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/checkout-widget.es.js +5399 -5368
- package/dist/checkout-widget.es.js.map +1 -1
- package/dist/checkout-widget.umd.js +46 -46
- package/dist/checkout-widget.umd.js.map +1 -1
- package/package.json +2 -1
- package/src/components/AmountInput.tsx +233 -0
- package/src/components/steps/ExchangeFlow.tsx +62 -143
- package/src/components/steps/WalletFlow/WalletAmountStep.tsx +75 -155
- package/src/util/common.tsx +23 -0
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
import { Box, Icon, Text } from "@chakra-ui/react";
|
|
2
1
|
import { BodyWrapper } from "@/components/ui/styled";
|
|
3
|
-
import {
|
|
4
|
-
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { useState, useEffect, useCallback } from "react";
|
|
5
3
|
import { Address } from "viem";
|
|
6
4
|
import { useAppStore } from "@/store";
|
|
7
|
-
import { Button
|
|
5
|
+
import { Button } from "@/components/ui";
|
|
6
|
+
import { AmountInput, AmountInputValue } from "@/components/AmountInput";
|
|
8
7
|
import CurrencySwapDisplay from "@/components/CurrencySwapDisplay";
|
|
9
8
|
import { useEnsoPrice } from "@/enso-api/api";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
denormalizeValue,
|
|
13
|
-
formatNumber,
|
|
14
|
-
formatUSD,
|
|
15
|
-
} from "@/util";
|
|
9
|
+
import { normalizeValue, denormalizeValue } from "@/util";
|
|
10
|
+
import { precisionizeNumber } from "@/util/common";
|
|
16
11
|
import { useTokenBalance } from "@/util/wallet";
|
|
17
12
|
import { useAppDetails } from "@/util/enso-hooks";
|
|
18
|
-
|
|
19
|
-
type InputMode = "usd" | "token";
|
|
20
|
-
|
|
21
|
-
const percentageOptions = [
|
|
22
|
-
{ label: "25%", value: 25 },
|
|
23
|
-
{ label: "50%", value: 50 },
|
|
24
|
-
{ label: "75%", value: 75 },
|
|
25
|
-
{ label: "Max", value: 100 },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
13
|
const WalletAmountStep = ({ setStep }: { setStep: (step: string) => void }) => {
|
|
29
|
-
const [
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
const [amountInput, setAmountInput] = useState<AmountInputValue>({
|
|
15
|
+
tokenAmount: "",
|
|
16
|
+
usdAmount: "10.10",
|
|
17
|
+
mode: "usd",
|
|
18
|
+
});
|
|
32
19
|
|
|
33
20
|
const setAmountIn = useAppStore((state) => state.setAmountIn);
|
|
34
21
|
const amountIn = useAppStore((state) => state.amountIn);
|
|
@@ -43,157 +30,90 @@ const WalletAmountStep = ({ setStep }: { setStep: (step: string) => void }) => {
|
|
|
43
30
|
} = useAppDetails();
|
|
44
31
|
|
|
45
32
|
const { data: priceData } = useEnsoPrice(chainIdIn, tokenIn);
|
|
33
|
+
const tokenInputPrecision = tokenInData?.decimals
|
|
34
|
+
? Math.min(tokenInData.decimals, 6)
|
|
35
|
+
: 6;
|
|
46
36
|
|
|
47
|
-
const
|
|
48
|
-
return normalizeValue(amountIn, tokenInData?.decimals);
|
|
49
|
-
}, [amountIn, tokenInData?.decimals]);
|
|
37
|
+
const tokenAmount = amountInput.tokenAmount;
|
|
50
38
|
|
|
51
39
|
const balanceIn = useTokenBalance(tokenIn as Address, chainIdIn);
|
|
52
40
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
41
|
+
const getPercentAmounts = useCallback(
|
|
42
|
+
(percent: number) => {
|
|
43
|
+
if (!balanceIn || !priceData || !tokenInData?.decimals) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
47
|
+
const amountToSet = (
|
|
48
|
+
(BigInt(balanceIn) * BigInt(percent)) /
|
|
49
|
+
BigInt(100)
|
|
50
|
+
).toString();
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
const normalizedTokenAmount = normalizeValue(
|
|
53
|
+
amountToSet,
|
|
54
|
+
tokenInData.decimals,
|
|
55
|
+
);
|
|
56
|
+
const roundedTokenAmount = precisionizeNumber(
|
|
57
|
+
normalizedTokenAmount,
|
|
58
|
+
tokenInputPrecision,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
tokenAmount: roundedTokenAmount,
|
|
63
|
+
usdAmount: (
|
|
64
|
+
+parseFloat(roundedTokenAmount || "0") * priceData
|
|
65
|
+
).toFixed(2),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
[balanceIn, priceData, tokenInData?.decimals, tokenInputPrecision],
|
|
69
|
+
);
|
|
72
70
|
|
|
73
71
|
useEffect(() => {
|
|
74
72
|
if (initialLoad && priceData && tokenInData && +balanceIn > 0) {
|
|
75
73
|
setInitialLoad(false);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const cleanUsd = value.replace("$", "");
|
|
84
|
-
// Clean the input from usd sign
|
|
85
|
-
console.log(cleanUsd, priceData, tokenInData?.decimals);
|
|
86
|
-
setUsdValue(cleanUsd);
|
|
87
|
-
setAmountIn(
|
|
88
|
-
denormalizeValue(
|
|
89
|
-
(parseFloat(cleanUsd) / priceData).toString(),
|
|
90
|
-
tokenInData?.decimals,
|
|
91
|
-
),
|
|
92
|
-
);
|
|
93
|
-
} else {
|
|
94
|
-
setAmountIn(denormalizeValue(value, tokenInData?.decimals));
|
|
95
|
-
setUsdValue((parseFloat(value) * priceData).toFixed(2));
|
|
74
|
+
const percentAmounts = getPercentAmounts(100);
|
|
75
|
+
if (!percentAmounts) return;
|
|
76
|
+
|
|
77
|
+
setAmountInput((prev) => ({
|
|
78
|
+
...prev,
|
|
79
|
+
...percentAmounts,
|
|
80
|
+
}));
|
|
96
81
|
}
|
|
97
|
-
};
|
|
82
|
+
}, [balanceIn, initialLoad, priceData, tokenInData, getPercentAmounts]);
|
|
98
83
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
setInputMode(inputMode === "usd" ? "token" : "usd");
|
|
102
|
-
};
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!tokenInData?.decimals) return;
|
|
103
86
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const safeUsdValue = parseFloat(usdValue) > 0 ? usdValue : 0;
|
|
87
|
+
const normalizedAmount = tokenAmount.endsWith(".")
|
|
88
|
+
? tokenAmount.slice(0, -1)
|
|
89
|
+
: tokenAmount;
|
|
108
90
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
displayValue: safeUsdValue ? `$${safeUsdValue}` : "",
|
|
113
|
-
equivalentValue: tokenValue
|
|
114
|
-
? `${formattedValue} ${tokenInData?.symbol}`
|
|
115
|
-
: "—",
|
|
116
|
-
};
|
|
91
|
+
if (!normalizedAmount || normalizedAmount === ".") {
|
|
92
|
+
setAmountIn("0");
|
|
93
|
+
return;
|
|
117
94
|
}
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
};
|
|
96
|
+
try {
|
|
97
|
+
setAmountIn(denormalizeValue(normalizedAmount, tokenInData.decimals));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
setAmountIn("0");
|
|
100
|
+
}
|
|
101
|
+
}, [tokenAmount, tokenInData?.decimals, setAmountIn]);
|
|
125
102
|
|
|
126
|
-
const
|
|
127
|
-
const notEnoughBalance = +balanceIn < +amountIn;
|
|
103
|
+
const hasTokenAmount = !!tokenAmount && tokenAmount !== ".";
|
|
104
|
+
const notEnoughBalance = hasTokenAmount ? +balanceIn < +amountIn : false;
|
|
105
|
+
const isAmountInvalid = !hasTokenAmount || notEnoughBalance;
|
|
128
106
|
|
|
129
107
|
return (
|
|
130
108
|
<BodyWrapper>
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{/* Main Input */}
|
|
140
|
-
<Input
|
|
141
|
-
inputMode="decimal"
|
|
142
|
-
marginY={"8px"}
|
|
143
|
-
variant={"text"}
|
|
144
|
-
placeholder={placeholder}
|
|
145
|
-
value={displayValue}
|
|
146
|
-
onChange={(e) => handleInputChange(e.target.value)}
|
|
147
|
-
/>
|
|
148
|
-
|
|
149
|
-
{/* Toggle Button and Equivalent Display */}
|
|
150
|
-
<Box
|
|
151
|
-
display={"flex"}
|
|
152
|
-
gap={"3"}
|
|
153
|
-
alignItems={"center"}
|
|
154
|
-
onClick={handleToggleMode}
|
|
155
|
-
_hover={{ background: "bg.subtle" }}
|
|
156
|
-
cursor={"pointer"}
|
|
157
|
-
borderRadius={"lg"}
|
|
158
|
-
px={"3"}
|
|
159
|
-
>
|
|
160
|
-
<IconButton
|
|
161
|
-
minWidth={"24px"}
|
|
162
|
-
minHeight={"24px"}
|
|
163
|
-
maxWidth={"24px"}
|
|
164
|
-
background={"transparent"}
|
|
165
|
-
>
|
|
166
|
-
<Icon
|
|
167
|
-
as={ArrowDownUpIcon}
|
|
168
|
-
color="gray"
|
|
169
|
-
width={"16px"}
|
|
170
|
-
height={"16px"}
|
|
171
|
-
/>
|
|
172
|
-
</IconButton>
|
|
173
|
-
|
|
174
|
-
{/* Small equivalent value display */}
|
|
175
|
-
<Text fontSize="sm" color="fg.muted">
|
|
176
|
-
{equivalentValue}
|
|
177
|
-
</Text>
|
|
178
|
-
</Box>
|
|
179
|
-
</Box>
|
|
180
|
-
|
|
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>
|
|
109
|
+
<AmountInput
|
|
110
|
+
value={amountInput}
|
|
111
|
+
onChange={setAmountInput}
|
|
112
|
+
tokenSymbol={tokenInData?.symbol}
|
|
113
|
+
tokenPriceUsd={priceData}
|
|
114
|
+
roundingPrecision={tokenInputPrecision}
|
|
115
|
+
onPercentSelect={getPercentAmounts}
|
|
116
|
+
/>
|
|
197
117
|
|
|
198
118
|
<CurrencySwapDisplay
|
|
199
119
|
tokenOut={effectiveTokenOutData}
|
|
@@ -205,7 +125,7 @@ const WalletAmountStep = ({ setStep }: { setStep: (step: string) => void }) => {
|
|
|
205
125
|
|
|
206
126
|
<Button
|
|
207
127
|
onClick={() => setStep("quote")}
|
|
208
|
-
disabled={
|
|
128
|
+
disabled={isAmountInvalid}
|
|
209
129
|
>
|
|
210
130
|
Continue
|
|
211
131
|
</Button>
|
package/src/util/common.tsx
CHANGED
|
@@ -350,3 +350,26 @@ export const getChainEtherscanUrl = ({
|
|
|
350
350
|
|
|
351
351
|
export const precisionizeNumber = (value: number | string, precision: number) =>
|
|
352
352
|
Number(parseFloat(value.toString()).toFixed(precision)).toString();
|
|
353
|
+
|
|
354
|
+
export const sanitizeDecimalInput = (value: string, maxDecimals?: number) => {
|
|
355
|
+
const cleaned = value.replace(/[^\d.]/g, "");
|
|
356
|
+
if (!cleaned) return "";
|
|
357
|
+
|
|
358
|
+
const hasTrailingDot = cleaned.endsWith(".");
|
|
359
|
+
const [integerPart, ...rest] = cleaned.split(".");
|
|
360
|
+
let fractionPart = rest.join("");
|
|
361
|
+
|
|
362
|
+
if (maxDecimals !== undefined) {
|
|
363
|
+
fractionPart = fractionPart.slice(0, maxDecimals);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (hasTrailingDot && fractionPart.length === 0) {
|
|
367
|
+
return integerPart ? `${integerPart}.` : ".";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (fractionPart.length > 0) {
|
|
371
|
+
return integerPart ? `${integerPart}.${fractionPart}` : `.${fractionPart}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return integerPart;
|
|
375
|
+
};
|