@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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { CheckoutConfig } from '../../../../../../../src/types';
2
- import { CheckoutModalProps } from '../../../../../../../src/types';
3
1
  import { ComponentType } from 'react';
4
2
  import { JSX as JSX_2 } from 'react/jsx-runtime';
5
3
  import { SystemConfig as WidgetTheme } from '@chakra-ui/react';
@@ -10,14 +8,33 @@ export declare const Checkout: ({ config: { apiKey, tokenOut, chainIdOut, theme,
10
8
  onClose?: () => void;
11
9
  }) => JSX_2.Element;
12
10
 
13
- export { CheckoutConfig }
11
+ export declare type CheckoutConfig = {
12
+ tokenOut: string;
13
+ chainIdOut: number;
14
+ apiKey: string;
15
+ theme?: WidgetTheme;
16
+ enableExchange?: SupportedExchanges[];
17
+ /** Override the default CEX bridge chain mapping (maps target chains to intermediate chains for withdrawal + bridge) */
18
+ cexBridgeChainMapping?: Record<number, number>;
19
+ /** Override recipient address (defaults to connected wallet's smart account) */
20
+ recipient?: string;
21
+ /** Force the widget to open in a specific flow, bypassing the selector */
22
+ enforceFlow?: EnforceFlow;
23
+ };
14
24
 
15
25
  export declare const CheckoutModal: ({ config, setIsActive, isActive, onClose, }: CheckoutModalProps) => JSX_2.Element;
16
26
 
17
- export { CheckoutModalProps }
27
+ export declare type CheckoutModalProps = {
28
+ config: CheckoutConfig;
29
+ isActive: boolean;
30
+ setIsActive: (active: boolean) => void;
31
+ onClose?: () => void;
32
+ };
18
33
 
19
34
  export declare const DEFAULT_CEX_BRIDGE_CHAIN_MAPPING: Record<number, number>;
20
35
 
36
+ declare type EnforceFlow = "exchange" | "wallet";
37
+
21
38
  export declare enum SupportedExchanges {
22
39
  Binance = "binance",
23
40
  Kraken = "kraken",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ensofinance/checkout-widget",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "homepage": "https://www.enso.build/",
6
6
  "repository": {
@@ -0,0 +1,233 @@
1
+ import { Box, Icon, Text } from "@chakra-ui/react";
2
+ import { ArrowDownUpIcon } from "lucide-react";
3
+ import { Input, IconButton, Tab } from "@/components/ui";
4
+ import { formatNumber, formatUSD } from "@/util";
5
+ import { precisionizeNumber, sanitizeDecimalInput } from "@/util/common";
6
+
7
+ export type InputMode = "usd" | "token";
8
+
9
+ export type AmountInputValue = {
10
+ tokenAmount: string;
11
+ usdAmount: string;
12
+ mode: InputMode;
13
+ };
14
+
15
+ type PercentOption = {
16
+ label: string;
17
+ value: number;
18
+ };
19
+
20
+ type AmountInputProps = {
21
+ value: AmountInputValue;
22
+ onChange: (value: AmountInputValue) => void;
23
+ tokenSymbol?: string;
24
+ tokenPriceUsd?: number;
25
+ tokenBalance?: number;
26
+ roundingPrecision?: number;
27
+ usdPrecision?: number;
28
+ percentOptions?: PercentOption[];
29
+ onPercentSelect?: (
30
+ percent: number,
31
+ ) => { tokenAmount: string; usdAmount: string } | null | undefined;
32
+ showPercentTabs?: boolean;
33
+ };
34
+
35
+ const defaultPercentOptions: PercentOption[] = [
36
+ { label: "25%", value: 25 },
37
+ { label: "50%", value: 50 },
38
+ { label: "75%", value: 75 },
39
+ { label: "Max", value: 100 },
40
+ ];
41
+
42
+ export const AmountInput = ({
43
+ value,
44
+ onChange,
45
+ tokenSymbol,
46
+ tokenPriceUsd,
47
+ tokenBalance,
48
+ roundingPrecision = 6,
49
+ usdPrecision = 2,
50
+ percentOptions = defaultPercentOptions,
51
+ onPercentSelect,
52
+ showPercentTabs = true,
53
+ }: AmountInputProps) => {
54
+ const updateValue = (next: Partial<AmountInputValue>) => {
55
+ onChange({ ...value, ...next });
56
+ };
57
+
58
+ const handleInputChange = (rawValue: string) => {
59
+ if (value.mode === "usd") {
60
+ const cleanUsd = sanitizeDecimalInput(
61
+ rawValue.replace("$", ""),
62
+ usdPrecision,
63
+ );
64
+
65
+ if (!cleanUsd || cleanUsd === ".") {
66
+ updateValue({ usdAmount: cleanUsd, tokenAmount: "" });
67
+ return;
68
+ }
69
+
70
+ if (!tokenPriceUsd || tokenPriceUsd <= 0) {
71
+ updateValue({ usdAmount: cleanUsd, tokenAmount: "" });
72
+ return;
73
+ }
74
+
75
+ const tokenAmountFromUsd = precisionizeNumber(
76
+ parseFloat(cleanUsd) / tokenPriceUsd,
77
+ roundingPrecision,
78
+ );
79
+ updateValue({
80
+ usdAmount: cleanUsd,
81
+ tokenAmount: tokenAmountFromUsd,
82
+ });
83
+ return;
84
+ }
85
+
86
+ const cleanTokenAmount = sanitizeDecimalInput(
87
+ rawValue,
88
+ roundingPrecision,
89
+ );
90
+
91
+ if (!cleanTokenAmount || cleanTokenAmount === ".") {
92
+ updateValue({ tokenAmount: cleanTokenAmount, usdAmount: "" });
93
+ return;
94
+ }
95
+
96
+ if (!tokenPriceUsd || tokenPriceUsd <= 0) {
97
+ updateValue({ tokenAmount: cleanTokenAmount });
98
+ return;
99
+ }
100
+
101
+ updateValue({
102
+ tokenAmount: cleanTokenAmount,
103
+ usdAmount: (
104
+ parseFloat(cleanTokenAmount) * tokenPriceUsd
105
+ ).toFixed(usdPrecision),
106
+ });
107
+ };
108
+
109
+ const handleToggleMode = () => {
110
+ updateValue({
111
+ mode: value.mode === "usd" ? "token" : "usd",
112
+ });
113
+ };
114
+
115
+ const handlePercentSelect = (percent: number) => {
116
+ if (onPercentSelect) {
117
+ const nextValues = onPercentSelect(percent);
118
+ if (!nextValues) return;
119
+
120
+ updateValue(nextValues);
121
+ return;
122
+ }
123
+
124
+ if (!tokenBalance || !tokenPriceUsd || tokenPriceUsd <= 0) return;
125
+
126
+ const tokenAmount = precisionizeNumber(
127
+ (tokenBalance * percent) / 100,
128
+ roundingPrecision,
129
+ );
130
+
131
+ updateValue({
132
+ tokenAmount,
133
+ usdAmount: (
134
+ parseFloat(tokenAmount || "0") * tokenPriceUsd
135
+ ).toFixed(usdPrecision),
136
+ });
137
+ };
138
+
139
+ const placeholder = value.mode === "usd" ? "$10.00" : "0.00";
140
+ const displayValue =
141
+ value.mode === "usd"
142
+ ? value.usdAmount
143
+ ? `$${value.usdAmount}`
144
+ : ""
145
+ : value.tokenAmount;
146
+
147
+ const hasTokenValue = !!value.tokenAmount && value.tokenAmount !== ".";
148
+ const hasUsdValue = !!value.usdAmount && value.usdAmount !== ".";
149
+
150
+ const formattedTokenValue = hasTokenValue
151
+ ? formatNumber(value.tokenAmount)
152
+ : "";
153
+ const tokenSuffix = tokenSymbol ? ` ${tokenSymbol}` : "";
154
+ const equivalentValue =
155
+ value.mode === "usd"
156
+ ? hasTokenValue
157
+ ? `${formattedTokenValue}${tokenSuffix}`
158
+ : "—"
159
+ : hasUsdValue
160
+ ? formatUSD(value.usdAmount)
161
+ : "—";
162
+
163
+ return (
164
+ <Box display={"flex"} flexDirection={"column"} gap={"8px"}>
165
+ <Box
166
+ display={"flex"}
167
+ flexDirection={"column"}
168
+ gap={"8px"}
169
+ alignItems={"center"}
170
+ padding={"25.5px"}
171
+ >
172
+ {/* Main Input */}
173
+ <Input
174
+ inputMode="decimal"
175
+ marginY={"8px"}
176
+ variant={"text"}
177
+ placeholder={placeholder}
178
+ value={displayValue}
179
+ onChange={(event) => handleInputChange(event.target.value)}
180
+ />
181
+
182
+ {/* Toggle Button and Equivalent Display */}
183
+ <Box
184
+ display={"flex"}
185
+ gap={"3"}
186
+ alignItems={"center"}
187
+ onClick={handleToggleMode}
188
+ _hover={{ background: "bg.subtle" }}
189
+ cursor={"pointer"}
190
+ borderRadius={"lg"}
191
+ px={"3"}
192
+ >
193
+ <IconButton
194
+ minWidth={"24px"}
195
+ minHeight={"24px"}
196
+ maxWidth={"24px"}
197
+ background={"transparent"}
198
+ >
199
+ <Icon
200
+ as={ArrowDownUpIcon}
201
+ color="gray"
202
+ width={"16px"}
203
+ height={"16px"}
204
+ />
205
+ </IconButton>
206
+
207
+ {/* Small equivalent value display */}
208
+ <Text fontSize="sm" color="fg.muted">
209
+ {equivalentValue}
210
+ </Text>
211
+ </Box>
212
+ </Box>
213
+
214
+ {showPercentTabs && percentOptions.length > 0 && (
215
+ <Box
216
+ display={"flex"}
217
+ gap={"4px"}
218
+ justifyContent={"center"}
219
+ paddingBottom={"35px"}
220
+ >
221
+ {percentOptions.map((option) => (
222
+ <Tab
223
+ key={option.label}
224
+ onClick={() => handlePercentSelect(option.value)}
225
+ >
226
+ {option.label}
227
+ </Tab>
228
+ ))}
229
+ </Box>
230
+ )}
231
+ </Box>
232
+ );
233
+ };
@@ -8,7 +8,7 @@ import {
8
8
  type CheckoutConfig,
9
9
  type SupportedExchanges,
10
10
  type EnforceFlow,
11
- } from "@/types";
11
+ } from "../types";
12
12
  import posthog from "posthog-js";
13
13
 
14
14
  type ICheckoutContext = {
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import { Checkout } from "./Checkout";
3
3
  import Modal from "./modal";
4
- import { type CheckoutModalProps } from "@/types";
4
+ import { type CheckoutModalProps } from "../types";
5
5
 
6
6
  export const CheckoutModal = ({
7
7
  config,
@@ -9,7 +9,7 @@ import {
9
9
  Image,
10
10
  Table,
11
11
  } from "@chakra-ui/react";
12
- import { ChevronLeft, X, ArrowDownUpIcon, TriangleAlert } from "lucide-react";
12
+ import { ChevronLeft, X, TriangleAlert } from "lucide-react";
13
13
  import { useContext, useEffect, useMemo, useState, useCallback } from "react";
14
14
  import { useAccount, useSignMessage } from "wagmi";
15
15
  import { getUserOperationHash } from "viem/account-abstraction";
@@ -20,7 +20,8 @@ import {
20
20
  HeaderWrapper,
21
21
  ListWrapper,
22
22
  } from "../ui/styled";
23
- import { IconButton, Button, Tab, Input } from "../ui";
23
+ import { IconButton, Button, Input } from "../ui";
24
+ import { AmountInput, AmountInputValue } from "../AmountInput";
24
25
  import { CheckoutContext } from "../Checkout";
25
26
  import Modal from "../modal";
26
27
  import {
@@ -55,7 +56,7 @@ import { ConfirmExchangeStep } from "../ExchangeConfirmSecurity";
55
56
 
56
57
  import SuccessIcon from "@/assets/success.svg";
57
58
  import FailIcon from "@/assets/fail.svg";
58
- import { SupportedExchanges } from "@/types";
59
+ import { SupportedExchanges } from "../../types";
59
60
  import { useLayerZeroStatus } from "@/util/tx-tracker";
60
61
  import { STARGATE_CHAIN_NAMES, CHAINS_ETHERSCAN } from "@/util/constants";
61
62
 
@@ -797,13 +798,17 @@ const ChooseAmountStep = ({
797
798
  setStep: (step: WithdrawalStep) => void;
798
799
  selectedToken: MatchedToken | null;
799
800
  }) => {
800
- const [amount, setAmount] = useState<string>("");
801
- const [inputMode, setInputMode] = useState<"usd" | "token">("usd");
802
- const [usdValue, setUsdValue] = useState<string>("");
801
+ const [amountInput, setAmountInput] = useState<AmountInputValue>({
802
+ tokenAmount: "",
803
+ usdAmount: "",
804
+ mode: "usd",
805
+ });
803
806
  const setAmountIn = useAppStore((s) => s.setAmountIn);
804
807
  const { tokenInData, selectedIntegration } = useAppDetails();
805
808
  const isStable = selectedToken?.symbol.toLowerCase().includes("USD");
806
809
  const roundingPrecision = isStable ? 2 : 6;
810
+ const amount = amountInput.tokenAmount;
811
+ const usdValue = amountInput.usdAmount;
807
812
 
808
813
  // Only apply CEX withdrawal limits if using a CEX holding (not smart account)
809
814
  const isWithdrawal = !isDelayedBalanceUsed(selectedIntegration.type);
@@ -816,8 +821,13 @@ const ChooseAmountStep = ({
816
821
  : selectedToken.marketValue.toFixed(2)
817
822
  : 0;
818
823
 
824
+ const tokenPriceUsd = useMemo(() => {
825
+ if (!selectedToken || !selectedToken.balance) return undefined;
826
+ return selectedToken.marketValue / selectedToken.balance;
827
+ }, [selectedToken]);
828
+
819
829
  // Handle percentage selection with limits (only for CEX withdrawals)
820
- const handlePercentageSelect = useCallback(
830
+ const getPercentAmounts = useCallback(
821
831
  (percent: number) => {
822
832
  if (!selectedToken) return;
823
833
 
@@ -851,8 +861,13 @@ const ChooseAmountStep = ({
851
861
  finalTokenAmount = (selectedToken.balance * percent) / 100;
852
862
  }
853
863
 
854
- setAmount(precisionizeNumber(finalTokenAmount, roundingPrecision));
855
- setUsdValue(finalUsdAmount.toFixed(2));
864
+ return {
865
+ tokenAmount: precisionizeNumber(
866
+ finalTokenAmount,
867
+ roundingPrecision,
868
+ ),
869
+ usdAmount: finalUsdAmount.toFixed(2),
870
+ };
856
871
  },
857
872
  [selectedToken, isWithdrawal, maxUsdAmount, roundingPrecision],
858
873
  );
@@ -860,85 +875,43 @@ const ChooseAmountStep = ({
860
875
  // Set max value on load
861
876
  useEffect(() => {
862
877
  if (selectedToken) {
863
- handlePercentageSelect(100);
878
+ const percentAmounts = getPercentAmounts(100);
879
+ if (!percentAmounts) return;
880
+
881
+ setAmountInput((prev) => ({
882
+ ...prev,
883
+ ...percentAmounts,
884
+ }));
864
885
  }
865
- }, [selectedToken, handlePercentageSelect]);
886
+ }, [selectedToken, getPercentAmounts]);
866
887
 
867
888
  useEffect(() => {
868
- if (tokenInData?.decimals)
869
- setAmountIn(
870
- Number(
871
- denormalizeValue(amount || "0", tokenInData?.decimals),
872
- ).toFixed(),
873
- );
874
- }, [amount, tokenInData?.decimals]);
889
+ if (!tokenInData?.decimals) return;
875
890
 
876
- // Handle input change based on current mode
877
- const handleInputChange = (value: string) => {
878
- if (!selectedToken) return;
891
+ const normalizedAmount = amount.endsWith(".")
892
+ ? amount.slice(0, -1)
893
+ : amount;
879
894
 
880
- if (inputMode === "usd") {
881
- const cleanUsd = value.replace("$", "");
882
- setUsdValue(cleanUsd);
883
- if (!cleanUsd) {
884
- setAmount("");
885
- return;
886
- }
887
- // Calculate token amount from USD value
888
- const tokenPrice =
889
- selectedToken.marketValue / selectedToken.balance;
890
- const tokenAmount = parseFloat(cleanUsd) / tokenPrice;
891
- setAmount(
892
- isNaN(tokenAmount)
893
- ? ""
894
- : precisionizeNumber(tokenAmount, roundingPrecision),
895
- );
896
- } else {
897
- if (!value) {
898
- setAmount("");
899
- setUsdValue("");
900
- return;
901
- }
902
- setAmount(precisionizeNumber(value, roundingPrecision));
903
- // Calculate USD value from token amount
904
- const tokenPrice =
905
- selectedToken.marketValue / selectedToken.balance;
906
- const usdAmount = parseFloat(value) * tokenPrice;
907
- setUsdValue(isNaN(usdAmount) ? "" : usdAmount.toFixed(2));
895
+ if (!normalizedAmount || normalizedAmount === ".") {
896
+ setAmountIn("0");
897
+ return;
908
898
  }
909
- };
910
899
 
911
- // Toggle between USD and token input modes
912
- const handleToggleMode = () => {
913
- setInputMode(inputMode === "usd" ? "token" : "usd");
914
- };
915
-
916
- // Get input placeholder and display value
917
- const getInputDisplay = () => {
918
- if (inputMode === "usd") {
919
- return {
920
- placeholder: "$10.00",
921
- displayValue: usdValue ? `$${usdValue}` : "",
922
- equivalentValue: amount
923
- ? `${formatNumber(parseFloat(amount))} ${selectedToken?.symbol}`
924
- : "—",
925
- };
926
- } else {
927
- return {
928
- placeholder: "0.00",
929
- displayValue: amount,
930
- equivalentValue: usdValue ? `$${usdValue}` : "—",
931
- };
900
+ try {
901
+ setAmountIn(denormalizeValue(normalizedAmount, tokenInData.decimals));
902
+ } catch (error) {
903
+ setAmountIn("0");
932
904
  }
933
- };
905
+ }, [amount, tokenInData?.decimals, setAmountIn]);
934
906
 
935
- const { placeholder, displayValue, equivalentValue } = getInputDisplay();
907
+ const hasAmount = !!amount && amount !== ".";
908
+ const hasUsdValue = !!usdValue && usdValue !== ".";
936
909
  const notEnoughBalance = selectedToken
937
- ? parseFloat(amount) > selectedToken.balance
910
+ ? hasAmount && parseFloat(amount) > selectedToken.balance
938
911
  : true;
939
912
 
940
913
  // Limits validation logic - only for CEX withdrawals
941
- const currentUsdValue = parseFloat(usdValue);
914
+ const currentUsdValue = hasUsdValue ? parseFloat(usdValue) : 0;
942
915
  const minValueForToken =
943
916
  isWithdrawal && selectedToken
944
917
  ? EXCHANGE_MIN_LIMIT[
@@ -949,17 +922,19 @@ const ChooseAmountStep = ({
949
922
  const isBelowMinAmount =
950
923
  isWithdrawal &&
951
924
  selectedToken &&
925
+ hasAmount &&
952
926
  currentUsdValue > 0 &&
953
927
  minValueForToken &&
954
928
  +amount < minValueForToken;
955
929
  const isAboveMaxAmount =
956
930
  isWithdrawal &&
957
931
  selectedToken &&
932
+ hasAmount &&
958
933
  currentUsdValue > 0 &&
959
934
  currentUsdValue > +maxUsdAmount;
960
935
 
961
936
  const isAmountInvalid =
962
- isBelowMinAmount || isAboveMaxAmount || notEnoughBalance;
937
+ !hasAmount || isBelowMinAmount || isAboveMaxAmount || notEnoughBalance;
963
938
 
964
939
  if (!selectedToken) {
965
940
  return (
@@ -988,70 +963,14 @@ const ChooseAmountStep = ({
988
963
  gap={"8px"}
989
964
  width="100%"
990
965
  >
991
- <Box
992
- display={"flex"}
993
- flexDirection={"column"}
994
- gap={"8px"}
995
- alignItems={"center"}
996
- padding={"25.5px"}
997
- >
998
- {/* Main Input */}
999
- <Input
1000
- inputMode="decimal"
1001
- marginY={"8px"}
1002
- variant={"text"}
1003
- placeholder={placeholder}
1004
- value={displayValue}
1005
- onChange={(e) => handleInputChange(e.target.value)}
1006
- />
1007
-
1008
- {/* Toggle Button and Equivalent Display */}
1009
- <Box
1010
- display={"flex"}
1011
- gap={"3"}
1012
- alignItems={"center"}
1013
- onClick={handleToggleMode}
1014
- _hover={{ background: "bg.subtle" }}
1015
- cursor={"pointer"}
1016
- borderRadius={"lg"}
1017
- px={"3"}
1018
- >
1019
- <IconButton
1020
- minWidth={"24px"}
1021
- minHeight={"24px"}
1022
- maxWidth={"24px"}
1023
- background={"transparent"}
1024
- >
1025
- <Icon
1026
- as={ArrowDownUpIcon}
1027
- color="gray"
1028
- width={"16px"}
1029
- height={"16px"}
1030
- />
1031
- </IconButton>
1032
-
1033
- {/* Small equivalent value display */}
1034
- <Text fontSize="sm" color="fg.muted">
1035
- {equivalentValue}
1036
- </Text>
1037
- </Box>
1038
- </Box>
1039
-
1040
- <Box
1041
- display={"flex"}
1042
- gap={"4px"}
1043
- justifyContent={"center"}
1044
- paddingBottom={"35px"}
1045
- >
1046
- {[25, 50, 75, 100].map((percent) => (
1047
- <Tab
1048
- key={percent}
1049
- onClick={() => handlePercentageSelect(percent)}
1050
- >
1051
- {percent === 100 ? "Max" : `${percent}%`}
1052
- </Tab>
1053
- ))}
1054
- </Box>
966
+ <AmountInput
967
+ value={amountInput}
968
+ onChange={setAmountInput}
969
+ tokenSymbol={selectedToken.symbol}
970
+ tokenPriceUsd={tokenPriceUsd}
971
+ roundingPrecision={roundingPrecision}
972
+ onPercentSelect={getPercentAmounts}
973
+ />
1055
974
  </Box>
1056
975
 
1057
976
  {
@@ -1062,10 +981,10 @@ const ChooseAmountStep = ({
1062
981
  h={3}
1063
982
  m={-1}
1064
983
  visibility={
1065
- isAmountInvalid || !amount ? "visible" : "hidden"
984
+ isAmountInvalid ? "visible" : "hidden"
1066
985
  }
1067
986
  >
1068
- {!amount
987
+ {!hasAmount
1069
988
  ? "Please enter an amount"
1070
989
  : isBelowMinAmount
1071
990
  ? `Minimum amount is ${formatNumber(minValueForToken)} ${selectedToken.symbol}`
@@ -1079,11 +998,11 @@ const ChooseAmountStep = ({
1079
998
 
1080
999
  <Button
1081
1000
  onClick={() =>
1082
- isAmountInvalid || !amount
1001
+ isAmountInvalid
1083
1002
  ? undefined
1084
1003
  : setStep(WithdrawalStep.SignUserOp)
1085
1004
  }
1086
- disabled={isAmountInvalid || !amount}
1005
+ disabled={isAmountInvalid}
1087
1006
  >
1088
1007
  Continue
1089
1008
  </Button>