@ensofinance/checkout-widget 0.0.18 → 0.0.20

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
@@ -4,7 +4,7 @@ import { ComponentType } from 'react';
4
4
  import { JSX as JSX_2 } from 'react/jsx-runtime';
5
5
  import { SystemConfig as WidgetTheme } from '@chakra-ui/react';
6
6
 
7
- export declare const Checkout: ({ config: { apiKey, tokenOut, chainIdOut, theme, enableExchange, cexBridgeChainMapping }, onClose, wrapper, }: {
7
+ export declare const Checkout: ({ config: { apiKey, tokenOut, chainIdOut, theme, enableExchange, cexBridgeChainMapping, recipient, enforceFlow, }, onClose, wrapper, }: {
8
8
  config: CheckoutConfig;
9
9
  wrapper?: ComponentType;
10
10
  onClose?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ensofinance/checkout-widget",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "homepage": "https://www.enso.build/",
6
6
  "repository": {
@@ -283,6 +283,9 @@ export const config = defineConfig({
283
283
  border: {
284
284
  value: "#E4E4E7",
285
285
  },
286
+ "border.subtle": {
287
+ value: "#F4F4F5",
288
+ },
286
289
  "border.emphasized": {
287
290
  value: "#1e171f4d",
288
291
  },
@@ -4,18 +4,33 @@ import FlowSelector from "./steps/FlowSelector";
4
4
  import { useAppStore } from "@/store";
5
5
  import { TxTracker } from "@/util/tx-tracker";
6
6
  import ChakraProvider from "./ChakraProvider";
7
- import { type CheckoutConfig, type SupportedExchanges } from "@/types";
7
+ import {
8
+ type CheckoutConfig,
9
+ type SupportedExchanges,
10
+ type EnforceFlow,
11
+ } from "@/types";
12
+ import posthog from "posthog-js";
8
13
 
9
14
  type ICheckoutContext = {
10
15
  handleClose: () => void;
11
16
  enableExchange?: SupportedExchanges[];
12
17
  cexBridgeChainMapping?: Record<number, number>;
18
+ enforceFlow?: EnforceFlow;
13
19
  };
14
20
 
15
21
  const CheckoutContext = createContext<ICheckoutContext>({} as ICheckoutContext);
16
22
 
17
23
  const Checkout = ({
18
- config: { apiKey, tokenOut, chainIdOut, theme, enableExchange, cexBridgeChainMapping },
24
+ config: {
25
+ apiKey,
26
+ tokenOut,
27
+ chainIdOut,
28
+ theme,
29
+ enableExchange,
30
+ cexBridgeChainMapping,
31
+ recipient,
32
+ enforceFlow,
33
+ },
19
34
  onClose,
20
35
  wrapper,
21
36
  }: {
@@ -26,6 +41,7 @@ const Checkout = ({
26
41
  const setEnsoApiToken = useAppStore((state) => state.setEnsoApiToken);
27
42
  const setTokenOut = useAppStore((state) => state.setTokenOut);
28
43
  const setChainIdOut = useAppStore((state) => state.setChainIdOut);
44
+ const setRecipient = useAppStore((state) => state.setRecipient);
29
45
 
30
46
  const handleClose = useMemo(
31
47
  () =>
@@ -42,9 +58,14 @@ const Checkout = ({
42
58
  useEffect(() => {
43
59
  setTokenOut(tokenOut);
44
60
  setChainIdOut(chainIdOut);
45
- }, [tokenOut, chainIdOut]);
61
+ setRecipient(recipient);
62
+ }, [tokenOut, chainIdOut, recipient]);
46
63
 
47
64
  useEffect(() => {
65
+ posthog.init("phc_capPDVae4W7y6QIqVTugTtx5geVthX4YVswtXa6DrjM", {
66
+ api_host: "https://eu.i.posthog.com",
67
+ person_profiles: "always", // or 'always' to create profiles for anonymous users as well
68
+ });
48
69
  if (!apiKey) alert("Please provide an API key");
49
70
  setEnsoApiToken(apiKey);
50
71
  }, [apiKey]);
@@ -66,6 +87,7 @@ const Checkout = ({
66
87
  handleClose,
67
88
  enableExchange,
68
89
  cexBridgeChainMapping,
90
+ enforceFlow,
69
91
  }}
70
92
  >
71
93
  <ChakraProvider themeConfig={theme}>
@@ -294,9 +294,9 @@ const ChooseExchangeStep = ({
294
294
 
295
295
  return (
296
296
  <BodyWrapper>
297
- <Box mb={4} width="100%" textAlign="left">
298
- <HeaderTitle>Choose Exchange</HeaderTitle>
299
- </Box>
297
+ {/*<Box mb={4} width="100%" textAlign="left">*/}
298
+ {/* <HeaderTitle>Choose Exchange</HeaderTitle>*/}
299
+ {/*</Box>*/}
300
300
 
301
301
  {integrations?.length > 0 ? (
302
302
  <ListWrapper>
@@ -478,7 +478,6 @@ const DEVICE_ID_KEY = "meshDeviceId";
478
478
  const useDeviceId = () => {
479
479
  return useMemo(() => {
480
480
  let deviceId = localStorage.getItem(DEVICE_ID_KEY);
481
- console.log(deviceId);
482
481
 
483
482
  if (!deviceId) {
484
483
  deviceId = `device_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
@@ -1967,7 +1966,7 @@ const ExchangeFlow = ({
1967
1966
  setFlow: (string) => void;
1968
1967
  initialStep?: WithdrawalStep;
1969
1968
  }) => {
1970
- const { handleClose } = useContext(CheckoutContext);
1969
+ const { handleClose, enforceFlow } = useContext(CheckoutContext);
1971
1970
  const [currentStep, setCurrentStep] = useState(initialStep);
1972
1971
  const [selectedToken, setSelectedToken] = useState<MatchedToken | null>(
1973
1972
  null,
@@ -2047,30 +2046,38 @@ const ExchangeFlow = ({
2047
2046
  <>
2048
2047
  <Modal.Header>
2049
2048
  <HeaderWrapper>
2050
- <IconButton
2051
- minWidth={"16px"}
2052
- minHeight={"16px"}
2053
- maxWidth={"16px"}
2054
- onClick={() => {
2055
- const index =
2056
- (selectedIntegration?.type === "delayed"
2057
- ? balanceSteps
2058
- : withdrawalSteps
2059
- ).findIndex((step) => step === currentStep) - 1;
2060
- if (index > 0) {
2061
- setCurrentStep(withdrawalSteps[index]);
2062
- } else {
2063
- setFlow("");
2064
- }
2065
- }}
2066
- >
2067
- <Icon
2068
- as={ChevronLeft}
2069
- color="gray"
2070
- width={"16px"}
2071
- height={"16px"}
2072
- />
2073
- </IconButton>
2049
+ {!(
2050
+ enforceFlow &&
2051
+ (currentStep === WithdrawalStep.ChooseExchange ||
2052
+ currentStep === WithdrawalStep.ChooseBalanceAsset)
2053
+ ) && (
2054
+ <IconButton
2055
+ minWidth={"16px"}
2056
+ minHeight={"16px"}
2057
+ maxWidth={"16px"}
2058
+ onClick={() => {
2059
+ const index =
2060
+ (selectedIntegration?.type === "delayed"
2061
+ ? balanceSteps
2062
+ : withdrawalSteps
2063
+ ).findIndex(
2064
+ (step) => step === currentStep,
2065
+ ) - 1;
2066
+ if (index >= 0) {
2067
+ setCurrentStep(withdrawalSteps[index]);
2068
+ } else {
2069
+ setFlow("");
2070
+ }
2071
+ }}
2072
+ >
2073
+ <Icon
2074
+ as={ChevronLeft}
2075
+ color="gray"
2076
+ width={"16px"}
2077
+ height={"16px"}
2078
+ />
2079
+ </IconButton>
2080
+ )}
2074
2081
 
2075
2082
  <Box
2076
2083
  display="flex"
@@ -1,6 +1,6 @@
1
- import { Box, Icon, Skeleton } from "@chakra-ui/react";
2
- import { X } from "lucide-react";
3
- import { useContext, useMemo, useState } from "react";
1
+ import { Box, Icon, Skeleton, Text } from "@chakra-ui/react";
2
+ import { X, AlertCircle } from "lucide-react";
3
+ import { useContext, useEffect, useMemo, useState } from "react";
4
4
  import { useAccount } from "wagmi";
5
5
  import { IconButton } from "../ui";
6
6
  import {
@@ -22,17 +22,21 @@ import ExchangeFlow, {
22
22
  EXCHANGE_ICON_BY_TYPE,
23
23
  ExchangeToIntegrationType,
24
24
  } from "@/components/steps/ExchangeFlow";
25
- import WalletFlow from "@/components/steps/WalletFlow/WalletFlow";
25
+ import WalletFlow, {
26
+ WalletFlowStep,
27
+ } from "@/components/steps/WalletFlow/WalletFlow";
26
28
 
27
29
  const FLOWS = {
28
- exchangeFlow: ExchangeFlow,
29
- walletFlow: WalletFlow,
30
+ exchange: ExchangeFlow,
31
+ wallet: WalletFlow,
30
32
  };
31
33
 
32
34
  const FlowSelector = () => {
33
- const { handleClose, enableExchange } = useContext(CheckoutContext);
35
+ const { handleClose, enableExchange, enforceFlow } =
36
+ useContext(CheckoutContext);
34
37
  const [flow, setFlow] = useState("");
35
38
  const [initialStep, setInitialStep] = useState<string | number>("");
39
+ const [enforceError, setEnforceError] = useState<string | null>(null);
36
40
 
37
41
  const { total, isLoading } = useWalletBalance();
38
42
  const { address } = useAccount();
@@ -43,6 +47,28 @@ const FlowSelector = () => {
43
47
  const { total: smartAccountTotal, isLoading: isLoadingSmartAccount } =
44
48
  useSmartAccountBalances(1);
45
49
 
50
+ // Handle enforceFlow on mount
51
+ useEffect(() => {
52
+ if (!enforceFlow) return;
53
+
54
+ if (enforceFlow === "exchange") {
55
+ if (!Array.isArray(enableExchange) || enableExchange.length === 0) {
56
+ setEnforceError(
57
+ "No exchanges configured. Please enable at least one exchange in the widget configuration.",
58
+ );
59
+ return;
60
+ }
61
+ setFlow("exchange");
62
+ setInitialStep(WithdrawalStep.ChooseExchange);
63
+ } else if (enforceFlow === "wallet") {
64
+ if (address) {
65
+ setFlow("wallet");
66
+ setInitialStep(WalletFlowStep.SelectToken);
67
+ }
68
+ // If no address, we'll show the wallet connection prompt in the UI
69
+ }
70
+ }, [enforceFlow, enableExchange, address]);
71
+
46
72
  const formattedBalance = useMemo(() => {
47
73
  if (isLoading)
48
74
  return (
@@ -100,16 +126,10 @@ const FlowSelector = () => {
100
126
  limit: formatUSD(smartAccountTotal),
101
127
  delay: "2 min",
102
128
  icons: [],
103
- flow: "exchangeFlow",
129
+ flow: "exchange",
104
130
  firstStep: WithdrawalStep.ChooseBalanceAsset,
105
131
  });
106
132
  }
107
- console.log(
108
- enableExchange,
109
- enableExchange.map(
110
- (integration) => EXCHANGE_ICON_BY_TYPE[integration],
111
- ),
112
- );
113
133
 
114
134
  if (Array.isArray(enableExchange) && enableExchange.length > 0)
115
135
  options.unshift({
@@ -125,7 +145,7 @@ const FlowSelector = () => {
125
145
  ],
126
146
  )
127
147
  .filter(Boolean),
128
- flow: "exchangeFlow",
148
+ flow: "exchange",
129
149
  // Start at ChooseExchange to allow picking among multiple integrations
130
150
  firstStep: WithdrawalStep.ChooseExchange,
131
151
  });
@@ -138,6 +158,89 @@ const FlowSelector = () => {
138
158
  return <FlowComponent setFlow={setFlow} initialStep={initialStep} />;
139
159
  }
140
160
 
161
+ // Error state for enforced exchange mode with no exchanges configured
162
+ if (enforceError) {
163
+ return (
164
+ <>
165
+ <Modal.Header>
166
+ <InitialStepHeaderWrapper>
167
+ <Box display="flex" flexDirection="column" gap={"4px"}>
168
+ <HeaderTitle>Configuration Error</HeaderTitle>
169
+ </Box>
170
+ {handleClose && (
171
+ <IconButton onClick={handleClose} width={"40px"}>
172
+ <Icon
173
+ as={X}
174
+ color="fg.muted"
175
+ width={"16px"}
176
+ height={"16px"}
177
+ />
178
+ </IconButton>
179
+ )}
180
+ </InitialStepHeaderWrapper>
181
+ </Modal.Header>
182
+ <Modal.Body>
183
+ <BodyWrapper>
184
+ <Box
185
+ display="flex"
186
+ flexDirection="column"
187
+ alignItems="center"
188
+ gap="4"
189
+ p="6"
190
+ textAlign="center"
191
+ >
192
+ <Icon
193
+ as={AlertCircle}
194
+ color="red.500"
195
+ width="48px"
196
+ height="48px"
197
+ />
198
+ <Text color="fg.muted">{enforceError}</Text>
199
+ </Box>
200
+ </BodyWrapper>
201
+ </Modal.Body>
202
+ </>
203
+ );
204
+ }
205
+
206
+ // Wallet connection prompt for enforced wallet mode without wallet connected
207
+ if (enforceFlow === "wallet" && !address) {
208
+ return (
209
+ <>
210
+ <Modal.Header>
211
+ <InitialStepHeaderWrapper>
212
+ <Box display="flex" flexDirection="column" gap={"4px"}>
213
+ <HeaderTitle>Connect Wallet</HeaderTitle>
214
+ </Box>
215
+ {handleClose && (
216
+ <IconButton onClick={handleClose} width={"40px"}>
217
+ <Icon
218
+ as={X}
219
+ color="fg.muted"
220
+ width={"16px"}
221
+ height={"16px"}
222
+ />
223
+ </IconButton>
224
+ )}
225
+ </InitialStepHeaderWrapper>
226
+ </Modal.Header>
227
+ <Modal.Body>
228
+ <BodyWrapper>
229
+ <ListWrapper>
230
+ <WalletCard
231
+ icon={walletIcon}
232
+ walletHash="Not Connected"
233
+ balance="Connect your wallet to continue"
234
+ delay=""
235
+ status={WalletStatus.NONE}
236
+ />
237
+ </ListWrapper>
238
+ </BodyWrapper>
239
+ </Modal.Body>
240
+ </>
241
+ );
242
+ }
243
+
141
244
  return (
142
245
  <>
143
246
  <Modal.Header>
@@ -172,7 +275,7 @@ const FlowSelector = () => {
172
275
  status={WalletStatus.CONNECTED}
173
276
  badge={walletDisplayName}
174
277
  onClick={() => {
175
- setFlow("walletFlow");
278
+ setFlow("wallet");
176
279
  }}
177
280
  />
178
281
  }
@@ -32,7 +32,7 @@ const WalletFlow = ({
32
32
  setFlow?: (step: string) => void;
33
33
  }) => {
34
34
  const [currentStep, setCurrentStep] = useState<WalletFlowStep>(initialStep);
35
- const { handleClose } = useContext(CheckoutContext);
35
+ const { handleClose, enforceFlow } = useContext(CheckoutContext);
36
36
 
37
37
  const handleSetStep = (step: WalletFlowStep | string) => {
38
38
  setCurrentStep(step as WalletFlowStep);
@@ -62,23 +62,31 @@ const WalletFlow = ({
62
62
  }
63
63
  })();
64
64
 
65
+ const enforcedInitialStep =
66
+ enforceFlow && currentStep === WalletFlowStep.SelectToken;
67
+
65
68
  return (
66
69
  <>
67
70
  <Modal.Header>
68
71
  <HeaderWrapper>
69
- <IconButton
70
- minWidth={"16px"}
71
- minHeight={"16px"}
72
- maxWidth={"16px"}
73
- onClick={handleBackClick}
74
- >
75
- <Icon
76
- as={ChevronLeft}
77
- color="gray"
78
- width={"16px"}
79
- height={"16px"}
80
- />
81
- </IconButton>
72
+ {!(
73
+ enforceFlow &&
74
+ currentStep === WalletFlowStep.SelectToken
75
+ ) && (
76
+ <IconButton
77
+ minWidth={"16px"}
78
+ minHeight={"16px"}
79
+ maxWidth={"16px"}
80
+ onClick={handleBackClick}
81
+ >
82
+ <Icon
83
+ as={ChevronLeft}
84
+ color="gray"
85
+ width={"16px"}
86
+ height={"16px"}
87
+ />
88
+ </IconButton>
89
+ )}
82
90
 
83
91
  <Box
84
92
  display="flex"
package/src/store.ts CHANGED
@@ -30,6 +30,10 @@ type Store = {
30
30
  slippage?: number;
31
31
  setSlippage: (slippage: number) => void;
32
32
 
33
+ // Override recipient address (different from connected wallet)
34
+ recipient?: string;
35
+ setRecipient: (recipient: string | undefined) => void;
36
+
33
37
  // Mesh integration
34
38
  meshAccessToken: AccessTokenPayload | null;
35
39
  setMeshAccessToken: (meshAccessToken: AccessTokenPayload) => void;
@@ -63,6 +67,10 @@ export const useAppStore = create<Store>((set) => ({
63
67
  slippage: DEFAULT_SLIPPAGE,
64
68
  setSlippage: (slippage: number) => set({ slippage }),
65
69
 
70
+ // Override recipient address
71
+ recipient: undefined,
72
+ setRecipient: (recipient: string | undefined) => set({ recipient }),
73
+
66
74
  // Mesh integration
67
75
  meshAccessToken: null,
68
76
  setMeshAccessToken: (meshAccessToken: AccessTokenPayload) =>
@@ -8,6 +8,8 @@ enum SupportedExchanges {
8
8
  Bybit = "bybit",
9
9
  }
10
10
 
11
+ export type EnforceFlow = "exchange" | "wallet";
12
+
11
13
  export type CheckoutConfig = {
12
14
  tokenOut: string;
13
15
  chainIdOut: number;
@@ -16,6 +18,10 @@ export type CheckoutConfig = {
16
18
  enableExchange?: SupportedExchanges[];
17
19
  /** Override the default CEX bridge chain mapping (maps target chains to intermediate chains for withdrawal + bridge) */
18
20
  cexBridgeChainMapping?: Record<number, number>;
21
+ /** Override recipient address (defaults to connected wallet's smart account) */
22
+ recipient?: string;
23
+ /** Force the widget to open in a specific flow, bypassing the selector */
24
+ enforceFlow?: EnforceFlow;
19
25
  };
20
26
 
21
27
  export type CheckoutModalProps = {
@@ -18,7 +18,7 @@ export const EXCHANGE_MIN_LIMIT = {
18
18
  [MESH_SYMBOLS.USDT]: 20,
19
19
  [MESH_SYMBOLS.WBTC]: 0.00000001,
20
20
  };
21
- export const EXCHANGE_MAX_LIMIT_GAP_USD = 5
21
+ export const EXCHANGE_MAX_LIMIT_GAP_USD = 5;
22
22
 
23
23
  export const ETH_TOKEN: Token = {
24
24
  address: ETH_ADDRESS,
@@ -197,14 +197,6 @@ export const DEFAULT_CEX_BRIDGE_CHAIN_MAPPING: Record<number, number> = {
197
197
  [SupportedChainId.HYPERLIQUID]: SupportedChainId.ARBITRUM_ONE, // 999 → 42161
198
198
  };
199
199
 
200
- /** @deprecated Use the context-aware version with custom mapping parameter */
201
- export const CEX_BRIDGE_CHAIN_MAPPING = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING;
202
-
203
- export const isCexBridgeRequired = (
204
- chainId: number,
205
- mapping: Record<number, number> = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
206
- ): boolean => chainId in mapping;
207
-
208
200
  export const getCexIntermediateChain = (
209
201
  chainId: number,
210
202
  mapping: Record<number, number> = DEFAULT_CEX_BRIDGE_CHAIN_MAPPING,
@@ -14,9 +14,9 @@ import {
14
14
  import { getWalletDisplayName, getWalletIcon } from "@/util/wallet";
15
15
  import { formatUSD, normalizeValue } from "@/util";
16
16
  import { useAppStore } from "@/store";
17
- import { VITALIK_ADDRESS } from "./constants";
17
+ import { SupportedChainId, VITALIK_ADDRESS } from "./constants";
18
18
 
19
- export function getERC4337CloneFactory(chainId: ChainIds): AddressArg {
19
+ export function getERC4337CloneFactory(chainId: SupportedChainId): Address {
20
20
  return "0x1a59347d28f64091079fa04a2cbd03da63dff154";
21
21
  }
22
22
 
@@ -84,6 +84,7 @@ export const useAppDetails = () => {
84
84
  const selectedIntegration = useAppStore(
85
85
  (state) => state.selectedIntegration,
86
86
  );
87
+ const recipient = useAppStore((state) => state.recipient);
87
88
 
88
89
  const {
89
90
  data: [tokenInData],
@@ -115,7 +116,8 @@ export const useAppDetails = () => {
115
116
  }),
116
117
  );
117
118
 
118
- const protocolName = protocolData?.name || nontokenizedData.protocol;
119
+ const protocolName =
120
+ protocolData?.name || nontokenizedData.protocol;
119
121
 
120
122
  // Return first underlying token as primary, with all underlyingTokens available
121
123
  return {
@@ -153,6 +155,7 @@ export const useAppDetails = () => {
153
155
  effectiveTokenOutData,
154
156
  effectivePrice,
155
157
  isNontokenized,
158
+ recipient,
156
159
  };
157
160
  };
158
161
 
@@ -262,12 +265,16 @@ export const useRouteData = () => {
262
265
  tokenOut,
263
266
  selectedIntegration,
264
267
  isNontokenized,
268
+ recipient,
265
269
  } = useAppDetails();
266
270
 
271
+ // Use recipient override if provided, otherwise use connected wallet address
272
+ const receiver = recipient || address;
273
+
267
274
  const standardRoute = useTokenizedRouteData({
268
275
  routingStrategy: selectedIntegration?.type ? "checkout" : "router",
269
276
  fromAddress: address,
270
- receiver: address,
277
+ receiver,
271
278
  spender: address,
272
279
  amountIn,
273
280
  tokenIn,
@@ -281,7 +288,7 @@ export const useRouteData = () => {
281
288
  const nontokenizedRoute = useNontokenizedRouteData({
282
289
  routingStrategy: selectedIntegration?.type ? "checkout" : "router",
283
290
  fromAddress: address,
284
- receiver: address,
291
+ receiver,
285
292
  spender: address,
286
293
  amountIn,
287
294
  tokenIn,