@ensofinance/checkout-widget 0.1.9 → 1.0.1

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.
Files changed (62) hide show
  1. package/dist/checkout-widget.es.js +25671 -24394
  2. package/dist/checkout-widget.es.js.map +1 -0
  3. package/dist/checkout-widget.umd.js +65 -59
  4. package/dist/checkout-widget.umd.js.map +1 -0
  5. package/dist/index.d.ts +3 -1
  6. package/package.json +2 -2
  7. package/src/assets/providers/alchemypay.svg +21 -0
  8. package/src/assets/providers/banxa.svg +21 -0
  9. package/src/assets/providers/binanceconnect.svg +14 -0
  10. package/src/assets/providers/kryptonim.svg +6 -0
  11. package/src/assets/providers/mercuryo.svg +21 -0
  12. package/src/assets/providers/moonpay.svg +14 -0
  13. package/src/assets/providers/stripe.svg +16 -0
  14. package/src/assets/providers/swapped.svg +1 -0
  15. package/src/assets/providers/topper.svg +14 -0
  16. package/src/assets/providers/transak.svg +21 -0
  17. package/src/assets/providers/unlimit.svg +21 -0
  18. package/src/components/AmountInput.tsx +41 -25
  19. package/src/components/ChakraProvider.tsx +36 -13
  20. package/src/components/Checkout.tsx +3 -0
  21. package/src/components/CurrencySwapDisplay.tsx +59 -22
  22. package/src/components/DepositProcessing.tsx +1 -1
  23. package/src/components/ExchangeConfirmSecurity.tsx +1 -1
  24. package/src/components/QuoteParameters.tsx +1 -1
  25. package/src/components/TransactionDetailRow.tsx +2 -2
  26. package/src/components/cards/ExchangeCard.tsx +1 -1
  27. package/src/components/cards/OptionCard.tsx +2 -1
  28. package/src/components/cards/WalletCard.tsx +1 -1
  29. package/src/components/modal.tsx +3 -3
  30. package/src/components/steps/CardBuyFlow/CardBuyFlow.tsx +420 -0
  31. package/src/components/steps/CardBuyFlow/ChooseAmountStep.tsx +343 -0
  32. package/src/components/steps/CardBuyFlow/OpenWidgetStep.tsx +189 -0
  33. package/src/components/steps/ExchangeFlow.tsx +237 -1410
  34. package/src/components/steps/FlowSelector.tsx +118 -61
  35. package/src/components/steps/SmartAccountFlow.tsx +372 -0
  36. package/src/components/steps/WalletFlow/WalletAmountStep.tsx +2 -2
  37. package/src/components/steps/WalletFlow/WalletConfirmStep.tsx +93 -52
  38. package/src/components/steps/WalletFlow/WalletFlow.tsx +17 -16
  39. package/src/components/steps/WalletFlow/WalletQuoteStep.tsx +2 -2
  40. package/src/components/steps/WalletFlow/WalletTokenStep.tsx +6 -4
  41. package/src/components/steps/shared/ChooseAmountStep.tsx +325 -0
  42. package/src/components/steps/shared/SignUserOpStep.tsx +117 -0
  43. package/src/components/steps/shared/TrackUserOpStep.tsx +650 -0
  44. package/src/components/steps/shared/exchangeIntegration.ts +19 -0
  45. package/src/components/steps/shared/types.ts +22 -0
  46. package/src/components/ui/index.tsx +23 -6
  47. package/src/components/ui/toaster.tsx +2 -1
  48. package/src/components/ui/transitions.tsx +16 -0
  49. package/src/enso-api/custom-instance.ts +5 -1
  50. package/src/enso-api/model/bridgeTransactionResponse.ts +37 -0
  51. package/src/enso-api/model/bridgeTransactionResponseStatus.ts +25 -0
  52. package/src/enso-api/model/ensoEvent.ts +30 -0
  53. package/src/enso-api/model/ensoMetadata.ts +23 -0
  54. package/src/enso-api/model/layerZeroControllerCheckBridgeTransactionParams.ts +21 -0
  55. package/src/enso-api/model/layerZeroMessageStatus.ts +39 -0
  56. package/src/enso-api/model/refundDetails.ts +21 -0
  57. package/src/types/index.ts +97 -0
  58. package/src/util/constants.tsx +27 -0
  59. package/src/util/enso-hooks.tsx +75 -61
  60. package/src/util/meld-hooks.tsx +494 -0
  61. package/src/assets/usdc.webp +0 -0
  62. package/src/assets/usdt.webp +0 -0
@@ -0,0 +1,343 @@
1
+ import {
2
+ Center,
3
+ Spinner,
4
+ Box,
5
+ Icon,
6
+ Text,
7
+ Flex,
8
+ Skeleton,
9
+ Image,
10
+ } from "@chakra-ui/react";
11
+ import { ChevronRight, Check } from "lucide-react";
12
+ import { useState } from "react";
13
+ import { BodyWrapper } from "../../ui/styled";
14
+ import { Button, Card, Input, Tag } from "../../ui";
15
+ import { AnimatedStep } from "../../ui/transitions";
16
+ import { formatProviderName, getProviderIcon } from "@/util/meld-hooks";
17
+ import type { MeldQuote } from "@/types";
18
+
19
+ interface ChooseAmountStepProps {
20
+ fiatCurrency: "USD" | "EUR";
21
+ setFiatCurrency: (currency: "USD" | "EUR") => void;
22
+ fiatAmount: string;
23
+ setFiatAmount: (amount: string) => void;
24
+ quotes: MeldQuote[] | undefined;
25
+ quotesLoading: boolean;
26
+ quotesError: string | null;
27
+ selectedQuote: MeldQuote | null;
28
+ setSelectedQuote: (quote: MeldQuote | null) => void;
29
+ destinationSymbol: string;
30
+ onContinue: () => void;
31
+ }
32
+
33
+ const ChooseAmountStep = ({
34
+ fiatCurrency,
35
+ setFiatCurrency,
36
+ fiatAmount,
37
+ setFiatAmount,
38
+ quotes,
39
+ quotesLoading,
40
+ quotesError,
41
+ selectedQuote,
42
+ setSelectedQuote,
43
+ destinationSymbol,
44
+ onContinue,
45
+ }: ChooseAmountStepProps) => {
46
+ const [showProviderSelect, setShowProviderSelect] = useState(false);
47
+ const fiatPrefix = fiatCurrency === "EUR" ? "€" : "$";
48
+
49
+ const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
50
+ const raw = e.target.value.replace(/[^0-9.]/g, "");
51
+ // Prevent multiple decimals
52
+ const parts = raw.split(".");
53
+ const sanitized =
54
+ parts.length > 2 ? parts[0] + "." + parts.slice(1).join("") : raw;
55
+ setFiatAmount(sanitized);
56
+ };
57
+
58
+ const displayValue = fiatAmount ? `${fiatPrefix}${fiatAmount}` : "";
59
+
60
+ // Check if selected quote is the best (first in list)
61
+ const isBestRate =
62
+ selectedQuote &&
63
+ quotes &&
64
+ quotes.length > 0 &&
65
+ quotes[0].serviceProvider === selectedQuote.serviceProvider;
66
+
67
+ // Initial loading: no quote yet and quotes are being fetched
68
+ const isInitialLoading =
69
+ quotesLoading && !selectedQuote && parseFloat(fiatAmount) > 0;
70
+
71
+ // Provider Selection View
72
+ if (showProviderSelect) {
73
+ return (
74
+ <AnimatedStep key="provider-select">
75
+ <BodyWrapper>
76
+ {quotesLoading ? (
77
+ <Flex direction="column" gap={2}>
78
+ <Skeleton height="48px" borderRadius="card" />
79
+ <Skeleton height="48px" borderRadius="card" />
80
+ <Skeleton height="48px" borderRadius="card" />
81
+ </Flex>
82
+ ) : quotes && quotes.length > 0 ? (
83
+ <Flex direction="column" gap={2}>
84
+ {quotes.map((quote, idx) => {
85
+ const isSelected =
86
+ selectedQuote?.serviceProvider ===
87
+ quote.serviceProvider;
88
+ return (
89
+ <Card
90
+ key={`${quote.serviceProvider}-${idx}`}
91
+ onClick={() => {
92
+ setSelectedQuote(quote);
93
+ setShowProviderSelect(false);
94
+ }}
95
+ selected={isSelected}
96
+ >
97
+ <Box
98
+ display="flex"
99
+ alignItems="center"
100
+ gap="1"
101
+ minWidth="320px"
102
+ >
103
+ <Box
104
+ display="flex"
105
+ alignItems="center"
106
+ gap="1"
107
+ marginRight="auto"
108
+ >
109
+ <Center
110
+ boxSize="32px"
111
+ borderRadius="card"
112
+ bg="bg.subtle"
113
+ overflow="hidden"
114
+ flexShrink={0}
115
+ >
116
+ <Image
117
+ src={getProviderIcon(
118
+ quote.serviceProvider,
119
+ )}
120
+ alt={quote.serviceProvider}
121
+ boxSize="32px"
122
+ objectFit="contain"
123
+ onError={(e) => {
124
+ (
125
+ e.target as HTMLImageElement
126
+ ).style.display =
127
+ "none";
128
+ }}
129
+ />
130
+ </Center>
131
+ <Box>
132
+ <Card.Title>
133
+ {formatProviderName(
134
+ quote.serviceProvider,
135
+ )}
136
+ </Card.Title>
137
+ <Card.Description>
138
+ {quote.destinationAmount.toFixed(
139
+ 4,
140
+ )}{" "}
141
+ {destinationSymbol} • Fee:{" "}
142
+ {fiatPrefix}
143
+ {quote.totalFee.toFixed(2)}
144
+ </Card.Description>
145
+ </Box>
146
+ </Box>
147
+ {idx === 0 && <Tag>Best price</Tag>}
148
+ {quote.lowKyc && <Tag>Low KYC</Tag>}
149
+ <Icon
150
+ as={Check}
151
+ color={
152
+ isSelected
153
+ ? "primary"
154
+ : "transparent"
155
+ }
156
+ boxSize={5}
157
+ flexShrink={0}
158
+ />
159
+ </Box>
160
+ </Card>
161
+ );
162
+ })}
163
+ </Flex>
164
+ ) : (
165
+ <Text color="fg.muted" textAlign="center" py={4}>
166
+ No providers available
167
+ </Text>
168
+ )}
169
+ </BodyWrapper>
170
+ </AnimatedStep>
171
+ );
172
+ }
173
+
174
+ // Main View
175
+ return (
176
+ <AnimatedStep key="main-view">
177
+ <BodyWrapper>
178
+ {/* Currency Toggle */}
179
+ <Flex gap={2} width="100%">
180
+ <Button
181
+ visual={fiatCurrency === "USD" ? "solid" : "lightGray"}
182
+ onClick={() => setFiatCurrency("USD")}
183
+ flex={1}
184
+ size="sm"
185
+ >
186
+ USD
187
+ </Button>
188
+ <Button
189
+ visual={fiatCurrency === "EUR" ? "solid" : "lightGray"}
190
+ onClick={() => setFiatCurrency("EUR")}
191
+ flex={1}
192
+ size="sm"
193
+ >
194
+ EUR
195
+ </Button>
196
+ </Flex>
197
+
198
+ {/* Amount Input - styled like AmountInput */}
199
+ <Box
200
+ display="flex"
201
+ flexDirection="column"
202
+ alignItems="center"
203
+ padding="25.5px"
204
+ >
205
+ <Input
206
+ variant="text"
207
+ inputMode="decimal"
208
+ placeholder={`${fiatPrefix}100`}
209
+ value={displayValue}
210
+ onChange={handleAmountChange}
211
+ marginY="8px"
212
+ w={"full"}
213
+ />
214
+ {/* Crypto equivalent */}
215
+ <Flex
216
+ fontSize="md"
217
+ color="fg.muted"
218
+ mt={2}
219
+ gap={1}
220
+ justifyContent="space-between"
221
+ w={"full"}
222
+ >
223
+ {quotesLoading ? (
224
+ <Skeleton height="16px" width="120px" />
225
+ ) : selectedQuote ? (
226
+ <>
227
+ <Text fontSize="sm">Estimated amount:</Text>
228
+ <Text fontSize="sm" fontWeight="semibold">
229
+ {selectedQuote.destinationAmount?.toString()}{" "}
230
+ {destinationSymbol}
231
+ </Text>
232
+ </>
233
+ ) : (
234
+ <Text>Enter amount to see quote</Text>
235
+ )}
236
+ </Flex>
237
+ </Box>
238
+
239
+ {/* Error */}
240
+ {!!quotesError && (
241
+ <Text color="error" fontSize="sm" textAlign="center">
242
+ {quotesError}
243
+ </Text>
244
+ )}
245
+
246
+ {/* Provider Row */}
247
+ {isInitialLoading ? (
248
+ <Card>
249
+ <Box
250
+ display="flex"
251
+ alignItems="center"
252
+ justifyContent="center"
253
+ gap="1"
254
+ minWidth="320px"
255
+ >
256
+ <Spinner size="sm" color="fg.muted" />
257
+ <Text fontSize="sm" color="fg.muted">
258
+ Finding best rates...
259
+ </Text>
260
+ </Box>
261
+ </Card>
262
+ ) : (
263
+ <Card onClick={() => setShowProviderSelect(true)}>
264
+ <Box
265
+ display="flex"
266
+ alignItems="center"
267
+ gap="1"
268
+ minWidth="320px"
269
+ >
270
+ <Box
271
+ display="flex"
272
+ alignItems="center"
273
+ gap="1"
274
+ marginRight="auto"
275
+ >
276
+ {quotesLoading ? (
277
+ <Skeleton height="20px" width="100px" />
278
+ ) : selectedQuote ? (
279
+ <>
280
+ <Center
281
+ boxSize="32px"
282
+ borderRadius="card"
283
+ bg="bg.subtle"
284
+ overflow="hidden"
285
+ flexShrink={0}
286
+ >
287
+ <Image
288
+ src={getProviderIcon(
289
+ selectedQuote.serviceProvider,
290
+ )}
291
+ alt={selectedQuote.serviceProvider}
292
+ boxSize="32px"
293
+ objectFit="contain"
294
+ onError={(e) => {
295
+ (
296
+ e.target as HTMLImageElement
297
+ ).style.display = "none";
298
+ }}
299
+ />
300
+ </Center>
301
+ <Box>
302
+ <Card.Title>
303
+ {formatProviderName(
304
+ selectedQuote.serviceProvider,
305
+ )}
306
+ </Card.Title>
307
+ <Card.Description>
308
+ {isBestRate && "Best price"}
309
+ {isBestRate &&
310
+ selectedQuote.lowKyc &&
311
+ " • "}
312
+ {selectedQuote.lowKyc && "Low KYC"}
313
+ </Card.Description>
314
+ </Box>
315
+ </>
316
+ ) : (
317
+ <Card.Title>Select provider</Card.Title>
318
+ )}
319
+ </Box>
320
+ <Icon
321
+ as={ChevronRight}
322
+ color="fg.muted"
323
+ width="16px"
324
+ height="16px"
325
+ />
326
+ </Box>
327
+ </Card>
328
+ )}
329
+
330
+ {/* Continue Button */}
331
+ <Button
332
+ onClick={onContinue}
333
+ disabled={!selectedQuote || parseFloat(fiatAmount) < 10}
334
+ width="100%"
335
+ >
336
+ Continue
337
+ </Button>
338
+ </BodyWrapper>
339
+ </AnimatedStep>
340
+ );
341
+ };
342
+
343
+ export default ChooseAmountStep;
@@ -0,0 +1,189 @@
1
+ import { Spinner, Text, Flex, Image, Box, Center } from "@chakra-ui/react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { BodyWrapper } from "../../ui/styled";
4
+ import { Button } from "../../ui";
5
+ import { formatProviderName } from "@/util/meld-hooks";
6
+ import { CircleTimer } from "@/components/CircleTimer";
7
+ import type { MeldQuote, MeldTransactionStatus } from "@/types";
8
+ import FailIcon from "@/assets/fail.svg";
9
+ import { useSmartAccountAddress } from "@/util/enso-hooks";
10
+
11
+ interface OpenWidgetStepProps {
12
+ selectedQuote: MeldQuote;
13
+ isCreatingSession: boolean;
14
+ widgetUrl: string | null;
15
+ sessionId: string | null;
16
+ onCreateSession: () => void;
17
+ onOpenWidget: () => void;
18
+ isTransferDetected: boolean;
19
+ detectedAmount: string | null;
20
+ error?: string | null;
21
+ }
22
+ // Use after implementing Meld webhooks if needed
23
+ function getStatusMessage(status: MeldTransactionStatus | undefined): string {
24
+ switch (status) {
25
+ case "PENDING_CREATED":
26
+ case "PENDING":
27
+ return "Waiting for payment...";
28
+ case "PROCESSING":
29
+ return "Processing payment...";
30
+ case "AUTHORIZED":
31
+ return "Payment authorized...";
32
+ case "TWO_FA_REQUIRED":
33
+ return "Two-factor authentication required...";
34
+ case "TWO_FA_PROVIDED":
35
+ return "Verifying authentication...";
36
+ case "SETTLING":
37
+ return "Payment confirmed, crypto being sent...";
38
+ case "SETTLED":
39
+ return "Payment complete!";
40
+ case "DECLINED":
41
+ return "Payment was declined.";
42
+ case "CANCELLED":
43
+ return "Payment was cancelled.";
44
+ case "FAILED":
45
+ case "ERROR":
46
+ return "Payment failed.";
47
+ case "REFUNDED":
48
+ return "Payment was refunded.";
49
+ case "VOIDED":
50
+ return "Payment was voided.";
51
+ case "AUTHORIZATION_EXPIRED":
52
+ return "Authorization expired.";
53
+ default:
54
+ return "Waiting for payment...";
55
+ }
56
+ }
57
+
58
+ const OpenWidgetStep = ({
59
+ selectedQuote,
60
+ isCreatingSession,
61
+ widgetUrl,
62
+ sessionId,
63
+ onCreateSession,
64
+ onOpenWidget,
65
+ isTransferDetected,
66
+ detectedAmount,
67
+ error,
68
+ }: OpenWidgetStepProps) => {
69
+ const openedWidgetUrlRef = useRef<string | null>(null);
70
+ const [isTimerFinished, setIsTimerFinished] = useState(false);
71
+ const { smartAccountAddress } = useSmartAccountAddress();
72
+
73
+ // Auto-create session on mount
74
+ useEffect(() => {
75
+ if (!widgetUrl && !isCreatingSession && !error) {
76
+ onCreateSession();
77
+ }
78
+ }, [widgetUrl, isCreatingSession, error, onCreateSession]);
79
+
80
+ // Auto-open each newly created widget URL once.
81
+ useEffect(() => {
82
+ if (!widgetUrl) return;
83
+ if (openedWidgetUrlRef.current === widgetUrl) return;
84
+ openedWidgetUrlRef.current = widgetUrl;
85
+ onOpenWidget();
86
+ }, [widgetUrl, onOpenWidget]);
87
+
88
+ useEffect(() => {
89
+ if (!sessionId) {
90
+ setIsTimerFinished(false);
91
+ }
92
+ }, [sessionId]);
93
+
94
+ const fiatPrefix = selectedQuote.sourceCurrencyCode === "EUR" ? "€" : "$";
95
+
96
+ // Creating session
97
+ if (isCreatingSession && !widgetUrl) {
98
+ return (
99
+ <BodyWrapper>
100
+ <Center>
101
+ <Spinner size="lg" color="primary" />
102
+ </Center>
103
+ </BodyWrapper>
104
+ );
105
+ }
106
+
107
+ // Error before session created
108
+ if (error && !widgetUrl) {
109
+ return (
110
+ <BodyWrapper>
111
+ <Flex direction="column" gap={4} align="center">
112
+ <Box
113
+ display="flex"
114
+ flexDirection="column"
115
+ alignItems="center"
116
+ >
117
+ <Image
118
+ src={FailIcon}
119
+ boxShadow="0px 0px 20px var(--chakra-colors-error)"
120
+ borderRadius="90%"
121
+ width="58px"
122
+ height="58px"
123
+ />
124
+ </Box>
125
+ <Text color="error" textAlign="center">
126
+ {error}
127
+ </Text>
128
+ </Flex>
129
+ </BodyWrapper>
130
+ );
131
+ }
132
+
133
+ return (
134
+ <BodyWrapper>
135
+ <Flex direction="column" gap={4} align="center">
136
+ <CircleTimer
137
+ start={!!sessionId}
138
+ duration={300}
139
+ onFinish={() => setIsTimerFinished(true)}
140
+ />
141
+ <Text textAlign="center" color="fg" fontWeight="semibold">
142
+ Complete your purchase of{" "}
143
+ <Text as="span" fontWeight="bold">
144
+ {fiatPrefix}
145
+ {selectedQuote.sourceAmount}
146
+ </Text>{" "}
147
+ via {formatProviderName(selectedQuote.serviceProvider)}
148
+ </Text>
149
+ {/*<Text fontSize="sm" color="fg.muted" textAlign="center">*/}
150
+ {/* {getStatusMessage(txStatus)}*/}
151
+ {/*</Text>*/}
152
+ <Text fontSize="sm" color="fg.muted" textAlign="center" mb={-2}>
153
+ Tracking incoming USDC transfer to your smart account:{" "}
154
+ </Text>{" "}
155
+ <Text fontSize="sm" fontWeight={500}>
156
+ {smartAccountAddress}
157
+ </Text>
158
+ {isTransferDetected && detectedAmount && (
159
+ <Text fontSize="sm" color="success" textAlign="center">
160
+ Detected {detectedAmount} USDC. Redirecting to Smart
161
+ Account quote...
162
+ </Text>
163
+ )}
164
+ {isTimerFinished && !isTransferDetected && (
165
+ <Text fontSize="sm" color="fg.muted" textAlign="center">
166
+ Believe transfer was not detected? Withdrawn funds can
167
+ be used for transacting any time later using Smart
168
+ Wallet as source.
169
+ </Text>
170
+ )}
171
+ {error && widgetUrl && (
172
+ <Text color="error" textAlign="center" fontSize="sm">
173
+ {error}
174
+ </Text>
175
+ )}
176
+ <Flex direction="column" gap={2} width="100%">
177
+ <Button
178
+ onClick={onOpenWidget}
179
+ disabled={!widgetUrl || isCreatingSession}
180
+ >
181
+ {widgetUrl ? "Open Provider" : "Preparing Provider..."}
182
+ </Button>
183
+ </Flex>
184
+ </Flex>
185
+ </BodyWrapper>
186
+ );
187
+ };
188
+
189
+ export default OpenWidgetStep;