@ensofinance/checkout-widget 0.1.2 → 0.1.4

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,34 +1,21 @@
1
- import { Box, Icon, Text } from "@chakra-ui/react";
2
1
  import { BodyWrapper } from "@/components/ui/styled";
3
- import { ArrowDownUpIcon } from "lucide-react";
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, IconButton, Tab, Input } from "@/components/ui";
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
- normalizeValue,
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 [usdValue, setUsdValue] = useState<string>("10.10");
30
- // const [tokenAmount, setTokenAmount] = useState<string>("");
31
- const [inputMode, setInputMode] = useState<InputMode>("usd");
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 tokenValue = useMemo(() => {
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
- // Handle percentage selection
54
- const handlePercentageSelect = (percent: number) => {
55
- if (!balanceIn || !priceData || !tokenInData?.decimals) {
56
- setUsdValue("0.00");
57
- return;
58
- }
41
+ const getPercentAmounts = useCallback(
42
+ (percent: number) => {
43
+ if (!balanceIn || !priceData || !tokenInData?.decimals) {
44
+ return null;
45
+ }
59
46
 
60
- const amountToSet = (
61
- (BigInt(balanceIn) * BigInt(percent)) /
62
- BigInt(100)
63
- ).toString();
47
+ const amountToSet = (
48
+ (BigInt(balanceIn) * BigInt(percent)) /
49
+ BigInt(100)
50
+ ).toString();
64
51
 
65
- setAmountIn(amountToSet);
66
- setUsdValue(
67
- (
68
- +normalizeValue(amountToSet, tokenInData?.decimals) * priceData
69
- ).toFixed(2),
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
- handlePercentageSelect(100);
77
- }
78
- }, [balanceIn, initialLoad, priceData, tokenInData, balanceIn]);
79
-
80
- // Handle input change based on current mode
81
- const handleInputChange = (value: string) => {
82
- if (inputMode === "usd") {
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
- // Toggle between USD and token input modes
100
- const handleToggleMode = () => {
101
- setInputMode(inputMode === "usd" ? "token" : "usd");
102
- };
84
+ useEffect(() => {
85
+ if (!tokenInData?.decimals) return;
103
86
 
104
- // Get input placeholder and display value
105
- const getInputDisplay = () => {
106
- const formattedValue = formatNumber(tokenValue);
107
- const safeUsdValue = parseFloat(usdValue) > 0 ? usdValue : 0;
87
+ const normalizedAmount = tokenAmount.endsWith(".")
88
+ ? tokenAmount.slice(0, -1)
89
+ : tokenAmount;
108
90
 
109
- if (inputMode === "usd") {
110
- return {
111
- placeholder: "$10.00",
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
- return {
120
- placeholder: "0.00",
121
- displayValue: formattedValue,
122
- equivalentValue: formatUSD(safeUsdValue),
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 { placeholder, displayValue, equivalentValue } = getInputDisplay();
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
- <Box display={"flex"} flexDirection={"column"} gap={"8px"}>
132
- <Box
133
- display={"flex"}
134
- flexDirection={"column"}
135
- gap={"8px"}
136
- alignItems={"center"}
137
- padding={"25.5px"}
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={notEnoughBalance}
128
+ disabled={isAmountInvalid}
209
129
  >
210
130
  Continue
211
131
  </Button>
package/src/index.ts CHANGED
@@ -1,22 +1,11 @@
1
- import {
2
- CheckoutModal,
3
- type CheckoutModalProps,
4
- } from "./components/CheckoutModal";
5
- import { Checkout, type CheckoutConfig } from "./components/Checkout";
1
+ import { CheckoutModal } from "./components/CheckoutModal";
2
+ import { Checkout } from "./components/Checkout";
6
3
 
7
- // Export two versions for different integration needs
8
- export {
9
- // Modal version with isActive/setIsActive control
10
- CheckoutModal,
11
- CheckoutModalProps,
4
+ // Export components
5
+ export { CheckoutModal, Checkout };
12
6
 
13
- // Standalone widget version without modal
14
- Checkout,
15
- CheckoutConfig,
16
- };
17
-
18
- // Export theme type for TypeScript users
19
- export type { WidgetTheme } from "./types";
7
+ // Export types directly from types module for proper bundling
8
+ export type { CheckoutConfig, CheckoutModalProps, WidgetTheme } from "./types";
20
9
  export { SupportedExchanges } from "./types";
21
10
 
22
11
  // Export default CEX bridge chain mapping for users to extend
@@ -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
+ };
package/tsconfig.json CHANGED
@@ -4,10 +4,12 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "Bundler",
6
6
  "skipLibCheck": true,
7
+ "baseUrl": ".",
7
8
  "paths": {
8
9
  "@/*": ["./src/*"]
9
10
  },
10
11
  "jsx": "react-jsx",
11
12
  "declaration": true
12
- }
13
+ },
14
+ "include": ["src"]
13
15
  }
package/vite.config.ts CHANGED
@@ -8,7 +8,7 @@ export default defineConfig(({ mode }) => ({
8
8
  dts({
9
9
  insertTypesEntry: true,
10
10
  rollupTypes: true,
11
- bundledPackages: ["./src/types"],
11
+ tsconfigPath: "./tsconfig.json",
12
12
  }), // generates *.d.ts beside the JS
13
13
  ].filter(Boolean),
14
14
  resolve: {