@ensofinance/checkout-widget 0.1.6 → 0.1.8

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