@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
@@ -12,7 +12,7 @@ export const DetailRowContainer = chakra("div", {
12
12
  alignItems: "center",
13
13
  h: "36px",
14
14
  p: "8px 12px",
15
- borderRadius: "8px",
15
+ borderRadius: "card",
16
16
  bg: "bg",
17
17
  w: "100%",
18
18
  },
@@ -57,7 +57,7 @@ export const TransactionDetailRow = () => {
57
57
  <Box
58
58
  width="100%"
59
59
  bg="bg.subtle"
60
- borderRadius="md"
60
+ borderRadius="card"
61
61
  padding="12px"
62
62
  transition="all 0.2s"
63
63
  >
@@ -32,7 +32,7 @@ const ExchangeCard: FC<IProps> = ({
32
32
  ),
33
33
  low: <Tag>Low Balance</Tag>,
34
34
  connected: (
35
- <Icon as={CheckIcon} color="green" width={"16px"} height={"16px"} />
35
+ <Icon as={CheckIcon} color="success" width={"16px"} height={"16px"} />
36
36
  ),
37
37
  none: null,
38
38
  };
@@ -39,6 +39,7 @@ const OptionCard = ({
39
39
  <Image
40
40
  w={"32px"}
41
41
  h={"32px"}
42
+ objectFit={"contain"}
42
43
  borderRadius={"full"}
43
44
  position={"relative"}
44
45
  zIndex={index}
@@ -49,7 +50,7 @@ const OptionCard = ({
49
50
  </Box>
50
51
  <Icon
51
52
  as={ChevronRight}
52
- color="gray"
53
+ color="fg.muted"
53
54
  width={"16px"}
54
55
  height={"16px"}
55
56
  />
@@ -85,7 +85,7 @@ const WalletCard = ({
85
85
  {status === WalletStatus.CONNECTED && (
86
86
  <Icon
87
87
  as={CheckIcon}
88
- color="green"
88
+ color="success"
89
89
  width={"16px"}
90
90
  height={"16px"}
91
91
  />
@@ -35,11 +35,11 @@ export const ModalWrapper = chakra("div", {
35
35
  maxWidth: "374px",
36
36
  justifyContent: "center",
37
37
  alignItems: "center",
38
- borderRadius: "lg",
38
+ borderRadius: "card",
39
39
  border: "1px solid",
40
- borderColor: "gray.300",
40
+ borderColor: "border",
41
41
  padding: "16px",
42
- backgroundColor: "white",
42
+ backgroundColor: "bg",
43
43
  },
44
44
  });
45
45
 
@@ -0,0 +1,420 @@
1
+ import { Center, Box, Icon, Text } from "@chakra-ui/react";
2
+ import { ChevronLeft, X, XCircle } from "lucide-react";
3
+ import {
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ useCallback,
10
+ } from "react";
11
+ import { useAccount } from "wagmi";
12
+ import { BodyWrapper, HeaderTitle, HeaderWrapper } from "../../ui/styled";
13
+ import { IconButton } from "../../ui";
14
+ import { CheckoutContext } from "../../Checkout";
15
+ import Modal from "../../modal";
16
+ import { useAppStore } from "@/store";
17
+ import { denormalizeValue } from "@/util";
18
+ import { getUsdcAddress } from "@/util/constants";
19
+ import { useSmartAccountAddress } from "@/util/enso-hooks";
20
+ import {
21
+ useMeldQuotes,
22
+ useMeldCardBuyFlow,
23
+ useCountryCode,
24
+ getMeldDestinationCurrencyCode,
25
+ isMeldCardBuySupportedChain,
26
+ useMeldSupportedCrypto,
27
+ useIncomingUsdcTransferDetector,
28
+ } from "@/util/meld-hooks";
29
+ import type { MeldQuote } from "@/types";
30
+
31
+ import ChooseAmountStep from "./ChooseAmountStep";
32
+ import OpenWidgetStep from "./OpenWidgetStep";
33
+ import { AnimatedStep } from "../../ui/transitions";
34
+
35
+ interface CardBuyFlowProps {
36
+ setFlow: (flow: string) => void;
37
+ initialStep?: CardBuyStep;
38
+ onCardBuyDepositDetected: () => void;
39
+ }
40
+
41
+ // Card buy flow steps
42
+ export enum CardBuyStep {
43
+ ChooseAmount = 0,
44
+ OpenWidget = 1,
45
+ }
46
+
47
+ export const STEP_TITLES: Record<CardBuyStep, string> = {
48
+ [CardBuyStep.ChooseAmount]: "Buy with Card",
49
+ [CardBuyStep.OpenWidget]: "Complete Purchase",
50
+ };
51
+
52
+ const cardPreviousStep: Partial<Record<CardBuyStep, CardBuyStep>> = {
53
+ [CardBuyStep.OpenWidget]: CardBuyStep.ChooseAmount,
54
+ };
55
+
56
+ const CardBuyFlow = ({
57
+ setFlow,
58
+ initialStep = CardBuyStep.ChooseAmount,
59
+ onCardBuyDepositDetected,
60
+ }: CardBuyFlowProps) => {
61
+ const { handleClose } = useContext(CheckoutContext);
62
+ const { address } = useAccount();
63
+
64
+ // Store
65
+ const chainIdOut = useAppStore((s) => s.chainIdOut);
66
+ const setAmountIn = useAppStore((s) => s.setAmountIn);
67
+ const setTokenIn = useAppStore((s) => s.setTokenIn);
68
+ const setChainIdIn = useAppStore((s) => s.setChainIdIn);
69
+ const setSelectedIntegration = useAppStore((s) => s.setSelectedIntegration);
70
+ const handoffTriggeredRef = useRef(false);
71
+
72
+ // Set integration on mount so useRouteData picks "checkout" strategy,
73
+ // and clean up on unmount (same pattern as ExchangeFlow).
74
+ useEffect(() => {
75
+ setSelectedIntegration({ id: "meld", type: "meld", name: "Card Buy" });
76
+ return () => setSelectedIntegration(null);
77
+ }, [setSelectedIntegration]);
78
+
79
+ // Local state
80
+ const [currentStep, setCurrentStep] = useState<CardBuyStep>(initialStep);
81
+ const [fiatCurrency, setFiatCurrency] = useState<"USD" | "EUR">("USD");
82
+ const [fiatAmount, setFiatAmount] = useState<string>("100");
83
+ const [debouncedFiatAmount, setDebouncedFiatAmount] =
84
+ useState<string>(fiatAmount);
85
+ const [selectedQuote, setSelectedQuote] = useState<MeldQuote | null>(null);
86
+ const [error, setError] = useState<string | null>(null);
87
+
88
+ // Debounce fiat amount for quote requests
89
+ useEffect(() => {
90
+ const timer = setTimeout(() => setDebouncedFiatAmount(fiatAmount), 500);
91
+ return () => clearTimeout(timer);
92
+ }, [fiatAmount]);
93
+
94
+ // Smart account address - use recipient override if provided
95
+ const { smartAccountAddress } = useSmartAccountAddress(chainIdOut);
96
+
97
+ // Country code detection
98
+ const { countryCode, isLoading: countryCodeLoading } = useCountryCode();
99
+ const { data: supportedCryptos = [], isLoading: supportedCryptosLoading } =
100
+ useMeldSupportedCrypto({
101
+ countryCode,
102
+ enabled: !!chainIdOut,
103
+ });
104
+
105
+ const destinationSymbol = "USDC";
106
+
107
+ const destinationCryptoCode = useMemo(() => {
108
+ if (!chainIdOut) return null;
109
+ return getMeldDestinationCurrencyCode({
110
+ symbol: destinationSymbol,
111
+ chainId: chainIdOut,
112
+ supportedCryptos,
113
+ });
114
+ }, [destinationSymbol, chainIdOut, supportedCryptos]);
115
+
116
+ const isCardBuySupported =
117
+ !!chainIdOut &&
118
+ isMeldCardBuySupportedChain(chainIdOut, supportedCryptos);
119
+
120
+ // Fetch quotes (uses debounced amount to avoid excessive requests)
121
+ const {
122
+ data: quotes,
123
+ isLoading: quotesLoading,
124
+ error: quotesError,
125
+ } = useMeldQuotes({
126
+ sourceCurrency: fiatCurrency,
127
+ destinationCurrency: destinationCryptoCode || "",
128
+ amount: parseFloat(debouncedFiatAmount) || 0,
129
+ countryCode: countryCode || "",
130
+ walletAddress: smartAccountAddress || "",
131
+ enabled:
132
+ !!countryCode &&
133
+ !!destinationCryptoCode &&
134
+ parseFloat(debouncedFiatAmount) > 0 &&
135
+ !!smartAccountAddress,
136
+ });
137
+
138
+ // Select best quote automatically
139
+ useEffect(() => {
140
+ if (!quotes || quotes.length === 0) {
141
+ if (selectedQuote) setSelectedQuote(null);
142
+ return;
143
+ }
144
+
145
+ // Default to best quote
146
+ if (!selectedQuote) {
147
+ setSelectedQuote(quotes[0]);
148
+ return;
149
+ }
150
+
151
+ // Keep the same provider selected if still available, but refresh the
152
+ // quote object (rates/fees can update on refetch).
153
+ const matching = quotes.find(
154
+ (q) => q.serviceProvider === selectedQuote.serviceProvider,
155
+ );
156
+ if (!matching) {
157
+ setSelectedQuote(quotes[0]);
158
+ return;
159
+ }
160
+
161
+ const didChange =
162
+ matching.destinationAmount !== selectedQuote.destinationAmount ||
163
+ matching.totalFee !== selectedQuote.totalFee ||
164
+ matching.exchangeRate !== selectedQuote.exchangeRate;
165
+ if (didChange) setSelectedQuote(matching);
166
+ }, [quotes, selectedQuote]);
167
+
168
+ // MELD flow state
169
+ const meldFlow = useMeldCardBuyFlow();
170
+
171
+ const shouldTrackIncomingTransfer =
172
+ currentStep === CardBuyStep.OpenWidget &&
173
+ !!chainIdOut &&
174
+ !!smartAccountAddress;
175
+ const {
176
+ detectedAmount,
177
+ isDetected,
178
+ reset: resetIncomingDetector,
179
+ } = useIncomingUsdcTransferDetector({
180
+ chainId: chainIdOut,
181
+ walletAddress: smartAccountAddress,
182
+ enabled: shouldTrackIncomingTransfer,
183
+ thresholdUsdc: "0.01",
184
+ });
185
+
186
+ const resetToChooseAmount = useCallback(
187
+ (clearQuote = false) => {
188
+ meldFlow.reset();
189
+ resetIncomingDetector();
190
+ handoffTriggeredRef.current = false;
191
+ setError(null);
192
+ if (clearQuote) {
193
+ setSelectedQuote(null);
194
+ }
195
+ setCurrentStep(CardBuyStep.ChooseAmount);
196
+ },
197
+ [meldFlow, resetIncomingDetector],
198
+ );
199
+
200
+ // Navigation
201
+ const goBack = useCallback(() => {
202
+ if (currentStep === CardBuyStep.ChooseAmount) {
203
+ setFlow("");
204
+ return;
205
+ }
206
+ const previousStep = cardPreviousStep[currentStep];
207
+ if (previousStep !== undefined) {
208
+ resetToChooseAmount(false);
209
+ return;
210
+ }
211
+ setFlow("");
212
+ }, [currentStep, setFlow, resetToChooseAmount]);
213
+
214
+ const goToOpenWidget = useCallback(() => {
215
+ if (!selectedQuote) return;
216
+ setError(null);
217
+ handoffTriggeredRef.current = false;
218
+ resetIncomingDetector();
219
+ setCurrentStep(CardBuyStep.OpenWidget);
220
+ }, [selectedQuote, resetIncomingDetector]);
221
+
222
+ // Start MELD session
223
+ const handleStartSession = useCallback(async () => {
224
+ if (
225
+ !selectedQuote ||
226
+ !smartAccountAddress ||
227
+ !destinationCryptoCode ||
228
+ !countryCode
229
+ )
230
+ return;
231
+
232
+ try {
233
+ await meldFlow.startSession({
234
+ countryCode: countryCode,
235
+ sourceCurrencyCode: fiatCurrency,
236
+ destinationCurrencyCode: destinationCryptoCode,
237
+ sourceAmount: selectedQuote.sourceAmount,
238
+ walletAddress: smartAccountAddress,
239
+ serviceProvider: selectedQuote.serviceProvider,
240
+ paymentMethodType: "CREDIT_DEBIT_CARD",
241
+ externalCustomerId: address,
242
+ externalSessionId: `enso-${Date.now()}`,
243
+ });
244
+ } catch (err: any) {
245
+ console.error(err);
246
+ setError(err.message || "Failed to create session");
247
+ }
248
+ }, [
249
+ selectedQuote,
250
+ smartAccountAddress,
251
+ destinationCryptoCode,
252
+ countryCode,
253
+ fiatCurrency,
254
+ address,
255
+ meldFlow,
256
+ ]);
257
+
258
+ const handleOpenWidget = useCallback(() => {
259
+ meldFlow.openWidget();
260
+ }, [meldFlow]);
261
+
262
+ // Funds arrival is the source of truth for handing off into Smart Account flow.
263
+ useEffect(() => {
264
+ if (currentStep !== CardBuyStep.OpenWidget) return;
265
+ if (!isDetected || !detectedAmount || !chainIdOut) return;
266
+ if (handoffTriggeredRef.current) return;
267
+
268
+ try {
269
+ handoffTriggeredRef.current = true;
270
+ setError(null);
271
+ resetIncomingDetector();
272
+
273
+ // Store the detected amount and token before switching flow
274
+ setAmountIn(denormalizeValue(detectedAmount, 6));
275
+ setTokenIn(getUsdcAddress(chainIdOut));
276
+ setChainIdIn(chainIdOut);
277
+
278
+ onCardBuyDepositDetected();
279
+ } catch (err: any) {
280
+ handoffTriggeredRef.current = false;
281
+ setError(
282
+ err.message || "Failed to redirect to Smart Account quote",
283
+ );
284
+ }
285
+ }, [
286
+ currentStep,
287
+ isDetected,
288
+ detectedAmount,
289
+ chainIdOut,
290
+ setAmountIn,
291
+ setTokenIn,
292
+ setChainIdIn,
293
+ onCardBuyDepositDetected,
294
+ resetIncomingDetector,
295
+ ]);
296
+
297
+ // Check if chain is supported
298
+ if (!chainIdOut || (!supportedCryptosLoading && !isCardBuySupported)) {
299
+ return (
300
+ <>
301
+ <Modal.Header>
302
+ <HeaderWrapper>
303
+ <IconButton onClick={() => setFlow("")} width="40px">
304
+ <Icon
305
+ as={ChevronLeft}
306
+ color="fg.muted"
307
+ width="16px"
308
+ height="16px"
309
+ />
310
+ </IconButton>
311
+ <Box>
312
+ <HeaderTitle>Buy with Card</HeaderTitle>
313
+ </Box>
314
+ {handleClose && (
315
+ <IconButton onClick={handleClose} width="40px">
316
+ <Icon
317
+ as={X}
318
+ color="fg.muted"
319
+ width="16px"
320
+ height="16px"
321
+ />
322
+ </IconButton>
323
+ )}
324
+ </HeaderWrapper>
325
+ </Modal.Header>
326
+ <Modal.Body>
327
+ <BodyWrapper>
328
+ <Center py={8} flexDirection="column" gap={4}>
329
+ <Icon as={XCircle} color="error" boxSize={12} />
330
+ <Text color="fg.muted" textAlign="center">
331
+ Card purchases are not supported for this chain.
332
+ </Text>
333
+ </Center>
334
+ </BodyWrapper>
335
+ </Modal.Body>
336
+ </>
337
+ );
338
+ }
339
+
340
+ // Render step content
341
+ const renderStepContent = () => {
342
+ switch (currentStep) {
343
+ case CardBuyStep.ChooseAmount:
344
+ return (
345
+ <ChooseAmountStep
346
+ fiatCurrency={fiatCurrency}
347
+ setFiatCurrency={setFiatCurrency}
348
+ fiatAmount={fiatAmount}
349
+ setFiatAmount={setFiatAmount}
350
+ quotes={quotes}
351
+ quotesLoading={quotesLoading || countryCodeLoading}
352
+ quotesError={
353
+ quotesError
354
+ ? quotesError.message || String(quotesError)
355
+ : null
356
+ }
357
+ selectedQuote={selectedQuote}
358
+ setSelectedQuote={setSelectedQuote}
359
+ destinationSymbol={destinationSymbol}
360
+ onContinue={goToOpenWidget}
361
+ />
362
+ );
363
+
364
+ case CardBuyStep.OpenWidget:
365
+ return (
366
+ <OpenWidgetStep
367
+ selectedQuote={selectedQuote!}
368
+ isCreatingSession={meldFlow.isCreatingSession}
369
+ widgetUrl={meldFlow.widgetUrl}
370
+ sessionId={meldFlow.sessionId}
371
+ onCreateSession={handleStartSession}
372
+ onOpenWidget={handleOpenWidget}
373
+ isTransferDetected={isDetected}
374
+ detectedAmount={detectedAmount}
375
+ error={error || meldFlow.sessionError?.message}
376
+ />
377
+ );
378
+
379
+ default:
380
+ return null;
381
+ }
382
+ };
383
+
384
+ return (
385
+ <>
386
+ <Modal.Header>
387
+ <HeaderWrapper>
388
+ <IconButton onClick={goBack} width="40px">
389
+ <Icon
390
+ as={ChevronLeft}
391
+ color="fg.muted"
392
+ width="16px"
393
+ height="16px"
394
+ />
395
+ </IconButton>
396
+ <Box>
397
+ <HeaderTitle>{STEP_TITLES[currentStep]}</HeaderTitle>
398
+ </Box>
399
+ {handleClose && (
400
+ <IconButton onClick={handleClose} width="40px">
401
+ <Icon
402
+ as={X}
403
+ color="fg.muted"
404
+ width="16px"
405
+ height="16px"
406
+ />
407
+ </IconButton>
408
+ )}
409
+ </HeaderWrapper>
410
+ </Modal.Header>
411
+ <Modal.Body>
412
+ <AnimatedStep key={currentStep}>
413
+ {renderStepContent()}
414
+ </AnimatedStep>
415
+ </Modal.Body>
416
+ </>
417
+ );
418
+ };
419
+
420
+ export default CardBuyFlow;